Implement react-native AES-GCM-256 decryption (#771)

This commit is contained in:
Leendert de Borst
2025-04-11 15:24:02 +02:00
parent cfcce0ec3e
commit baf1f24379
5 changed files with 54 additions and 51 deletions

View File

@@ -53,11 +53,14 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
// Attempt to decrypt the blob.
console.log('attempt to decrypt vault');
const decryptedBlob = await EncryptionUtility.symmetricDecrypt(
vaultResponse.vault.blob,
derivedKey
);
console.log('decrypted blob', decryptedBlob);
// Initialize the SQLite client.
const client = new SqliteClient();
await client.initializeFromBase64(decryptedBlob);

View File

@@ -1553,6 +1553,8 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-aes-gcm-crypto (0.2.2):
- React-Core
- react-native-get-random-values (1.11.0):
- React-Core
- react-native-safe-area-context (4.12.0):
@@ -2157,6 +2159,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-aes-gcm-crypto (from `../node_modules/react-native-aes-gcm-crypto`)
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-webview (from `../node_modules/react-native-webview`)
@@ -2320,6 +2323,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-aes-gcm-crypto:
:path: "../node_modules/react-native-aes-gcm-crypto"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-safe-area-context:
@@ -2455,6 +2460,7 @@ SPEC CHECKSUMS:
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de
React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead
react-native-aes-gcm-crypto: d572dd7a69f31c539bb8309b3a829bfa3bfad244
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-safe-area-context: cd916088cac5300c3266876218377518987b995e
react-native-webview: 6b9fc65c1951203a3e958ff3cc0a858d4b6be901

View File

@@ -29,6 +29,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.9",
"react-native-aes-gcm-crypto": "^0.2.2",
"react-native-argon2": "^2.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
@@ -11883,6 +11884,16 @@
}
}
},
"node_modules/react-native-aes-gcm-crypto": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-native-aes-gcm-crypto/-/react-native-aes-gcm-crypto-0.2.2.tgz",
"integrity": "sha512-vUwkh2zBiIQMRY191IfZhDmhHVT+nV4sxet/A0V8J35lVShCA4kuFzBL+QVB06RM2EF8oZSnNMt/uvFkKAx6QQ==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-argon2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-native-argon2/-/react-native-argon2-2.0.1.tgz",

View File

@@ -36,6 +36,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.9",
"react-native-aes-gcm-crypto": "^0.2.2",
"react-native-argon2": "^2.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",

View File

@@ -1,4 +1,5 @@
import argon2 from 'react-native-argon2';
import AesGcmCrypto from 'react-native-aes-gcm-crypto';
import { Email } from './types/webapi/Email';
import { EncryptionKey } from './types/EncryptionKey';
import { MailboxEmail } from './types/webapi/MailboxEmail';
@@ -61,36 +62,18 @@ class EncryptionUtility {
return plaintext;
}
const key = await crypto.subtle.importKey(
"raw",
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
{
name: "AES-GCM",
length: 256,
},
false,
["encrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const encoded = encoder.encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
encoded
);
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
return btoa(
Array.from(combined)
.map(byte => String.fromCharCode(byte))
.join('')
);
try {
const result = await AesGcmCrypto.encrypt(plaintext, false, base64Key);
// Combine IV, tag, and content into a single string for storage
return JSON.stringify({
iv: result.iv,
tag: result.tag,
content: result.content
});
} catch (error) {
console.error('AES-GCM encryption failed:', error);
throw error;
}
}
/**
@@ -101,29 +84,28 @@ class EncryptionUtility {
return base64Ciphertext;
}
const key = await crypto.subtle.importKey(
"raw",
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
{
name: "AES-GCM",
length: 256,
},
false,
["decrypt"]
);
try {
const ciphertext = Uint8Array.from(atob(base64Ciphertext), c => c.charCodeAt(0));
const iv = ciphertext.slice(0, 12);
const tag = ciphertext.slice(-16);
const content = ciphertext.slice(12, -16);
const ivAndCiphertext = Uint8Array.from(atob(base64Ciphertext), c => c.charCodeAt(0));
const iv = ivAndCiphertext.slice(0, 12);
const ciphertext = ivAndCiphertext.slice(12);
const contentBase64 = Buffer.from(content).toString('base64');
const ivHex = Buffer.from(iv).toString('hex');
const tagHex = Buffer.from(tag).toString('hex');
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
key,
ciphertext
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
const decryptedData = await AesGcmCrypto.decrypt(
contentBase64,
base64Key,
ivHex,
tagHex,
false
);
return decryptedData;
} catch (error) {
console.error('AES-GCM decryption failed:', error);
throw error;
}
}
/**