Skip to content

Commit

Permalink
Allow RSA key generation to use dynamic parameters
Browse files Browse the repository at this point in the history
Requires passing the parameters in the request to ensure generation of the correct decryption key.
  • Loading branch information
sisou committed May 22, 2023
1 parent 47315ed commit da83212
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 23 deletions.
8 changes: 8 additions & 0 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -198,6 +204,7 @@ export type MultisigConfig = {
} | {
encryptedSecrets: Uint8Array[],
bScalar: Uint8Array,
keyParams: EncryptionKeyParams,
},
aggregatedCommitment: Uint8Array,
userName?: string,
Expand Down Expand Up @@ -496,6 +503,7 @@ export type ConnectResult = {
keyData: Uint8Array,
algorithm: { name: string, hash: string },
keyUsages: ['encrypt'],
keyParams: EncryptionKeyParams,
},
};

Expand Down
9 changes: 9 additions & 0 deletions src/config/config.local.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number[]>} */
RSA_SUPPORTED_KDF_ITERATIONS: {
'PBKDF2-SHA512': [1024],
},
};
9 changes: 9 additions & 0 deletions src/config/config.mainnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number[]>} */
RSA_SUPPORTED_KDF_ITERATIONS: {
'PBKDF2-SHA512': [1024],
},
};
9 changes: 9 additions & 0 deletions src/config/config.testnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number[]>} */
RSA_SUPPORTED_KDF_ITERATIONS: {
'PBKDF2-SHA512': [1024],
},
};
79 changes: 63 additions & 16 deletions src/lib/Key.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -124,12 +155,15 @@ class Key {
}

/**
* @param {EncryptionKeyParams} keyParams
* @returns {Promise<CryptoKey>}
*/
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(
Expand All @@ -142,12 +176,15 @@ class Key {
}

/**
* @param {EncryptionKeyParams} keyParams
* @returns {Promise<CryptoKey>}
*/
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(
Expand All @@ -160,9 +197,10 @@ class Key {
}

/**
* @param {EncryptionKeyParams} keyParams
* @returns {Promise<RsaKeyPairExport>}
*/
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');
Expand All @@ -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} */
Expand Down Expand Up @@ -216,6 +262,7 @@ class Key {
resolver({
privateKey: new Uint8Array(data.privateKey),
publicKey: new Uint8Array(data.publicKey),
keyParams,
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/KeyStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class KeyStore {
* @param {RsaKeyPairExport} rsaKeyPair
* @returns {Promise<string>}
*/
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;
Expand Down
6 changes: 5 additions & 1 deletion src/request/connect/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 8 additions & 0 deletions types/Keyguard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -55,6 +62,7 @@ type MultisigConfig = {
} | {
encryptedSecrets: Uint8Array[]
bScalar: Uint8Array
keyParams: EncryptionKeyParams
}
aggregatedCommitment: Nimiq.Commitment
userName?: string
Expand Down

0 comments on commit da83212

Please sign in to comment.