# Share Encryption/Decryption Documentation This document describes how wallet shares are encrypted and decrypted in the RTOS backend system. ## Overview Wallet shares are encrypted using a **hybrid encryption scheme** that combines: - **RSA-OAEP** (asymmetric encryption) for encrypting the AES key - **AES-256-GCM** (symmetric encryption) for encrypting the actual share data This approach provides both security and efficiency: RSA ensures only the holder of the private key can decrypt the AES key, while AES-GCM provides fast encryption of the share data with built-in authentication. ## Encryption Process ### Input Parameters - `plaintext`: The share data to encrypt (typically a JSON string containing the share information) - `publicKeyPEM`: RSA public key in PEM format ### Steps 1. **Generate AES-256 Key** - Generate a random 32-byte (256-bit) AES key - This key is unique for each encryption operation 2. **Generate Initialization Vector (IV)** - Generate a random 12-byte nonce/IV for AES-GCM - The IV ensures that encrypting the same data multiple times produces different ciphertexts 3. **Encrypt Share Data with AES-256-GCM** - Use the AES key and IV to encrypt the plaintext share data - AES-GCM provides both confidentiality and authenticity - Extract the 16-byte authentication tag from the GCM operation 4. **Encrypt AES Key with RSA-OAEP** - Use RSA-OAEP with SHA-256 hash function - Encrypt the AES key using the provided RSA public key - This ensures only the holder of the corresponding private key can decrypt the AES key 5. **Format Encrypted Data** - Create a JSON object with the following fields: - `encryptedAesKey`: Base64-encoded encrypted AES key - `iv`: Hex-encoded 12-byte initialization vector - `authTag`: Hex-encoded 16-byte GCM authentication tag - `encrypted`: Hex-encoded encrypted share data - Return the JSON object as a string ### Encrypted Data Structure ```json { "encryptedAesKey": "<base64-encoded-rsa-encrypted-aes-key>", "iv": "<hex-encoded-12-byte-iv>", "authTag": "<hex-encoded-16-byte-auth-tag>", "encrypted": "<hex-encoded-encrypted-share-data>" } ``` ## Decryption Process ### Input Parameters - `encryptedDataJSON`: JSON string containing the encrypted share data - `privateKeyPEM`: RSA private key in PEM format (corresponding to the public key used for encryption) ### Steps 1. **Parse Encrypted Data** - Parse the JSON string to extract the encrypted share data structure - Handle potential double-encoded JSON (nested JSON strings) - Validate that all required fields are present: - `encryptedAesKey` - `iv` - `authTag` - `encrypted` 2. **Decode Binary Data** - Decode `encryptedAesKey` from Base64 to bytes - Decode `iv` from hex to bytes (must be exactly 12 bytes) - Decode `authTag` from hex to bytes (must be exactly 16 bytes) - Decode `encrypted` from hex to bytes 3. **Decrypt AES Key with RSA-OAEP** - Use RSA-OAEP with SHA-256 hash function - Decrypt the AES key using the provided RSA private key - Validate that the decrypted AES key is exactly 32 bytes 4. **Decrypt Share Data with AES-256-GCM** - Use the decrypted AES key and IV to initialize the AES-GCM decipher - Set the authentication tag for verification - Decrypt the encrypted share data - GCM automatically verifies authenticity during decryption 5. **Return Plaintext** - Convert the decrypted bytes to UTF-8 string - Return the plaintext share data ## Security Features ### Why Hybrid Encryption? - **Efficiency**: RSA encryption is computationally expensive and has size limitations. By encrypting only a small AES key with RSA, we avoid these limitations. - **Security**: The AES key is protected by RSA, ensuring only authorized parties can decrypt shares. - **Authenticity**: AES-GCM provides built-in authentication, detecting any tampering with the encrypted data. ### Key Security Properties 1. **Confidentiality**: Only holders of the private key can decrypt shares 2. **Authenticity**: GCM authentication tag ensures data integrity 3. **Non-repudiation**: Each encryption uses unique random values (AES key, IV) 4. **Forward Secrecy**: Each encryption operation uses a new AES key ## Implementation Details ### Encryption Algorithm Specifications - **RSA Padding**: PKCS#1 OAEP - **OAEP Hash**: SHA-256 - **AES Mode**: GCM (Galois/Counter Mode) - **AES Key Size**: 256 bits (32 bytes) - **IV/Nonce Size**: 12 bytes (96 bits) - **Auth Tag Size**: 16 bytes (128 bits) ### Encoding Formats - **RSA-encrypted AES key**: Base64 - **IV**: Hexadecimal (lowercase) - **Auth Tag**: Hexadecimal (lowercase) - **Encrypted data**: Hexadecimal (lowercase) - **Final output**: JSON string ## Usage in the System ### Share Types The system uses three types of shares, each encrypted with a different public key: 1. **Social Share**: Encrypted with the social public key 2. **Device Share**: Encrypted with the device public key 3. **Recovery Share**: Encrypted with the recovery public key ### Typical Flow 1. **Share Generation** (in TEE enclave): - Generate Shamir secret shares - Serialize each share to JSON - Encrypt each share with its corresponding public key - Return encrypted shares to the backend 2. **Share Storing** (partner): - Retrieve encrypted share - Decrypt share using their respective private key - Result is pure share that needs to be encrypted and stored securly 3. **Using share** (partner): - Encrypt pure share with Sorted public key using desribed method - Send encrypted share with wanted API endpoint In Enclave - Provided Share is decrypted and serialized in json - Social share decrypted and serialized in json ## Examples ### Example: Complete Encryption/Decryption Flow This example demonstrates the complete encryption and decryption process using real data. #### 1. Pure Share (Plaintext Input) This is the unencrypted share data that needs to be encrypted: ```json { "share": { "share": "0xf9fab8da4c726ede2525291b4ecb7db7959e0b918624364643bffd2cb89123e4", "shareIndex": "0x263a98ab39c7bdd9c7b453f50d3a44b262c8d58e9f14a03a6cbbade5a78b8a83" }, "polynomialID": "0465edb4ff30c9f6e234f36103e92840b602d73592f64fb3a4c45ae9223c6c1d" } ``` **Note**: When encrypting, this JSON object is converted to a string (using `JSON.stringify()`). #### 2. RSA Key Pair **Public Key** (used for encryption): ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApFCMrE9cgHMPfj1IvsRj OVTrr+87ekyTzi3ZNoh/lCCtMYPUlxBohzK4OYOLNDQnR3OWrxmuI5V/3+9oEaNl RybBTZacnhyPm92NYOrcd8wDNnxXHlUGbQQ6JhVjYqnqRKQSM1NQQW85ekKLiSxg 98X+p7vFXNOTwYcUkg04wUytyD1V0AbN4Vvr9008Vnsukr5w+hgYibawmf363gy0 056QuBkwQtBw6ChVFsRPEPmnJD/emK7dyGuW384ByVuRXnLCwrZ3XHJDNiYps7oF 6KoYfe8Bb8Xi0kvA049sCvPMDpppFX8Kn2xwqF1ZqRmTteH1d5dfiQiVYGtuDjuS ywIDAQAB -----END PUBLIC KEY----- ``` **Private Key** (used for decryption): ``` -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCkUIysT1yAcw9+ PUi+xGM5VOuv7zt6TJPOLdk2iH+UIK0xg9SXEGiHMrg5g4s0NCdHc5avGa4jlX/f 72gRo2VHJsFNlpyeHI+b3Y1g6tx3zAM2fFceVQZtBDomFWNiqepEpBIzU1BBbzl6 QouJLGD3xf6nu8Vc05PBhxSSDTjBTK3IPVXQBs3hW+v3TTxWey6SvnD6GBiJtrCZ /freDLTTnpC4GTBC0HDoKFUWxE8Q+ackP96Yrt3Ia5bfzgHJW5FecsLCtndcckM2 JimzugXoqhh97wFvxeLSS8DTj2wK88wOmmkVfwqfbHCoXVmpGZO14fV3l1+JCJVg a24OO5LLAgMBAAECggEAQ2uDlO4JQ/EyIbeuogu9tNf5ztoH2xIRsY8RabVPN63C sqbnc0Z5xUOT4JFvbC+cEE7GvLj6QUKF0hIO7vHOzAeEWDwcrimE6UzaRjKH3KW6 icAyFNGJTfDmlRrAiUqzw49YonOkYfzrphUo4NNzRCca6qL6g7CSl11AHP9M0s7l UtHa52NXVZoQVsul0opG/IqRv6Y1x2uLc+aDY/NSAK5UONsHSJ4F+U1Q5EA8RDh0 mcFceHFqNn35+2ET6i6X2qipaZfUYtGMPoFtOFt6Z0M/C2BROTimG7GlKh5o7/2f rD53dwc83VfFW0zGw3dNs8NqmsDP7dHwDaI0O0aDOQKBgQDdOMD3fo47CiegICdV S/My3f4vxwCUTC3MNKgqmuoapDwVycaGQzfdKxC+foWFdZX0Hbr1RuzIiaE8E2gp 8uQujhVFbMq8eIui9JhnUUYw0Ngvyl1jiMeFfkOVu6V0ypBEA5BIyFGIxNGP5jiR tnPu3o+GViouq8UsZ9VIzYXTWQKBgQC+JYaHbHZ/AFY4vpTi+DHNZu2w+ka/FTMl W1DNnbKh1tg9aQ20NkunZjfzMPSxR2CWg/rlgPDYHObhwukVk+z0OeUu+5xmOgZn SY77AeNnxrxlOlJ/Zkrvr9YkQC7md/KM2OaSR+MuHoPoqgS8Xn1wYBANQL5gYLkG 9z3hm6OGwwKBgQCAYZxSxQYjqyv92EsbTB1SCtSlw3ZJmcAGPxTMzORJG8Cm4Fcu ubmkx4ZLFr/ECM5CQjKqf0OQyZZhd92+YnAbuGcNyCoLoGVg51O+ucLjN4AOlAEI b2tixDjSn3hQw/FfFzZXlrECjz4SpYd0wI6dNFnUmxKVzSd7MrLrMiKU2QKBgD+k 79KES2VXB4UWxV7zFDhcK1MBcoyi2+u1FOa90fy5nMdxUlTvl4MAg9mymTR9X6cB pgkLj5HGmsEWLE48pSNZ/YwsS70rztjjligOCb/ClWyMiPB7nLl9cX/Qu+IIY/cg 4owz46acMCrAPbWPKw1iGf1VnmND/KqINv48yg9hAoGBAIkAky1RQh0yUQYfpEBS N5dq3fV3cj9YEHuFs4wPDp4p+rzNylNuVQVJfRIXN/LIPgBeWe04X3zuArkfVJZv LG5c5hagkIfhKIgqJ7d9t94ORWVt8xbCgDDd0omK2hom0ITn0IiH6qt9yZGZy+4b 73Vtw71JTIHraPml8kAlPD3p -----END PRIVATE KEY----- ``` #### 3. Encrypted Share (Output) After encrypting the pure share with the public key, the result is: ```json { "encryptedAesKey": "M4Mh3VOaLDU0rXjL83Ok4gBVVtdOvmNbPuEd4RN/bNMskHX5iLR0zgwQq7CHlLM0lTyCXPovPc06i26OqeS40c1G76ie0UOZyjeJsbvn8IlVxdiXxNHXOhD1RhW/M5FJmXaESjzC5mjQJXXR7t6tdEgIjSrfdqHOCx84hZUmsJDKwhjCZW4VqoDLrDn2w3rQOLlLZygHLPeWU3hOMiw2cX+TIJTe2G2OV07YSTjSa1WlpEKbIdUQrdxT7kCI7XBNsR6nrhuG2i4XzmqfM9GhcPQR94yVpoPcpMqqjOHFU/I4K6/NkYDaQqK4Sp6C2N4Y+gJAEzMNSh4YNn35ofqjmg==", "iv": "08e54f87b4324500d9e9c14e", "authTag": "55c07236a0e590a9fd735e5bd63d84e4", "encrypted": "42416e485875e7411468d8297b50209e51341ee5c77cf15786b2eb63838fdad75a4c9f2303ff98691e8aa1bbf44b901c19bcf3cec8c089fafdd0005e6374dcf3cc4e5f07698131eb2f4bb6ff9a421c749d108fbe19bb5944f2bb20b38e50c2f1aed2034fca127d8236957abd02d272ed291b3b70b3253293c88ed6911b214e57a0d96949e3ab137bd6a8aa4a0d63d23ec3d186fdc326dba2287f7c5ffc5f405667d2bd6adf17dd94c6d750d2baedb660beb00fc551191543cb5bb1b078cffa1ff4189f4dae1cd6092a8b1b716fa150232aeb7eb4354f707a2a2a0fabd378c0daf1f3b1775cb4277d325950a4a52743de2b44ff8d83768d1a6f33bf43" } ``` **Field Breakdown:** - `encryptedAesKey`: Base64-encoded RSA-OAEP encrypted AES-256 key (256 bytes when decoded) - `iv`: Hex-encoded 12-byte initialization vector (`08e54f87b4324500d9e9c14e` = 12 bytes) - `authTag`: Hex-encoded 16-byte GCM authentication tag (`55c07236a0e590a9fd735e5bd63d84e4` = 16 bytes) - `encrypted`: Hex-encoded AES-GCM encrypted share data #### 4. Code Example **Encryption** (TypeScript): ```typescript const plaintextShare = JSON.stringify({ share: { share: "0xf9fab8da4c726ede2525291b4ecb7db7959e0b918624364643bffd2cb89123e4", shareIndex: "0x263a98ab39c7bdd9c7b453f50d3a44b262c8d58e9f14a03a6cbbade5a78b8a83" }, polynomialID: "0465edb4ff30c9f6e234f36103e92840b602d73592f64fb3a4c45ae9223c6c1d" }); const publicKeyPEM = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApFCMrE9cgHMPfj1IvsRj OVTrr+87ekyTzi3ZNoh/lCCtMYPUlxBohzK4OYOLNDQnR3OWrxmuI5V/3+9oEaNl RybBTZacnhyPm92NYOrcd8wDNnxXHlUGbQQ6JhVjYqnqRKQSM1NQQW85ekKLiSxg 98X+p7vFXNOTwYcUkg04wUytyD1V0AbN4Vvr9008Vnsukr5w+hgYibawmf363gy0 056QuBkwQtBw6ChVFsRPEPmnJD/emK7dyGuW384ByVuRXnLCwrZ3XHJDNiYps7oF 6KoYfe8Bb8Xi0kvA049sCvPMDpppFX8Kn2xwqF1ZqRmTteH1d5dfiQiVYGtuDjuS ywIDAQAB -----END PUBLIC KEY-----`; const encryptedShare = ShareEncryption.encryptShareWithPublicKey( plaintextShare, publicKeyPEM ); console.log(encryptedShare); ``` **Decryption** (TypeScript): ```typescript const encryptedShareJSON = `{ "encryptedAesKey": "M4Mh3VOaLDU0rXjL83Ok4gBVVtdOvmNbPuEd4RN/bNMskHX5iLR0zgwQq7CHlLM0lTyCXPovPc06i26OqeS40c1G76ie0UOZyjeJsbvn8IlVxdiXxNHXOhD1RhW/M5FJmXaESjzC5mjQJXXR7t6tdEgIjSrfdqHOCx84hZUmsJDKwhjCZW4VqoDLrDn2w3rQOLlLZygHLPeWU3hOMiw2cX+TIJTe2G2OV07YSTjSa1WlpEKbIdUQrdxT7kCI7XBNsR6nrhuG2i4XzmqfM9GhcPQR94yVpoPcpMqqjOHFU/I4K6/NkYDaQqK4Sp6C2N4Y+gJAEzMNSh4YNn35ofqjmg==", "iv": "08e54f87b4324500d9e9c14e", "authTag": "55c07236a0e590a9fd735e5bd63d84e4", "encrypted": "42416e485875e7411468d8297b50209e51341ee5c77cf15786b2eb63838fdad75a4c9f2303ff98691e8aa1bbf44b901c19bcf3cec8c089fafdd0005e6374dcf3cc4e5f07698131eb2f4bb6ff9a421c749d108fbe19bb5944f2bb20b38e50c2f1aed2034fca127d8236957abd02d272ed291b3b70b3253293c88ed6911b214e57a0d96949e3ab137bd6a8aa4a0d63d23ec3d186fdc326dba2287f7c5ffc5f405667d2bd6adf17dd94c6d750d2baedb660beb00fc551191543cb5bb1b078cffa1ff4189f4dae1cd6092a8b1b716fa150232aeb7eb4354f707a2a2a0fabd378c0daf1f3b1775cb4277d325950a4a52743de2b44ff8d83768d1a6f33bf43" }`; const privateKeyPEM = `-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCkUIysT1yAcw9+ PUi+xGM5VOuv7zt6TJPOLdk2iH+UIK0xg9SXEGiHMrg5g4s0NCdHc5avGa4jlX/f 72gRo2VHJsFNlpyeHI+b3Y1g6tx3zAM2fFceVQZtBDomFWNiqepEpBIzU1BBbzl6 QouJLGD3xf6nu8Vc05PBhxSSDTjBTK3IPVXQBs3hW+v3TTxWey6SvnD6GBiJtrCZ /freDLTTnpC4GTBC0HDoKFUWxE8Q+ackP96Yrt3Ia5bfzgHJW5FecsLCtndcckM2 JimzugXoqhh97wFvxeLSS8DTj2wK88wOmmkVfwqfbHCoXVmpGZO14fV3l1+JCJVg a24OO5LLAgMBAAECggEAQ2uDlO4JQ/EyIbeuogu9tNf5ztoH2xIRsY8RabVPN63C sqbnc0Z5xUOT4JFvbC+cEE7GvLj6QUKF0hIO7vHOzAeEWDwcrimE6UzaRjKH3KW6 icAyFNGJTfDmlRrAiUqzw49YonOkYfzrphUo4NNzRCca6qL6g7CSl11AHP9M0s7l UtHa52NXVZoQVsul0opG/IqRv6Y1x2uLc+aDY/NSAK5UONsHSJ4F+U1Q5EA8RDh0 mcFceHFqNn35+2ET6i6X2qipaZfUYtGMPoFtOFt6Z0M/C2BROTimG7GlKh5o7/2f rD53dwc83VfFW0zGw3dNs8NqmsDP7dHwDaI0O0aDOQKBgQDdOMD3fo47CiegICdV S/My3f4vxwCUTC3MNKgqmuoapDwVycaGQzfdKxC+foWFdZX0Hbr1RuzIiaE8E2gp 8uQujhVFbMq8eIui9JhnUUYw0Ngvyl1jiMeFfkOVu6V0ypBEA5BIyFGIxNGP5jiR tnPu3o+GViouq8UsZ9VIzYXTWQKBgQC+JYaHbHZ/AFY4vpTi+DHNZu2w+ka/FTMl W1DNnbKh1tg9aQ20NkunZjfzMPSxR2CWg/rlgPDYHObhwukVk+z0OeUu+5xmOgZn SY77AeNnxrxlOlJ/Zkrvr9YkQC7md/KM2OaSR+MuHoPoqgS8Xn1wYBANQL5gYLkG 9z3hm6OGwwKBgQCAYZxSxQYjqyv92EsbTB1SCtSlw3ZJmcAGPxTMzORJG8Cm4Fcu ubmkx4ZLFr/ECM5CQjKqf0OQyZZhd92+YnAbuGcNyCoLoGVg51O+ucLjN4AOlAEI b2tixDjSn3hQw/FfFzZXlrECjz4SpYd0wI6dNFnUmxKVzSd7MrLrMiKU2QKBgD+k 79KES2VXB4UWxV7zFDhcK1MBcoyi2+u1FOa90fy5nMdxUlTvl4MAg9mymTR9X6cB pgkLj5HGmsEWLE48pSNZ/YwsS70rztjjligOCb/ClWyMiPB7nLl9cX/Qu+IIY/cg 4owz46acMCrAPbWPKw1iGf1VnmND/KqINv48yg9hAoGBAIkAky1RQh0yUQYfpEBS N5dq3fV3cj9YEHuFs4wPDp4p+rzNylNuVQVJfRIXN/LIPgBeWe04X3zuArkfVJZv LG5c5hagkIfhKIgqJ7d9t94ORWVt8xbCgDDd0omK2hom0ITn0IiH6qt9yZGZy+4b 73Vtw71JTIHraPml8kAlPD3p -----END PRIVATE KEY-----`; const decryptedShare = ShareEncryption.decryptShareWithPrivateKey( encryptedShareJSON, privateKeyPEM ); const shareData = JSON.parse(decryptedShare); console.log(shareData); ``` #### 5. Verification When you decrypt the encrypted share using the private key, you should get back the original pure share: ```json { "share": { "share": "0xf9fab8da4c726ede2525291b4ecb7db7959e0b918624364643bffd2cb89123e4", "shareIndex": "0x263a98ab39c7bdd9c7b453f50d3a44b262c8d58e9f14a03a6cbbade5a78b8a83" }, "polynomialID": "0465edb4ff30c9f6e234f36103e92840b602d73592f64fb3a4c45ae9223c6c1d" } ``` **Important Notes:** - The encryption process generates a new random AES key and IV each time, so encrypting the same share multiple times will produce different encrypted outputs - However, decrypting any of these encrypted outputs with the correct private key will always produce the same original share - The `iv` and `authTag` values are unique for each encryption operation #### 6. Fully working share encryption/decryption script ``` import crypto from 'crypto' /** * Encrypted share data structure matching Go implementation */ interface EncryptedShareData { encryptedAesKey: string // Base64 encoded iv: string // Hex encoded (12 bytes) authTag: string // Hex encoded (16 bytes) encrypted: string // Hex encoded } /** * ShareEncryption service for encrypting/decrypting wallet shares * Matches the Go implementation in enclave/go/encryption.go */ export class ShareEncryption { /** * Encrypt share data using RSA-OAEP + AES-GCM * Matches Go EncryptShareWithPublicKey format * * @param plaintext - Plaintext data to encrypt (e.g., JSON string of share) * @param publicKeyPEM - RSA public key in PEM format * @returns JSON string containing encrypted share data */ static encryptShareWithPublicKey(plaintext: string, publicKeyPEM: string): string { // Parse RSA public key const publicKey = crypto.createPublicKey(publicKeyPEM) // Generate random AES-256 key (32 bytes) const aesKey = crypto.randomBytes(32) // Generate random nonce (12 bytes for GCM) const iv = crypto.randomBytes(12) // Encrypt plaintext with AES-256-GCM const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv) const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) const authTag = cipher.getAuthTag() // Encrypt AES key with RSA-OAEP SHA256 const encryptedAesKey = crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256', }, aesKey ) // Create encrypted share data structure (matching Go format) const encryptedShare: EncryptedShareData = { encryptedAesKey: encryptedAesKey.toString('base64'), iv: iv.toString('hex'), authTag: authTag.toString('hex'), encrypted: encrypted.toString('hex'), } // Convert to JSON string return JSON.stringify(encryptedShare) } /** * Decrypt share data encrypted with RSA-OAEP + AES-GCM * Matches Go DecryptShareWithPrivateKey format * * @param encryptedDataJSON - JSON string containing encrypted share data * @param privateKeyPEM - RSA private key in PEM format * @returns Decrypted plaintext string */ static decryptShareWithPrivateKey(encryptedDataJSON: string, privateKeyPEM: string): string { // Parse encrypted share data let encryptedShare: EncryptedShareData try { encryptedShare = JSON.parse(encryptedDataJSON) } catch (error) { // Try parsing as string first (handle double-encoded JSON) try { const jsonStr = JSON.parse(encryptedDataJSON) encryptedShare = JSON.parse(jsonStr) } catch { throw new Error('Failed to parse encrypted data: invalid JSON format') } } // Validate required fields if ( !encryptedShare.encryptedAesKey || !encryptedShare.iv || !encryptedShare.authTag || !encryptedShare.encrypted ) { throw new Error( 'Invalid encrypted data format: missing required fields (encryptedAesKey, iv, authTag, or encrypted)' ) } // Decode base64 encrypted AES key const encryptedAesKeyBytes = Buffer.from(encryptedShare.encryptedAesKey, 'base64') if (encryptedAesKeyBytes.length === 0) { throw new Error('Invalid encrypted AES key: empty buffer') } // Decode hex nonce const iv = Buffer.from(encryptedShare.iv, 'hex') if (iv.length !== 12) { throw new Error(`Invalid nonce length: expected 12 bytes, got ${iv.length}`) } // Decode hex auth tag const authTag = Buffer.from(encryptedShare.authTag, 'hex') if (authTag.length !== 16) { throw new Error(`Invalid auth tag length: expected 16 bytes, got ${authTag.length}`) } // Decode hex encrypted data const encryptedData = Buffer.from(encryptedShare.encrypted, 'hex') // Parse RSA private key const privateKey = crypto.createPrivateKey(privateKeyPEM) // Decrypt AES key with RSA-OAEP SHA256 let aesKey: Buffer try { aesKey = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256', }, encryptedAesKeyBytes ) } catch (error) { throw new Error( 'RSA OAEP decryption failed: The encrypted data was likely encrypted with a different public key' ) } if (aesKey.length !== 32) { throw new Error(`Invalid decrypted AES key length: expected 32 bytes, got ${aesKey.length}`) } // Decrypt data with AES-256-GCM const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv) decipher.setAuthTag(authTag) const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]) return decrypted.toString('utf8') } } ``` ````