diff --git a/src/index.ts b/src/index.ts index e6c8268..12a1d8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ export { FailedToCreateKeyStores, FailedToOpenRecoveryKeyStore, FailedToChangePasswordForKeyStore, + InvalidInputKeyStore, } from './keystore-crypto'; export { generateKyberKeys, encapsulateKyber, decapsulateKyber } from './post-quantum-crypto'; export { encryptSymmetrically, decryptSymmetrically, genSymmetricKey } from './symmetric-crypto'; diff --git a/src/keystore-crypto/core.ts b/src/keystore-crypto/core.ts index 8f3139f..c240865 100644 --- a/src/keystore-crypto/core.ts +++ b/src/keystore-crypto/core.ts @@ -22,11 +22,12 @@ export async function encryptKeystoreContent( salt?: Uint8Array, ): Promise { try { - const aux = UTF8ToUint8(userEmail + type); const publicKey = uint8ArrayToBase64(keys.publicKey); + + const aux = UTF8ToUint8(userEmail + type + publicKey); const secretKeyEncrypted = await encryptSymmetrically(secretKey, keys.secretKey, aux); - const keystoreSalt = salt ? uint8ArrayToBase64(salt) : undefined; + const keystoreSalt = salt ? uint8ArrayToBase64(salt) : undefined; const keystore: EncryptedKeystore = { userEmail, type, @@ -52,7 +53,7 @@ export async function decryptKeystoreContent( encryptedKeystore: EncryptedKeystore, ): Promise { try { - const aux = UTF8ToUint8(encryptedKeystore.userEmail + encryptedKeystore.type); + const aux = UTF8ToUint8(encryptedKeystore.userEmail + encryptedKeystore.type + encryptedKeystore.publicKey); const publicKey = base64ToUint8Array(encryptedKeystore.publicKey); const ciphertext = base64ToUint8Array(encryptedKeystore.privateKeyEncrypted); const secretKey = await decryptSymmetrically(kesytoreOpeningKey, ciphertext, aux); diff --git a/src/keystore-crypto/emailEncryptionKey.ts b/src/keystore-crypto/emailEncryptionKey.ts index 179359d..5a1a0e3 100644 --- a/src/keystore-crypto/emailEncryptionKey.ts +++ b/src/keystore-crypto/emailEncryptionKey.ts @@ -13,6 +13,7 @@ import { FailedToCreateKeyStores, FailedToOpenRecoveryKeyStore, FailedToChangePasswordForKeyStore, + InvalidInputKeyStore, } from './errors'; import { ARGON2ID_SALT_BYTE_LENGTH } from '../constants'; @@ -67,12 +68,13 @@ export async function openEncryptionKeystore( try { const salt = encryptedKeystore.salt ? base64ToUint8Array(encryptedKeystore.salt) : new Uint8Array(); if (encryptedKeystore.type !== KeystoreType.ENCRYPTION || salt.length !== ARGON2ID_SALT_BYTE_LENGTH) { - throw new Error('Input is invalid'); + throw new InvalidInputKeyStore(); } const secretKey = await deriveEncryptionKeystoreKey(password, salt); const keys = await decryptKeystoreContent(secretKey, encryptedKeystore); return keys; } catch (error) { + if (error instanceof InvalidInputKeyStore) throw error; throw new FailedToOpenEncryptionKeyStore(error instanceof Error ? error.message : String(error)); } } @@ -91,12 +93,13 @@ export async function openRecoveryKeystore( ): Promise { try { if (encryptedKeystore.type !== KeystoreType.RECOVERY) { - throw new Error('Input is invalid'); + throw new InvalidInputKeyStore(); } const recoveryKey = await deriveRecoveryKey(recoveryCodes); const keys = await decryptKeystoreContent(recoveryKey, encryptedKeystore); return keys; } catch (error) { + if (error instanceof InvalidInputKeyStore) throw error; throw new FailedToOpenRecoveryKeyStore(error instanceof Error ? error.message : String(error)); } } @@ -129,6 +132,7 @@ export async function changePasswordForEncryptionKeystore( return { newKeystore, keys }; } catch (error) { + if (error instanceof InvalidInputKeyStore) throw error; throw new FailedToChangePasswordForKeyStore(error instanceof Error ? error.message : String(error)); } } diff --git a/src/keystore-crypto/errors.ts b/src/keystore-crypto/errors.ts index e0d955d..367e07d 100644 --- a/src/keystore-crypto/errors.ts +++ b/src/keystore-crypto/errors.ts @@ -6,6 +6,14 @@ export class FailedToCreateKeyStores extends Error { } } +export class InvalidInputKeyStore extends Error { + constructor() { + super('Invalid input'); + + Object.setPrototypeOf(this, InvalidInputKeyStore.prototype); + } +} + export class FailedToOpenEncryptionKeyStore extends Error { constructor(errorMsg?: string) { super('Failed to open encryption keystore: ' + errorMsg); diff --git a/tests/keystore-crypto/emailEncryptionKeys.test.ts b/tests/keystore-crypto/emailEncryptionKeys.test.ts index 80185b5..bff300f 100644 --- a/tests/keystore-crypto/emailEncryptionKeys.test.ts +++ b/tests/keystore-crypto/emailEncryptionKeys.test.ts @@ -8,8 +8,12 @@ import { FailedToCreateKeyStores, FailedToOpenRecoveryKeyStore, FailedToChangePasswordForKeyStore, + InvalidInputKeyStore, } from '../../src/keystore-crypto'; import { XWING_PUBLIC_KEY_LENGTH, XWING_SECRET_KEY_LENGTH } from '../../src/constants'; +import { generateSalt } from '../../src/derive-password'; +import { uint8ArrayToBase64 } from '../../src/utils'; +import { genHybridKeys } from '../../src/hybrid-crypto'; describe('Test keystore create/open functions', async () => { const mockUserEmail = 'mock user email'; @@ -57,8 +61,8 @@ describe('Test keystore create/open functions', async () => { password, ); - await expect(openEncryptionKeystore(recoveryKeystore, password)).rejects.toThrow(FailedToOpenEncryptionKeyStore); - await expect(openRecoveryKeystore(recoveryCodes, encryptionKeystore)).rejects.toThrow(FailedToOpenRecoveryKeyStore); + await expect(openEncryptionKeystore(recoveryKeystore, password)).rejects.toThrow(InvalidInputKeyStore); + await expect(openRecoveryKeystore(recoveryCodes, encryptionKeystore)).rejects.toThrow(InvalidInputKeyStore); }); it('should successfully re-encrypt and open encryption keystore with a new password', async () => { @@ -102,4 +106,41 @@ describe('Test keystore create/open functions', async () => { FailedToChangePasswordForKeyStore, ); }); + + it('should throw an error if salt, email or pk changed', async () => { + const password = 'user password'; + const { encryptionKeystore } = await createEncryptionAndRecoveryKeystores(mockUserEmail, password); + + const wrongSaltKeystore = { ...encryptionKeystore }; + wrongSaltKeystore.salt = uint8ArrayToBase64(generateSalt()); + await expect(openEncryptionKeystore(wrongSaltKeystore, password)).rejects.toThrow(FailedToOpenEncryptionKeyStore); + + const wrongEmailKeystore = { ...encryptionKeystore }; + wrongEmailKeystore.userEmail = 'wrong email'; + await expect(openEncryptionKeystore(wrongEmailKeystore, password)).rejects.toThrow(FailedToOpenEncryptionKeyStore); + + const wrongPublicKeyKeystore = { ...encryptionKeystore }; + const newKeys = genHybridKeys(); + wrongPublicKeyKeystore.publicKey = uint8ArrayToBase64(newKeys.publicKey); + await expect(openEncryptionKeystore(wrongPublicKeyKeystore, password)).rejects.toThrow( + FailedToOpenEncryptionKeyStore, + ); + }); + + it('should throw an error if salt is too short or too long', async () => { + const password = 'user password'; + const { encryptionKeystore } = await createEncryptionAndRecoveryKeystores(mockUserEmail, password); + + const shortSaltKeystore = { ...encryptionKeystore }; + shortSaltKeystore.salt = 'WzEsIDIsIDMsIDQsIDUsIDYsIDcsIDhd'; + + expect(shortSaltKeystore.salt).not.toEqual(encryptionKeystore.salt); + await expect(openEncryptionKeystore(shortSaltKeystore, password)).rejects.toThrow(InvalidInputKeyStore); + + const longSaltKeystore = { ...encryptionKeystore }; + longSaltKeystore.salt = 'WzEsIDIsIDMsIDQsIDUsIDYsIDcsIDgsIDksIDEwLCAxMSwgMTIsIDEzLCAxNCwgMTUsIDE2LCAxN10='; + + expect(longSaltKeystore.salt).not.toEqual(encryptionKeystore.salt); + await expect(openEncryptionKeystore(longSaltKeystore, password)).rejects.toThrow(InvalidInputKeyStore); + }); });