From 33be02538a0ceb39507842a90fb752fddf502ce8 Mon Sep 17 00:00:00 2001 From: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:32:34 +0530 Subject: [PATCH] refactor(INJI-449): replace crypto-js with node-forge for encryption/decryption (#1034) * refactor(INJI-449): replace crypo-js with node-forge crypto-js has vulneraribitiles prior to version 4.2.0 for encryption / decryption & 4.x.x version is not compatible with our react native project For this reason we had to move to different library for encryption / decryption Co-authored-by: Sreenadh S <32409698+sree96@users.noreply.github.com> Signed-off-by: Kiruthika Jeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> * fix(INJI-449): secure-keystore warning popup shown on reload of app settings key which was stored in storage was not loaded into settings machine context correctly, which caused the bug - on reload settings related flows was falling back to initial setting. Co-authored-by: Sreenadh S <32409698+sree96@users.noreply.github.com> Signed-off-by: Kiruthika Jeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> * refactor(INJI-449): gitignore automation test results Signed-off-by: Kiruthika Jeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> * refactor(INJI-449): simplify usage of methods in node-forge Signed-off-by: Kiruthika Jeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> --------- Signed-off-by: Kiruthika Jeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com> Co-authored-by: Sreenadh S <32409698+sree96@users.noreply.github.com> --- .gitignore | 9 ++ .talismanrc | 6 +- .../suites/ed255192018/ed25519.ts | 142 +++++++++++++----- machines/app.ts | 8 +- machines/settings.ts | 3 +- package-lock.json | 30 ++-- package.json | 2 +- shared/cryptoutil/cryptoUtil.ts | 48 +++++- shared/cryptoutil/encryptedOutput.ts | 32 ++++ shared/storage.ts | 4 +- shared/vcjs/verifyCredential.ts | 1 + 11 files changed, 225 insertions(+), 60 deletions(-) create mode 100644 shared/cryptoutil/encryptedOutput.ts diff --git a/.gitignore b/.gitignore index 97e64c4d39..310ccdba79 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,12 @@ android/app/debug.keystore .expo dist/ web-build/ + +# automation test results +# test reports generated after running test +injitest/report/ +injitest/testng-report/ +# logs from tests ran +injitest/src/logs/ +# test case class files +injitest/target/ \ No newline at end of file diff --git a/.talismanrc b/.talismanrc index c47d2c90b6..480e2f374f 100644 --- a/.talismanrc +++ b/.talismanrc @@ -4,7 +4,9 @@ fileignoreconfig: - filename: package.json checksum: fdd5905228a1afbfb004c710fd6c61adf073a12840200327c0592b76bea5e7e3 - filename: package-lock.json - checksum: 489ccd69f2deecedb8b2ff9a3a02d74c704dfba01fdfb6179316a9df698c4562 + checksum: 3d98844cbc77fe3721077ea606713cd5adc2f238db1bbc10081141a7e4cd06a9 +- filename: lib/jsonld-signatures/suites/ed255192018/ed25519.ts + checksum: 493b6e31144116cb612c24d98b97d8adcad5609c0a52c865a6847ced0a0ddc3a - filename: components/PasscodeVerify.tsx checksum: 14654c0f038979fcd0d260170a45894a072f81e0767ca9a0e66935d33b5cc703 - filename: i18n.ts @@ -57,7 +59,7 @@ fileignoreconfig: - filename: shared/openId4VCI/Utils.ts checksum: ba3041b2ce380f44f6f52dc2c3df337d857df4494bd3c8727df9bf6fb5734750 - filename: shared/cryptoutil/cryptoUtil.ts - checksum: b785ff3f01ab9530119072c4d38195048bfeee6155c54ea7dd031559acb722f3 + checksum: 350524d0d0d18993903b056a1d0a396ec2b2566b6531fd83bd7cafce06d1c332 - filename: machines/store.typegen.ts checksum: 6d22bc5c77398316b943c512c208ce0846a9fff674c1ccac79e07f21962acd5f - filename: machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.typegen.ts diff --git a/lib/jsonld-signatures/suites/ed255192018/ed25519.ts b/lib/jsonld-signatures/suites/ed255192018/ed25519.ts index 8f1f3273a0..d891809ac1 100644 --- a/lib/jsonld-signatures/suites/ed255192018/ed25519.ts +++ b/lib/jsonld-signatures/suites/ed255192018/ed25519.ts @@ -1,23 +1,31 @@ /*! * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. */ -import { Buffer } from 'buffer'; +import {Buffer} from 'buffer'; -// FIXME: Some methods is missing from crypto-js. -import { - sign, - verify, - createPrivateKey, - createPublicKey, - randomBytes, -} from 'crypto-js'; +import forge, {pki, asn1, util, random, md} from 'node-forge'; + +type PrivateKey = forge.pki.PrivateKey; +type PublicKey = forge.pki.PublicKey; + +const { + publicKeyToAsn1, + publicKeyFromAsn1, + privateKeyToAsn1, + privateKeyFromAsn1, + ed25519, +} = pki; +const {toDer, fromDer: forgeFromDer} = asn1; +const {createBuffer} = util; +const {getBytesSync: getRandomBytes} = random; +const {sha256} = md; // used to export node's public keys to buffers -const publicKeyEncoding = { format: 'der', type: 'spki' }; +const publicKeyEncoding = {format: 'der', type: 'spki'}; // used to turn private key bytes into a buffer in DER format const DER_PRIVATE_KEY_PREFIX = Buffer.from( '302e020100300506032b657004220420', - 'hex' + 'hex', ); // used to turn public key bytes into a buffer in DER format const DER_PUBLIC_KEY_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); @@ -31,15 +39,18 @@ const api = { * @returns {object} The object with the public and private key material. */ async generateKeyPairFromSeed(seedBytes) { - const privateKey = await createPrivateKey({ - // node is more than happy to create a new private key using a DER - key: _privateKeyDerEncode({ seedBytes }), - format: 'der', - type: 'pkcs8', - }); + const privateKey: PrivateKey = forgePrivateKey( + _privateKeyDerEncode({seedBytes}), + ); + // this expects either a PEM encoded key or a node privateKeyObject - const publicKey = await createPublicKey(privateKey); - const publicKeyBuffer = publicKey.export(publicKeyEncoding); + const publicKey: PublicKey = + createForgePublicKeyFromPrivateKeyBuffer(privateKey); + const publicKeyBuffer: Buffer = Buffer.from( + toDer(publicKeyToAsn1(publicKey)).getBytes(), + 'binary', + ); + const publicKeyBytes = getKeyMaterial(publicKeyBuffer); return { publicKey: publicKeyBytes, @@ -52,25 +63,88 @@ const api = { return api.generateKeyPairFromSeed(seed); }, async sign(privateKeyBytes, data) { - const privateKey = await createPrivateKey({ - key: _privateKeyDerEncode({ privateKeyBytes }), - format: 'der', - type: 'pkcs8', - }); - return sign(null, data, privateKey); + const privateKey: PrivateKey = forgePrivateKey( + _privateKeyDerEncode({privateKeyBytes}), + ); + const signature: string = forgeSign(data, privateKey); + + return signature; }, - async verify(publicKeyBytes, data, signature) { - const publicKey = await createPublicKey({ - key: _publicKeyDerEncode({ publicKeyBytes }), - format: 'der', - type: 'spki', - }); - return verify(null, data, publicKey, signature); + async verify(publicKeyBytes: Uint8Array, data: string, signature: string) { + const publicKey = await createForgePublicKeyFromPublicKeyBuffer( + _publicKeyDerEncode({publicKeyBytes}), + ); + return forgeVerifyEd25519(data, publicKey, signature); }, }; export default api; +function forgePrivateKey(privateKeyBuffer: Buffer): PrivateKey { + return privateKeyFromAsn1(fromDer(privateKeyBuffer)); +} + +function fromDer(keyBuffer: Buffer) { + return forgeFromDer(keyBuffer.toString('binary')); +} + +function createForgePublicKeyFromPrivateKeyBuffer( + privateKeyObject: PrivateKey, +): PublicKey { + const privateKeyBuffer = privateKeyToBuffer(privateKeyObject); + const publicKey = ed25519.publicKeyFromPrivateKey({ + privateKey: privateKeyBuffer, + }); + return publicKey; +} + +function createForgePublicKeyFromPublicKeyBuffer( + publicKeyBuffer: Buffer, +): string { + const publicKeyObject = publicKeyFromAsn1(fromDer(publicKeyBuffer)); + const publicKeyDer = toDer(publicKeyToAsn1(publicKeyObject)).getBytes(); + + return publicKeyDer; +} + +function forgeSign(data: string, privateKeyObject: PrivateKey): string { + const privateKeyBytes = toDer(privateKeyToAsn1(privateKeyObject)).getBytes(); + + const privateKey = createBuffer(privateKeyBytes); + + const signature = ed25519.sign({ + privateKey, + md: sha256.create(), + message: data, + }); + + return signature.toString('binary'); +} + +function forgeVerifyEd25519( + data: string, + publicKey: string, + signature: string, +): boolean { + return ed25519.verify({ + publicKey: publicKey, + signature: createBuffer(signature), + message: createBuffer(data), + }); +} + +function randomBytes(length: number) { + return Buffer.from(getRandomBytes(length), 'binary'); +} + +function privateKeyToBuffer(privateKey: PrivateKey): Buffer { + const privateKeyAsn1 = privateKeyToAsn1(privateKey); + const privateKeyDer = toDer(privateKeyAsn1).getBytes(); + + const privateKeyBuffer = Buffer.from(privateKeyDer, 'binary'); + + return privateKeyBuffer; +} /** * The key material is the part of the buffer after the DER Prefix. * @@ -103,7 +177,7 @@ function getKeyMaterial(buffer) { * * @returns {Buffer} DER private key prefix + key bytes. */ -export function _privateKeyDerEncode({ privateKeyBytes, seedBytes }: any) { +export function _privateKeyDerEncode({privateKeyBytes, seedBytes}: any) { if (!(privateKeyBytes || seedBytes)) { throw new TypeError('`privateKeyBytes` or `seedBytes` is required.'); } @@ -141,7 +215,7 @@ export function _privateKeyDerEncode({ privateKeyBytes, seedBytes }: any) { * * @returns {Buffer} DER Public key Prefix + key bytes. */ -export function _publicKeyDerEncode({ publicKeyBytes }) { +export function _publicKeyDerEncode({publicKeyBytes}) { if (!(publicKeyBytes instanceof Uint8Array && publicKeyBytes.length === 32)) { throw new TypeError('`publicKeyBytes` must be a 32 byte Buffer.'); } diff --git a/machines/app.ts b/machines/app.ts index 69833e287c..6587e14115 100644 --- a/machines/app.ts +++ b/machines/app.ts @@ -319,17 +319,17 @@ export const appMachine = model.createMachine( loadCredentialRegistryInConstants: (_context, event) => { changeCrendetialRegistry( - !event.response?.credentialRegistry + !event.response?.encryptedData?.credentialRegistry ? MIMOTO_BASE_URL - : event.response?.credentialRegistry, + : event.response?.encryptedData?.credentialRegistry, ); }, loadEsignetHostFromConstants: (_context, event) => { changeEsignetUrl( - !event.response?.esignetHostUrl + !event.response?.encryptedData?.esignetHostUrl ? ESIGNET_BASE_URL - : event.response?.esignetHostUrl, + : event.response?.encryptedData?.esignetHostUrl, ); }, }, diff --git a/machines/settings.ts b/machines/settings.ts index 2a9feee326..347736a085 100644 --- a/machines/settings.ts +++ b/machines/settings.ts @@ -195,7 +195,8 @@ export const settingsMachine = model.createMachine( __AppId.setValue(newContext.appId); return { ...context, - ...newContext, + ...newContext.encryptedData, + appId: newContext.appId, }; }), diff --git a/package-lock.json b/package-lock.json index 4d929928b0..8ba504c4cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "@xstate/react": "^3.0.1", "base64url-universal": "^1.1.0", "buffer": "^6.0.3", - "crypto-js": "^3.3.0", "date-fns": "^2.26.0", "expo": "~48.0.18", "expo-app-loading": "~1.3.0", @@ -91,6 +90,7 @@ "@react-native-community/eslint-config": "^3.2.0", "@react-navigation/devtools": "^6.0.19", "@tsconfig/react-native": "^2.0.2", + "@types/node-forge": "^1.3.9", "@types/react": "^18.0.24", "@types/react-native": "~0.64.12", "@typescript-eslint/eslint-plugin": "^5.17.0", @@ -9256,6 +9256,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==" }, + "node_modules/@types/node-forge": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.9.tgz", + "integrity": "sha512-meK88cx/sTalPSLSoCzkiUB4VPIFHmxtXm5FaaqRDqBX2i/Sy8bJ4odsan0b20RBjPh06dAQ+OTTdnyQyhJZyQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -12038,11 +12047,6 @@ "node": "*" } }, - "node_modules/crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" - }, "node_modules/crypto-ld": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/crypto-ld/-/crypto-ld-5.1.0.tgz", @@ -37269,6 +37273,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==" }, + "@types/node-forge": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.9.tgz", + "integrity": "sha512-meK88cx/sTalPSLSoCzkiUB4VPIFHmxtXm5FaaqRDqBX2i/Sy8bJ4odsan0b20RBjPh06dAQ+OTTdnyQyhJZyQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -39390,11 +39403,6 @@ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" }, - "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" - }, "crypto-ld": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/crypto-ld/-/crypto-ld-5.1.0.tgz", diff --git a/package.json b/package.json index 521a419e4c..11084d8c3c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@xstate/react": "^3.0.1", "base64url-universal": "^1.1.0", "buffer": "^6.0.3", - "crypto-js": "^3.3.0", "date-fns": "^2.26.0", "expo": "~48.0.18", "expo-app-loading": "~1.3.0", @@ -93,6 +92,7 @@ "@react-native-community/eslint-config": "^3.2.0", "@react-navigation/devtools": "^6.0.19", "@tsconfig/react-native": "^2.0.2", + "@types/node-forge": "^1.3.9", "@types/react": "^18.0.24", "@types/react-native": "~0.64.12", "@typescript-eslint/eslint-plugin": "^5.17.0", diff --git a/shared/cryptoutil/cryptoUtil.ts b/shared/cryptoutil/cryptoUtil.ts index 72aa932aed..9675fc778e 100644 --- a/shared/cryptoutil/cryptoUtil.ts +++ b/shared/cryptoutil/cryptoUtil.ts @@ -2,8 +2,8 @@ import {KeyPair, RSA} from 'react-native-rsa-native'; import forge from 'node-forge'; import {BIOMETRIC_CANCELLED, DEBUG_MODE_ENABLED, isIOS} from '../constants'; import SecureKeystore from 'react-native-secure-keystore'; -import CryptoJS from 'crypto-js'; import {BiometricCancellationError} from '../error/BiometricCancellationError'; +import {EncryptedOutput} from './encryptedOutput'; // 5min export const AUTH_TIMEOUT = 5 * 60; @@ -110,7 +110,7 @@ export async function encryptJson( } if (!isHardwareKeystoreExists) { - return CryptoJS.AES.encrypt(data, encryptionKey).toString(); + return encryptWithForge(data, encryptionKey).toString(); } return await SecureKeystore.encryptData(ENCRYPTION_ID, data); } catch (error) { @@ -137,9 +137,7 @@ export async function decryptJson( } if (!isHardwareKeystoreExists) { - return CryptoJS.AES.decrypt(encryptedData, encryptionKey).toString( - CryptoJS.enc.Utf8, - ); + return decryptWithForge(encryptedData, encryptionKey); } return await SecureKeystore.decryptData(ENCRYPTION_ID, encryptedData); @@ -152,3 +150,43 @@ export async function decryptJson( throw e; } } + +function encryptWithForge(text: string, key: string): EncryptedOutput { + //iv - initialization vector + const iv = forge.random.getBytesSync(16); + const salt = forge.random.getBytesSync(128); + const encryptionKey = forge.pkcs5.pbkdf2(key, salt, 4, 16); + const cipher = forge.cipher.createCipher('AES-CBC', encryptionKey); + cipher.start({iv: iv}); + cipher.update(forge.util.createBuffer(text, 'utf8')); + cipher.finish(); + var cipherText = forge.util.encode64(cipher.output.getBytes()); + const encryptedData = new EncryptedOutput( + cipherText, + forge.util.encode64(iv), + forge.util.encode64(salt), + ); + return encryptedData; +} + +function decryptWithForge(encryptedData: string, key: string): string { + const encryptedOutput = EncryptedOutput.fromString(encryptedData); + const salt = forge.util.decode64(encryptedOutput.salt); + const encryptionKey = forge.pkcs5.pbkdf2(key, salt, 4, 16); + const decipher = forge.cipher.createDecipher('AES-CBC', encryptionKey); + decipher.start({iv: forge.util.decode64(encryptedOutput.iv)}); + decipher.update( + forge.util.createBuffer(forge.util.decode64(encryptedOutput.encryptedData)), + ); + decipher.finish(); + const decryptedData = decipher.output.toString(); + return decryptedData; +} + +export function hmacSHA(encryptionKey: string, data: string) { + const hmac = forge.hmac.create(); + hmac.start('sha256', encryptionKey); + hmac.update(data); + const resultBytes = hmac.digest().getBytes().toString(); + return resultBytes; +} diff --git a/shared/cryptoutil/encryptedOutput.ts b/shared/cryptoutil/encryptedOutput.ts new file mode 100644 index 0000000000..d0b6f9ab5d --- /dev/null +++ b/shared/cryptoutil/encryptedOutput.ts @@ -0,0 +1,32 @@ +export class EncryptedOutput { + encryptedData: string; + iv: string; + salt: string; + + static ENCRYPTION_DELIMITER = '_'; + + constructor(encryptedData: string, iv: string, salt: string) { + this.encryptedData = encryptedData; + this.iv = iv; + this.salt = salt; + } + + static fromString(encryptedOutput: string): EncryptedOutput { + const split = encryptedOutput.split(EncryptedOutput.ENCRYPTION_DELIMITER); + const iv = split[0]; + const salt = split[1]; + const encryptedData = split[2]; + + return new EncryptedOutput(encryptedData, iv, salt); + } + + toString(): string { + return ( + this.iv + + EncryptedOutput.ENCRYPTION_DELIMITER + + this.salt + + EncryptedOutput.ENCRYPTION_DELIMITER + + this.encryptedData + ); + } +} diff --git a/shared/storage.ts b/shared/storage.ts index 293fadca7b..363c5db150 100644 --- a/shared/storage.ts +++ b/shared/storage.ts @@ -1,5 +1,4 @@ import {MMKVLoader} from 'react-native-mmkv-storage'; -import CryptoJS from 'crypto-js'; import getAllConfigurations from './commonprops/commonProps'; import {Platform} from 'react-native'; import { @@ -11,6 +10,7 @@ import { decryptJson, encryptJson, HMAC_ALIAS, + hmacSHA, isHardwareKeystoreExists, } from './cryptoutil/cryptoUtil'; import {VCMetadata} from './VCMetadata'; @@ -39,7 +39,7 @@ async function generateHmac( data: string, ): Promise { if (!isHardwareKeystoreExists) { - return CryptoJS.HmacSHA256(encryptionKey, data).toString(); + return hmacSHA(encryptionKey, data); } return await SecureKeystore.generateHmacSha(HMAC_ALIAS, data); } diff --git a/shared/vcjs/verifyCredential.ts b/shared/vcjs/verifyCredential.ts index fe8b726d49..788aa6896c 100644 --- a/shared/vcjs/verifyCredential.ts +++ b/shared/vcjs/verifyCredential.ts @@ -8,6 +8,7 @@ import {VerifiableCredential} from '../../types/VC/ExistingMosipVC/vc'; import {Credential} from '../../types/VC/EsignetMosipVC/vc'; // FIXME: Ed25519Signature2018 not fully supported yet. +// Ed25519Signature2018 proof type check is not tested with its real credential const ProofType = { ED25519: 'Ed25519Signature2018', RSA: 'RsaSignature2018',