Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,11 @@ const emailBody: EmailBody = {
createdAt: '2025-06-14T08:11:22.000Z',
labels: ['label 1', 'label2'],
};

const userAlice = {
email: 'alice email',
name: 'alice',
};

const userBob = {
email: 'bob email',
name: 'bob',
};
const { privateKeys: alicePrivateKeys, publicKeys: alicePublicKeys } = await emailCrypto.generateEmailKeys();
const { privateKeys: bobPrivateKeys, publicKeys: bobPublicKeys } = await emailCrypto.generateEmailKeys();
const { secretKey: bobPrivateKeys, publicKey: bobPublicKeys } = await generateEmailKeys();

const emailBody: EmailBody = {
text: 'email body',
Expand All @@ -150,8 +143,8 @@ const email: Email = {
params: emailParams,
body: emailBody,
};
const encryptedEmail = await emailCrypto.encryptEmailHybrid(email, bobPublicKeys, alicePrivateKeys);
const decryptedEmail = await emailCrypto.decryptEmailHybrid(encryptedEmail, alicePublicKeys, bobPrivateKeys);
const encryptedEmail = await emailCrypto.encryptEmailHybrid(email, bobPublicKeys);
const decryptedEmail = await emailCrypto.decryptEmailHybrid(encryptedEmail, bobPrivateKeys);
expect(decryptedEmail).toStrictEqual(email);


Expand Down
41 changes: 11 additions & 30 deletions src/email-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import {
HybridEncKey,
PwdProtectedKey,
PublicKeys,
PrivateKeys,
EmailBody,
EmailBodyEncrypted,
Email,
EmailPublicParameters,
} from '../types';
import { HybridEncKey, PwdProtectedKey, EmailBody, EmailBodyEncrypted, Email, EmailPublicParameters } from '../types';
import { encryptSymmetrically, decryptSymmetrically, genSymmetricKey } from '../symmetric-crypto';
import { encapsulateKyber, decapsulateKyber } from '../post-quantum-crypto';
import { deriveWrappingKey, wrapKey, unwrapKey } from '../key-wrapper';
import { deriveSecretKey } from '../asymmetric-crypto';
import { encapsulateHybrid, decapsulateHybrid } from '../hybrid-crypto';
import { wrapKey, unwrapKey } from '../key-wrapper';
import { getKeyFromPassword, getKeyFromPasswordAndSalt } from '../derive-key';
import { UTF8ToUint8, base64ToUint8Array, uint8ArrayToBase64, uint8ToUTF8, uuidToBytes } from '../utils';
import { getAux } from './utils';
Expand Down Expand Up @@ -253,24 +243,18 @@ export async function decryptEmailSymmetrically(
* Encrypts the email symmetric key using hybrid encryption.
*
* @param emailEncryptionKey - The symmetric key used for email encryption.
* @param recipientPublicKey - The public key of the recipient.
* @param senderPrivateKey - The private key of the sender.
* @param recipientPublicHybridKey - The public key of the recipient.
* @returns The encrypted email symmetric key
*/
export async function encryptKeysHybrid(
emailEncryptionKey: Uint8Array,
recipientPublicKey: PublicKeys,
senderPrivateKey: PrivateKeys,
recipientPublicHybridKey: Uint8Array,
): Promise<HybridEncKey> {
try {
const eccSecret = await deriveSecretKey(recipientPublicKey.eccPublicKey, senderPrivateKey.eccPrivateKey);
const { cipherText: kyberCiphertext, sharedSecret: kyberSecret } = encapsulateKyber(
recipientPublicKey.kyberPublicKey,
);
const wrappingKey = await deriveWrappingKey(eccSecret, kyberSecret);
const encryptedKey = await wrapKey(emailEncryptionKey, wrappingKey);
const { cipherText, sharedSecret } = encapsulateHybrid(recipientPublicHybridKey);
const encryptedKey = await wrapKey(emailEncryptionKey, sharedSecret);
const encryptedKeyBase64 = uint8ArrayToBase64(encryptedKey);
const kyberCiphertextBase64 = uint8ArrayToBase64(kyberCiphertext);
const kyberCiphertextBase64 = uint8ArrayToBase64(cipherText);

return { encryptedKey: encryptedKeyBase64, kyberCiphertext: kyberCiphertextBase64 };
} catch (error) {
Expand All @@ -288,16 +272,13 @@ export async function encryptKeysHybrid(
*/
export async function decryptKeysHybrid(
encryptedKey: HybridEncKey,
senderPublicKey: PublicKeys,
recipientPrivateKey: PrivateKeys,
recipientPrivateKey: Uint8Array,
): Promise<Uint8Array> {
try {
const kyberCiphertext = base64ToUint8Array(encryptedKey.kyberCiphertext);
const encKey = base64ToUint8Array(encryptedKey.encryptedKey);
const eccSecret = await deriveSecretKey(senderPublicKey.eccPublicKey, recipientPrivateKey.eccPrivateKey);
const kyberSecret = decapsulateKyber(kyberCiphertext, recipientPrivateKey.kyberPrivateKey);
const wrappingKey = await deriveWrappingKey(eccSecret, kyberSecret);
const encryptionKey = await unwrapKey(encKey, wrappingKey);
const sharedSecret = decapsulateHybrid(kyberCiphertext, recipientPrivateKey);
const encryptionKey = await unwrapKey(encKey, sharedSecret);
return encryptionKey;
} catch (error) {
throw new Error('Failed to decrypt email key encrypted via hybrid encryption', { cause: error });
Expand Down
27 changes: 4 additions & 23 deletions src/email-crypto/emailKeys.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
import { generateEccKeys } from '../asymmetric-crypto';
import { generateKyberKeys } from '../post-quantum-crypto';
import { EmailKeys } from '../types';
import { genHybridKeys } from '../hybrid-crypto';
import { HybridKeyPair } from '../types';

/**
* Generates public and private keys for email service.
*
* @returns The user's private and public keys
*/
export async function generateEmailKeys(): Promise<EmailKeys> {
try {
const kyberKeys = generateKyberKeys();
const keys = await generateEccKeys();

const emailKeys: EmailKeys = {
publicKeys: {
eccPublicKey: keys.publicKey,
kyberPublicKey: kyberKeys.publicKey,
},
privateKeys: {
eccPrivateKey: keys.privateKey,
kyberPrivateKey: kyberKeys.secretKey,
},
};

return emailKeys;
} catch (error) {
throw new Error('Failed to generate keys for email service', { cause: error });
}
export async function generateEmailKeys(): Promise<HybridKeyPair> {
return genHybridKeys();
}
19 changes: 8 additions & 11 deletions src/email-crypto/hybridEncyptedEmail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PublicKeys, PrivateKeys, HybridEncryptedEmail, Email, UserWithPublicKeys } from '../types';
import { HybridEncryptedEmail, Email, UserWithPublicKey } from '../types';
import { decryptEmailBody, encryptKeysHybrid, decryptKeysHybrid, encryptEmailBody } from './core';

/**
Expand All @@ -12,13 +12,12 @@ import { decryptEmailBody, encryptKeysHybrid, decryptKeysHybrid, encryptEmailBod
*/
export async function encryptEmailHybrid(
email: Email,
recipient: UserWithPublicKeys,
senderPrivateKey: PrivateKeys,
recipient: UserWithPublicKey,
isSubjectEncrypted: boolean = false,
): Promise<HybridEncryptedEmail> {
try {
const { encryptionKey, params, enc } = await encryptEmailBody(email, isSubjectEncrypted);
const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient.publicKeys, senderPrivateKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why sender key has been removed here? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because X-Wing doesn't need it. Before, I was doing ECC secret derivation, Kyber KEM encapsulation that gave another secret, then deriving one key from both secrets. X-Wing does it differently; they take public key of the intended recipient as input and handle everything else. The hybrid key is this:
Screenshot 2026-03-18 at 13 43 11

const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient.publicHybridKey);
return { enc, encryptedKey, recipientEmail: recipient.email, params, isSubjectEncrypted, id: email.id };
} catch (error) {
throw new Error('Failed to encrypt email with hybrid encryption', { cause: error });
Expand All @@ -36,16 +35,15 @@ export async function encryptEmailHybrid(
*/
export async function encryptEmailHybridForMultipleRecipients(
email: Email,
recipients: UserWithPublicKeys[],
senderPrivateKey: PrivateKeys,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same

recipients: UserWithPublicKey[],
isSubjectEncrypted: boolean = false,
): Promise<HybridEncryptedEmail[]> {
try {
const { encryptionKey, params, enc } = await encryptEmailBody(email, isSubjectEncrypted);

const encryptedEmails: HybridEncryptedEmail[] = [];
for (const recipient of recipients) {
const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient.publicKeys, senderPrivateKey);
const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient.publicHybridKey);
encryptedEmails.push({
enc,
encryptedKey,
Expand All @@ -66,17 +64,16 @@ export async function encryptEmailHybridForMultipleRecipients(
*
* @param encryptedEmail - The encrypted email.
* @param senderPublicKeys - The public key of the sender.
* @param recipientPrivateKeys - The private key of the recipient.
* @param recipientPrivateHybridKeys - The private key of the recipient.
* @returns The decrypted email
*/
export async function decryptEmailHybrid(
encryptedEmail: HybridEncryptedEmail,
senderPublicKeys: PublicKeys,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same (also remember to remove the params from the JSDoc) :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

true, will check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

recipientPrivateKeys: PrivateKeys,
recipientPrivateHybridKeys: Uint8Array,
): Promise<Email> {
try {
const { isSubjectEncrypted, params: encParams, enc, encryptedKey, id } = encryptedEmail;
const encryptionKey = await decryptKeysHybrid(encryptedKey, senderPublicKeys, recipientPrivateKeys);
const encryptionKey = await decryptKeysHybrid(encryptedKey, recipientPrivateHybridKeys);
const { body, params } = await decryptEmailBody(enc, encParams, encryptionKey, isSubjectEncrypted);
return { body, params, id };
} catch (error) {
Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ export {
uint8ArrayToBase64,
base64ToUint8Array,
genMnemonic,
base64ToPublicKey,
publicKeyToBase64,
generateUuid,
uuidToBytes,
bytesToUuid,
Expand Down
53 changes: 16 additions & 37 deletions src/keystore-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { encryptSymmetrically, decryptSymmetrically } from '../symmetric-crypto';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8, mnemonicToBytes, publicKeyToBase64 } from '../utils';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8, mnemonicToBytes } from '../utils';
import { deriveSymmetricKeyFromContext } from '../derive-key';
import { CONTEXT_ENC_KEYSTORE, AES_KEY_BIT_LENGTH, CONTEXT_RECOVERY } from '../constants';
import { getBytesFromData } from '../hash';
import { EmailKeys, EncryptedKeystore, KeystoreType } from '../types';
import { exportPrivateKey, importPrivateKey, importPublicKey } from '../asymmetric-crypto';
import { EncryptedKeystore, HybridKeyPair, KeystoreType } from '../types';

/**
* Encrypts the keystore content using symmetric encryption
Expand All @@ -17,28 +16,20 @@ import { exportPrivateKey, importPrivateKey, importPublicKey } from '../asymmetr
*/
export async function encryptKeystoreContent(
secretKey: Uint8Array,
keys: EmailKeys,
keys: HybridKeyPair,
userEmail: string,
type: KeystoreType,
): Promise<EncryptedKeystore> {
try {
const aux = UTF8ToUint8(userEmail + type);
const publicKeys = await publicKeyToBase64(keys.publicKeys);
const kyberPrivateKeyEnc = await encryptSymmetrically(secretKey, keys.privateKeys.kyberPrivateKey, aux);
const eccPrivateKey = await exportPrivateKey(keys.privateKeys.eccPrivateKey);
const eccPrivateKeyEnc = await encryptSymmetrically(secretKey, eccPrivateKey, aux);
const encryptedKeys = {
publicKeys,
privateKeys: {
kyberPrivateKeyBase64: uint8ArrayToBase64(kyberPrivateKeyEnc),
eccPrivateKeyBase64: uint8ArrayToBase64(eccPrivateKeyEnc),
},
};
const publicKey = uint8ArrayToBase64(keys.publicKey);
const secretKeyEncrypted = await encryptSymmetrically(secretKey, keys.secretKey, aux);

const keystore: EncryptedKeystore = {
userEmail,
type,
encryptedKeys,
publicKey,
privateKeyEncrypted: uint8ArrayToBase64(secretKeyEncrypted),
};
return keystore;
} catch (error) {
Expand All @@ -49,37 +40,25 @@ export async function encryptKeystoreContent(
/**
* Decrypts the keystore content using symmetric encryption
*
* @param secretKey - The symmetric key to decrypt the keystore content
* @param kesytoreOpeningKey - The symmetric key to decrypt the keystore content
* @param encryptedKeys - The encrypted keystore content
* @param userEmail - The ID of the user
* @param tag - The keystore type-specific tag string
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Are these params being used in the function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

was before, fixed

* @returns The decrypted keystore content
*/
export async function decryptKeystoreContent(
secretKey: Uint8Array,
kesytoreOpeningKey: Uint8Array,
encryptedKeystore: EncryptedKeystore,
): Promise<EmailKeys> {
): Promise<HybridKeyPair> {
try {
const aux = UTF8ToUint8(encryptedKeystore.userEmail + encryptedKeystore.type);
const kyberPublicKey = base64ToUint8Array(encryptedKeystore.encryptedKeys.publicKeys.kyberPublicKeyBase64);
const eccPublicArray = base64ToUint8Array(encryptedKeystore.encryptedKeys.publicKeys.eccPublicKeyBase64);
const eccPublicKey = await importPublicKey(eccPublicArray);
const encKyberPrivateKey = base64ToUint8Array(encryptedKeystore.encryptedKeys.privateKeys.kyberPrivateKeyBase64);
const kyberPrivateKey = await decryptSymmetrically(secretKey, encKyberPrivateKey, aux);
const eccEncArray = base64ToUint8Array(encryptedKeystore.encryptedKeys.privateKeys.eccPrivateKeyBase64);
const eccKey = await decryptSymmetrically(secretKey, eccEncArray, aux);
const eccPrivateKey = await importPrivateKey(eccKey);
const keys = {
publicKeys: {
kyberPublicKey,
eccPublicKey,
},
privateKeys: {
kyberPrivateKey,
eccPrivateKey,
},
const publicKey = base64ToUint8Array(encryptedKeystore.publicKey);
const ciphertext = base64ToUint8Array(encryptedKeystore.privateKeyEncrypted);
const secretKey = await decryptSymmetrically(kesytoreOpeningKey, ciphertext, aux);
return {
publicKey,
secretKey,
};
return keys;
} catch (error) {
throw new Error('Failed to decrypt keystore content', { cause: error });
}
Expand Down
16 changes: 8 additions & 8 deletions src/keystore-crypto/emailEncryptionKey.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { EmailKeys, EncryptedKeystore, KeystoreType } from '../types';
import { EncryptedKeystore, KeystoreType, HybridKeyPair } from '../types';
import { genMnemonic } from '../utils';
import { encryptKeystoreContent, decryptKeystoreContent, deriveEncryptionKeystoreKey, deriveRecoveryKey } from './core';
import { generateEmailKeys } from '../email-crypto';
import { genHybridKeys } from '../hybrid-crypto';

/**
* Generates email keys and creates encrypted main and recovery keystores
* 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 recovery keystore encryption key is derived from the recovery codes
*
* @returns The encryption and recovery keystores, recovery codes and email keys
* @returns The encryption and recovery keystores, recovery codes and hybrid keys
*/
export async function createEncryptionAndRecoveryKeystores(
userEmail: string,
Expand All @@ -17,10 +17,10 @@ export async function createEncryptionAndRecoveryKeystores(
encryptionKeystore: EncryptedKeystore;
recoveryKeystore: EncryptedKeystore;
recoveryCodes: string;
keys: EmailKeys;
keys: HybridKeyPair;
}> {
try {
const keys = await generateEmailKeys();
const keys = genHybridKeys();

const secretKey = await deriveEncryptionKeystoreKey(baseKey);
const encryptionKeystore = await encryptKeystoreContent(secretKey, keys, userEmail, KeystoreType.ENCRYPTION);
Expand All @@ -46,7 +46,7 @@ export async function createEncryptionAndRecoveryKeystores(
export async function openEncryptionKeystore(
encryptedKeystore: EncryptedKeystore,
baseKey: Uint8Array,
): Promise<EmailKeys> {
): Promise<HybridKeyPair> {
try {
if (encryptedKeystore.type != KeystoreType.ENCRYPTION) {
throw new Error('Input is invalid');
Expand All @@ -70,7 +70,7 @@ export async function openEncryptionKeystore(
export async function openRecoveryKeystore(
recoveryCodes: string,
encryptedKeystore: EncryptedKeystore,
): Promise<EmailKeys> {
): Promise<HybridKeyPair> {
try {
if (encryptedKeystore.type != KeystoreType.RECOVERY) {
throw new Error('Input is invalid');
Expand Down
27 changes: 4 additions & 23 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
export type EncryptedKeystore = {
userEmail: string;
type: KeystoreType;
encryptedKeys: EmailKeysEncrypted;
publicKey: string;
privateKeyEncrypted: string;
};

export type User = {
email: string;
name: string;
};

export type UserWithPublicKeys = User & {
publicKeys: PublicKeys;
export type UserWithPublicKey = User & {
publicHybridKey: Uint8Array;
};

export type PublicKeys = {
Expand All @@ -28,26 +29,6 @@ export type HybridKeyPair = {
secretKey: Uint8Array;
};

export type PrivateKeys = {
eccPrivateKey: CryptoKey;
kyberPrivateKey: Uint8Array;
};

export type PrivateKeysEncrypted = {
eccPrivateKeyBase64: string;
kyberPrivateKeyBase64: string;
};

export type EmailKeys = {
publicKeys: PublicKeys;
privateKeys: PrivateKeys;
};

export type EmailKeysEncrypted = {
publicKeys: PublicKeysBase64;
privateKeys: PrivateKeysEncrypted;
};

export type HybridEncryptedEmail = {
encryptedKey: HybridEncKey;
enc: EmailBodyEncrypted;
Expand Down
Loading
Loading