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>
This commit is contained in:
KiruthikaJeyashankar
2023-11-29 14:32:34 +05:30
committed by GitHub
parent 4dc60ac243
commit 33be02538a
11 changed files with 225 additions and 60 deletions

9
.gitignore vendored
View File

@@ -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/

View File

@@ -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

View File

@@ -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.');
}

View File

@@ -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,
);
},
},

View File

@@ -195,7 +195,8 @@ export const settingsMachine = model.createMachine(
__AppId.setValue(newContext.appId);
return {
...context,
...newContext,
...newContext.encryptedData,
appId: newContext.appId,
};
}),

30
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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
);
}
}

View File

@@ -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<string> {
if (!isHardwareKeystoreExists) {
return CryptoJS.HmacSHA256(encryptionKey, data).toString();
return hmacSHA(encryptionKey, data);
}
return await SecureKeystore.generateHmacSha(HMAC_ALIAS, data);
}

View File

@@ -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',