Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,33 @@ expect(decryptedEmail).toStrictEqual(email);

// keystore
const userEmail = 'user email';
const secretKey = genSymmetricKey();
const { encryptionKeystore, recoveryKeystore, recoveryCodes } = await createEncryptionAndRecoveryKeystores(
const password = 'user password';
const { encryptionKeystore, recoveryKeystore, recoveryCodes, salt } = await createEncryptionAndRecoveryKeystores(
userEmail,
secretKey,
password
);
const resultEnc = await openEncryptionKeystore(encryptionKeystore, secretKey);
const resultEnc = await openEncryptionKeystore(encryptionKeystore, password, salt);
const resultRec = await openRecoveryKeystore(recoveryCodes, recoveryKeystore);

expect(resultEnc).toStrictEqual(resultRec);

// Email storage and search

// Between sessions emails are stored encrypted in IndexedDB. The encryption key is derived from user's baseKey
// Between sessions emails are stored encrypted in IndexedDB. The encryption key is derived from user's mnemonic
// During the session, all emails are decrypted and stored in the cache (up to 600 MB, if excides - we delete oldests emails)
// For search, we build a search index from cache, then use Flexsearch for the search.
// The search is doen separately for email content, subject, sender and recivers.

// Open IndexedDB database
const userID = 'user ID';
const db = await openDatabase(userID);
const mnemonic = genMnemonic();

// Derive database key
const key = await deriveDatabaseKey(baseKey);
// Derive key for encrypting emails before storing them in a local database
const key = await deriveDatabaseKey(mnemonic);

// Derive key for encrypting email draft
const key = await deriveDatabaseKey(mnemonic);

// Encrypt and store one or several emails
await encryptAndStoreEmail(email, key, db);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "internxt-crypto",
"version": "1.1.1",
"version": "1.2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.js",
Expand Down
5 changes: 3 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ export const IV_LEN_BYTES = 12;

export const CONTEXT_ENC_KEYSTORE = 'CRYPTO library 2025-07-30 16:18:03 key for opening encryption keys keystore';
export const CONTEXT_RECOVERY = 'CRYPTO library 2025-07-30 16:20:00 key for account recovery';
export const CONTEXT_INDEX = 'CRYPTO library 2025-07-30 17:20:00 key for protecting current search indices';
export const CONTEXT_DERIVE = 'CRYPTO library 2025-08-27 17:08:00 derive one key from two keys';
export const CONTEXT_DATABASE = 'CRYPTO library 2025-07-30 17:20:00 key for protecting current search indices';
export const CONTEXT_DRAFT = 'CRYPTO library 2026-04-23 10:56:00 key for protecting email drafts';
export const CONTEXT_TWO_KEYS = 'CRYPTO library 2025-08-27 17:08:00 derive one key from two keys';

// Second recommended parameter set from RFC 9106
export const ARGON2ID_PARALLELISM = 3;
Expand Down
14 changes: 2 additions & 12 deletions src/derive-key/deriveKeysFromKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { blake3 } from '@noble/hashes/blake3.js';
import { AES_KEY_BYTE_LENGTH, CONTEXT_DERIVE, CONTEXT_INDEX } from '../constants';
import { AES_KEY_BYTE_LENGTH, CONTEXT_TWO_KEYS } from '../constants';
import { UTF8ToUint8 } from '../utils';

/**
Expand Down Expand Up @@ -27,18 +27,8 @@ export function deriveSymmetricKeyFromTwoKeys(key1: Uint8Array, key2: Uint8Array
throw new Error(`Input key length must be exactly ${AES_KEY_BYTE_LENGTH} bytes`);
}
const key = blake3(key1, { key: key2 });
return deriveSymmetricKeyFromContext(CONTEXT_DERIVE, key);
return deriveSymmetricKeyFromContext(CONTEXT_TWO_KEYS, key);
} catch (error) {
throw new Error('Failed to derive symmetric key from two keys and context', { cause: error });
}
}

/**
* Derives database encryption key for the given user
*
* @param baseKey - The base key (NOT PASSWORD!)
* @returns The symmetric key for protecting database
*/
export const deriveDatabaseKey = async (baseKey: Uint8Array): Promise<Uint8Array> => {
return deriveSymmetricKeyFromContext(CONTEXT_INDEX, baseKey);
};
16 changes: 16 additions & 0 deletions src/derive-key/deriveKeysFromMnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { mnemonicToBytes } from '../utils';
import { deriveSymmetricKeyFromContext } from './deriveKeysFromKey';

/**
* Derives encryption key from the user's mnemonic and context string
*
* @param mnemonic - The user's mnemonic (machine-generated with secure PRNG)
* @param context - The context string.
* The context string should be hardcoded, globally unique, and application-specific.
* @returns The symmetric key for protecting database
*/
export const deriveKeyFromMnemonic = async (mnemonic: string, context: string): Promise<Uint8Array> => {
// mnemonic is always machine-generated with secure PRNG, so it is safe to convert it to bytes without additional processing
const entropy = mnemonicToBytes(mnemonic);
return deriveSymmetricKeyFromContext(context, entropy);
};
1 change: 1 addition & 0 deletions src/derive-key/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './deriveKeysFromKey';
export * from './deriveKeysFromMnemonic';
11 changes: 0 additions & 11 deletions src/derive-password/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import {
ARGON2ID_ITERATIONS,
ARGON2ID_MEMORY_SIZE,
ARGON2ID_PARALLELISM,
ARGON2ID_SALT_BYTE_LENGTH,
ARGON2ID_OUTPUT_BYTE_LENGTH,
} from '../constants';
import { randomBytes } from '@noble/hashes/utils.js';

/**
* Calculates hash using the argon2id password-hashing function
Expand Down Expand Up @@ -37,12 +35,3 @@ export async function argon2(
outputType: 'binary',
});
}

/**
* Samples a salt
*
* @returns The salt
*/
export function sampleSalt(): Uint8Array {
return randomBytes(ARGON2ID_SALT_BYTE_LENGTH);
}
15 changes: 13 additions & 2 deletions src/derive-password/deriveKeysFromPassword.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { argon2, sampleSalt } from './core';
import { argon2 } from './core';
import { ARGON2ID_SALT_BYTE_LENGTH } from '../constants';
import { randomBytes } from '@noble/hashes/utils.js';

/**
* Samples a salt
*
* @returns The salt
*/
export function generateSalt(): Uint8Array {
return randomBytes(ARGON2ID_SALT_BYTE_LENGTH);
}

/**
* Derives a symmetric key from a user's password with a randomly sampled salt
Expand All @@ -8,7 +19,7 @@ import { argon2, sampleSalt } from './core';
*/
export async function getKeyFromPassword(password: string): Promise<{ key: Uint8Array; salt: Uint8Array }> {
try {
const salt = sampleSalt();
const salt = generateSalt();
const key = await argon2(password, salt);
return { key, salt };
} catch (error) {
Expand Down
22 changes: 22 additions & 0 deletions src/email-crypto/emailKeys.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { genHybridKeys } from '../hybrid-crypto';
import { HybridKeyPair } from '../types';
import { deriveKeyFromMnemonic } from '../derive-key';
import { CONTEXT_DATABASE, CONTEXT_DRAFT } from '../constants';

/**
* Generates public and private keys for email encryption.
Expand All @@ -9,3 +11,23 @@ import { HybridKeyPair } from '../types';
export async function generateEmailKeys(): Promise<HybridKeyPair> {
return genHybridKeys();
}

/**
* Derives database encryption key for the given user
*
* @param mnemonic - The user's mnemonic (machine-generated with secure PRNG)
* @returns The symmetric key for protecting database
*/
export const deriveDatabaseKey = async (mnemonic: string): Promise<Uint8Array> => {
return deriveKeyFromMnemonic(mnemonic, CONTEXT_DATABASE);
};

/**
* Derives email draft encryption key for the given user
*
* @param mnemonic - The user's mnemonic (machine-generated with secure PRNG)
* @returns The symmetric key for protecting email drafts
*/
export const deriveEmailDraftKey = async (mnemonic: string): Promise<Uint8Array> => {
return deriveKeyFromMnemonic(mnemonic, CONTEXT_DRAFT);
};
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { deriveSecretKey, generateEccKeys } from './asymmetric-crypto';
export { deriveSymmetricKeyFromTwoKeys, deriveSymmetricKeyFromContext, deriveDatabaseKey } from './derive-key';
export { getKeyFromPassword, getKeyFromPasswordAndSalt } from './derive-password';
export { deriveSymmetricKeyFromTwoKeys, deriveSymmetricKeyFromContext, deriveKeyFromMnemonic } from './derive-key';
export { getKeyFromPassword, getKeyFromPasswordAndSalt, generateSalt } from './derive-password';
export {
encryptEmailHybrid,
encryptEmailHybridForMultipleRecipients,
Expand All @@ -11,6 +11,8 @@ export {
decryptEmailBody,
encryptEmailBody,
encryptEmailBodyWithKey,
deriveDatabaseKey,
deriveEmailDraftKey,
} from './email-crypto';
export {
hashDataArray,
Expand Down
41 changes: 34 additions & 7 deletions src/keystore-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { encryptSymmetrically, decryptSymmetrically } from '../symmetric-crypto';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8, mnemonicToBytes } from '../utils';
import { deriveSymmetricKeyFromContext } from '../derive-key';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8 } from '../utils';
import { deriveKeyFromMnemonic, deriveSymmetricKeyFromContext } from '../derive-key';
import { CONTEXT_ENC_KEYSTORE, CONTEXT_RECOVERY } from '../constants';
import { EncryptedKeystore, HybridKeyPair, KeystoreType } from '../types';
import { getKeyFromPasswordAndSalt, getKeyFromPassword } from '../derive-password';

/**
* Encrypts the user's hybrid key using symmetric encryption to get a keystore
Expand Down Expand Up @@ -64,20 +65,46 @@ export async function decryptKeystoreContent(
/**
* Derives a secret key for protecting the recovery keystore
*
* @param recoveryCodes - The recovery codes
* @param recoveryCodes - The recovery codes (machine-generated with secure PRNG)
* @returns The derived secret key for protecting the recovery keystore
*/
export async function deriveRecoveryKey(recoveryCodes: string): Promise<Uint8Array> {
const recoverCodesArray = mnemonicToBytes(recoveryCodes);
return deriveSymmetricKeyFromContext(CONTEXT_RECOVERY, recoverCodesArray);
return deriveKeyFromMnemonic(recoveryCodes, CONTEXT_RECOVERY);
}

/**
* Derives a secret key for protecting the encryption keystore from user's mnemonic
*
* @param mnemonic - The user's mnemonic (machine-generated with secure PRNG)
* @returns The derived secret key for protecting the encryption keystore
*/
export async function deriveEncryptionKeystoreKeyFromMnemonic(mnemonic: string): Promise<Uint8Array> {
return deriveKeyFromMnemonic(mnemonic, CONTEXT_ENC_KEYSTORE);
}

/**
* Derives a secret key for protecting the encryption keystore
*
* @param baseKey - The base secret key from which a new key secret will be derived
* @param password - The user's password
* @param salt - The keystore salt
* @returns The derived secret key for protecting the encryption keystore
*/
export async function deriveEncryptionKeystoreKey(baseKey: Uint8Array): Promise<Uint8Array> {
export async function deriveEncryptionKeystoreKey(password: string, salt: Uint8Array): Promise<Uint8Array> {
const baseKey = await getKeyFromPasswordAndSalt(password, salt);
return deriveSymmetricKeyFromContext(CONTEXT_ENC_KEYSTORE, baseKey);
}

/**
* Derives a secret key for protecting the encryption keystore
*
* @param password - The user's password
* @param salt - The keystore salt
* @returns The derived secret key for protecting the encryption keystore
*/
export async function deriveNewEncryptionKeystoreKey(
password: string,
): Promise<{ secretKey: Uint8Array; salt: Uint8Array }> {
const { key, salt } = await getKeyFromPassword(password);
const secretKey = deriveSymmetricKeyFromContext(CONTEXT_ENC_KEYSTORE, key);
return { secretKey, salt };
}
33 changes: 23 additions & 10 deletions src/keystore-crypto/emailEncryptionKey.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,70 @@
import { EncryptedKeystore, KeystoreType, HybridKeyPair } from '../types';
import { genMnemonic } from '../utils';
import { encryptKeystoreContent, decryptKeystoreContent, deriveEncryptionKeystoreKey, deriveRecoveryKey } from './core';
import {
encryptKeystoreContent,
decryptKeystoreContent,
deriveEncryptionKeystoreKey,
deriveNewEncryptionKeystoreKey,
deriveRecoveryKey,
} from './core';
import { genHybridKeys } from '../hybrid-crypto';

/**
* Generates hybrid keys and creates encrypted main and recovery keystores
* The main keystore encryption key is derived from the base key (stored in session storage)
* The main keystore encryption key is derived from the user's password
* The recovery keystore encryption key is derived from the recovery codes
*
* @param userEmail - The user's email
* @param password - The user's password
* @returns The encryption keys
*
* @returns The encryption and recovery keystores, recovery codes and hybrid keys
*/
export async function createEncryptionAndRecoveryKeystores(
userEmail: string,
baseKey: Uint8Array,
password: string,
): Promise<{
encryptionKeystore: EncryptedKeystore;
recoveryKeystore: EncryptedKeystore;
recoveryCodes: string;
keys: HybridKeyPair;
salt: Uint8Array;
}> {
try {
const keys = genHybridKeys();

const secretKey = await deriveEncryptionKeystoreKey(baseKey);
const { secretKey, salt } = await deriveNewEncryptionKeystoreKey(password);
const encryptionKeystore = await encryptKeystoreContent(secretKey, keys, userEmail, KeystoreType.ENCRYPTION);

const recoveryCodes = genMnemonic();
const recoveryKey = await deriveRecoveryKey(recoveryCodes);
const recoveryKeystore = await encryptKeystoreContent(recoveryKey, keys, userEmail, KeystoreType.RECOVERY);

return { encryptionKeystore, recoveryKeystore, recoveryCodes, keys };
return { encryptionKeystore, recoveryKeystore, recoveryCodes, keys, salt };
} catch (error) {
throw new Error('Failed to create encryption and recovery keystores', { cause: error });
}
}

/**
* Opens the encryption keystore and returns the email encryption keys
* The decryption key is derived from the base key (stored in session storage)
* The decryption key is derived from the user password
*
* @param encryptedKeystore - The encrypted keystore containing encryption keys
* @param baseKey - The base key from which the decryption key will be derived
* @param password - The user's password
* @param salt - The keystore's salt
* @returns The encryption keys
*/
export async function openEncryptionKeystore(
encryptedKeystore: EncryptedKeystore,
baseKey: Uint8Array,
password: string,
salt: Uint8Array,
): Promise<HybridKeyPair> {
try {
if (encryptedKeystore.type != KeystoreType.ENCRYPTION) {
throw new Error('Input is invalid');
}
const secretKey = await deriveEncryptionKeystoreKey(baseKey);
const secretKey = await deriveEncryptionKeystoreKey(password, salt);
const keys = await decryptKeystoreContent(secretKey, encryptedKeystore);
return keys;
} catch (error) {
Expand All @@ -61,7 +74,7 @@ export async function openEncryptionKeystore(

/**
* Opens the recovery keystore and returns the email encryption keys
* The decryption key is derived from the base key (stored in session storage)
* The decryption key is derived from the recovery codes (machine-generated mnemonic)
*
* @param recoveryCodes - The user's recovery codes
* @param encryptedKeystore - The encrypted keystore containing encryption keys
Expand Down
10 changes: 1 addition & 9 deletions tests/derive-keys/deriveKeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { deriveSymmetricKeyFromTwoKeys, deriveSymmetricKeyFromContext, deriveDatabaseKey } from '../../src/derive-key';
import { deriveSymmetricKeyFromTwoKeys, deriveSymmetricKeyFromContext } from '../../src/derive-key';
import { uint8ArrayToHex } from '../../src/utils';
import { AES_KEY_BYTE_LENGTH } from '../../src/constants';
import { genSymmetricKey } from '../../src/symmetric-crypto';
Expand Down Expand Up @@ -38,12 +38,4 @@ describe('Test derive key', () => {
/Failed to derive symmetric key from two keys/,
);
});

it('should derive symmetric key for database encryption', async () => {
const baseKey = genSymmetricKey();
const key = await deriveDatabaseKey(baseKey);
expect(key.length).toBe(AES_KEY_BYTE_LENGTH);
const key2 = await deriveDatabaseKey(baseKey);
expect(key2).toStrictEqual(key);
});
});
Loading
Loading