diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79a8954..1a17117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0284f6e..83f5d94 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.github/workflows/sonar-analysis.yml b/.github/workflows/sonar-analysis.yml index 2334c95..6e0dc40 100644 --- a/.github/workflows/sonar-analysis.yml +++ b/.github/workflows/sonar-analysis.yml @@ -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 @@ -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) diff --git a/README.md b/README.md index 6727a8b..3a43ab0 100644 --- a/README.md +++ b/README.md @@ -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'], }; @@ -119,10 +119,10 @@ 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'], @@ -130,7 +130,7 @@ const emailAndSubject: EmailBodyAndSubject = { 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); @@ -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); - ``` \ No newline at end of file diff --git a/src/email-crypto/core.ts b/src/email-crypto/core.ts index 4a960f4..9f4c138 100644 --- a/src/email-crypto/core.ts +++ b/src/email-crypto/core.ts @@ -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'; @@ -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 { +): Promise { 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); }); @@ -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 { +): Promise { 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)); } diff --git a/src/email-crypto/coreSubject.ts b/src/email-crypto/coreSubject.ts index ab129f4..f0002eb 100644 --- a/src/email-crypto/coreSubject.ts +++ b/src/email-crypto/coreSubject.ts @@ -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 { +): Promise { 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 { +): Promise { 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)); } } diff --git a/src/email-crypto/hybridEncryptedEmailAndSubject.ts b/src/email-crypto/hybridEncryptedEmailAndSubject.ts index 9c1033c..180a011 100644 --- a/src/email-crypto/hybridEncryptedEmailAndSubject.ts +++ b/src/email-crypto/hybridEncryptedEmailAndSubject.ts @@ -1,6 +1,6 @@ -import { RecipientWithPublicKey, EmailBodyAndSubject, HybridEncryptedEmailAndSubject } from '../types'; +import { RecipientWithPublicKey, EmailAndSubject, HybridEncryptedEmailAndSubject } from '../types'; import { encryptKeysHybrid, decryptKeysHybrid } from './core'; -import { encryptEmailBodyAndSubject, decryptEmailBodyAndSubject } from './coreSubject'; +import { encryptEmailAndSubject, decryptEmailAndSubject } from './coreSubject'; import { FailedToDecryptEmail, FailedToEncryptEmail, @@ -12,22 +12,22 @@ import { } from './errors'; /** - * Encrypts the email body and its subject using hybrid encryption. + * Encrypts the email and its subject using hybrid encryption. * - * @param body - The email body and subject to encrypt. + * @param email - The email and subject to encrypt. * @param recipientPublicKeys - The public keys of the recipient. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The encrypted email body + * @returns The encrypted email and subject */ export async function encryptEmailAndSubjectHybrid( - body: EmailBodyAndSubject, + email: EmailAndSubject, recipient: RecipientWithPublicKey, aux?: Uint8Array, ): Promise { try { - const { encryptionKey, encEmailBody } = await encryptEmailBodyAndSubject(body, aux); + const { encryptionKey, encEmail } = await encryptEmailAndSubject(email, aux); const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient); - return { encEmailBody, encryptedKey }; + return { encEmail, encryptedKey }; } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailSymmetricEncryptionError) throw error; @@ -37,15 +37,15 @@ export async function encryptEmailAndSubjectHybrid( } /** - * Encrypts the email body and its subject using hybrid encryption for multiple recipients. + * Encrypts the email and its subject using hybrid encryption for multiple recipients. * - * @param body - The email body and subject to encrypt for multiple recipients. + * @param email - The email and subject to encrypt for multiple recipients. * @param recipients - The recipients with corresponding public keys. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The set of encrypted email bodies + * @returns The set of encrypted emails and subjects */ export async function encryptEmailAndSubjectHybridForMultipleRecipients( - body: EmailBodyAndSubject, + email: EmailAndSubject, recipients: RecipientWithPublicKey[], aux?: Uint8Array, ): Promise { @@ -53,13 +53,13 @@ export async function encryptEmailAndSubjectHybridForMultipleRecipients( if (!recipients || recipients.length === 0) { throw new InvalidInputEmail(); } - const { encryptionKey, encEmailBody } = await encryptEmailBodyAndSubject(body, aux); + const { encryptionKey, encEmail } = await encryptEmailAndSubject(email, aux); const encryptedEmails: HybridEncryptedEmailAndSubject[] = []; for (const recipient of recipients) { const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient); encryptedEmails.push({ - encEmailBody: encEmailBody, + encEmail, encryptedKey, }); } @@ -78,16 +78,16 @@ export async function encryptEmailAndSubjectHybridForMultipleRecipients( * @param hybridEmail - The encrypted email and subject. * @param recipientPrivateHybridKeys - The private key of the recipient. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The decrypted email body + * @returns The decrypted email and subject */ export async function decryptEmailAndSubjectHybrid( hybridEmail: HybridEncryptedEmailAndSubject, recipientPrivateHybridKeys: Uint8Array, aux?: Uint8Array, -): Promise { +): Promise { try { const encryptionKey = await decryptKeysHybrid(hybridEmail.encryptedKey, recipientPrivateHybridKeys); - return await decryptEmailBodyAndSubject(hybridEmail.encEmailBody, encryptionKey, aux); + return await decryptEmailAndSubject(hybridEmail.encEmail, encryptionKey, aux); } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailHybridDecryptionError) throw error; diff --git a/src/email-crypto/hybridEncyptedEmail.ts b/src/email-crypto/hybridEncyptedEmail.ts index 31440a4..75be566 100644 --- a/src/email-crypto/hybridEncyptedEmail.ts +++ b/src/email-crypto/hybridEncyptedEmail.ts @@ -1,5 +1,5 @@ -import { HybridEncryptedEmail, EmailBody, RecipientWithPublicKey } from '../types'; -import { decryptEmailBody, encryptKeysHybrid, decryptKeysHybrid, encryptEmailBody } from './core'; +import { HybridEncryptedEmail, Email, RecipientWithPublicKey } from '../types'; +import { decryptEmail, encryptKeysHybrid, decryptKeysHybrid, encryptEmail } from './core'; import { FailedToDecryptEmail, FailedToEncryptEmail, @@ -10,22 +10,22 @@ import { EmailSymmetricEncryptionError, } from './errors'; /** - * Encrypts the email body using hybrid encryption. + * Encrypts the email using hybrid encryption. * - * @param body - The email body to encrypt. + * @param email - The email to encrypt. * @param recipientPublicKeys - The public keys of the recipient. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The encrypted email body + * @returns The encrypted email */ export async function encryptEmailHybrid( - body: EmailBody, + email: Email, recipient: RecipientWithPublicKey, aux?: Uint8Array, ): Promise { try { - const { encryptionKey, encEmailBody } = await encryptEmailBody(body, aux); + const { encryptionKey, encEmail } = await encryptEmail(email, aux); const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient); - return { encEmailBody, encryptedKey }; + return { encEmail, encryptedKey }; } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailSymmetricEncryptionError) throw error; @@ -35,15 +35,15 @@ export async function encryptEmailHybrid( } /** - * Encrypts the email body using hybrid encryption for multiple recipients. + * Encrypts the email using hybrid encryption for multiple recipients. * - * @param body - The email body to encrypt for multiple recipients. + * @param email - The email to encrypt for multiple recipients. * @param recipients - The recipients with corresponding public keys. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The set of encrypted email bodies + * @returns The set of encrypted emails */ export async function encryptEmailHybridForMultipleRecipients( - body: EmailBody, + email: Email, recipients: RecipientWithPublicKey[], aux?: Uint8Array, ): Promise { @@ -51,13 +51,13 @@ export async function encryptEmailHybridForMultipleRecipients( if (!recipients || recipients.length === 0) { throw new InvalidInputEmail(); } - const { encryptionKey, encEmailBody } = await encryptEmailBody(body, aux); + const { encryptionKey, encEmail } = await encryptEmail(email, aux); const encryptedEmails: HybridEncryptedEmail[] = []; for (const recipient of recipients) { const encryptedKey = await encryptKeysHybrid(encryptionKey, recipient); encryptedEmails.push({ - encEmailBody: encEmailBody, + encEmail, encryptedKey, }); } @@ -73,20 +73,19 @@ export async function encryptEmailHybridForMultipleRecipients( /** * Decrypts the email using hybrid encryption. * - * @param encEmailBody - The encrypted email. + * @param encEmail - The encrypted email. * @param recipientPrivateHybridKeys - The private key of the recipient. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The decrypted email body + * @returns The decrypted email */ export async function decryptEmailHybrid( - encEmailBody: HybridEncryptedEmail, + encEmail: HybridEncryptedEmail, recipientPrivateHybridKeys: Uint8Array, aux?: Uint8Array, -): Promise { +): Promise { try { - const encryptionKey = await decryptKeysHybrid(encEmailBody.encryptedKey, recipientPrivateHybridKeys); - const body = await decryptEmailBody(encEmailBody.encEmailBody, encryptionKey, aux); - return body; + const encryptionKey = await decryptKeysHybrid(encEmail.encryptedKey, recipientPrivateHybridKeys); + return await decryptEmail(encEmail.encEmail, encryptionKey, aux); } catch (error) { if (error instanceof EmailHybridDecryptionError) throw error; if (error instanceof EmailSymmetricDecryptionError) throw error; diff --git a/src/email-crypto/pwdProtectedEmail.ts b/src/email-crypto/pwdProtectedEmail.ts index c9ed5cc..ef2d3df 100644 --- a/src/email-crypto/pwdProtectedEmail.ts +++ b/src/email-crypto/pwdProtectedEmail.ts @@ -1,5 +1,5 @@ -import { PwdProtectedEmail, EmailBody } from '../types'; -import { decryptEmailBody, passwordProtectKey, removePasswordProtection, encryptEmailBody } from './core'; +import { PwdProtectedEmail, Email } from '../types'; +import { decryptEmail, passwordProtectKey, removePasswordProtection, encryptEmail } from './core'; import { EmailSymmetricEncryptionError, FailedToDecryptEmail, @@ -18,15 +18,15 @@ import { * @returns The password-protected email */ export async function createPwdProtectedEmail( - emailBody: EmailBody, + email: Email, password: string, aux?: Uint8Array, ): Promise { try { - const { encryptionKey, encEmailBody } = await encryptEmailBody(emailBody, aux); + const { encryptionKey, encEmail } = await encryptEmail(email, aux); const encryptedKey = await passwordProtectKey(encryptionKey, password); - return { encEmailBody, encryptedKey }; + return { encEmail, encryptedKey }; } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailSymmetricEncryptionError) throw error; @@ -41,16 +41,16 @@ export async function createPwdProtectedEmail( * @param encryptedEmail - The encrypted email * @param password - The secret password shared among recipients. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The decrypted email body + * @returns The decrypted email */ export async function decryptPwdProtectedEmail( encryptedEmail: PwdProtectedEmail, password: string, aux?: Uint8Array, -): Promise { +): Promise { try { const encryptionKey = await removePasswordProtection(encryptedEmail.encryptedKey, password); - return await decryptEmailBody(encryptedEmail.encEmailBody, encryptionKey, aux); + return await decryptEmail(encryptedEmail.encEmail, encryptionKey, aux); } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailPasswordOpenError) throw error; diff --git a/src/email-crypto/pwdProtectedEmailAndSubject.ts b/src/email-crypto/pwdProtectedEmailAndSubject.ts index 77bc069..2bdf596 100644 --- a/src/email-crypto/pwdProtectedEmailAndSubject.ts +++ b/src/email-crypto/pwdProtectedEmailAndSubject.ts @@ -1,6 +1,6 @@ -import { EmailBodyAndSubject, PwdProtectedEmailAndSubject } from '../types'; +import { EmailAndSubject, PwdProtectedEmailAndSubject } from '../types'; import { passwordProtectKey, removePasswordProtection } from './core'; -import { encryptEmailBodyAndSubject, decryptEmailBodyAndSubject } from './coreSubject'; +import { encryptEmailAndSubject, decryptEmailAndSubject } from './coreSubject'; import { FailedToDecryptEmail, FailedToEncryptEmail, @@ -20,15 +20,15 @@ import { * @returns The password-protected email */ export async function createPwdProtectedEmailAndSubject( - emailBody: EmailBodyAndSubject, + email: EmailAndSubject, password: string, aux?: Uint8Array, ): Promise { try { - const { encryptionKey, encEmailBody } = await encryptEmailBodyAndSubject(emailBody, aux); + const { encryptionKey, encEmail } = await encryptEmailAndSubject(email, aux); const encryptedKey = await passwordProtectKey(encryptionKey, password); - return { encEmailBody, encryptedKey }; + return { encEmail, encryptedKey }; } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailSymmetricEncryptionError) throw error; @@ -43,18 +43,16 @@ export async function createPwdProtectedEmailAndSubject( * @param encryptedEmail - The encrypted email and subject * @param password - The secret password shared among recipients. * @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp). - * @returns The decrypted email body + * @returns The decrypted email and subject */ export async function decryptPwdProtectedEmailAndSubject( encryptedEmail: PwdProtectedEmailAndSubject, password: string, aux?: Uint8Array, -): Promise { +): Promise { try { const encryptionKey = await removePasswordProtection(encryptedEmail.encryptedKey, password); - - const body = await decryptEmailBodyAndSubject(encryptedEmail.encEmailBody, encryptionKey, aux); - return body; + return await decryptEmailAndSubject(encryptedEmail.encEmail, encryptionKey, aux); } catch (error) { if (error instanceof InvalidInputEmail) throw error; if (error instanceof EmailPasswordOpenError) throw error; diff --git a/src/index.ts b/src/index.ts index 6e5b921..c3ec703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,12 +13,12 @@ export { createPwdProtectedEmailAndSubject, decryptPwdProtectedEmailAndSubject, generateEmailKeys, - decryptEmailBody, - encryptEmailBody, - encryptEmailBodyWithKey, - encryptEmailBodyAndSubject, - encryptEmailBodyAndSubjectWithKey, - decryptEmailBodyAndSubject, + decryptEmail, + encryptEmail, + encryptEmailWithKey, + encryptEmailAndSubject, + encryptEmailAndSubjectWithKey, + decryptEmailAndSubject, deriveDatabaseKey, deriveEmailDraftKey, FailedToEncryptEmail, diff --git a/src/types.ts b/src/types.ts index d509628..47e7843 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,22 +17,22 @@ export type HybridKeyPair = { export type HybridEncryptedEmail = { encryptedKey: HybridEncKey; - encEmailBody: EmailBodyEncrypted; + encEmail: EmailEncrypted; }; export type HybridEncryptedEmailAndSubject = { encryptedKey: HybridEncKey; - encEmailBody: EmailBodyAndSubjectEncrypted; + encEmail: EmailAndSubjectEncrypted; }; export type PwdProtectedEmail = { encryptedKey: PwdProtectedKey; - encEmailBody: EmailBodyEncrypted; + encEmail: EmailEncrypted; }; export type PwdProtectedEmailAndSubject = { encryptedKey: PwdProtectedKey; - encEmailBody: EmailBodyAndSubjectEncrypted; + encEmail: EmailAndSubjectEncrypted; }; export type HybridEncKey = { @@ -46,21 +46,21 @@ export type PwdProtectedKey = { salt: string; }; -export type EmailBodyEncrypted = { +export type EmailEncrypted = { encText: string; encAttachments?: string[]; }; -export type EmailBodyAndSubjectEncrypted = EmailBodyEncrypted & { +export type EmailAndSubjectEncrypted = EmailEncrypted & { encSubject: string; }; -export type EmailBody = { +export type Email = { text: string; attachments?: string[]; }; -export type EmailBodyAndSubject = EmailBody & { +export type EmailAndSubject = Email & { subject: string; }; diff --git a/tests/email-crypto/core.test.ts b/tests/email-crypto/core.test.ts index a99d0ad..015b34e 100644 --- a/tests/email-crypto/core.test.ts +++ b/tests/email-crypto/core.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { EmailBody, EmailBodyAndSubject } from '../../src/types'; +import { Email, EmailAndSubject } from '../../src/types'; import { - decryptEmailBody, - encryptEmailBody, + decryptEmail, + encryptEmail, deriveDatabaseKey, deriveEmailDraftKey, - encryptEmailBodyAndSubject, - decryptEmailBodyAndSubject, + encryptEmailAndSubject, + decryptEmailAndSubject, } from '../../src/email-crypto'; import { genMnemonic } from '../../src/utils'; import { genSymmetricKey } from '../../src/symmetric-crypto'; @@ -14,47 +14,47 @@ import { AES_KEY_BYTE_LENGTH } from '../../src/constants'; import { EmailSymmetricDecryptionError, InvalidInputEmail } from '../../src/email-crypto/errors'; describe('Test email crypto functions', () => { - const emailBody: EmailBody = { - text: 'test body', + const email: Email = { + text: 'test email', }; - const emailBodyAndSubject: EmailBodyAndSubject = { - text: 'test body', - subject: 'test subject', + const emailAndSubject: EmailAndSubject = { + text: 'test email text', + subject: 'test email subject', }; const aux = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); it('should encrypt and decrypt email', async () => { - const { encEmailBody, encryptionKey } = await encryptEmailBody(emailBody, aux); - const result = await decryptEmailBody(encEmailBody, encryptionKey, aux); - expect(result).toEqual(emailBody); + const { encEmail, encryptionKey } = await encryptEmail(email, aux); + const result = await decryptEmail(encEmail, encryptionKey, aux); + expect(result).toEqual(email); }); it('should encrypt and decrypt email with subject', async () => { - const { encEmailBody, encryptionKey } = await encryptEmailBodyAndSubject(emailBodyAndSubject, aux); - const result = await decryptEmailBodyAndSubject(encEmailBody, encryptionKey, aux); - expect(result).toEqual(emailBodyAndSubject); + const { encEmail, encryptionKey } = await encryptEmailAndSubject(emailAndSubject, aux); + const result = await decryptEmailAndSubject(encEmail, encryptionKey, aux); + expect(result).toEqual(emailAndSubject); }); it('should throw an error if decryption fails', async () => { - const { encEmailBody, encryptionKey } = await encryptEmailBody(emailBody, aux); + const { encEmail, encryptionKey } = await encryptEmail(email, aux); const badEncryptionKey = await genSymmetricKey(); - await expect(decryptEmailBody(encEmailBody, badEncryptionKey, aux)).rejects.toThrow(EmailSymmetricDecryptionError); + await expect(decryptEmail(encEmail, badEncryptionKey, aux)).rejects.toThrow(EmailSymmetricDecryptionError); const badAux = new Uint8Array([4, 5, 6, 7, 8]); - await expect(decryptEmailBody(encEmailBody, encryptionKey, badAux)).rejects.toThrow(EmailSymmetricDecryptionError); + await expect(decryptEmail(encEmail, encryptionKey, badAux)).rejects.toThrow(EmailSymmetricDecryptionError); }); it('should throw an error if decryption fails', async () => { - const { encEmailBody, encryptionKey } = await encryptEmailBodyAndSubject(emailBodyAndSubject, aux); + const { encEmail, encryptionKey } = await encryptEmailAndSubject(emailAndSubject, aux); const badEncryptionKey = await genSymmetricKey(); - await expect(decryptEmailBodyAndSubject(encEmailBody, badEncryptionKey, aux)).rejects.toThrow( + await expect(decryptEmailAndSubject(encEmail, badEncryptionKey, aux)).rejects.toThrow( EmailSymmetricDecryptionError, ); const badAux = new Uint8Array([4, 5, 6, 7, 8]); - await expect(decryptEmailBodyAndSubject(encEmailBody, encryptionKey, badAux)).rejects.toThrow( + await expect(decryptEmailAndSubject(encEmail, encryptionKey, badAux)).rejects.toThrow( EmailSymmetricDecryptionError, ); }); @@ -63,8 +63,8 @@ describe('Test email crypto functions', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const badEmail: any = {}; badEmail.self = badEmail; - await expect(encryptEmailBody(badEmail, aux)).rejects.toThrow(InvalidInputEmail); - await expect(encryptEmailBodyAndSubject(badEmail, aux)).rejects.toThrow(InvalidInputEmail); + await expect(encryptEmail(badEmail, aux)).rejects.toThrow(InvalidInputEmail); + await expect(encryptEmailAndSubject(badEmail, aux)).rejects.toThrow(InvalidInputEmail); }); it('should derive symmetric key for database encryption', async () => { diff --git a/tests/email-crypto/hybridEmail.test.ts b/tests/email-crypto/hybridEmail.test.ts index 8e9d3f6..d76ccbf 100644 --- a/tests/email-crypto/hybridEmail.test.ts +++ b/tests/email-crypto/hybridEmail.test.ts @@ -10,10 +10,10 @@ import { } from '../../src/email-crypto'; import { - EmailBody, + Email, HybridEncryptedEmail, HybridEncKey, - EmailBodyAndSubject, + EmailAndSubject, HybridEncryptedEmailAndSubject, RecipientWithPublicKey, } from '../../src/types'; @@ -25,13 +25,13 @@ import { } from '../../src/email-crypto/errors'; describe('Test email crypto functions', async () => { - const email: EmailBody = { - text: 'test body', + const email: Email = { + text: 'test email text', }; - const emailAndSubject: EmailBodyAndSubject = { - text: 'test body', - subject: 'test subject', + const emailAndSubject: EmailAndSubject = { + text: 'test email text', + subject: 'test email subject', }; const { secretKey: alicePrivateKeys, publicKey: alicePublicKeys } = await generateEmailKeys(); @@ -56,7 +56,7 @@ describe('Test email crypto functions', async () => { it('should encrypt and decrypt email and subject sucessfully', async () => { const encryptedEmail = await encryptEmailAndSubjectHybrid(emailAndSubject, bobWithPublicKeys); - expect(encryptedEmail.encEmailBody?.encSubject).not.toBe(emailAndSubject.subject); + expect(encryptedEmail.encEmail?.encSubject).not.toBe(emailAndSubject.subject); const decryptedEmail = await decryptEmailAndSubjectHybrid(encryptedEmail, bobPrivateKeys); expect(decryptedEmail).toStrictEqual(emailAndSubject); @@ -88,14 +88,14 @@ describe('Test email crypto functions', async () => { }; const badEncryptedEmail: HybridEncryptedEmail = { encryptedKey: encKey, - encEmailBody: { + encEmail: { encText: 'mock encrypted text', }, }; const badEncryptedEmailAndSubject: HybridEncryptedEmailAndSubject = { encryptedKey: encKey, - encEmailBody: { + encEmail: { encText: 'mock encrypted text', encSubject: 'mock encrypted subject', }, @@ -115,7 +115,7 @@ describe('Test email crypto functions', async () => { ]); expect(encryptedEmail.length).toBe(2); - expect(encryptedEmail[0].encEmailBody).toBe(encryptedEmail[1].encEmailBody); + expect(encryptedEmail[0].encEmail).toBe(encryptedEmail[1].encEmail); }); it('should encrypt email and subject to multiple senders sucessfully', async () => { @@ -125,7 +125,7 @@ describe('Test email crypto functions', async () => { ]); expect(encryptedEmail.length).toBe(2); - expect(encryptedEmail[0].encEmailBody).toBe(encryptedEmail[1].encEmailBody); + expect(encryptedEmail[0].encEmail).toBe(encryptedEmail[1].encEmail); const emailDecryptedByBob = await decryptEmailAndSubjectHybrid(encryptedEmail[0], bobPrivateKeys); const emailDecryptedByAlice = await decryptEmailAndSubjectHybrid(encryptedEmail[1], alicePrivateKeys); @@ -170,18 +170,18 @@ describe('Test email crypto functions', async () => { }); it('should throw an error if input is invalid', async () => { - await expect(encryptEmailHybrid({} as EmailBody, bobWithPublicKeys)).rejects.toThrow(InvalidInputEmail); + await expect(encryptEmailHybrid({} as Email, bobWithPublicKeys)).rejects.toThrow(InvalidInputEmail); - await expect(encryptEmailAndSubjectHybrid({} as EmailBodyAndSubject, bobWithPublicKeys)).rejects.toThrow( + await expect(encryptEmailAndSubjectHybrid({} as EmailAndSubject, bobWithPublicKeys)).rejects.toThrow( InvalidInputEmail, ); await expect( - encryptEmailHybridForMultipleRecipients({} as EmailBody, [bobWithPublicKeys, aliceWithPublicKeys]), + encryptEmailHybridForMultipleRecipients({} as Email, [bobWithPublicKeys, aliceWithPublicKeys]), ).rejects.toThrow(InvalidInputEmail); await expect( - encryptEmailAndSubjectHybridForMultipleRecipients({} as EmailBodyAndSubject, [ + encryptEmailAndSubjectHybridForMultipleRecipients({} as EmailAndSubject, [ bobWithPublicKeys, aliceWithPublicKeys, ]), @@ -200,7 +200,7 @@ describe('Test email crypto functions', async () => { const encryptedEmail = await encryptEmailHybrid(emailAndSubject, bobWithPublicKeys); const modifiedCiphertext = encryptedEmail; - modifiedCiphertext.encEmailBody.encText += 'modified ciphertext'; + modifiedCiphertext.encEmail.encText += 'modified ciphertext'; await expect(decryptEmailHybrid(modifiedCiphertext, bobPrivateKeys)).rejects.toThrow(EmailSymmetricDecryptionError); const modifiedKey = encryptedEmail; @@ -212,7 +212,7 @@ describe('Test email crypto functions', async () => { const encryptedEmail = await encryptEmailAndSubjectHybrid(emailAndSubject, bobWithPublicKeys); const modifiedCiphertext = encryptedEmail; - modifiedCiphertext.encEmailBody.encText += 'modified ciphertext'; + modifiedCiphertext.encEmail.encText += 'modified ciphertext'; await expect(decryptEmailAndSubjectHybrid(modifiedCiphertext, bobPrivateKeys)).rejects.toThrow( EmailSymmetricDecryptionError, ); diff --git a/tests/email-crypto/pwdProtectedEmail.test.ts b/tests/email-crypto/pwdProtectedEmail.test.ts index 7c38251..dadb3c4 100644 --- a/tests/email-crypto/pwdProtectedEmail.test.ts +++ b/tests/email-crypto/pwdProtectedEmail.test.ts @@ -8,14 +8,14 @@ import { EmailSymmetricDecryptionError, InvalidInputEmail, } from '../../src/email-crypto'; -import { EmailBody, EmailBodyAndSubject } from '../../src/types'; +import { Email, EmailAndSubject } from '../../src/types'; describe('Test email crypto functions', () => { - const email: EmailBody = { + const email: Email = { text: 'Hi Bob, This is a test message. -Alice.', }; - const emailAndSubject: EmailBodyAndSubject = { + const emailAndSubject: EmailAndSubject = { text: 'Hi Bob, This is a test message. -Alice.', subject: 'test subject', }; @@ -35,8 +35,8 @@ describe('Test email crypto functions', () => { }); it('should throw an error if encryption fails', async () => { - await expect(createPwdProtectedEmail({} as unknown as EmailBody, sharedSecret)).rejects.toThrow(InvalidInputEmail); - await expect(createPwdProtectedEmailAndSubject({} as unknown as EmailBodyAndSubject, sharedSecret)).rejects.toThrow( + await expect(createPwdProtectedEmail({} as unknown as Email, sharedSecret)).rejects.toThrow(InvalidInputEmail); + await expect(createPwdProtectedEmailAndSubject({} as unknown as EmailAndSubject, sharedSecret)).rejects.toThrow( InvalidInputEmail, ); }); @@ -56,7 +56,7 @@ describe('Test email crypto functions', () => { const encryptedEmail = await createPwdProtectedEmail(email, sharedSecret); const modifiedCiphertext = encryptedEmail; - modifiedCiphertext.encEmailBody.encText += 'modified ciphertext'; + modifiedCiphertext.encEmail.encText += 'modified ciphertext'; await expect(decryptPwdProtectedEmail(modifiedCiphertext, sharedSecret)).rejects.toThrow( EmailSymmetricDecryptionError, ); @@ -66,7 +66,7 @@ describe('Test email crypto functions', () => { const encryptedEmail = await createPwdProtectedEmailAndSubject(emailAndSubject, sharedSecret); const modifiedCiphertext = encryptedEmail; - modifiedCiphertext.encEmailBody.encText += 'modified ciphertext'; + modifiedCiphertext.encEmail.encText += 'modified ciphertext'; await expect(decryptPwdProtectedEmailAndSubject(modifiedCiphertext, sharedSecret)).rejects.toThrow( EmailSymmetricDecryptionError, ); diff --git a/tests/email-crypto/pwdProtectedEmailCoreErrors.test.ts b/tests/email-crypto/pwdProtectedEmailCoreErrors.test.ts new file mode 100644 index 0000000..792eb7e --- /dev/null +++ b/tests/email-crypto/pwdProtectedEmailCoreErrors.test.ts @@ -0,0 +1,66 @@ +import { vi, it, describe, beforeEach, expect } from 'vitest'; +import { + createPwdProtectedEmail, + createPwdProtectedEmailAndSubject, + decryptPwdProtectedEmail, + decryptPwdProtectedEmailAndSubject, +} from '../../src/email-crypto'; +import { FailedToDecryptEmail, FailedToEncryptEmail } from '../../src/email-crypto/errors'; +import { Email, EmailAndSubject, PwdProtectedEmail, PwdProtectedEmailAndSubject } from '../../src/types'; +import * as core from '../../src/email-crypto/core'; + +vi.mock('../../src/email-crypto/core', async () => { + const actual = await vi.importActual('../../src/email-crypto/core'); + + return { + ...actual, + passwordProtectKey: vi.fn(), + removePasswordProtection: vi.fn(), + }; +}); + +describe('Test email crypto functions', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + const email: Email = { + text: 'Hi Bob, This is a test message. -Alice.', + attachments: ['file1.txt', 'file2.txt'], + }; + + const emailAndSubject: EmailAndSubject = { + text: 'Hi Bob, This is a test message. -Alice.', + attachments: ['file1.txt', 'file2.txt'], + subject: 'test subject', + }; + + const sharedSecret = 'test shared secret'; + + it('throws FailedToEncryptEmail when encryption fails', async () => { + const spy = vi.spyOn(core, 'passwordProtectKey'); + + spy.mockRejectedValue(new Error('passwordProtectKey: unexpected failure')); + + await expect(createPwdProtectedEmail(email, sharedSecret)).rejects.toBeInstanceOf(FailedToEncryptEmail); + + await expect(createPwdProtectedEmailAndSubject(emailAndSubject, sharedSecret)).rejects.toBeInstanceOf( + FailedToEncryptEmail, + ); + }); + + it('throws FailedToDecryptEmail when decryption fails', async () => { + const encryptedEmail = {} as PwdProtectedEmail; + const encryptedEmailAndSubject = {} as PwdProtectedEmailAndSubject; + + const spy = vi.spyOn(core, 'removePasswordProtection'); + + spy.mockRejectedValue(new Error('removePasswordProtection: unexpected failure')); + + await expect(decryptPwdProtectedEmail(encryptedEmail, sharedSecret)).rejects.toBeInstanceOf(FailedToDecryptEmail); + + await expect(decryptPwdProtectedEmailAndSubject(encryptedEmailAndSubject, sharedSecret)).rejects.toBeInstanceOf( + FailedToDecryptEmail, + ); + }); +}); diff --git a/tests/email-crypto/pwdProtectedEmailNobleErrors.test.ts b/tests/email-crypto/pwdProtectedEmailNobleErrors.test.ts new file mode 100644 index 0000000..b9d176f --- /dev/null +++ b/tests/email-crypto/pwdProtectedEmailNobleErrors.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createPwdProtectedEmail, + createPwdProtectedEmailAndSubject, + EmailPasswordProtectError, + EmailSymmetricEncryptionError, +} from '../../src/email-crypto'; +import { Email, EmailAndSubject } from '../../src/types'; +import * as nobleUtils from '@noble/hashes/utils.js'; +import * as nobleWrapper from '@noble/ciphers/aes.js'; + +// Noble is ESM module and doesn't work with spyOn directly (module namespace is not configurable in ESM), must be mocked before. +// To mock it but keep the original implementation for most tests, we use importActual. +// vi.resetAllMocks(); before each test is a must to reset the mock back to importActual. +vi.mock('@noble/hashes/utils.js', async () => { + const actual = await vi.importActual('@noble/hashes/utils.js'); + + return { + ...actual, + randomBytes: vi.fn(actual.randomBytes), + }; +}); + +vi.mock('@noble/ciphers/aes.js', async () => { + const actual = await vi.importActual('@noble/ciphers/aes.js'); + + return { + ...actual, + aeskw: vi.fn(actual.aeskw), + }; +}); + +describe('Test email crypto functions', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + const email: Email = { + text: 'Hi Bob, This is a test message. -Alice.', + }; + + const emailAndSubject: EmailAndSubject = { + text: 'Hi Bob, This is a test message. -Alice.', + subject: 'test subject', + }; + + const sharedSecret = 'test shared secret'; + + it('throws EmailSymmetricEncryptionError when symmetric encryption fails', async () => { + vi.spyOn(nobleUtils, 'randomBytes').mockImplementation(() => { + throw new Error('randomBytes: unexpected crypto failure'); + }); + + await expect(createPwdProtectedEmail(email, sharedSecret)).rejects.toThrow(EmailSymmetricEncryptionError); + await expect(createPwdProtectedEmailAndSubject(emailAndSubject, sharedSecret)).rejects.toThrow( + EmailSymmetricEncryptionError, + ); + }); + + it('throws EmailPasswordProtectError when key wrapping fails', async () => { + vi.spyOn(nobleWrapper, 'aeskw').mockImplementation(() => { + throw new Error('aeskw: unexpected crypto failure'); + }); + + await expect(createPwdProtectedEmail(email, sharedSecret)).rejects.toThrow(EmailPasswordProtectError); + await expect(createPwdProtectedEmailAndSubject(emailAndSubject, sharedSecret)).rejects.toThrow( + EmailPasswordProtectError, + ); + }); +});