diff --git a/docs/kao-ec-wrapped.md b/docs/kao-ec-wrapped.md new file mode 100644 index 00000000..cdd2d6e5 --- /dev/null +++ b/docs/kao-ec-wrapped.md @@ -0,0 +1,148 @@ +# How we use EC Encryption to encapsulate the DEK + +## Overview + +Our system uses hybrid Elliptic Curve (EC) encryption to encapsulate splits or shares of the Data Encryption Key (DEK). +We place each share in a Key Access Object (KAO), which includes metadata binding the fragment to a policy, +how the fragment can be used to reconstruct the DEK, +and information about how the fragment is encapsulated, such as the KAS URL. +This document explains the process and the underlying mechanisms involved. + +## Terms + +1. **Elliptic Curve Cryptography (ECC)**: +A public key cryptography approach based on the algebraic structure of elliptic curves over finite fields. +2. **Data Encryption Key (DEK)**: +A symmetric key used to encrypt the actual data. +3. **Key Access Object (KAO)**: +An object that stores information about how the DEK is stored and accessed. +4. **Ephemeral Key Pair**: +A temporary key pair generated for each encryption session. +5. **Key Agreement**: +A process to derive a shared secret between two parties using their private and public keys. + +## Process + +### 1. Initialization + +When creating an ECWrapped KAO, +we generate an ephemeral key pair using the P-256 curve: + +```typescript +const ephemera = crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256', + }, + /* extractable: */ false, + /* keyUsages: */ ['deriveBits', 'deriveKey'] +); +``` + +### 2. Key Agreement + +To securely transmit the DEK, +we perform a key agreement between the ephemeral private key and the recipient's public key. +This derives a shared secret (KEK - Key Encryption Key): + +```typescript +const kasPublicKey: CryptoKey = /* Fetch or otherwise load known KAS public key value */; +const sharedSecret = await crypto.subtle.deriveBits( + { + name: 'ECDH', + public: kasPublicKey, + }, + ephemera.privateKey, + 256 +); +const ikm = await crypto.subtle.importKey( + 'raw', + sharedSecret, + { + name: 'HKDF', + }, + false, + ['deriveKey'] +); +const kek = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: new TextEncoder().encode('salt'), + }, + ikm, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'] +); +``` + +### 3. Encryption + +We then encrypt the DEK using the derived KEK with the AES-GCM algorithm. +We also generate a 12-byte initialization vector (IV) for this encryption. + +```typescript +const iv = generateRandomNumber(12); +const cek = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, kek, dek); +const entityWrappedKey = new Uint8Array(iv.length + cek.byteLength); +entityWrappedKey.set(iv); +entityWrappedKey.set(new Uint8Array(cek), iv.length); +``` + +### 4. Storing in KAO + +We store the encrypted DEK (entityWrappedKey) along with other metadata in the KAO: + +```typescript +const ephemeralPublicKeyPEM = await cryptoPublicToPem(ek.publicKey); +const kao: KeyAccessObject = { + type: 'ec-wrapped', + url: this.url, + protocol: 'kas', + wrappedKey: base64.encodeArrayBuffer(entityWrappedKey), + encryptedMetadata: base64.encodeArrayBuffer(encryptedMetadata), + policyBinding: { + alg: 'HS256', + hash: base64.encodeArrayBuffer(policyBinding), + }, + schemaVersion, + ephemeralPublicKey: ephemeralPublicKeyPEM, +}; +``` + +### 5. Decrypting Server Responses + +Rewrap requests can indicate which algorithm they wish to use for the response +by the the server responds to the KAS rewrap requests, +we decrypt the response using key agreement and ECDH. +The `unwrapKey` method in `tdf.ts` handles this process. + +We use the server's ephemeral public key to derive a shared secret (KEK) with the client's ephemeral private key: + +```typescript +const serverEphemeralKey: CryptoKey = await pemPublicToCrypto(sessionPublicKey); +const ekr = ephemeralEncryptionKeysRaw as CryptoKeyPair; +const kek = await keyAgreement(ekr.privateKey, serverEphemeralKey, { + hkdfSalt: new TextEncoder().encode('salt'), + hkdfHash: 'SHA-256', +}); +``` + +We then decrypt the encrypted DEK using the derived KEK and the initialization vector (IV) from the response: + +```typescript +const wrappedKeyAndNonce = base64.decodeArrayBuffer(entityWrappedKey); +const iv = wrappedKeyAndNonce.slice(0, 12); +const wrappedKey = wrappedKeyAndNonce.slice(12); +const dek = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, kek, wrappedKey); +``` + +## Conclusion + +By using EC encryption and ephemeral key pairs, +we ensure that the DEK is securely transmitted and stored within the ECWrapped KAO. +This approach leverages the strength of ECC and the security of AES-GCM to protect the DEK from unauthorized access. diff --git a/lib/src/nanotdf-crypto/enums.ts b/lib/src/nanotdf-crypto/enums.ts index aab54233..df464737 100644 --- a/lib/src/nanotdf-crypto/enums.ts +++ b/lib/src/nanotdf-crypto/enums.ts @@ -34,14 +34,3 @@ export enum KeyType { Private = 'private', Public = 'public', } - -export enum KeyUsageType { - Encrypt = 'encrypt', - Decrypt = 'decrypt', - DeriveBits = 'deriveBits', - DeriveKey = 'deriveKey', - Verify = 'verify', - Sign = 'sign', - UnwrapKey = 'unwrapKey', - WrapKey = 'wrapKey', -} diff --git a/lib/src/nanotdf-crypto/generateKeyPair.ts b/lib/src/nanotdf-crypto/generateKeyPair.ts index dedb5de6..9f7369ac 100644 --- a/lib/src/nanotdf-crypto/generateKeyPair.ts +++ b/lib/src/nanotdf-crypto/generateKeyPair.ts @@ -1,9 +1,11 @@ -import { AlgorithmName, NamedCurve, KeyUsageType } from './enums.js'; +import { AlgorithmName, NamedCurve } from './enums.js'; -interface GenerateKeyPairOptions { +type Subset = T; + +type GenerateKeyPairOptions = { type: AlgorithmName.ECDH | AlgorithmName.ECDSA; curve: NamedCurve; - keyUsages: Array; + keyUsages: Array; isExtractable: boolean; } @@ -11,7 +13,7 @@ export async function generateKeyPair( { type: name, curve: namedCurve, keyUsages, isExtractable }: GenerateKeyPairOptions = { type: AlgorithmName.ECDH, curve: NamedCurve.P256, - keyUsages: [KeyUsageType.DeriveBits, KeyUsageType.DeriveKey], + keyUsages: ['deriveBits', 'deriveKey'], isExtractable: true, } ): Promise { diff --git a/lib/src/nanotdf-crypto/keyAgreement.ts b/lib/src/nanotdf-crypto/keyAgreement.ts index 40be7d42..9f735627 100644 --- a/lib/src/nanotdf-crypto/keyAgreement.ts +++ b/lib/src/nanotdf-crypto/keyAgreement.ts @@ -92,8 +92,6 @@ export async function keyAgreement( } const { - bitLength = 256, - hkdfHash = HashType.Sha256, hkdfInfo = new Uint8Array(), hkdfSalt = new Uint8Array(), keyCipher = CipherType.AesGcm, @@ -107,33 +105,33 @@ export async function keyAgreement( ], } = options; - const derivedBits = await crypto.subtle.deriveBits( + const sharedSecret = await crypto.subtle.deriveBits( { name: AlgorithmName.ECDH, public: publicKey, }, privateKey, - bitLength + 256 ); - const derivedKey = await crypto.subtle.importKey( + const ikm = await crypto.subtle.importKey( KeyFormat.Raw, - derivedBits, + sharedSecret, { name: AlgorithmName.HKDF, }, false, - [KEY_USAGE_DERIVE_KEY] + ['deriveKey'] ); const symmetricKey = await crypto.subtle.deriveKey( { - name: AlgorithmName.HKDF, - hash: hkdfHash, + name: 'HKDF', + hash: 'SHA-256', salt: hkdfSalt, info: hkdfInfo, }, - derivedKey, + ikm, { name: keyCipher, length: keyLength, diff --git a/lib/src/nanotdf/encrypt.ts b/lib/src/nanotdf/encrypt.ts index 055606ab..6b495502 100644 --- a/lib/src/nanotdf/encrypt.ts +++ b/lib/src/nanotdf/encrypt.ts @@ -8,7 +8,7 @@ import getHkdfSalt from './helpers/getHkdfSalt.js'; import { getBitLength as authTagLengthForCipher } from './models/Ciphers.js'; import { TypedArray } from '../tdf/TypedArray.js'; import { GMAC_BINDING_LEN } from './constants.js'; -import { AlgorithmName, KeyFormat, KeyUsageType } from './../nanotdf-crypto/enums.js'; +import { AlgorithmName, KeyFormat } from './../nanotdf-crypto/enums.js'; import { encrypt as cryptoEncrypt, @@ -190,7 +190,7 @@ async function convertECDHToECDSA(key: CryptoKey, curveName: string): Promise { const policyStr = JSON.stringify(policy); - const [ek, clientPublicKey] = await Promise.all([ + const [ek, kasPublic] = await Promise.all([ this.ephemeralKeyPair, pemPublicToCrypto(this.publicKey), ]); - const kek = await keyAgreement(ek.privateKey, clientPublicKey, { + const kek = await keyAgreement(ek.privateKey, kasPublic, { hkdfSalt: new TextEncoder().encode('salt'), hkdfHash: 'SHA-256', }); diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index b0583247..62c882b8 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -674,12 +674,15 @@ async function unwrapKey({ const url = `${keySplitInfo.url}/v2/rewrap`; let ephemeralEncryptionKeysRaw: AnyKeyPair; let ephemeralEncryptionKeys: PemKeyPair; + let algorithm: string; if (wrappingKeyAlgorithm === 'ec:secp256r1') { ephemeralEncryptionKeysRaw = await generateKeyPair(); ephemeralEncryptionKeys = await cryptoService.cryptoToPemPair(ephemeralEncryptionKeysRaw); + algorithm = 'ES256'; } else if (wrappingKeyAlgorithm === 'rsa:2048' || !wrappingKeyAlgorithm) { ephemeralEncryptionKeysRaw = await cryptoService.generateKeyPair(); ephemeralEncryptionKeys = await cryptoService.cryptoToPemPair(ephemeralEncryptionKeysRaw); + algorithm = 'RS256'; } else { throw new ConfigurationError(`Unsupported wrapping key algorithm [${wrappingKeyAlgorithm}]`); } @@ -687,7 +690,7 @@ async function unwrapKey({ const clientPublicKey = ephemeralEncryptionKeys.publicKey; const requestBodyStr = JSON.stringify({ - algorithm: 'RS256', + algorithm, keyAccess: keySplitInfo, policy: manifest.encryptionInformation.policy, clientPublicKey,