From 0482b2d573d4ae934bfe2f7cba2446e093a4cbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 5 Dec 2022 11:26:34 +0100 Subject: [PATCH] Allow RSA key generation to use dynamic parameters Requires passing the parameters in the request to ensure generation of the correct decryption key. --- client/src/PublicRequest.ts | 8 ++ src/config/config.local.js | 9 +++ src/config/config.mainnet.js | 9 +++ src/config/config.testnet.js | 9 +++ src/lib/Key.js | 79 +++++++++++++++---- src/lib/KeyStore.js | 2 +- src/request/connect/Connect.js | 6 +- .../SignMultisigTransaction.js | 2 +- .../SignMultisigTransactionApi.js | 43 +++++++++- types/Keyguard.d.ts | 8 ++ 10 files changed, 152 insertions(+), 23 deletions(-) diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index da2a164a0..e87696bf1 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -189,6 +189,12 @@ export type SignTransactionRequest | SignTransactionRequestCheckout | SignTransactionRequestCashlink; +export type EncryptionKeyParams = { + kdf: string, + iterations: number, + keySize: number, +}; + export type MultisigConfig = { publicKeys: Uint8Array[], numberOfSigners: number, @@ -198,6 +204,7 @@ export type MultisigConfig = { } | { encryptedSecrets: Uint8Array[], bScalar: Uint8Array, + keyParams: EncryptionKeyParams, }, aggregatedCommitment: Uint8Array, userName?: string, @@ -496,6 +503,7 @@ export type ConnectResult = { keyData: Uint8Array, algorithm: { name: string, hash: string }, keyUsages: ['encrypt'], + keyParams: EncryptionKeyParams, }, }; diff --git a/src/config/config.local.js b/src/config/config.local.js index 1cc666104..b09104df1 100644 --- a/src/config/config.local.js +++ b/src/config/config.local.js @@ -22,4 +22,13 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', RSA_KEY_BITS: 2048, // Possible values are 1024 (fast, but unsafe), 2048 (good compromise), 4096 (slow, but safe) + RSA_KDF_FUNCTION: 'PBKDF2-SHA512', + RSA_KDF_ITERATIONS: 1024, + + RSA_SUPPORTED_KEY_BITS: [2048], + RSA_SUPPORTED_KDF_FUNCTIONS: ['PBKDF2-SHA512'], + /** @type {Record} */ + RSA_SUPPORTED_KDF_ITERATIONS: { + 'PBKDF2-SHA512': [1024], + }, }; diff --git a/src/config/config.mainnet.js b/src/config/config.mainnet.js index ef97e23f9..02f3fd2b7 100644 --- a/src/config/config.mainnet.js +++ b/src/config/config.mainnet.js @@ -13,4 +13,13 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_HTLC_CONTRACT_ADDRESS: '0xF615bD7EA00C4Cc7F39Faad0895dB5f40891359f', RSA_KEY_BITS: 2048, // Possible values are 1024 (fast, but unsafe), 2048 (good compromise), 4096 (slow, but safe) + RSA_KDF_FUNCTION: 'PBKDF2-SHA512', + RSA_KDF_ITERATIONS: 1024, + + RSA_SUPPORTED_KEY_BITS: [2048], + RSA_SUPPORTED_KDF_FUNCTIONS: ['PBKDF2-SHA512'], + /** @type {Record} */ + RSA_SUPPORTED_KDF_ITERATIONS: { + 'PBKDF2-SHA512': [1024], + }, }; diff --git a/src/config/config.testnet.js b/src/config/config.testnet.js index dec79ed43..35abfc676 100644 --- a/src/config/config.testnet.js +++ b/src/config/config.testnet.js @@ -13,4 +13,13 @@ const CONFIG = { // eslint-disable-line no-unused-vars USDC_HTLC_CONTRACT_ADDRESS: '0x2EB7cd7791b947A25d629219ead941fCd8f364BF', RSA_KEY_BITS: 2048, // Possible values are 1024 (fast, but unsafe), 2048 (good compromise), 4096 (slow, but safe) + RSA_KDF_FUNCTION: 'PBKDF2-SHA512', + RSA_KDF_ITERATIONS: 1024, + + RSA_SUPPORTED_KEY_BITS: [2048], + RSA_SUPPORTED_KDF_FUNCTIONS: ['PBKDF2-SHA512'], + /** @type {Record} */ + RSA_SUPPORTED_KDF_ITERATIONS: { + 'PBKDF2-SHA512': [1024], + }, }; diff --git a/src/lib/Key.js b/src/lib/Key.js index 0333f027f..f38aefe50 100644 --- a/src/lib/Key.js +++ b/src/lib/Key.js @@ -17,6 +17,37 @@ class Key { return Nimiq.Hash.blake2b(input).toBase64(); } + /** + * @returns {EncryptionKeyParams} + */ + static get defaultEncryptionKeyParams() { + return { + kdf: CONFIG.RSA_KDF_FUNCTION, + iterations: CONFIG.RSA_KDF_ITERATIONS, + keySize: CONFIG.RSA_KEY_BITS, + }; + } + + /** + * @param {EncryptionKeyParams} paramsA + * @param {EncryptionKeyParams} paramsB + * @returns {boolean} + */ + static _isEncryptionKeyParamsEqual(paramsA, paramsB) { + return typeof paramsA === 'object' && typeof paramsB === 'object' + && paramsA.kdf === paramsB.kdf + && paramsA.iterations === paramsB.iterations + && paramsA.keySize === paramsB.keySize; + } + + /** + * @param {EncryptionKeyParams} params + * @returns {boolean} + */ + static _isDefaultEncryptionKeyParams(params) { + return Key._isEncryptionKeyParamsEqual(params, Key.defaultEncryptionKeyParams); + } + /** * @param {Nimiq.Entropy|Nimiq.PrivateKey} secret * @param {KeyConfig} [config] @@ -124,12 +155,15 @@ class Key { } /** + * @param {EncryptionKeyParams} keyParams * @returns {Promise} */ - async getRsaPrivateKey() { - if (!this.rsaKeyPair) { - this.rsaKeyPair = await this._computeRsaKeyPair(); - await KeyStore.instance.addRsaKeypair(this.id, this.rsaKeyPair); + async getRsaPrivateKey(keyParams) { + if (!this.rsaKeyPair || !Key._isEncryptionKeyParamsEqual(keyParams, this.rsaKeyPair.keyParams)) { + this.rsaKeyPair = await this._computeRsaKeyPair(keyParams); + if (Key._isDefaultEncryptionKeyParams(keyParams)) { + await KeyStore.instance.setRsaKeypair(this.id, this.rsaKeyPair); + } } return window.crypto.subtle.importKey( @@ -142,12 +176,15 @@ class Key { } /** + * @param {EncryptionKeyParams} keyParams * @returns {Promise} */ - async getRsaPublicKey() { - if (!this.rsaKeyPair) { - this.rsaKeyPair = await this._computeRsaKeyPair(); - await KeyStore.instance.addRsaKeypair(this.id, this.rsaKeyPair); + async getRsaPublicKey(keyParams) { + if (!this.rsaKeyPair || !Key._isEncryptionKeyParamsEqual(keyParams, this.rsaKeyPair.keyParams)) { + this.rsaKeyPair = await this._computeRsaKeyPair(keyParams); + if (Key._isDefaultEncryptionKeyParams(keyParams)) { + await KeyStore.instance.setRsaKeypair(this.id, this.rsaKeyPair); + } } return window.crypto.subtle.importKey( @@ -160,9 +197,10 @@ class Key { } /** + * @param {EncryptionKeyParams} keyParams * @returns {Promise} */ - async _computeRsaKeyPair() { + async _computeRsaKeyPair(keyParams) { const iframe = document.createElement('iframe'); iframe.classList.add('rsa-sandboxed-iframe'); // Styles in common.css hide this class iframe.setAttribute('sandbox', 'allow-scripts'); @@ -177,18 +215,26 @@ class Key { } // Extend 32-byte secret into 1024-byte seed as bytestring - const seed = Nimiq.CryptoUtils.computePBKDF2sha512( - this.secret.serialize(), - this._defaultAddress.serialize(), - 1024, // Iterations - 1024, // Output size (required) - ); + /** @type {Nimiq.SerialBuffer} */ + let seed; + switch (keyParams.kdf) { + case 'PBKDF2-SHA512': + seed = Nimiq.CryptoUtils.computePBKDF2sha512( + this.secret.serialize(), + this._defaultAddress.serialize(), + keyParams.iterations, + 1024, // Output size (required) + ); + break; + default: + throw new Error(`Unsupported KDF function: ${keyParams.kdf}`); + } // Send computation command to iframe iframe.contentWindow.postMessage({ command: 'generateKey', seed: Nimiq.BufferUtils.toAscii(seed), // seed is a bytestring - keySize: CONFIG.RSA_KEY_BITS, + keySize: keyParams.keySize, }, '*'); /** @type {(keyPair: RsaKeyPairExport) => void} */ @@ -216,6 +262,7 @@ class Key { resolver({ privateKey: new Uint8Array(data.privateKey), publicKey: new Uint8Array(data.publicKey), + keyParams, }); } diff --git a/src/lib/KeyStore.js b/src/lib/KeyStore.js index 5bb144829..053b07761 100644 --- a/src/lib/KeyStore.js +++ b/src/lib/KeyStore.js @@ -194,7 +194,7 @@ class KeyStore { * @param {RsaKeyPairExport} rsaKeyPair * @returns {Promise} */ - async addRsaKeypair(id, rsaKeyPair) { + async setRsaKeypair(id, rsaKeyPair) { const record = await this._get(id); if (!record) throw new Error('Key does not exist'); record.rsaKeyPair = rsaKeyPair; diff --git a/src/request/connect/Connect.js b/src/request/connect/Connect.js index 7a7324884..fd5349565 100644 --- a/src/request/connect/Connect.js +++ b/src/request/connect/Connect.js @@ -115,14 +115,18 @@ class Connect { }); } + const rsaPublicCryptoKey = await key.getRsaPublicKey(Key.defaultEncryptionKeyParams); + const keyParams = /** @type {RsaKeyPairExport} */ (key.rsaKeyPair).keyParams; + /** @type {KeyguardRequest.ConnectResult} */ const result = { signatures, encryptionKey: { format: 'spki', - keyData: new Uint8Array(await window.crypto.subtle.exportKey('spki', await key.getRsaPublicKey())), + keyData: new Uint8Array(await window.crypto.subtle.exportKey('spki', rsaPublicCryptoKey)), algorithm: { name: 'RSA-OAEP', hash: 'SHA-256' }, keyUsages: ['encrypt'], + keyParams, }, }; diff --git a/src/request/sign-multisig-transaction/SignMultisigTransaction.js b/src/request/sign-multisig-transaction/SignMultisigTransaction.js index 028ef30f6..50997a525 100644 --- a/src/request/sign-multisig-transaction/SignMultisigTransaction.js +++ b/src/request/sign-multisig-transaction/SignMultisigTransaction.js @@ -246,7 +246,7 @@ class SignMultisigTransaction { aggregatedSecret = request.multisigConfig.secret.aggregatedSecret; } else { // If we only have encrypted secrets, decrypt them and aggregate them with the bScalar - const rsaKey = await key.getRsaPrivateKey(); + const rsaKey = await key.getRsaPrivateKey(request.multisigConfig.secret.keyParams); /** @type {Uint8Array[]} */ let secrets; diff --git a/src/request/sign-multisig-transaction/SignMultisigTransactionApi.js b/src/request/sign-multisig-transaction/SignMultisigTransactionApi.js index 87652f26b..fff8ee62b 100644 --- a/src/request/sign-multisig-transaction/SignMultisigTransactionApi.js +++ b/src/request/sign-multisig-transaction/SignMultisigTransactionApi.js @@ -135,25 +135,60 @@ class SignMultisigTransactionApi extends TopLevelApi { 'Invalid secret.encryptedSecrets: must be an array with at least 2 elements', ); } - const rsaCipherLength = CONFIG.RSA_KEY_BITS / 8; + // Validate encryptedSecrets are Uint8Arrays if (object.secret.encryptedSecrets.some( /** * @param {unknown} array * @returns {boolean} */ - array => !(array instanceof Uint8Array) - || array.length !== rsaCipherLength, + array => !(array instanceof Uint8Array), )) { throw new Errors.InvalidRequestError( - `Invalid secret.encryptedSecrets: must be an array of Uint8Array(${rsaCipherLength})`, + 'Invalid secret.encryptedSecrets: must be an array of Uint8Arrays', ); } + // Validate the RSA key used to encrypt the secrets is a supported size + const rsaKeySize = object.secret.encryptedSecrets[0].length * 8; + if (!CONFIG.RSA_SUPPORTED_KEY_BITS.includes(rsaKeySize)) { + throw new Errors.InvalidRequestError('Invalid secret.encryptedSecrets: invalid RSA key size'); + } + // Validate all encryptedSecrets are the same length + if (object.secret.encryptedSecrets.some( + /** + * @param {Uint8Array} array + * @returns {boolean} + */ + array => array.length * 8 !== rsaKeySize, + )) { + throw new Errors.InvalidRequestError( + 'Invalid secret.encryptedSecrets: encrypted strings must be the same length', + ); + } + // Validate bScalar if (!(object.secret.bScalar instanceof Uint8Array) || object.secret.bScalar.length !== 32) { throw new Errors.InvalidRequestError('Invalid secret.bScalar: must be an Uint8Array(32)'); } + // Validate keyParams + if (!object.secret.keyParams) { + throw new Errors.InvalidRequestError('Missing secret.keyParams'); + } + const keyParams = object.secret.keyParams; + if (!('kdf' in keyParams) || !('iterations' in keyParams) || !('keySize' in keyParams)) { + throw new Errors.InvalidRequestError('Invalid secret.keyParams: missing properties'); + } + if (!CONFIG.RSA_SUPPORTED_KDF_FUNCTIONS.includes(keyParams.kdf)) { + throw new Errors.InvalidRequestError(`Unsupported keyParams KDF function: ${keyParams.kdf}`); + } + if (!CONFIG.RSA_SUPPORTED_KDF_ITERATIONS[keyParams.kdf].includes(keyParams.iterations)) { + throw new Errors.InvalidRequestError(`Unsupported keyParams KDF iterations: ${keyParams.iterations}`); + } + if (keyParams.keySize !== rsaKeySize) { + throw new Errors.InvalidRequestError(`Wrong keyParams key size: ${keyParams.keySize}`); + } secret = { encryptedSecrets: object.secret.encryptedSecrets, bScalar: object.secret.bScalar, + keyParams, }; } else { throw new Errors.InvalidRequestError('Invalid secret format'); diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index d2a5e9e64..439dce3f1 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -32,9 +32,16 @@ type AccountRecord = AccountInfo & { encryptedKeyPair: Uint8Array } +type EncryptionKeyParams = { + kdf: string + iterations: number + keySize: number +} + type RsaKeyPairExport = { privateKey: Uint8Array publicKey: Uint8Array + keyParams: EncryptionKeyParams } type KeyRecord = { @@ -55,6 +62,7 @@ type MultisigConfig = { } | { encryptedSecrets: Uint8Array[] bScalar: Uint8Array + keyParams: EncryptionKeyParams } aggregatedCommitment: Nimiq.Commitment userName?: string