# 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')
}
}
```
````