Documentation Index Fetch the complete documentation index at: https://mintlify.com/iamnasirudeen/key-management/llms.txt
Use this file to discover all available pages before exploring further.
This guide demonstrates the complete encryption and decryption workflow using RSA-2048 asymmetric encryption with PKCS1_OAEP padding.
Overview
The Key Management Service uses asymmetric encryption:
Client-side : Encrypts sensitive data using the public key
Server-side : Decrypts data using the corresponding private key
Algorithm : RSA-2048 with PKCS1_OAEP padding
Encoding : Base64 for transport
Encryption/Decryption Flow
Obtain public key
Generate or retrieve an encryption key pair: mutation {
generateClientEncryptionKey ( input : {
deviceId : "device-12345"
appVersion : "1.0.0"
}) {
publicKey
keyId
}
}
Encrypt data client-side
Use the public key to encrypt sensitive data before transmission: const encryptedData = encryptData ( sensitiveData , publicKey );
Send encrypted data to server
Transmit the encrypted data along with the keyId: mutation {
setUserPasscode ( input : {
encryptedPasscode : encryptedData
keyId : "key_1234567890_abc123"
})
}
Decrypt on server
Server decrypts using the private key associated with keyId.
Client-Side Encryption
Encryption Implementation
Use the RSA public key to encrypt data before sending to the server:
// From: src/lib/common/helpers/crypto.ts:44-60
import * as crypto from 'crypto' ;
/**
* Encrypts data using a public key
* @param data Data to encrypt
* @param publicKey Public key to use for encryption
* @returns Encrypted data as a base64 string
*/
export function encryptData ( data : string , publicKey : string ) : string {
try {
const buffer = Buffer . from ( data , 'utf-8' );
const encrypted = crypto . publicEncrypt (
{
key: publicKey ,
padding: crypto . constants . RSA_PKCS1_OAEP_PADDING ,
},
buffer ,
);
return encrypted . toString ( 'base64' );
} catch ( error ) {
console . error ( 'Encryption error:' , error );
throw new Error ( 'Failed to encrypt data' );
}
}
Usage Example
import * as crypto from 'crypto' ;
// Encrypt sensitive data
const passcode = '123456' ;
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtPF7TYz4UeTwtko2yErl
...
-----END PUBLIC KEY-----` ;
const buffer = Buffer . from ( passcode , 'utf-8' );
const encrypted = crypto . publicEncrypt (
{
key: publicKey ,
padding: crypto . constants . RSA_PKCS1_OAEP_PADDING ,
},
buffer ,
);
const encryptedData = encrypted . toString ( 'base64' );
console . log ( 'Encrypted:' , encryptedData );
Browser Compatibility
For browser environments, use the Web Crypto API:
async function encryptDataBrowser (
data : string ,
publicKeyPem : string
) : Promise < string > {
// Convert PEM to ArrayBuffer
const pemHeader = '-----BEGIN PUBLIC KEY-----' ;
const pemFooter = '-----END PUBLIC KEY-----' ;
const pemContents = publicKeyPem
. replace ( pemHeader , '' )
. replace ( pemFooter , '' )
. replace ( / \s / g , '' );
const binaryDer = atob ( pemContents );
const publicKeyBuffer = new Uint8Array ( binaryDer . length );
for ( let i = 0 ; i < binaryDer . length ; i ++ ) {
publicKeyBuffer [ i ] = binaryDer . charCodeAt ( i );
}
// Import the key
const publicKey = await crypto . subtle . importKey (
'spki' ,
publicKeyBuffer ,
{
name: 'RSA-OAEP' ,
hash: 'SHA-256' ,
},
false ,
[ 'encrypt' ]
);
// Encrypt the data
const encodedData = new TextEncoder (). encode ( data );
const encryptedBuffer = await crypto . subtle . encrypt (
{ name: 'RSA-OAEP' },
publicKey ,
encodedData
);
// Convert to base64
const encryptedArray = new Uint8Array ( encryptedBuffer );
return btoa ( String . fromCharCode ( ... encryptedArray ));
}
Server-Side Decryption
Decryption Implementation
// From: src/lib/common/helpers/crypto.ts:68-84
/**
* Decrypts data using a private key
* @param encryptedData Encrypted data as a base64 string
* @param privateKey Private key to use for decryption
* @returns Decrypted data as a string
*/
export function decryptData ( encryptedData : string , privateKey : string ) : string {
try {
const buffer = Buffer . from ( encryptedData , 'base64' );
const decrypted = crypto . privateDecrypt (
{
key: privateKey ,
padding: crypto . constants . RSA_PKCS1_OAEP_PADDING ,
},
buffer ,
);
return decrypted . toString ( 'utf-8' );
} catch ( error ) {
console . error ( 'Decryption error:' , error );
throw new Error ( 'Failed to decrypt data' );
}
}
Decryption Service
The EncryptionService handles secure decryption with validation:
// From: src/encryption/encryption.service.ts:136-157
/**
* Decrypts data using the private key associated with the given keyId
* Validates encrypted data size and records metrics
*/
async decryptWithPrivateKey (
encryptedData : string ,
keyId : string ,
): Promise < string > {
try {
this . logger . debug ( `Attempting to decrypt data with keyId: ${ keyId } ` );
// Validate encrypted data size
this . validateEncryptedDataSize ( encryptedData );
const privateKey = await this . getPrivateKey ( keyId );
const decryptedData = await decryptData ( encryptedData , privateKey );
this . logger . debug ( `Successfully decrypted data with keyId: ${ keyId } ` );
this . recordDecryptionSuccessMetric ( keyId );
return decryptedData ;
} catch (error) {
this.logger. error ( `Decryption failed: ${ error . message } ` , error.stack);
this.recordDecryptionFailureMetric( keyId , error.message);
throw new Error ( 'Failed to decrypt data' );
}
}
Private Key Retrieval
Private keys are securely retrieved and validated:
// From: src/encryption/encryption.service.ts:104-130
private async getPrivateKey ( keyId : string ): Promise < string > {
try {
const keyEntry = await this . prismaService . clientEncryptionKey . findUnique ({
where: { id: keyId },
});
if ( ! keyEntry ) {
this . logger . warn ( `Private key not found for keyId: ${ keyId } ` );
throw new Error ( 'Encryption key not found' );
}
// Check if key has expired - key rotation enforcement
if ( keyEntry . expiresAt && keyEntry . expiresAt < new Date ()) {
this . logger . warn ( `Key with ID ${ keyId } has expired` );
throw new Error ( 'Encryption key has expired' );
}
return keyEntry . privateKey ;
} catch (error) {
this.logger. error (
`Failed to retrieve private key: ${ error . message } ` ,
error.stack,
);
this.recordDecryptionFailureMetric( keyId , 'key_retrieval_failed' );
throw new Error( 'Failed to retrieve encryption key' );
}
}
Data Size Validation
The service enforces maximum encrypted data size to prevent abuse:
// From: src/encryption/encryption.service.ts:215-222
private validateEncryptedDataSize ( encryptedData : string ): void {
if ( encryptedData . length > this . maxEncryptedDataSize ) {
this . logger . warn (
`Encrypted data exceeds maximum allowed size: ${ encryptedData . length } > ${ this . maxEncryptedDataSize } ` ,
);
throw new Error ( 'Encrypted data size exceeds limit' );
}
}
Default maximum size: 2048 bytes
Configure via environment variable:
MAX_ENCRYPTED_DATA_SIZE = 2048
Testing Encryption (Development Only)
The service provides a development mutation to test encryption like a client:
mutation TestEncryption {
encryptDataLikeClient ( input : {
body : "123456"
publicKey : "-----BEGIN PUBLIC KEY----- \n ... \n -----END PUBLIC KEY-----"
}) {
encryptedData
}
}
Implementation
// From: src/encryption/encryption.resolver.ts:58-63
@ Mutation (() => EncryptionKeyOutput )
async encryptDataLikeClient (
@ Args ( 'input' ) input : EncryptDataPayload ,
): Promise < EncryptDataPayloadOutput > {
return { encryptedData : encryptData ( input . body , input . publicKey ) };
}
Complete Example
import * as crypto from 'crypto' ;
// Step 1: Generate encryption key
const keyResponse = await fetch ( '/graphql' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
query: `
mutation {
generateClientEncryptionKey(input: {
deviceId: "device-12345"
appVersion: "1.0.0"
}) {
publicKey
keyId
}
}
`
})
});
const { data : { generateClientEncryptionKey } } = await keyResponse . json ();
const { publicKey , keyId } = generateClientEncryptionKey ;
// Step 2: Encrypt sensitive data
function encryptData ( data : string , publicKey : string ) : string {
const buffer = Buffer . from ( data , 'utf-8' );
const encrypted = crypto . publicEncrypt (
{
key: publicKey ,
padding: crypto . constants . RSA_PKCS1_OAEP_PADDING ,
},
buffer ,
);
return encrypted . toString ( 'base64' );
}
const passcode = '123456' ;
const encryptedPasscode = encryptData ( passcode , publicKey );
// Step 3: Send encrypted data to server
const setResponse = await fetch ( '/graphql' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
query: `
mutation SetPasscode($input: EncryptedPasscodeInput!) {
setUserPasscode(input: $input)
}
` ,
variables: {
input: {
encryptedPasscode ,
keyId
}
}
})
});
const { data : { setUserPasscode } } = await setResponse . json ();
console . log ( 'Passcode set:' , setUserPasscode ); // true
Security Considerations
RSA-2048 Encryption Uses industry-standard 2048-bit RSA keys with PKCS1_OAEP padding for secure encryption.
Private Keys Never Exposed Private keys remain on the server and are never transmitted to clients.
Key Expiration Keys automatically expire after 30 days, enforcing regular rotation.
Size Validation Encrypted data is validated to prevent oversized payloads (max 2048 bytes).
Error Handling
Common Errors
try {
const decrypted = await decryptWithPrivateKey ( encryptedData , keyId );
} catch ( error ) {
if ( error . message === 'Encryption key not found' ) {
// Key was deleted or never existed
// Action: Generate a new key
} else if ( error . message === 'Encryption key has expired' ) {
// Key has expired
// Action: Rotate the key
} else if ( error . message === 'Encrypted data size exceeds limit' ) {
// Data too large
// Action: Reduce payload size
} else if ( error . message === 'Failed to decrypt data' ) {
// Decryption failed (wrong key, corrupted data, etc.)
// Action: Verify keyId matches encryption key
}
}
Client-Side Error Handling
function encryptWithErrorHandling ( data : string , publicKey : string ) : string | null {
try {
if ( data . length > 200 ) {
throw new Error ( 'Data too large for RSA encryption' );
}
const buffer = Buffer . from ( data , 'utf-8' );
const encrypted = crypto . publicEncrypt (
{
key: publicKey ,
padding: crypto . constants . RSA_PKCS1_OAEP_PADDING ,
},
buffer ,
);
return encrypted . toString ( 'base64' );
} catch ( error ) {
console . error ( 'Encryption failed:' , error );
return null ;
}
}
RSA Data Size Limits
RSA-2048 can encrypt a maximum of 214 bytes of plaintext data with PKCS1_OAEP padding:
Key size : 2048 bits (256 bytes)
PKCS1_OAEP overhead : ~42 bytes
Maximum plaintext : ~214 bytes
For larger data:
Use hybrid encryption (RSA + AES)
Split data into smaller chunks
Hash data and encrypt the hash
Encryption Speed
Client-side encryption : ~1-5ms per operation
Server-side decryption : ~2-10ms per operation
Both operations are fast enough for real-time user authentication.
Monitoring
All decryption operations are automatically logged for monitoring:
// From: src/encryption/encryption.service.ts:282-290
private recordDecryptionSuccessMetric ( keyId : string ): void {
this . logger . log ( `METRIC: decryption_success keyId= ${ keyId } ` );
}
private recordDecryptionFailureMetric ( keyId : string , reason : string ): void {
this . logger . log (
`METRIC: decryption_failure keyId= ${ keyId } reason= ${ reason } ` ,
);
}
See the Monitoring Guide for details on tracking encryption metrics.
Next Steps
Authentication Implement secure passcode and PIN authentication
Monitoring Track encryption metrics and monitor system health