Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24.x'
node-version: '22.x'
cache: yarn

- name: Add .npmrc
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24.x'
node-version: '22.x'
cache: yarn

- name: Install dependencies
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/sonar-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node_version: [24.x]
node_version: [22.x]

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Setup Node
uses: actions/setup-node@v4
Expand All @@ -38,5 +40,4 @@ jobs:
- name: Analyze with SonarCloud
uses: SonarSource/sonarqube-scan-action@v5.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret)
77 changes: 4 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const password = 'your password';
const { key, salt } = await getKeyFromPassword(password);

// Hybrid email encryption
const email: EmailBody = {
const email: Email = {
text: 'email text',
attachments: ['email attachements'],
};
Expand All @@ -119,18 +119,18 @@ const bobWithPublicKeys = {
const encryptedEmail = await encryptEmailHybrid(email, bobWithPublicKeys);
const decryptedEmail = await decryptEmailHybrid(encryptedEmail, bobPrivateKeys);

expect(decryptedEmailBody).toStrictEqual(email);
expect(decryptedEmail).toStrictEqual(email);

// Hybrid email and subject encryption
const emailAndSubject: EmailBodyAndSubject = {
const emailAndSubject: EmailAndSubject = {
text: 'email text',
subject: 'email subject'
attachments: ['email attachements'],
};
const encryptedEmailAndSubject = await encryptEmailAndSubjectHybrid(emailAndSubject, bobWithPublicKeys);
const decryptedEmailAndSubject = await decryptEmailAndSubjectHybrid(encryptedEmailAndSubject, bobPrivateKeys);

expect(encryptedEmailAndSubject.encEmailBody.encSubject).not.toBe(emailAndSubject.subject);
expect(encryptedEmailAndSubject.encEmail.encSubject).not.toBe(emailAndSubject.subject);
expect(decryptedEmailAndSubject).toStrictEqual(emailAndSubject);


Expand All @@ -152,73 +152,4 @@ 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 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 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);
await encryptAndStoreManyEmail(emails, key, db);

// Delete given email by its ID
await deleteEmail(emailID, db);

// Delete oldests emails
const number = 5;
await deleteOldestEmails(number, db);

// Get all emails with or without sorting
const allEmails = await getAndDecryptAllEmails(key, db);
const newestFirst = await getAllEmailsSortedNewestFirst(db, key);
const oldestFirst = await getAllEmailsSortedOldestFirst(db, key);

// Get the number of stored emails
const count = await getEmailCount(db);

// Close IndexedDB database
closeDatabase(db);

// Delete IndexedDB database
await deleteDatabase(userID);

// Create email cache
const esCache = await createCacheFromDB(key, db);

// Add one or multiple emails to cache
const result = addEmailToCache(email, esCache);
expect(result.success).toBe(true);

const result = addEmailsToCache(emails, esCache);
expect(result.success).toBe(true);

// Get email from cache by its ID
const email = await getEmailFromCache(emailID, esCache);

// Delete email from cache by its ID
await deleteEmailFromCache(emailID, esCache);

// Create search index and search by query
const searchIndex = await buildSearchIndexFromCache(esCache);
const query = 'keywords to search';
const options = {
fields: ['subject'], // in which fields to search, all by deafult (subject, body, from, to)
limit: 5, // result limit, 50 by default
boost: { subject: 3, body: 1, from: 2, to: 2 }, // custom waights for matches in different email parts
};
const result: EmailSearchResult = await searchEmails(query, esCache, searchIndex);

```
69 changes: 34 additions & 35 deletions src/email-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HybridEncKey, PwdProtectedKey, EmailBody, RecipientWithPublicKey, EmailBodyEncrypted } from '../types';
import { HybridEncKey, PwdProtectedKey, Email, RecipientWithPublicKey, EmailEncrypted } from '../types';
import { encryptSymmetrically, decryptSymmetrically, genSymmetricKey } from '../symmetric-crypto';
import { encapsulateHybrid, decapsulateHybrid } from '../hybrid-crypto';
import { wrapKey, unwrapKey } from '../key-wrapper';
Expand All @@ -15,54 +15,53 @@ import {
} from './errors';

/**
* Symmetrically encrypts email body.
* Symmetrically encrypts email.
*
* @param body - The email body to encrypt.
* @param email - The email to encrypt.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting encrypted email body and symmetric key used for encryption
* @returns The resulting encrypted email and symmetric key used for encryption
*/
export async function encryptEmailBody(
body: EmailBody,
export async function encryptEmail(
email: Email,
aux?: Uint8Array,
): Promise<{
encEmailBody: EmailBodyEncrypted;
encEmail: EmailEncrypted;
encryptionKey: Uint8Array;
}> {
if (!email.text) {
throw new InvalidInputEmail();
}
try {
if (!body.text) {
throw new InvalidInputEmail();
}
const encryptionKey = genSymmetricKey();
const encEmailBody = await encryptEmailBodyWithKey(body, encryptionKey, aux);
const encEmail = await encryptEmailWithKey(email, encryptionKey, aux);

return { encEmailBody, encryptionKey };
return { encEmail, encryptionKey };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
throw new EmailSymmetricEncryptionError(error instanceof Error ? error.message : String(error));
}
}

/**
* Symmetrically encrypts email body with the given key.
* Symmetrically encrypts email with the given key.
*
* @param body - The email body to encrypt.
* @param email - The email to encrypt.
* @param encryptionKey - The symmetric key to encrypt the email.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting encrypted email body and symmetric key used for encryption
* @returns The resulting encrypted email and symmetric key used for encryption
*/
export async function encryptEmailBodyWithKey(
body: EmailBody,
export async function encryptEmailWithKey(
email: Email,
encryptionKey: Uint8Array,
aux?: Uint8Array,
): Promise<EmailBodyEncrypted> {
): Promise<EmailEncrypted> {
try {
const text = UTF8ToUint8(body.text);
const text = UTF8ToUint8(email.text);

const encryptedText = await encryptSymmetrically(encryptionKey, text, aux);
const encText = uint8ArrayToBase64(encryptedText);
const enc: EmailBodyEncrypted = { encText };
if (body.attachments) {
const promises = body.attachments.map((attachment) => {
const enc: EmailEncrypted = { encText };
if (email.attachments) {
const promises = email.attachments.map((attachment) => {
const binaryAttachment = UTF8ToUint8(attachment);
return encryptSymmetrically(encryptionKey, binaryAttachment, aux);
});
Expand All @@ -76,32 +75,32 @@ export async function encryptEmailBodyWithKey(
}

/**
* Decrypts symmetrically encrypted email body.
* Decrypts symmetrically encrypted email.
*
* @param encEmailBody - The email body to decrypt.
* @param encEmail - The email to decrypt.
* @param encryptionKey - The symmetric key to decrypt the email.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting decrypted email body
* @returns The resulting decrypted email
*/
export async function decryptEmailBody(
encEmailBody: EmailBodyEncrypted,
export async function decryptEmail(
encEmail: EmailEncrypted,
encryptionKey: Uint8Array,
aux?: Uint8Array,
): Promise<EmailBody> {
): Promise<Email> {
try {
const encText = base64ToUint8Array(encEmailBody.encText);
const encText = base64ToUint8Array(encEmail.encText);
const textArray = await decryptSymmetrically(encryptionKey, encText, aux);
const text = uint8ToUTF8(textArray);
const body: EmailBody = { text };
const email: Email = { text };

if (encEmailBody.encAttachments) {
const encAttachments = encEmailBody.encAttachments?.map(base64ToUint8Array);
if (encEmail.encAttachments) {
const encAttachments = encEmail.encAttachments?.map(base64ToUint8Array);
const promises = encAttachments?.map((encAtt) => decryptSymmetrically(encryptionKey, encAtt, aux));
const decryptedAttachments = await Promise.all(promises);
body.attachments = decryptedAttachments?.map((att) => uint8ToUTF8(att));
email.attachments = decryptedAttachments?.map((att) => uint8ToUTF8(att));
}

return body;
return email;
} catch (error) {
throw new EmailSymmetricDecryptionError(error instanceof Error ? error.message : String(error));
}
Expand Down
63 changes: 30 additions & 33 deletions src/email-crypto/coreSubject.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,82 @@
import { EmailBodyAndSubject, EmailBodyAndSubjectEncrypted } from '../types';
import { EmailAndSubject, EmailAndSubjectEncrypted } from '../types';
import { encryptSymmetrically, decryptSymmetrically, genSymmetricKey } from '../symmetric-crypto';
import { encryptEmailBodyWithKey, decryptEmailBody } from './core';
import { encryptEmailWithKey, decryptEmail } from './core';
import { UTF8ToUint8, base64ToUint8Array, uint8ArrayToBase64, uint8ToUTF8 } from '../utils';
import { InvalidInputEmail, EmailSymmetricDecryptionError, EmailSymmetricEncryptionError } from './errors';

/**
* Symmetrically encrypts email body and subject.
* Symmetrically encrypts email and subject.
*
* @param body - The email body and subject to encrypt.
* @param email - The email and subject to encrypt.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting encrypted email body and symmetric key used for encryption
* @returns The resulting encrypted email and symmetric key used for encryption
*/
export async function encryptEmailBodyAndSubject(
body: EmailBodyAndSubject,
export async function encryptEmailAndSubject(
email: EmailAndSubject,
aux?: Uint8Array,
): Promise<{
encEmailBody: EmailBodyAndSubjectEncrypted;
encEmail: EmailAndSubjectEncrypted;
encryptionKey: Uint8Array;
}> {
if (!email.text || !email.subject) {
throw new InvalidInputEmail();
}
try {
if (!body.text || !body.subject) {
throw new InvalidInputEmail();
}
const encryptionKey = genSymmetricKey();
const encEmailBody = await encryptEmailBodyAndSubjectWithKey(body, encryptionKey, aux);
const encEmail = await encryptEmailAndSubjectWithKey(email, encryptionKey, aux);

return { encEmailBody, encryptionKey };
return { encEmail, encryptionKey };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
throw new EmailSymmetricEncryptionError(error instanceof Error ? error.message : String(error));
}
}

/**
* Symmetrically encrypts email body and subject with the given key.
* Symmetrically encrypts email and subject with the given key.
*
* @param body - The email body and subject to encrypt.
* @param email - The email and subject to encrypt.
* @param encryptionKey - The symmetric key to encrypt the email.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting encrypted email body and symmetric key used for encryption
* @returns The resulting encrypted email and symmetric key used for encryption
*/
export async function encryptEmailBodyAndSubjectWithKey(
body: EmailBodyAndSubject,
export async function encryptEmailAndSubjectWithKey(
email: EmailAndSubject,
encryptionKey: Uint8Array,
aux?: Uint8Array,
): Promise<EmailBodyAndSubjectEncrypted> {
): Promise<EmailAndSubjectEncrypted> {
try {
const enc = await encryptEmailBodyWithKey(body, encryptionKey, aux);
const subject = UTF8ToUint8(body.subject);
const enc = await encryptEmailWithKey(email, encryptionKey, aux);
const subject = UTF8ToUint8(email.subject);
const subjectEnc = await encryptSymmetrically(encryptionKey, subject, aux);
const encSubject = uint8ArrayToBase64(subjectEnc);

return { ...enc, encSubject };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
throw new EmailSymmetricEncryptionError(error instanceof Error ? error.message : String(error));
}
}

/**
* Decrypts symmetrically encrypted email body and subject.
* Decrypts symmetrically encrypted email and email subject.
*
* @param encEmailBody - The email body and subject to decrypt.
* @param encEmail - The encrypted email and subject to decrypt.
* @param encryptionKey - The symmetric key to decrypt the email.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting decrypted email body
* @returns The resulting decrypted email and subject
*/
export async function decryptEmailBodyAndSubject(
encEmailBody: EmailBodyAndSubjectEncrypted,
export async function decryptEmailAndSubject(
encEmail: EmailAndSubjectEncrypted,
encryptionKey: Uint8Array,
aux?: Uint8Array,
): Promise<EmailBodyAndSubject> {
): Promise<EmailAndSubject> {
try {
const encSubject = base64ToUint8Array(encEmailBody.encSubject);
const encSubject = base64ToUint8Array(encEmail.encSubject);
const subjectArray = await decryptSymmetrically(encryptionKey, encSubject, aux);
const subject = uint8ToUTF8(subjectArray);
const body = await decryptEmailBody(encEmailBody, encryptionKey, aux);
const email = await decryptEmail(encEmail, encryptionKey, aux);

return { ...body, subject };
return { ...email, subject };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
throw new EmailSymmetricDecryptionError(error instanceof Error ? error.message : String(error));
}
}
Loading
Loading