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
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
12 changes: 12 additions & 0 deletions src/email-crypto/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { UTF8ToUint8 } from '../utils';
import { User, Email } from '../types';
import { concatBytes } from '@noble/hashes/utils.js';

/**
* Converts a Users into a Uint8Array.
*
* @param user - The user.
* @returns The Uint8Array representation of the user.
*/
export function userToBytes(user: User): Uint8Array {
try {
const json = JSON.stringify(user);
Expand All @@ -11,6 +17,12 @@ export function userToBytes(user: User): Uint8Array {
}
}

/**
* Converts an array of Users into a Uint8Array.
*
* @param recipients - The array of Users.
* @returns The Uint8Array representation of the array of Users.
*/
export function recipientsToBytes(recipients: User[]): Uint8Array {
try {
const array = recipients.map((user) => userToBytes(user));
Expand Down
95 changes: 57 additions & 38 deletions src/email-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
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';

/**
* Symmetrically encrypts email body.
*
* @param email - The email to encrypt.
* @param isSubjectEncrypted - Indicates if the email subject field was encrypted
* @returns The resulting encrypted email body, updated public parameters (with encrypted subject if it was encrypted) and symmetric key used for encryption
*/
export async function encryptEmailBody(
email: Email,
isSubjectEncrypted: boolean,
Expand Down Expand Up @@ -48,6 +45,15 @@ export async function encryptEmailBody(
}
}

/**
* Decrypts symmetrically encrypted email body.
*
* @param enc - The email body to decrypt.
* @param encParams - The email paramaters.
* @param encryptionKey - The symmetric key to decrypt the email.
* @param isSubjectEncrypted - Indicates if the email subject field was encrypted
* @returns The resulting decrypted email body and updated public parameters (with decrypted subject if it was encrypted)
*/
export async function decryptEmailBody(
enc: EmailBodyEncrypted,
encParams: EmailPublicParameters,
Expand Down Expand Up @@ -131,11 +137,13 @@ export async function encryptEmailContentAndSubjectSymmetrically(
}

/**
* Decrypts symmetrically encrypted email.
* Decrypts symmetrically encrypted email and its subject.
*
* @param encryptedEmail - The email to decrypt.
* @param encryptionKey - The symmetric key.
* @returns The decrypted email
* @param encryptionKey - The symmetric key for encryption.
* @param aux - The auxiliary data (e.g., email ID or timestamp) for AEAD.
* @param encSubject - The encrypted email subject.
* @param enc - The encrypted email body.
* @returns The resulting encrypted emailBody
*/
export async function decryptEmailAndSubjectSymmetrically(
encryptionKey: Uint8Array,
Expand All @@ -157,8 +165,11 @@ export async function decryptEmailAndSubjectSymmetrically(
/**
* Symmetrically encrypts an email with a randomly sampled key.
*
* @param email - The email to encrypt.
* @returns The resulting ciphertext and the used symmetric key
* @param emailBody - The email body to encrypt.
* @param encryptionKey - The symmetric key for encryption.
* @param aux - The auxiliary data (e.g., email ID or timestamp) for AEAD.
* @param emailID - The unique identifier of the email.
* @returns The resulting encrypted emailBody
*/
export async function encryptEmailContentSymmetricallyWithKey(
emailBody: EmailBody,
Expand All @@ -183,6 +194,15 @@ export async function encryptEmailContentSymmetricallyWithKey(
}
}

/**
* Symmetrically encrypts email attachements.
*
* @param attachments - The attachments.
* @param encryptionKey - The symmetric key.
* @param aux - The auxiliary data (e.g., email ID or timestamp) for AEAD.
* @param emailID - The unique identifier of the email.
* @returns The decrypted email attackements
*/
async function encryptEmailAttachements(
attachments: string[],
encryptionKey: Uint8Array,
Expand All @@ -203,6 +223,14 @@ async function encryptEmailAttachements(
}
}

/**
* Decrypts symmetrically encrypted email attachements.
*
* @param encryptedAttachments - The encrypted attachments.
* @param encryptionKey - The symmetric key.
* @param aux - The auxiliary data (e.g., email ID or timestamp) for AEAD.
* @returns The decrypted email attackements
*/
async function decryptEmailAttachements(
encryptedAttachments: Uint8Array[],
encryptionKey: Uint8Array,
Expand All @@ -223,8 +251,9 @@ async function decryptEmailAttachements(
/**
* Decrypts symmetrically encrypted email.
*
* @param encryptedEmail - The email to decrypt.
* @param encryptionKey - The symmetric key.
* @param aux - The auxiliary data (e.g., email ID or timestamp) for AEAD.
* @param enc - The email body to decrypt.
* @returns The decrypted email
*/
export async function decryptEmailSymmetrically(
Expand Down Expand Up @@ -253,24 +282,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 @@ -282,22 +305,18 @@ export async function encryptKeysHybrid(
* Decrypts the email symmetric key encrypted via hybrid encryption.
*
* @param encryptedKey - The encrypted email key.
* @param senderPublicKey - The public key of the sender.
* @param recipientPrivateKey - The private key of the recipient.
* @returns The email encryption key
*/
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
29 changes: 5 additions & 24 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.
* Generates public and private keys for email encryption.
*
* @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();
}
22 changes: 8 additions & 14 deletions src/email-crypto/hybridEncyptedEmail.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import { PublicKeys, PrivateKeys, HybridEncryptedEmail, Email, UserWithPublicKeys } from '../types';
import { HybridEncryptedEmail, Email, UserWithPublicKey } from '../types';
import { decryptEmailBody, encryptKeysHybrid, decryptKeysHybrid, encryptEmailBody } from './core';

/**
* Encrypts the email using hybrid encryption.
*
* @param email - The email to encrypt.
* @param recipientPublicKeys - The public keys of the recipient.
* @param senderPrivateKey - The private key of the sender.
* @param isSubjectEncrypted - Indicates if the email subject field should be encrypted
* @returns The encrypted email
*/
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 @@ -30,22 +28,20 @@ export async function encryptEmailHybrid(
*
* @param email - The email to encrypt.
* @param recipients - The recipients with corresponding public keys.
* @param senderPrivateKey - The private key of the sender.
* @param isSubjectEncrypted - Indicates if the email subject field should be encrypted
* @returns The set of encrypted email
*/
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 @@ -65,18 +61,16 @@ export async function encryptEmailHybridForMultipleRecipients(
* Decrypts the email using hybrid encryption.
*
* @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
Loading
Loading