Skip to content

Commit cb4355b

Browse files
authored
Merge pull request #122 from BitGo/WP-5534
docs(awm): added aws implementation docs
2 parents 998a213 + 6fd0981 commit cb4355b

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed

demo-kms-script/aws-interface.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# AWS HSM KMS Implementation Documentation
2+
3+
This document provides a reference implementation for integrating the 4 KMS API's with AWS HSM, covering the complete request-response flow from API handlers to HSM operations.
4+
5+
## ⚠️ Security Recommendation
6+
7+
**For production KMS implementations, consider implementing the KMS-API in a C++ like language, or use typed arrays like Uint8Array for all sensitive data because JavaScript does not support secure memory management.**
8+
9+
**Recommended Alternatives:**
10+
- **C++/Rust**: Languages with explicit memory management and secure allocation
11+
- **Node.js Typed Arrays**: Use `Uint8Array` for sensitive data with explicit zeroing
12+
- **Native Addons**: Implement cryptographic operations in native C++ modules
13+
- **Hardware Security**: Use HSM-backed secure memory when available
14+
15+
## API Overview
16+
17+
The KMS API provides secure key management through four main endpoints that integrate with AWS HSM:
18+
19+
- `POST /key` - Store private keys using envelope encryption
20+
- `GET /key/{pub}` - Retrieve private keys using envelope decryption
21+
- `POST /generateDataKey` - Generate AES keys in HSM for encryption
22+
- `POST /decryptDataKey` - Decrypt data keys using root keys
23+
24+
## Architecture Flow
25+
All 4 API's implementation should follow roughly the same dataflow as outlined bellow:
26+
27+
```
28+
API Request → Handler → KMS Provider → AWS HSM → KMS Provider → Database (if required) → Response
29+
```
30+
31+
A KMS provider is the implementation of the code that is in charge of making the necessary calls to the HSM directly. You might have multiple providers in your solution, one for each 3rd party HSM that you wish to use, for example.
32+
33+
### Handler-to-Provider Mapping
34+
35+
| API Endpoint | Handler File | Provider Method | HSM Operations |
36+
|--------------|--------------|-----------------|----------------|
37+
| `POST /key` | `storePrivateKey.ts` | `postKey()` | Create AES key, export, encrypt |
38+
| `GET /key/{pub}` | `getPrivateKey.ts` | `getKey()` | Decrypt data key locally |
39+
| `POST /generateDataKey` | `generateDataKey.ts` | `generateDataKey()` | Create/export AES key |
40+
| `POST /decryptDataKey` | `decryptDataKey.ts` | `decryptDataKey()` | Local SJCL decryption |
41+
42+
## Envelope Encryption Pattern (Recommended)
43+
44+
We recommend using a 3 level key encryption to store and protect the private keys of your advanced wallets.
45+
The 3 levels consist of the root-level key from the KMS, 2nd level data keys generated by the root level key, and the 3rd level private keys used by your wallets directly.
46+
47+
### Layer 1: KMS Keys (AWS HSM)
48+
- **Key spec**: `SYMMETRIC_DEFAULT`
49+
- **Algorithm**: AES-256-GCM, used by keys generated using the specification `SYMMETRIC_DEFAULT`
50+
- **Generation**: AWS HSM
51+
- **Storage**: AWS HSM
52+
- **Identification**: via its Amazon Resource Name (ARN), stored in local database
53+
- **Usage**: Generate lower level data keys. The ARN needs to be passed into AWS to generate a data key.
54+
55+
### Layer 2: Data Keys (Generated by HSM, Used Locally)
56+
- **Algorithm**: AES-256 symmetric keys
57+
- **Generation**: AWS HSM (temporary keys)
58+
- **Export**: Encrypted data key and plaintext data key, both as Uint8Arrays
59+
- **Storage**: AWS KMS Database (plaintext data key), local memory (encrypted data key)
60+
61+
### Layer 3: Private Keys (Application Data)
62+
- **Encryption**: AES-256-CCM using SJCL
63+
- **Key**: Data key plaintext (from Layer 2)
64+
- **Storage**: Database (encrypted only)
65+
66+
## Implementation Details
67+
68+
### Root Key Creation
69+
70+
This following needs to be only run once. The KMS should be functional with just one root-level key.
71+
72+
```typescript
73+
import * as awskms from '@aws-sdk/client-kms';
74+
75+
async createRootKey(): Promise<{ rootKey: string }> {
76+
const kms: awskms.KMSClient = new awskms.KMSClient({
77+
region: *YOUR_AWS_REGION*,
78+
credentials: *YOUR_AWS_CREDENTIALS*
79+
});
80+
81+
const input: awskms.CreateKeyRequest = {
82+
KeySpec: 'SYMMETRIC_DEFAULT',
83+
}
84+
const command = new awskms.CreateKeyCommand(input);
85+
const res = await kms.send(command);
86+
87+
return {
88+
rootKey: res.KeyMetadata.KeyId
89+
}
90+
}
91+
```
92+
93+
### Data Key Generation/Decryption
94+
Note that the root key returned from the above method is required for AWS to create data keys.
95+
96+
```typescript
97+
import * as awskms from '@aws-sdk/client-kms';
98+
99+
async generateDataKey(rootKey: string) {
100+
const kms: awskms.KMSClient = new awskms.KMSClient({
101+
region: *YOUR_AWS_REGION*,
102+
credentials: *YOUR_AWS_CREDENTIALS*
103+
});
104+
105+
const input: awskms.GenerateDataKeyRequest = {
106+
KeyId: rootKey,
107+
KeySpec: awskms.DataKeySpec.AES_256,
108+
}
109+
const command = new awskms.GenerateDataKeyCommand(input);
110+
const res = await kms.send(command);
111+
112+
if (
113+
res.CiphertextBlob === undefined ||
114+
res.Plaintext === undefined
115+
) throw {};
116+
117+
return {
118+
encryptedKey: res.CiphertextBlob.toString(),
119+
plaintextKey: res.Plaintext.toString(),
120+
};
121+
}
122+
123+
async decryptDataKey(rootKey: string, encryptedKey: string) {
124+
// parse comma-seperating-integer string (i.e. "1,127,34,23,...") back into Uint8Array
125+
const encryptedBuffer = Uint8Array.from(encryptedKey.split(',').map((x: any) => parseInt(x, 10)));
126+
const kms: awskms.KMSClient = new awskms.KMSClient({
127+
region: *YOUR_AWS_REGION*,
128+
credentials: *YOUR_AWS_CREDENTIALS*
129+
});
130+
131+
const input: awskms.DecryptRequest = {
132+
CiphertextBlob: encryptedBuffer,
133+
KeyId: rootKey,
134+
};
135+
136+
const command = new awskms.DecryptCommand(input);
137+
138+
const res = await this.kms.send(command);
139+
if (res.Plaintext === undefined) throw {};
140+
141+
return {
142+
plaintextKey: res.Plaintext.toString(),
143+
};
144+
}
145+
```
146+
**Security Considerations:**
147+
- **Immediate Use**: Plaintext keys should be used immediately after generation
148+
- **Memory Overwriting**: Overwrite memory locations with random data before deallocation
149+
- **Garbage Collection**: Force GC to clear memory pages containing sensitive data
150+
- **Process Isolation**: Consider using separate processes for key operations
151+
- **Hardware Security**: Use HSM-backed secure memory when available
152+
153+
154+
### Wallet Priate key storage/retrival
155+
```typescript
156+
async postKey(rootKey: string, prv: string, pub: string) {
157+
const dataKey = await this.generateDataKey(rootKey);
158+
const encryptedPrv = encrypt(dataKey.plaintextKey, prv);
159+
160+
// **CRITICAL**: Wipe plaintext data key from memory immediately after use
161+
// Production code should implement secure memory wiping here
162+
163+
// subroutine to store necessary, ENCRYPTED, info in database
164+
database.store(encryptedPrv, dataKey.encryptedKey, pub);
165+
166+
return {
167+
encryptedPrv
168+
rootKeyId: res.KeyId,
169+
metadata: res.$metadata,
170+
};
171+
}
172+
173+
async getKey(rootKey: string, pub: string) {
174+
const { encryptedPrv, encryptedKey } = database.select(pub);
175+
const { plaintextKey } = this.decryptDataKey(rootKey, encryptedKey);
176+
177+
const prv = decrypt(plaintextKey, encryptePrv);
178+
179+
// **CRITICAL**: Wipe plaintext data key from memory immediately after use
180+
// Production code should implement secure memory wiping here
181+
182+
return { prv };
183+
}
184+
```
185+
**Memory Security Notes:**
186+
- **Immediate Encryption**: Use plaintext data key immediately for encryption
187+
- **Secure Disposal**: Wipe plaintext key from memory after single use
188+
- **No Persistence**: Never store plaintext data keys in variables or logs
189+
- **Error Handling**: Ensure memory wiping occurs even if encryption fails
190+
191+
192+
## Database Schema
193+
194+
### private_keys Table
195+
196+
```sql
197+
CREATE TABLE private_keys (
198+
id INTEGER PRIMARY KEY AUTOINCREMENT,
199+
pub TEXT NOT NULL, -- Public key of the wallet
200+
source TEXT NOT NULL, -- 'user' or 'backup'
201+
encryptedPrv TEXT NOT NULL, -- Private key encrypted with data key
202+
encryptedDataKey TEXT NOT NULL, -- Data key encrypted with root key
203+
rootKey TEXT NOT NULL, -- Root key identifier (i.e. ARN)
204+
coin TEXT NOT NULL, -- Cryptocurrency type
205+
type TEXT NOT NULL, -- Key type (e.g. 'tss')
206+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
207+
);
208+
```
209+
210+
## SJCL Encryption Details
211+
212+
### Configuration
213+
- **Algorithm**: AES-256-CCM
214+
- **Iterations**: 10,000 (PBKDF2)
215+
- **Key Size**: 256 bits
216+
- **Tag Size**: 128 bits
217+
- **Mode**: CCM (Counter with CBC-MAC)
218+
219+
### Example SJCL Output
220+
```json
221+
{
222+
"iv": "a1b2c3d4e5f6...",
223+
"v": 1,
224+
"iter": 10000,
225+
"ks": 256,
226+
"ts": 128,
227+
"mode": "ccm",
228+
"adata": "",
229+
"cipher": "aes",
230+
"salt": "f6e5d4c3b2a1...",
231+
"ct": "base64-encrypted-data"
232+
}
233+
```

0 commit comments

Comments
 (0)