|
| 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