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
108 changes: 44 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@ Builds the app for production to the `build` folder.

### Key Management

- **`derive-key`** - Key derivation functions for deriving cryptographic keys from passwords (ARGON2) and base key (BLAKE3 in KDF mode)
- **`derive-key`** - Key derivation functions for deriving cryptographic keys from base key (BLAKE3 in KDF mode)
- **`derive-password`** - Key derivation functions for deriving cryptographic keys from passwords (ARGON2)
- **`key-wrapper`** - Key wrapping and unwrapping functions for secure symmetric key storage and transport
- **`keystore-crypto`** - Keystore cryptographic operations for securing user's keys
- **`keystore-service`** - Keystore management service for communicating with the server

### Email Security

- **`email-crypto`** - End-to-end email encryption and decryption using hybrid cryptography and password-protection
- **`email-search`** - Email indexing on the client side to enable search while preserving privacy
- **`email-service`** - Email management service for communicating with the server

### Infrastructure

Expand All @@ -65,101 +64,82 @@ Builds the app for production to the `build` folder.

```typescript
import {
asymmetric,
symmetric,
utils,
emailCrypto,
pq,
keystoreService,
deriveKey,
hash,
generateEccKeys,
deriveSecretKey,
UTF8ToUint8,
genSymmetricKey,
encryptSymmetrically,
} from 'internxt-crypto';

// Asymmetric encryption
const keysAlice = await asymmetric.generateEccKeys();
const keysBob = await asymmetric.generateEccKeys();
const resultAlice = await asymmetric.deriveSecretKey(keysBob.publicKey, keysAlice.privateKey);
const resultBob = await asymmetric.deriveSecretKey(keysAlice.publicKey, keysBob.privateKey);
const keysAlice = await generateEccKeys();
const keysBob = await generateEccKeys();
const resultAlice = await deriveSecretKey(keysBob.publicKey, keysAlice.privateKey);
const resultBob = await deriveSecretKey(keysAlice.publicKey, keysBob.privateKey);
expect(resultAlice).toStrictEqual(resultBob);

// Symmetric encryption
const data = utils.UTF8ToUint8('Sensitive information to encrypt'); // convert to Uint8Array
const data = UTF8ToUint8('Sensitive information to encrypt'); // convert to Uint8Array
const additionalData = 'Additional non-secret data';
const key = await symmetric.genSymmetricKey();
const ciphertext: Uint8Array = await symmetric.encryptSymmetrically(key, data, additionalData);
const plainText = await symmetric.decryptSymmetrically(encryptionKey, ciphertext, additionalData);
const key = genSymmetricKey();
const ciphertext: Uint8Array = await encryptSymmetrically(key, data, additionalData);
const plainText = await decryptSymmetrically(encryptionKey, ciphertext, additionalData);
expect(data).toStrictEqual(plainText);

// Post qunatum cryptography
const keys = pq.generateKyberKeys();
const { cipherText, sharedSecret } = pq.encapsulateKyber(keys.publicKey);
const result = pq.decapsulateKyber(cipherText, keys.secretKey);
const keys = generateKyberKeys();
const { cipherText, sharedSecret } = encapsulateKyber(keys.publicKey);
const result = decapsulateKyber(cipherText, keys.secretKey);
expect(result).toStrictEqual(sharedSecret);

// Hash
const result = await hash.hashData(['']);
const result = hashData(['']);
const expectedResult = 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262';
expect(result).toStrictEqual(expectedResult);

// Key derivation
const context = 'BLAKE3 2019-12-27 16:29:52 test vectors context';
const baseKey = symmetric.genSymmetricKey(); // Uint8Array
const key = await deriveKey.deriveSymmetricKeyFromContext(context, baseKey);
const baseKey = genSymmetricKey();
const key = deriveSymmetricKeyFromContext(context, baseKey);

// Key derivation from password
const password = 'your password';
const { keyHex, saltHex } = await deriveKey.getKeyFromPasswordHex(password);
const result = await deriveKey.verifyKeyFromPasswordHex(password, saltHex, keyHex);
expect(result).toBe(true);
const { key, salt } = await getKeyFromPassword(password);

// Hybrid email encryption

const emailBody: EmailBody = {
const email: EmailBody = {
text: 'email text',
createdAt: '2025-06-14T08:11:22.000Z',
labels: ['label 1', 'label2'],
};
const userBob = {
email: 'bob email',
name: 'bob',
subject: 'email subject',
};
const { secretKey: bobPrivateKeys, publicKey: bobPublicKeys } = await generateEmailKeys();

const emailBody: EmailBody = {
text: 'email body',
};

const emailParams: EmailPublicParameters = {
labels: ['label 1', 'label2'],
createdAt: '2025-06-14T08:11:22.000Z',
subject: 'email subject',
sender: userAlice,
recipient: userBob,
replyToEmailID: generateUuid(),
const bobWithPublicKeys = {
email: 'bob email',
publicHybridKey: bobPublicKeys,
};
const encryptedEmail = await encryptEmailHybrid(email, bobWithPublicKeys);
const decryptedEmail = await decryptEmailHybrid(encryptedEmail, bobPrivateKeys);

const email: Email = {
id: generateUuid(),
params: emailParams,
body: emailBody,
};
const encryptedEmail = await emailCrypto.encryptEmailHybrid(email, bobPublicKeys);
const decryptedEmail = await emailCrypto.decryptEmailHybrid(encryptedEmail, bobPrivateKeys);
expect(decryptedEmail).toStrictEqual(email);
expect(encryptedEmail.encEmailBody.encSubject).not.toBe(email.subject);
expect(decryptedEmailBody).toStrictEqual(email);


// password-protected email
const sharedSecret = 'secret shared between Alice and Bob';
const encryptedEmail = await emailCrypto.createPwdProtectedEmail(email, sharedSecret);
const decryptedEmail = await emailCrypto.decryptPwdProtectedEmail(encryptedEmail, sharedSecret);
const encryptedEmail = await createPwdProtectedEmail(email, sharedSecret);
const decryptedEmail = await decryptPwdProtectedEmail(encryptedEmail, sharedSecret);
expect(decryptedEmail).toStrictEqual(email);

// keystore

// For this to work, session storage must have UserID and baseKey
const { encryptionKeystore, recoveryKeystore, recoveryCodes } = await createEncryptionAndRecoveryKeystores();
const result_enc = await keystoreService.openEncryptionKeystore(encryptionKeystore);
const result_rec = await keystoreService.openRecoveryKeystore(recoveryCodes, recoveryKeystore);
expect(result_enc).toStrictEqual(result_rec);
const userEmail = 'user email';
const secretKey = genSymmetricKey();
const { encryptionKeystore, recoveryKeystore, recoveryCodes } = await createEncryptionAndRecoveryKeystores(
userEmail,
secretKey,
);
const resultEnc = await openEncryptionKeystore(encryptionKeystore, secretKey);
const resultRec = await openRecoveryKeystore(recoveryCodes, recoveryKeystore);

expect(resultEnc).toStrictEqual(resultRec);

// Email storage and search

Expand Down
8 changes: 4 additions & 4 deletions src/asymmetric-crypto/ellipticCurve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { x25519 } from '@noble/curves/webcrypto.js';
/**
* Derives secret key from the other user's public key and own private key
*
* @param aliceSecX - The secret key of the user deriving the shared secret key
* @param bobPubX - The public key of the other user
* @param aliceSecretKey - The secret key of the user deriving the shared secret key
* @param bobPublicKey - The public key of the other user
* @returns The derived secret key bits
*/
export async function deriveSecretKey(aliceSecX: Uint8Array, bobPubX: Uint8Array): Promise<Uint8Array> {
export async function deriveSecretKey(aliceSecretKey: Uint8Array, bobPublicKey: Uint8Array): Promise<Uint8Array> {
try {
return await x25519.getSharedSecret(aliceSecX, bobPubX);
return await x25519.getSharedSecret(aliceSecretKey, bobPublicKey);
} catch (error) {
throw new Error('Failed to derive elliptic curve secret key', { cause: error });
}
Expand Down
1 change: 0 additions & 1 deletion src/derive-key/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './deriveKeysFromKey';
export * from './deriveKeysFromPassword';
File renamed without changes.
1 change: 1 addition & 0 deletions src/derive-password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './deriveKeysFromPassword';
2 changes: 1 addition & 1 deletion src/email-crypto/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HybridEncKey, PwdProtectedKey, EmailBody, EmailBodyEncrypted, Recipient
import { encryptSymmetrically, decryptSymmetrically, genSymmetricKey } from '../symmetric-crypto';
import { encapsulateHybrid, decapsulateHybrid } from '../hybrid-crypto';
import { wrapKey, unwrapKey } from '../key-wrapper';
import { getKeyFromPassword, getKeyFromPasswordAndSalt } from '../derive-key';
import { getKeyFromPassword, getKeyFromPasswordAndSalt } from '../derive-password';
import { UTF8ToUint8, base64ToUint8Array, uint8ArrayToBase64, uint8ToUTF8 } from '../utils';

/**
Expand Down
8 changes: 2 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
export { deriveSecretKey, generateEccKeys } from './asymmetric-crypto';
export {
deriveSymmetricKeyFromTwoKeys,
deriveSymmetricKeyFromContext,
getKeyFromPassword,
getKeyFromPasswordAndSalt,
} from './derive-key';
export { deriveSymmetricKeyFromTwoKeys, deriveSymmetricKeyFromContext } from './derive-key';
export { getKeyFromPassword, getKeyFromPasswordAndSalt } from './derive-password';
export {
encryptEmailHybrid,
encryptEmailHybridForMultipleRecipients,
Expand Down
8 changes: 4 additions & 4 deletions src/post-quantum-crypto/kyber768.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
import { ml_kem768 as kyber } from '@noble/post-quantum/ml-kem.js';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀


/**
* Generates public and secret Kyber keys
Expand All @@ -11,7 +11,7 @@ export function generateKyberKeys(seed?: Uint8Array): {
secretKey: Uint8Array;
} {
try {
return ml_kem768.keygen(seed);
return kyber.keygen(seed);
} catch (error) {
throw new Error('Failed to generate Kyber keys', { cause: error });
}
Expand All @@ -28,7 +28,7 @@ export function encapsulateKyber(publicKey: Uint8Array): {
sharedSecret: Uint8Array;
} {
try {
return ml_kem768.encapsulate(publicKey);
return kyber.encapsulate(publicKey);
} catch (error) {
throw new Error('Failed to encapsulate', { cause: error });
}
Expand All @@ -43,7 +43,7 @@ export function encapsulateKyber(publicKey: Uint8Array): {
*/
export function decapsulateKyber(cipherText: Uint8Array, secretKey: Uint8Array): Uint8Array {
try {
return ml_kem768.decapsulate(cipherText, secretKey);
return kyber.decapsulate(cipherText, secretKey);
} catch (error) {
throw new Error('Failed to decapsulate', { cause: error });
}
Expand Down
6 changes: 3 additions & 3 deletions tests/derive-keys/deriveKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ describe('Test derive key', () => {
});

it('derive symmetric key from two keys should fail for small key', async () => {
const short_key = new Uint8Array([1, 2, 3]);
const shortKey = new Uint8Array([1, 2, 3]);
const key2 = genSymmetricKey();
expect(() => deriveSymmetricKeyFromTwoKeys(short_key, key2)).toThrowError(
expect(() => deriveSymmetricKeyFromTwoKeys(shortKey, key2)).toThrowError(
/Failed to derive symmetric key from two keys/,
);
expect(() => deriveSymmetricKeyFromTwoKeys(key2, short_key)).toThrowError(
expect(() => deriveSymmetricKeyFromTwoKeys(key2, shortKey)).toThrowError(
/Failed to derive symmetric key from two keys/,
);
});
Expand Down
48 changes: 23 additions & 25 deletions tests/derive-keys/deriveKeysFromPwd.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { describe, expect, it } from 'vitest';
import { getKeyFromPasswordAndSalt, getKeyFromPassword } from '../../src/derive-key';
import { getKeyFromPasswordAndSalt, getKeyFromPassword } from '../../src/derive-password';

import { argon2, sampleSalt } from '../../src/derive-key/core';
import { argon2, sampleSalt } from '../../src/derive-password/core';
import { uint8ArrayToHex } from '../../src/utils';

describe('Test Argon2', () => {
const testPassword = 'text demo';
const testSalt = sampleSalt();

it('should get correct key from the password', async () => {
const TEST_ARGON2_PARALLELISM = 7;
const TEST_ARGON2_ITERATIONS = 20;
const TEST_ARGON2_MEMORY_SIZE = 56;
const TEST_ARGON2_TAG_LENGTH = 32;
const testParallelism = 7;
const testIterations = 20;
const testMemorySize = 56;
const testTagLength = 32;

const test_password = 'text demo';
const test_salt = new Uint8Array([245, 166, 56, 228, 15, 96, 226, 174, 51, 22, 161, 34, 245, 194, 243, 16]);
const testSaltArray = new Uint8Array([245, 166, 56, 228, 15, 96, 226, 174, 51, 22, 161, 34, 245, 194, 243, 16]);

const result = await argon2(
test_password,
test_salt,
TEST_ARGON2_PARALLELISM,
TEST_ARGON2_ITERATIONS,
TEST_ARGON2_MEMORY_SIZE,
TEST_ARGON2_TAG_LENGTH,
testPassword,
testSaltArray,
testParallelism,
testIterations,
testMemorySize,
testTagLength,
);
const resultHEX = uint8ArrayToHex(result);
expect(resultHEX).toBe('53b7d7e24871060915166e96148bab3f6c9ff2a713eb2705e4ff19159aa7ebfb');
});

it('should generate different salt each time', async () => {
const test_salt_1 = uint8ArrayToHex(sampleSalt());
const test_salt_2 = uint8ArrayToHex(sampleSalt());
expect(test_salt_1).not.toBe(test_salt_2);
const testSalt1 = uint8ArrayToHex(sampleSalt());
const testSalt2 = uint8ArrayToHex(sampleSalt());
expect(testSalt1).not.toBe(testSalt2);
});

it('should give the same result for the same password and salt', async () => {
const test_password = 'text demo';
const test_salt = sampleSalt();
const result1 = await getKeyFromPasswordAndSalt(test_password, test_salt);
const result2 = await getKeyFromPasswordAndSalt(test_password, test_salt);
const result1 = await getKeyFromPasswordAndSalt(testPassword, testSalt);
const result2 = await getKeyFromPasswordAndSalt(testPassword, testSalt);
expect(result1).toStrictEqual(result2);
});

Expand All @@ -45,12 +45,10 @@ describe('Test Argon2', () => {
});

it('should throw an error if no salt or password given', async () => {
const test_password = 'text demo';
const test_salt = sampleSalt();
await expect(getKeyFromPasswordAndSalt(test_password, new Uint8Array())).rejects.toThrowError(
await expect(getKeyFromPasswordAndSalt(testPassword, new Uint8Array())).rejects.toThrowError(
/Failed to derive key from password and salt/,
);
await expect(getKeyFromPasswordAndSalt('', test_salt)).rejects.toThrowError(
await expect(getKeyFromPasswordAndSalt('', testSalt)).rejects.toThrowError(
/Failed to derive key from password and salt/,
);
});
Expand Down
14 changes: 7 additions & 7 deletions tests/email-crypto/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ describe('Test email crypto functions', () => {

it('should throw an error if decryption fails', async () => {
const { encEmailBody, encryptionKey } = await encryptEmailBody(emailBody, aux);
const bad_encryptionKey = await genSymmetricKey();
await expect(decryptEmailBody(encEmailBody, bad_encryptionKey, aux)).rejects.toThrowError(
const badEncryptionKey = await genSymmetricKey();
await expect(decryptEmailBody(encEmailBody, badEncryptionKey, aux)).rejects.toThrowError(
/Failed to symmetrically decrypt email body/,
);

const bad_aux = new Uint8Array([4, 5, 6, 7, 8]);
await expect(decryptEmailBody(encEmailBody, encryptionKey, bad_aux)).rejects.toThrowError(
const badAux = new Uint8Array([4, 5, 6, 7, 8]);
await expect(decryptEmailBody(encEmailBody, encryptionKey, badAux)).rejects.toThrowError(
/Failed to symmetrically decrypt email body/,
);
});

it('should throw an error if cannot encrypt', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bad_email: any = {};
bad_email.self = bad_email;
await expect(encryptEmailBody(bad_email, aux)).rejects.toThrowError(/Failed to symmetrically encrypt email body/);
const badEmail: any = {};
badEmail.self = badEmail;
await expect(encryptEmailBody(badEmail, aux)).rejects.toThrowError(/Failed to symmetrically encrypt email body/);
});
});
Loading
Loading