diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index b834e6cf918..f5cab2a84b9 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -26,27 +26,18 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v7 - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. with: - fetch-depth: 2 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - - name: Install ESLint - run: | - npm install @microsoft/eslint-formatter-sarif@3.1.0 - npm ci --ignore-scripts + - name: Install dependencies + run: npm ci --ignore-scripts - name: Run ESLint - run: SARIF_ESLINT_IGNORE_SUPPRESSED=true npm run test_eslint_ci + run: npm run test_eslint_ci continue-on-error: true - name: Upload analysis results to GitHub uses: github/codeql-action/upload-sarif@v4 with: sarif_file: eslint-results.sarif - wait-for-processing: true \ No newline at end of file + wait-for-processing: true diff --git a/conf/tsconfig.content_scripts.json b/conf/tsconfig.content_scripts.json index 530c4506bd5..507dfd129e5 100644 --- a/conf/tsconfig.content_scripts.json +++ b/conf/tsconfig.content_scripts.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2025", + "lib": ["ES2025", "DOM", "DOM.Iterable", "DOM.AsyncIterable", "WebWorker.ImportScripts", "ScriptHost", "ESNext.TypedArrays"], "module": "es2022", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, diff --git a/conf/tsconfig.test.json b/conf/tsconfig.test.json index 3e2adc23756..50f38267487 100644 --- a/conf/tsconfig.test.json +++ b/conf/tsconfig.test.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "lib": ["es6", "dom"], + "lib": ["es2022", "dom", "es2024.promise", "es2024.string", "es2025.regexp", "esnext.typedarrays"], "alwaysStrict": true, "noImplicitAny": true, "noFallthroughCasesInSwitch": true, diff --git a/extension/js/common/api/email-provider/gmail/gmail-parser.ts b/extension/js/common/api/email-provider/gmail/gmail-parser.ts index e870398187a..67389a5c0af 100644 --- a/extension/js/common/api/email-provider/gmail/gmail-parser.ts +++ b/extension/js/common/api/email-provider/gmail/gmail-parser.ts @@ -135,7 +135,7 @@ export namespace GmailRes { export class GmailParser { public static findHeader = (apiGmailMsgObj: GmailRes.GmailMsg | GmailRes.GmailMsg$payload, headerName: string) => { - const node: GmailRes.GmailMsg$payload = apiGmailMsgObj.hasOwnProperty('payload') + const node: GmailRes.GmailMsg$payload = Object.hasOwn(apiGmailMsgObj, 'payload') ? (apiGmailMsgObj as GmailRes.GmailMsg).payload! // eslint-disable-line @typescript-eslint/no-non-null-assertion : (apiGmailMsgObj as GmailRes.GmailMsg$payload); if (typeof node.headers !== 'undefined') { @@ -177,7 +177,7 @@ export class GmailParser { } } } - if ('body' in msgOrPayloadOrPart && msgOrPayloadOrPart.body?.hasOwnProperty('attachmentId')) { + if ('body' in msgOrPayloadOrPart && msgOrPayloadOrPart.body && Object.hasOwn(msgOrPayloadOrPart.body, 'attachmentId')) { const payload = msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part; const treatAs = Attachment.treatAsForPgpEncryptedAttachments(payload.mimeType, pgpEncryptedIndex); const inline = (GmailParser.findHeader(payload, 'content-disposition') || '').toLowerCase().startsWith('inline'); diff --git a/extension/js/common/api/email-provider/gmail/gmail.ts b/extension/js/common/api/email-provider/gmail/gmail.ts index 195305ed26e..4f5bacbad50 100644 --- a/extension/js/common/api/email-provider/gmail/gmail.ts +++ b/extension/js/common/api/email-provider/gmail/gmail.ts @@ -220,17 +220,16 @@ export class Gmail extends EmailProviderApi implements EmailProviderInterface { reject(new Error('Chunk response could not be parsed')); return; } - for (let i = 0; parsedJsonDataField && i < 50; i++) { - try { - resolve(Buf.fromBase64UrlStr(parsedJsonDataField)); - return; - } catch { - // the chunk of data may have been cut at an inconvenient index - // shave off up to 50 trailing characters until it can be decoded - parsedJsonDataField = parsedJsonDataField.slice(0, -1); - } + const base64Remainder = parsedJsonDataField.length % 4; + if (base64Remainder === 1 || (base64Remainder === 3 && parsedJsonDataField.endsWith('='))) { + parsedJsonDataField = parsedJsonDataField.slice(0, -1); + } + try { + resolve(Buf.fromBase64UrlStr(parsedJsonDataField)); + return; + } catch { + reject(new Error('Chunk response could not be decoded')); } - reject(new Error('Chunk response could not be decoded')); } }; GoogleOAuth.googleApiAuthHeader(this.acctEmail) diff --git a/extension/js/common/api/shared/api-error.ts b/extension/js/common/api/shared/api-error.ts index 0246ba57839..6dd5c1741f0 100644 --- a/extension/js/common/api/shared/api-error.ts +++ b/extension/js/common/api/shared/api-error.ts @@ -206,7 +206,7 @@ export class ApiErr { if (e instanceof AjaxErr && e.resDetails === internalType) { return true; } - if ((e as StandardError).hasOwnProperty('internal') && !!(e as StandardError).message && (e as StandardError).internal === internalType) { + if (Object.hasOwn(e, 'internal') && !!(e as StandardError).message && (e as StandardError).internal === internalType) { return true; } if ((e as StandardErrRes).error && typeof (e as StandardErrRes).error === 'object' && (e as StandardErrRes).error.internal === internalType) { diff --git a/extension/js/common/api/shared/api.ts b/extension/js/common/api/shared/api.ts index 6b77853e798..801c364d4ac 100644 --- a/extension/js/common/api/shared/api.ts +++ b/extension/js/common/api/shared/api.ts @@ -329,8 +329,7 @@ export class Api { } public static randomFortyHexChars(): string { - const bytes = Array.from(secureRandomBytes(20)); - return bytes.map(b => ('0' + (b & 0xff).toString(16)).slice(-2)).join(''); + return secureRandomBytes(20).toHex(); } public static isRecipientHeaderNameType(value: string): value is 'to' | 'cc' | 'bcc' { diff --git a/extension/js/common/browser/ui.ts b/extension/js/common/browser/ui.ts index f27ab7cad14..f0fbe4c949c 100644 --- a/extension/js/common/browser/ui.ts +++ b/extension/js/common/browser/ui.ts @@ -34,11 +34,10 @@ export class CommonHandlers { public static sendRequestAndHandleAsyncResult = async (send: (requestUid: string) => void): Promise => { const requestUid = Str.sloppyRandom(10); - const p = new Promise((resolve: (value: T) => void) => { - CommonHandlers.respondMap.set(requestUid, resolve); - }); + const { promise, resolve } = Promise.withResolvers(); + CommonHandlers.respondMap.set(requestUid, resolve); send(requestUid); - return await p; + return await promise; }; // for specific types diff --git a/extension/js/common/core/buf.ts b/extension/js/common/core/buf.ts index 31e0871e3e3..bf3d34dab18 100644 --- a/extension/js/common/core/buf.ts +++ b/extension/js/common/core/buf.ts @@ -2,17 +2,15 @@ 'use strict'; -import { base64decode, base64encode } from '../platform/util.js'; - export class Buf extends Uint8Array { public static concat = (arrays: Uint8Array[]): Buf => { - const result = new Uint8Array(arrays.reduce((totalLen, arr) => totalLen + arr.length, 0)); + const result = new Buf(arrays.reduce((totalLen, arr) => totalLen + arr.length, 0)); let offset = 0; for (const array of arrays) { result.set(array, offset); offset += array.length; } - return Buf.fromUint8(result); + return result; }; public static with = (input: Uint8Array | Buf | string): Buf => { @@ -39,57 +37,13 @@ export class Buf extends Uint8Array { }; public static fromUtfStr = (utfStr: string): Buf => { - // adapted from https://github.com/feross/buffer/blob/master/index.js see https://github.com/feross/buffer/blob/master/LICENSE (MIT as of Jan 2018) - let codePoint; - const length = utfStr.length; - let leadSurrogate: number | undefined; - const bytes: number[] = []; - for (let i = 0; i < length; ++i) { - codePoint = utfStr.charCodeAt(i); - if (codePoint > 0xd7ff && codePoint < 0xe000) { - // is surrogate component - if (!leadSurrogate) { - // last char was a lead - if (codePoint > 0xdbff) { - // no lead yet - bytes.push(0xef, 0xbf, 0xbd); // unexpected trail - continue; - } else if (i + 1 === length) { - bytes.push(0xef, 0xbf, 0xbd); - continue; - } - leadSurrogate = codePoint; // valid lead - continue; - } - if (codePoint < 0xdc00) { - // 2 leads in a row - bytes.push(0xef, 0xbf, 0xbd); - leadSurrogate = codePoint; - continue; - } - codePoint = (((leadSurrogate - 0xd800) << 10) | (codePoint - 0xdc00)) + 0x10000; // valid surrogate pair - } else if (leadSurrogate) { - bytes.push(0xef, 0xbf, 0xbd); - } - leadSurrogate = undefined; - // encode utf8 - if (codePoint < 0x80) { - bytes.push(codePoint); - } else if (codePoint < 0x800) { - bytes.push((codePoint >> 0x6) | 0xc0, (codePoint & 0x3f) | 0x80); - } else if (codePoint < 0x10000) { - bytes.push((codePoint >> 0xc) | 0xe0, ((codePoint >> 0x6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80); - } else if (codePoint < 0x110000) { - bytes.push((codePoint >> 0x12) | 0xf0, ((codePoint >> 0xc) & 0x3f) | 0x80, ((codePoint >> 0x6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80); - } else { - throw new Error('Invalid code point'); - } - } - return new Buf(bytes); + const bytes = new TextEncoder().encode(utfStr); + return new Buf(bytes.buffer, bytes.byteOffset, bytes.byteLength); }; public static fromBase64Str = (b64str: string): Buf => { - return Buf.fromRawBytesStr(base64decode(b64str)); + const bytes = Uint8Array.fromBase64(b64str, { lastChunkHandling: 'loose' }); + return new Buf(bytes.buffer, bytes.byteOffset, bytes.byteLength); }; public static fromBase64UrlStr = (b64UrlStr: string): Buf => { @@ -186,22 +140,15 @@ export class Buf extends Uint8Array { }; public toHexStr = (uppercaseFlag = true): string => { - const chars: string[] = []; - for (const v of this.values()) { - let char = ('00' + v.toString(16)).slice(-2); - if (uppercaseFlag) { - char = char.toUpperCase(); - } - chars.push(char); - } - return chars.join(''); + const hex = this.toHex(); + return uppercaseFlag ? hex.toUpperCase() : hex; }; public toBase64Str = (): string => { - return base64encode(this.toRawBytesStr()); + return this.toBase64(); }; public toBase64UrlStr = (): string => { - return this.toBase64Str().replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + return this.toBase64({ alphabet: 'base64url', omitPadding: true }); }; } diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index d5ccf6d5cde..6ce803258e5 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -2,7 +2,6 @@ 'use strict'; -import { base64decode, base64encode } from '../platform/util.js'; import { Xss } from '../platform/xss.js'; import { Buf } from './buf.js'; import { MOCK_PORT } from './const.js'; @@ -254,7 +253,7 @@ export class Str { }; public static regexEscape = (toBeUsedInRegex: string) => { - return toBeUsedInRegex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return RegExp.escape(toBeUsedInRegex); }; public static escapeTextAsRenderableHtml = (text: string) => { @@ -328,29 +327,21 @@ export class Str { }; private static base64urlUtfEncode = (str: string) => { - // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings if (typeof str === 'undefined') { return str; } - return base64encode(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(String(p1), 16)))) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); + if (!str.isWellFormed()) { + throw new URIError('URI malformed'); + } + return new TextEncoder().encode(str).toBase64({ alphabet: 'base64url', omitPadding: true }); }; private static base64urlUtfDecode = (str: string) => { - // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings if (typeof str === 'undefined') { return str; } - - return decodeURIComponent( - Array.prototype.map - .call(base64decode(str.replace(/-/g, '+').replace(/_/g, '/')), (c: string) => { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }) - .join('') - ); + const bytes = Uint8Array.fromBase64(str.replace(/-/g, '+').replace(/_/g, '/'), { lastChunkHandling: 'loose' }); + return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }; } @@ -368,15 +359,7 @@ export class DateUtility { export class Value { public static arr = { - unique: (array: T[]): T[] => { - const unique: T[] = []; - for (const v of array) { - if (!unique.includes(v)) { - unique.push(v); - } - } - return unique; - }, + unique: (array: T[]): T[] => [...new Set(array)], withoutKey: (array: T[], i: number) => array.splice(0, i).concat(array.splice(i + 1, array.length)), withoutVal: (array: T[], withoutVal: T) => { const result: T[] = []; @@ -560,12 +543,8 @@ export const stringTuple = (...data: T): T => { }; export const checkValidURL = (url: string): boolean => { - try { - const parsedUrl = new URL(url); - return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; - } catch { - return false; - } + const parsedUrl = URL.parse(url); + return parsedUrl?.protocol === 'http:' || parsedUrl?.protocol === 'https:'; }; /** diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index cca607dc3e7..2ca9bfd1962 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -527,9 +527,9 @@ export class KeyUtil { // Get the base64 after the '=' const checksumLine = dataCandidates[checksumIndex].slice(1); - let providedBytes: string; + let providedBytes: Uint8Array; try { - providedBytes = atob(checksumLine); + providedBytes = Uint8Array.fromBase64(checksumLine, { lastChunkHandling: 'loose' }); } catch { continue; // Not valid base64, skip } @@ -538,14 +538,14 @@ export class KeyUtil { if (providedBytes.length !== 3) { continue; } - const providedCRC = (providedBytes.charCodeAt(0) << 16) | (providedBytes.charCodeAt(1) << 8) | providedBytes.charCodeAt(2); + const providedCRC = (providedBytes[0] << 16) | (providedBytes[1] << 8) | providedBytes[2]; // Decode all lines before the checksum line const dataLines = dataCandidates.slice(0, checksumIndex); - const decodedChunks: string[] = []; + const decodedChunks: Uint8Array[] = []; for (const line of dataLines) { try { - decodedChunks.push(atob(line)); + decodedChunks.push(Uint8Array.fromBase64(line, { lastChunkHandling: 'loose' })); } catch { // skip lines that aren't valid base64 } @@ -556,9 +556,7 @@ export class KeyUtil { } // Join all decoded base64 data and calculate its CRC - const rawData = decodedChunks.join(''); - // eslint-disable-next-line @typescript-eslint/no-misused-spread - const dataBytes = new Uint8Array([...rawData].map(c => c.charCodeAt(0))); + const dataBytes = Buf.concat(decodedChunks); if (KeyUtil.crc24(dataBytes) !== providedCRC) { return true; } diff --git a/extension/js/common/core/crypto/pgp/msg-util.ts b/extension/js/common/core/crypto/pgp/msg-util.ts index eb72dd725b3..f8a37773fe2 100644 --- a/extension/js/common/core/crypto/pgp/msg-util.ts +++ b/extension/js/common/core/crypto/pgp/msg-util.ts @@ -315,7 +315,7 @@ export class MsgUtil { for (const term of disallowTerms) { // Escape term for regex - const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedTerm = Str.regexEscape(term); // Use regex to ensure the term appears as a separate token // (^|\W) ensures the term is at start or preceded by non-word char // (\W|$) ensures the term is followed by non-word char or end diff --git a/extension/js/common/core/crypto/pgp/pgp-armor.ts b/extension/js/common/core/crypto/pgp/pgp-armor.ts index fd436899deb..8accf108741 100644 --- a/extension/js/common/core/crypto/pgp/pgp-armor.ts +++ b/extension/js/common/core/crypto/pgp/pgp-armor.ts @@ -87,9 +87,9 @@ export class PgpArmor { // Build regex patterns from headers, escaping special regex characters const patterns = pgpHeaders.map(header => { - const escapedBegin = header.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedBegin = Str.regexEscape(header.begin); // header.end can be string or RegExp, handle both cases - const escapedEnd = typeof header.end === 'string' ? header.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : header.end.source; // If it's already a RegExp, use its source + const escapedEnd = typeof header.end === 'string' ? Str.regexEscape(header.end) : header.end.source; // If it's already a RegExp, use its source return `(${escapedBegin}[\\s\\S]*?${escapedEnd})`; }); diff --git a/extension/js/common/core/crypto/pgp/pgp-password.ts b/extension/js/common/core/crypto/pgp/pgp-password.ts index e44290f1673..00696e503df 100644 --- a/extension/js/common/core/crypto/pgp/pgp-password.ts +++ b/extension/js/common/core/crypto/pgp/pgp-password.ts @@ -2,8 +2,7 @@ 'use strict'; -import { base64encode, secureRandomBytes } from '../../../platform/util.js'; -import { Buf } from '../../buf.js'; +import { secureRandomBytes } from '../../../platform/util.js'; interface PwdStrengthResult { word: { @@ -106,7 +105,8 @@ export class PgpPwd { public static random = () => { // eg TDW6-DU5M-TANI-LJXY - return base64encode(Buf.fromUint8(secureRandomBytes(128)).toRawBytesStr()) + return secureRandomBytes(128) + .toBase64() .toUpperCase() .replace(/[^A-Z0-9]|0|O|1/g, '') .replace(/(.{4})/g, '$1-') diff --git a/extension/js/common/core/mime.ts b/extension/js/common/core/mime.ts index 639e7f0e48b..a05fbae4d8b 100644 --- a/extension/js/common/core/mime.ts +++ b/extension/js/common/core/mime.ts @@ -492,11 +492,8 @@ export class Mime { private static fromEqualSignNotationAsBuf(str: string): Buf { return Buf.fromRawBytesStr( str.replace(/(=[A-F0-9]{2})+/g, equalSignUtfPart => { - const bytes = equalSignUtfPart - .replace(/^=/, '') - .split('=') - .map(twoHexDigits => parseInt(twoHexDigits, 16)); - return new Buf(bytes).toRawBytesStr(); + const bytes = Uint8Array.fromHex(equalSignUtfPart.replaceAll('=', '')); + return new Buf(bytes.buffer, bytes.byteOffset, bytes.byteLength).toRawBytesStr(); }) ); } diff --git a/extension/js/common/platform/catch.ts b/extension/js/common/platform/catch.ts index 3da2f64f32c..81685abd122 100644 --- a/extension/js/common/platform/catch.ts +++ b/extension/js/common/platform/catch.ts @@ -252,7 +252,7 @@ export class Catch { if (thrown instanceof Error) { // reporting stack may differ from the stack of the actual error, both may be interesting thrown.stack += Catch.formattedStackBlock('Catch.reportErr calling stack', CatchHelper.stackTrace()); - if (thrown.hasOwnProperty('workerStack')) { + if (Object.hasOwn(thrown, 'workerStack')) { // https://github.com/openpgpjs/openpgpjs/issues/656#event-1498323188 thrown.stack += Catch.formattedStackBlock('openpgp.js worker stack', (thrown as Error & { workerStack: string }).workerStack); } @@ -316,7 +316,7 @@ export class Catch { exception = new Error(`LIMITED_ERROR: ${errMsg}`); } else if (thrown instanceof Error) { exception = thrown; - if (thrown.hasOwnProperty('thrown')) { + if (Object.hasOwn(thrown, 'thrown')) { // this is created by custom async stack reporting in tooling/tsc-compiler.ts exception.stack += `\n\ne.thrown:\n${Catch.stringify((thrown as Error & { thrown: string }).thrown)}`; } @@ -356,8 +356,8 @@ export class Catch { private static isPromiseRejectionEvent(ev: unknown): ev is PromiseRejectionEvent { if (ev && typeof ev === 'object') { - const eHasReason = ev.hasOwnProperty('reason') && typeof (ev as PromiseRejectionEvent).reason === 'object'; - const eHasPromise = ev.hasOwnProperty('promise') && Catch.isPromise((ev as PromiseRejectionEvent).promise); + const eHasReason = Object.hasOwn(ev, 'reason') && typeof (ev as PromiseRejectionEvent).reason === 'object'; + const eHasPromise = Object.hasOwn(ev, 'promise') && Catch.isPromise((ev as PromiseRejectionEvent).promise); return eHasReason && eHasPromise; } return false; diff --git a/extension/js/common/platform/util.ts b/extension/js/common/platform/util.ts index acecb3b9086..540c066b297 100644 --- a/extension/js/common/platform/util.ts +++ b/extension/js/common/platform/util.ts @@ -16,14 +16,6 @@ export const secureRandomBytes = (length: number): Uint8Array => { return secureRandomArray; }; -export const base64encode = (binary: string): string => { - return btoa(binary); -}; - -export const base64decode = (b64tr: string): string => { - return atob(b64tr); -}; - export const moveElementInArray = (arr: T[], oldIndex: number, newIndex: number) => { while (oldIndex < 0) { oldIndex += arr.length; diff --git a/extension/js/common/ui/attachment-ui.ts b/extension/js/common/ui/attachment-ui.ts index d46ded03e1f..fd14d236a1c 100644 --- a/extension/js/common/ui/attachment-ui.ts +++ b/extension/js/common/ui/attachment-ui.ts @@ -184,12 +184,6 @@ export class AttachmentUI { }; private readAttachmentDataAsUint8 = async (uploadFileId: string): Promise => { - return await new Promise(resolve => { - const reader = new FileReader(); - reader.onload = () => { - resolve(new Uint8Array(reader.result as ArrayBuffer)); // that's what we're getting - }; - reader.readAsArrayBuffer(this.attachedFiles[uploadFileId]); - }); + return new Uint8Array(await this.attachedFiles[uploadFileId].arrayBuffer()); }; } diff --git a/extension/manifest.json b/extension/manifest.json index cba9c90b9ea..5dc0fa1146b 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -90,7 +90,7 @@ "matches": ["https://mail.google.com/*", "https://accounts.google.com/*", "https://www.google.com/*"] } ], - "minimum_chrome_version": "106", + "minimum_chrome_version": "140", "content_security_policy": { "extension_pages": "script-src 'self'; default-src 'self'; frame-ancestors 'self' https://mail.google.com; img-src 'self' https://* data: blob:; frame-src 'self' blob:; worker-src 'self'; form-action 'none'; media-src 'none'; font-src 'none'; manifest-src 'none'; object-src 'none'; base-uri 'self'; connect-src 'self' *; style-src 'self' 'unsafe-inline';" } diff --git a/package-lock.json b/package-lock.json index 003ce82a0f5..75189d5d492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "sweetalert2": "11.26.25" }, "devDependencies": { + "@microsoft/eslint-formatter-sarif": "3.1.0", "@openpgp/web-stream-tools": "0.3.1", "@tony.ganchev/eslint-plugin-header": "3.4.4", "@types/chai": "5.2.3", @@ -33,8 +34,8 @@ "@types/fs-extra": "11.0.4", "@types/jquery": "4.0.1", "@types/mailparser": "3.4.6", - "@types/thunderbird-webext-browser": "127.0.0", "@typescript/native": "npm:typescript@7.0.2", + "@types/thunderbird-webext-browser": "127.0.0", "ava": "8.0.1", "chai": "6.2.2", "chai-as-promised": "8.0.2", @@ -691,6 +692,53 @@ "node": ">=18.18.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -705,6 +753,14 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -840,6 +896,361 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@microsoft/eslint-formatter-sarif": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-formatter-sarif/-/eslint-formatter-sarif-3.1.0.tgz", + "integrity": "sha512-/mn4UXziHzGXnKCg+r8HGgPy+w4RzpgdoqFuqaKOqUVBT5x2CygGefIrO4SusaY7t0C4gyIWMNu6YQT6Jw64Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint": "^8.9.0", + "jschardet": "latest", + "lodash": "^4.17.14", + "utf8": "^3.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/eslint-formatter-sarif/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@napi-rs/canvas": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.9.tgz", @@ -2112,6 +2523,13 @@ "node": ">=16.20.0" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@vercel/nft": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.11.0.tgz", @@ -4489,6 +4907,19 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5456,6 +5887,13 @@ "node": ">=14.14" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5859,6 +6297,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", @@ -6188,6 +6633,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6594,6 +7051,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jschardet": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-4.0.0.tgz", + "integrity": "sha512-EsNiLV+xzeMBtkGL/HeWoUivNkGIr8JmaC5A5j7DBlKnhp4CcBTWKEOtNg6vlmf+mkn9yQ9Wals47u7+LoTr7w==", + "dev": true, + "license": "0BSD", + "bin": { + "jschardet": "build/cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/jsdoc-type-pratt-parser": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-9.2.2.tgz", @@ -7667,6 +8137,16 @@ "node": ">=14.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/open": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/open/-/open-11.0.3.tgz", @@ -7972,6 +8452,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8726,6 +9216,76 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9816,6 +10376,13 @@ "license": "MIT", "peer": true }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -10177,6 +10744,13 @@ "dev": true, "license": "BSD" }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10736,6 +11310,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/write-file-atomic": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", diff --git a/package.json b/package.json index 131c306d4c8..6e59dae5f38 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "graceful-fs": "4.1.13" }, "devDependencies": { + "@microsoft/eslint-formatter-sarif": "3.1.0", "@openpgp/web-stream-tools": "0.3.1", "@tony.ganchev/eslint-plugin-header": "3.4.4", "@types/chai": "5.2.3", @@ -81,7 +82,8 @@ "test_local_chrome_consumer_mock_headless": "xvfb-run npm run test_local_chrome_consumer_mock", "test_stylelint": "stylelint extension/css/cryptup.css extension/css/settings.css extension/css/webmail.css && stylelint extension/**/*.htm --custom-syntax postcss-html", "test_eslint": "cross-env NODE_OPTIONS=--max-old-space-size=4096 eslint .", - "test_eslint_ci": "npm run test_eslint -- --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif", + "pretest_eslint_ci": "npm run symlink-core", + "test_eslint_ci": "cross-env SARIF_ESLINT_IGNORE_SUPPRESSED=true npm run test_eslint -- --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif", "test_patterns": "node build/test/test/source/patterns.js", "test_async_stack": "node build/test/test/source/async-stack.js", "test_buf": "npx ava --timeout=3m --verbose --concurrency=10 build/test/test/source/buf.js", diff --git a/test/source/buf.ts b/test/source/buf.ts index d0d915651a8..507e925d4ee 100644 --- a/test/source/buf.ts +++ b/test/source/buf.ts @@ -1,12 +1,10 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import './node-runtime-polyfills'; import test from 'ava'; import { Buf } from '../../extension/js/common/core/buf.js'; import { equals } from './tests/unit-node.js'; -global.btoa = (binary: string): string => Buffer.from(binary, 'binary').toString('base64'); -global.atob = (b64tr: string): string => Buffer.from(b64tr, 'base64').toString('binary'); - const lousyRandomBytes = (len = 30): Uint8Array => { const a = new Uint8Array(len); for (let i = 0; i < len; i++) { @@ -61,11 +59,43 @@ test('1000x Buf.fromBase64Str(Buf.fromUint8(data).toBase64Str()) = data', async t.pass(); }); +test('Buf Base64 input handling', t => { + for (const encoded of ['', 'Zg', 'Zg==', 'Zh==', 'Zm9v', ' Zm9v\n']) { + const expected = Uint8Array.from(atob(encoded), char => char.charCodeAt(0)); + const decoded = Buf.fromBase64Str(encoded); + t.true(decoded instanceof Buf); + equals(decoded, expected); + } + for (const encoded of ['Z', 'Zg=', 'Zm$v']) { + t.throws(() => Buf.fromBase64Str(encoded), { instanceOf: SyntaxError }); + } +}); + +test('Buf Base64URL decoding accepts standard and URL-safe alphabets', t => { + const expected = new Uint8Array([251, 255]); + equals(Buf.fromBase64UrlStr('+/8='), expected); + equals(Buf.fromBase64UrlStr('-_8'), expected); + t.is(Buf.fromUint8(expected).toBase64UrlStr(), '-_8'); +}); + +test('Buf hex encoding preserves uppercase-by-default behavior', t => { + const bytes = Buf.fromUint8(new Uint8Array([0, 10, 171, 255])); + t.is(bytes.toHexStr(), '000AABFF'); + t.is(bytes.toHexStr(false), '000aabff'); +}); + test('Buf.fromUtfStr(UTF8) = UTF8_AS_BYTES', async t => { equals(Buf.fromUtfStr(UTF8), UTF8_AS_BYTES); + equals(Buf.fromUtfStr('\ud800\ud800'), Buffer.from('\ufffd\ufffd')); t.pass(); }); +test('Buf.concat returns a Buf containing all input bytes', async t => { + const concatenated = Buf.concat([new Uint8Array([1, 2]), new Uint8Array(), new Uint8Array([3, 4])]); + t.true(concatenated instanceof Buf); + equals(concatenated, new Uint8Array([1, 2, 3, 4])); +}); + test('Buf.fromUint8(UTF8_AS_BYTES).toUtfStr() = UTF8', async t => { equals(Buf.fromUint8(UTF8_AS_BYTES).toUtfStr(), UTF8); t.pass(); diff --git a/test/source/mock/attester/attester-key-constants.ts b/test/source/mock/attester/attester-key-constants.ts index 64203298a2d..4aaedfc7193 100644 --- a/test/source/mock/attester/attester-key-constants.ts +++ b/test/source/mock/attester/attester-key-constants.ts @@ -22,7 +22,7 @@ export const get203FAE7076005381 = async () => { } const msg = data.getMessage('17dad75e63e47f97')!; - const msgText = Buf.fromBase64Str(msg.raw!).toUtfStr(); + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); return /\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s .exec(msgText)![0] .replace(/=\r\n/g, '') diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts index 96734457cf8..a3e5494b877 100644 --- a/test/source/mock/google/google-data.ts +++ b/test/source/mock/google/google-data.ts @@ -109,7 +109,7 @@ export class GmailMsg { export class GmailParser { public static findHeader = (apiGmailMsgObj: GmailMsg | GmailMsg$payload, headerName: string) => { - const node: GmailMsg$payload = apiGmailMsgObj.hasOwnProperty('payload') ? (apiGmailMsgObj as GmailMsg).payload! : (apiGmailMsgObj as GmailMsg$payload); + const node: GmailMsg$payload = Object.hasOwn(apiGmailMsgObj, 'payload') ? (apiGmailMsgObj as GmailMsg).payload! : (apiGmailMsgObj as GmailMsg$payload); if (typeof node.headers !== 'undefined') { for (const header of node.headers) { if (header.name.toLowerCase() === headerName.toLowerCase()) { @@ -228,7 +228,7 @@ export class GoogleData { let htmlData: string | undefined; let processedParts: GmailMsg$payload$part[] = []; if (payload.mimeType === 'text/plain') { - const textData = Buf.fromBase64Str(payload.body!.data!).toUtfStr(); + const textData = Buf.fromBase64UrlStr(payload.body!.data!).toUtfStr(); htmlData = GoogleData.htmlFromText(textData); } else { ({ htmlData, processedParts } = GoogleData.getHtmlDataToDisplay(payload) ?? { htmlData: undefined, processedParts: [] }); @@ -321,9 +321,9 @@ export class GoogleData { // consume both html and text processedParts.push(textPart); } - return { htmlData: Buf.fromBase64Str(htmlPart.body!.data!).toUtfStr(), processedParts }; + return { htmlData: Buf.fromBase64UrlStr(htmlPart.body!.data!).toUtfStr(), processedParts }; } else if (typeof textPart?.body?.data !== 'undefined') { - const textData = Buf.fromBase64Str(textPart.body.data).toUtfStr(); + const textData = Buf.fromBase64UrlStr(textPart.body.data).toUtfStr(); return { htmlData: GoogleData.htmlFromText(textData), processedParts: [textPart] }; } // search inside multipart/alternative diff --git a/test/source/node-runtime-polyfills.ts b/test/source/node-runtime-polyfills.ts new file mode 100644 index 00000000000..375a7cce7c7 --- /dev/null +++ b/test/source/node-runtime-polyfills.ts @@ -0,0 +1,113 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +type Base64Alphabet = 'base64' | 'base64url'; +type LastChunkHandling = 'loose' | 'strict' | 'stop-before-partial'; +type Base64DecodeOptions = { alphabet?: Base64Alphabet; lastChunkHandling?: LastChunkHandling }; +type Base64EncodeOptions = { alphabet?: Base64Alphabet; omitPadding?: boolean }; + +const getOptions = (options: unknown): Record | undefined => { + if (options === undefined) { + return undefined; + } + if (typeof options !== 'object' || !options) { + throw new TypeError('Options must be an object or undefined'); + } + return options as Record; +}; + +const getAlphabet = (options: Record | undefined): Base64Alphabet => { + const alphabet = options?.alphabet ?? 'base64'; + if (alphabet === 'base64' || alphabet === 'base64url') { + return alphabet; + } + throw new TypeError('Invalid alphabet option'); +}; + +const decodeBase64 = (input: string, options?: Base64DecodeOptions): Uint8Array => { + if (typeof input !== 'string') { + throw new TypeError('Base64 input must be a string'); + } + const parsedOptions = getOptions(options); + if (getAlphabet(parsedOptions) !== 'base64') { + throw new TypeError('Only the base64 alphabet is supported for decoding'); + } + const lastChunkHandling = parsedOptions?.lastChunkHandling ?? 'loose'; + if (lastChunkHandling !== 'loose') { + throw new TypeError('Only loose last-chunk handling is supported'); + } + + let decoded: string; + try { + decoded = atob(input); + } catch { + throw new SyntaxError('Base64 input is invalid'); + } + return Uint8Array.from(decoded, character => character.charCodeAt(0)); +}; + +const encodeBase64 = (bytes: Uint8Array, options?: Base64EncodeOptions): string => { + const parsedOptions = getOptions(options); + const alphabet = getAlphabet(parsedOptions); + const omitPadding = Boolean(parsedOptions?.omitPadding); + let encoded = Buffer.from(bytes).toString('base64'); + if (alphabet === 'base64url') { + encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_'); + } + return omitPadding ? encoded.replace(/=+$/, '') : encoded; +}; + +const decodeHex = (input: string): Uint8Array => { + if (typeof input !== 'string') { + throw new TypeError('Hex input must be a string'); + } + if (input.length % 2 !== 0) { + throw new SyntaxError('Hex input must contain an even number of characters'); + } + if (!/^[0-9a-f]*$/i.test(input)) { + throw new SyntaxError('Hex input contains an invalid character'); + } + return new Uint8Array(Buffer.from(input, 'hex')); +}; + +const validateUint8Array = (value: unknown): Uint8Array => { + if (!(value instanceof Uint8Array)) { + throw new TypeError('Method receiver must be a Uint8Array'); + } + return value; +}; + +if (typeof Uint8Array.fromBase64 !== 'function') { + Object.defineProperty(Uint8Array, 'fromBase64', { + configurable: true, + writable: true, + value: (input: string, options?: Base64DecodeOptions): Uint8Array => decodeBase64(input, options), + }); +} + +if (typeof Uint8Array.fromHex !== 'function') { + Object.defineProperty(Uint8Array, 'fromHex', { + configurable: true, + writable: true, + value: (input: string): Uint8Array => decodeHex(input), + }); +} + +if (typeof Uint8Array.prototype.toBase64 !== 'function') { + Object.defineProperty(Uint8Array.prototype, 'toBase64', { + configurable: true, + writable: true, + value(this: Uint8Array, options?: Base64EncodeOptions): string { + return encodeBase64(validateUint8Array(this), options); + }, + }); +} + +if (typeof Uint8Array.prototype.toHex !== 'function') { + Object.defineProperty(Uint8Array.prototype, 'toHex', { + configurable: true, + writable: true, + value(this: Uint8Array): string { + return Buffer.from(validateUint8Array(this)).toString('hex'); + }, + }); +} diff --git a/test/source/test.ts b/test/source/test.ts index 8e8e1d600f9..d006c5e922f 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -1,5 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import './node-runtime-polyfills'; import test, { Implementation } from 'ava'; import { exec } from 'child_process'; import { promisify } from 'util'; diff --git a/test/source/tests/browser-unit-tests/unit-Catch.js b/test/source/tests/browser-unit-tests/unit-Catch.js index b3223022f98..245bbae9450 100644 --- a/test/source/tests/browser-unit-tests/unit-Catch.js +++ b/test/source/tests/browser-unit-tests/unit-Catch.js @@ -71,6 +71,19 @@ BROWSER_UNIT_TEST_NAME(`Catcher does not include query string on report`); return 'pass'; })(); +BROWSER_UNIT_TEST_NAME(`ApiErr recognizes standard errors with shadowed or missing prototypes`); +(async () => { + const shadowed = { internal: 'auth', message: 'expired', hasOwnProperty: undefined }; + if (!ApiErr.isStandardErr(shadowed, 'auth')) { + throw new Error('Expected an error with a shadowed hasOwnProperty field to be recognized'); + } + const withoutPrototype = Object.assign(Object.create(null), { internal: 'subscription', message: 'expired' }); + if (!ApiErr.isStandardErr(withoutPrototype, 'subscription')) { + throw new Error('Expected an error without Object.prototype to be recognized'); + } + return 'pass'; +})(); + BROWSER_UNIT_TEST_NAME(`Catcher reports correct URL for Gmail environment`); (async () => { const originalEnv = Catch.RUNTIME_ENVIRONMENT; @@ -100,4 +113,4 @@ BROWSER_UNIT_TEST_NAME(`Catcher reports correct URL for Gmail environment`); Catch.RUNTIME_ENVIRONMENT = originalEnv; } return 'pass'; -})(); \ No newline at end of file +})(); diff --git a/test/source/tests/browser-unit-tests/unit-Gmail.js b/test/source/tests/browser-unit-tests/unit-Gmail.js index c9029aa58f5..71c9e295378 100644 --- a/test/source/tests/browser-unit-tests/unit-Gmail.js +++ b/test/source/tests/browser-unit-tests/unit-Gmail.js @@ -19,3 +19,27 @@ * the async functions. For the rest, do not change the structure or our parser will get confused. * Do not put any code whatsoever outside of the async functions. */ + +BROWSER_UNIT_TEST_NAME(`[unit][Gmail.attachmentGetChunk] decodes a response split between Base64 padding characters`); +(async () => { + const { GoogleOAuth } = await import('/js/common/api/authentication/google/google-oauth.js'); + const originalGoogleApiAuthHeader = GoogleOAuth.googleApiAuthHeader; + const originalFetch = window.fetch; + const attachment = new Uint8Array(1000); + const encodedAttachment = Buf.fromUint8(attachment).toBase64Str(); + const response = JSON.stringify({ size: attachment.length, data: encodedAttachment }); + const responseThroughFirstPaddingCharacter = response.slice(0, response.lastIndexOf('=')); + + try { + GoogleOAuth.googleApiAuthHeader = async () => ({ authorization: 'Bearer test' }); + window.fetch = async () => new Response(responseThroughFirstPaddingCharacter); + const decoded = await new Gmail('test@example.com').attachmentGetChunk('message', 'attachment', 'attachment'); + if (decoded.length !== attachment.length || decoded.some((byte, index) => byte !== attachment[index])) { + throw Error('Decoded attachment chunk does not match the attachment'); + } + } finally { + GoogleOAuth.googleApiAuthHeader = originalGoogleApiAuthHeader; + window.fetch = originalFetch; + } + return 'pass'; +})(); diff --git a/test/source/tests/browser-unit-tests/unit-Mime.js b/test/source/tests/browser-unit-tests/unit-Mime.js index a68566ba84d..a568168f133 100644 --- a/test/source/tests/browser-unit-tests/unit-Mime.js +++ b/test/source/tests/browser-unit-tests/unit-Mime.js @@ -312,6 +312,27 @@ BROWSER_UNIT_TEST_NAME(`Mime attachment file name issue 3352`); return 'pass'; })(); +BROWSER_UNIT_TEST_NAME(`Mime.decode decodes quoted-printable attachment bytes`); +(async () => { + const mime = [ + 'Content-Type: application/octet-stream; name="bytes.bin"', + 'Content-Disposition: attachment; filename="bytes.bin"', + 'Content-Transfer-Encoding: quoted-printable', + '', + '=00=0A=7F=80=FF', + ].join('\r\n'); + const decoded = await Mime.decode(mime); + if (decoded.attachments.length !== 1) { + throw Error(`Expected one attachment, got ${decoded.attachments.length}`); + } + const bytes = decoded.attachments[0].getData(); + const expected = [0, 10, 127, 128, 255]; + if (bytes.length !== expected.length || bytes.some((byte, index) => byte !== expected[index])) { + throw Error(`Expected quoted-printable bytes ${expected.join(',')}, got ${Array.from(bytes).join(',')}`); + } + return 'pass'; +})(); + BROWSER_UNIT_TEST_NAME(`Mime.decode parses nested signed message exactly once`); (async () => { const nestedSignedMime = [ diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index b3ceaa1a1a3..475778baae3 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1483,13 +1483,16 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage.waitForContent('@recipients-preview', 'sender@domain.comtest@gmail.comtest2@gmail.comtest3@gmail.comtest4@gmail.comtest5@gmail.com'); await composePage.waitAndClick('@action-show-reply-options-popover'); await composePage.waitAndClick('@action-toggle-a_reply'); + await composePage.waitForSelTestState('ready'); await composePage.waitForContent('@recipients-preview', 'sender@domain.com'); await composePage.waitAndClick('@action-show-reply-options-popover'); await composePage.waitAndClick('@action-toggle-a_forward'); + await composePage.waitForContent('@input-body', 'Forwarded message'); await composePage.waitUntilFocused('@input-to'); await expectRecipientElements(composePage, { to: [], cc: [], bcc: [] }); await composePage.waitAndClick('@action-show-reply-options-popover'); await composePage.waitAndClick('@action-toggle-a_reply_all'); + await composePage.waitForSelTestState('ready'); await composePage.waitForContent('@recipients-preview', 'sender@domain.comtest@gmail.comtest2@gmail.comtest3@gmail.comtest4@gmail.comtest5@gmail.com'); }) ); @@ -2093,7 +2096,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te // get sent msg from mock const sentMsg = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject)[0]; - const message = Buf.fromBase64Str(sentMsg.payload!.body!.data!).toUtfStr(); + const message = Buf.fromBase64UrlStr(sentMsg.payload!.body!.data!).toUtfStr(); const encryptedData = /\-\-\-\-\-BEGIN PGP MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP MESSAGE\-\-\-\-\-/s.exec(message)![0]; const decrypted0 = await MsgUtil.decryptMessage({ kisWithPp: [], encryptedData, verificationPubs: [] }); // decryption without a ki should fail @@ -2159,7 +2162,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te // get sent msg from mock const sentMsg = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject)[0]; - const message = Buf.fromBase64Str(sentMsg.payload!.body!.data!).toUtfStr(); + const message = Buf.fromBase64UrlStr(sentMsg.payload!.body!.data!).toUtfStr(); expect(message).to.include('-----BEGIN PGP MESSAGE-----'); expect(message).to.include('-----END PGP MESSAGE-----'); expect(message).to.not.include('Version'); @@ -3522,7 +3525,7 @@ const sendImgAndVerifyPresentInSentMsg = async (t: AvaContext, browser: BrowserH // get sent msg id from mock const sentMsg = (await GoogleData.withInitializedData(acctEmail)).searchMessagesBySubject(subject)[0]; if (sendingType === 'plain') { - const data = Buf.fromBase64Str(sentMsg.payload!.body!.data!).toUtfStr(); + const data = Buf.fromBase64UrlStr(sentMsg.payload!.body!.data!).toUtfStr(); expect(data).to.match(/Test Sending Plain Message With Image/); return; // todo - this test case is a stop-gap. We need to implement rendering of such messages below, diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 451d3952c54..2fef3535424 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -1673,16 +1673,17 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw== 'decrypt - public key is rendered minimized for outgoing messages', testWithBrowser(async (t, browser) => { const assertOutgoingPubkeyFrameIsMinimized = async (page: ControllablePage) => { - await page.waitAll('iframe.pgp_block.publicKey', { timeout: 30 }); - const pubkeyFrame = await page.getFrame(['pgp_pubkey.htm', 'minimized=___cu_true___'], { timeout: 30 }); - await pubkeyFrame.target.waitForFunction( - () => { - const pubkeyContainer = document.querySelector('[data-test="container-pgp-pubkey"]'); - const addContactLine = document.querySelector('.line.add_contact'); - return Boolean(pubkeyContainer?.textContent?.includes('Public Key') && addContactLine?.style.display === 'none'); - }, - { polling: 'mutation', timeout: 30_000 } - ); + const selector = 'iframe.pgp_block.publicKey[src*="minimized=___cu_true___"]'; + await page.waitAll(selector, { timeout: 30 }); + let height = 150; // default iframe height before pgp_pubkey renders and resizes itself + for (let attempt = 0; attempt < 300; attempt++) { + height = await page.target.evaluate(selector => document.querySelector(selector)!.getBoundingClientRect().height, selector); + if (height < 150) { + return; + } + await Util.sleep(0.1); + } + throw new Error(`Outgoing public key frame was not minimized within 30 seconds (height: ${height}px)`); }; const { acctEmail, authHdr } = await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'ci.tests.gmail'); diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index 00b44a14d46..48e84aea90b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -20,7 +20,7 @@ import { PgpArmor } from '../core/crypto/pgp/pgp-armor'; import { readFileSync } from 'fs'; import * as forge from 'node-forge'; import { ENVELOPED_DATA_OID, SmimeKey } from '../core/crypto/smime/smime-key'; -import { Str } from '../core/common'; +import { Str, checkValidURL } from '../core/common'; import { PgpPwd } from '../core/crypto/pgp/pgp-password'; use(chaiAsPromised); @@ -88,15 +88,18 @@ Something wrong with this key`), test(`[unit][KeyUtil.isChecksumMismatch] detects only present checksum mismatches`, t => { const armoredWithValidChecksum = `-----BEGIN PGP MESSAGE----- -aGVsbG8= +aGVs +bG8= =R/WK -----END PGP MESSAGE-----`; const armoredWithInvalidChecksum = armoredWithValidChecksum.replace('=R/WK', '=AAAA'); const armoredWithoutChecksum = armoredWithValidChecksum.replace('\n=R/WK', ''); + const armoredWithMalformedChecksum = armoredWithValidChecksum.replace('=R/WK', '=R$WK'); expect(KeyUtil.isChecksumMismatch(armoredWithValidChecksum)).to.equal(false); expect(KeyUtil.isChecksumMismatch(armoredWithInvalidChecksum)).to.equal(true); expect(KeyUtil.isChecksumMismatch(armoredWithoutChecksum)).to.equal(false); + expect(KeyUtil.isChecksumMismatch(armoredWithMalformedChecksum)).to.equal(false); t.pass(); }); test(`[unit][OpenPGPKey.parse] throws on invalid input`, async t => { @@ -416,6 +419,33 @@ qC2PFoU1J4aEVe5Jz2yovJnzkx/aa0Hs4g0= t.pass(); }); + test('[unit][Str.htmlAttrEncode/Decode] preserves UTF-8 Base64 compatibility', t => { + const value = { ascii: 'hello', unicode: 'გამარჯობა 👋' }; + const encoded = Str.htmlAttrEncode(value); + expect(encoded).to.match(/^[A-Za-z0-9_-]+$/); + expect(Str.htmlAttrDecode(encoded)).to.eql(value); + + const standardBase64 = Buf.fromUtfStr(JSON.stringify(value)).toBase64Str(); + expect(Str.htmlAttrDecode(standardBase64)).to.eql(value); + + const malformedUtf8 = Buf.fromUint8(new Uint8Array([0x22, 0xc3, 0x28, 0x22])).toBase64UrlStr(); + expect(Str.htmlAttrDecode(malformedUtf8)).to.be.undefined; + + const json = JSON.stringify(value); + const bomPrefixed = Buf.concat([new Uint8Array([0xef, 0xbb, 0xbf]), Buf.fromUtfStr(json)]).toBase64UrlStr(); + expect(Str.htmlAttrDecode(bomPrefixed)).to.be.undefined; + expect(Str.htmlAttrDecode('not valid base64!')).to.be.undefined; + t.pass(); + }); + + test('[unit][checkValidURL] accepts only valid HTTP(S) URLs', t => { + expect(checkValidURL('https://flowcrypt.com/path')).to.be.true; + expect(checkValidURL('http://localhost:8080')).to.be.true; + expect(checkValidURL('mailto:human@flowcrypt.com')).to.be.false; + expect(checkValidURL('not a URL')).to.be.false; + t.pass(); + }); + test('[unit][KeyUtil.parse] S/MIME key parsing works', async t => { /* // generate a key pair @@ -1147,7 +1177,8 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY const msg: GmailMsg = data.getMessage('166147ea9bb6669d')!; - const encryptedData = /-----BEGIN PGP MESSAGE-----.*-----END PGP MESSAGE-----/s.exec(Buf.fromBase64Str(msg.raw!).toUtfStr())![0]; + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); + const encryptedData = /-----BEGIN PGP MESSAGE-----.*-----END PGP MESSAGE-----/s.exec(msgText)![0]; const compatibilityKey1 = Config.key('flowcrypt.compatibility.1pp1'); const kisWithPp = [ @@ -1187,9 +1218,8 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY test('[unit][MsgUtil.decryptMessage] finds correct key to verify signature', async t => { const data = await GoogleData.withInitializedData('ci.tests.gmail@flowcrypt.test'); const msg: GmailMsg = data.getMessage('1766644f13510f58')!; - const encryptedData = /\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s.exec( - Buf.fromBase64Str(msg.raw!).toUtfStr() - )![0]; + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); + const encryptedData = /\-\-\-\-\-BEGIN PGP SIGNED MESSAGE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s.exec(msgText)![0]; // actual key the message was signed with const signerPubkey = testConstants.pubkey2864E326A5BE488A; // better key @@ -1241,7 +1271,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY test('[unit][MsgUtil.verifyDetached] verifies Thunderbird html signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); const msg: GmailMsg = data.getMessage('17daefa0eb077da6')!; - const msgText = Buf.fromBase64Str(msg.raw!).toUtfStr(); + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); const sigText = /\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s .exec(msgText)![0] .replace(/=\r\n/g, '') @@ -1261,7 +1291,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY test('[unit][MsgUtil.verifyDetached] verifies Thunderbird text signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); const msg: GmailMsg = data.getMessage('17dad75e63e47f97')!; - const msgText = Buf.fromBase64Str(msg.raw!).toUtfStr(); + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); const sigText = /\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-.*\-\-\-\-\-END PGP SIGNATURE\-\-\-\-\-/s .exec(msgText)![0] .replace(/=\r\n/g, '') @@ -1281,7 +1311,7 @@ jLwe8W9IMt765T5x5oux9MmPDXF05xHfm4qfH/BMO3a802x5u2gJjJjuknrFdgXY test('[unit][MsgUtil.verifyDetached] verifies Firefox rich text signed message', async t => { const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); const msg: GmailMsg = data.getMessage('175ccd8755eab85f')!; - const msgText = Buf.fromBase64Str(msg.raw!).toUtfStr(); + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); const sigBase64 = /Content\-Type: application\/pgp\-signature;.*\r\n\r\n(.*)\r\n\-\-/s.exec(msgText)![1]; const sigText = Buf.fromBase64Str(sigBase64).toUtfStr(); const plaintext = @@ -1305,7 +1335,7 @@ jSB6A93JmnQGIkAem/kzGkKclmfAdGfc4FS+3Cn+6Q==Xmrz -----END PGP SIGNATURE-----`; const data = await GoogleData.withInitializedData('flowcrypt.compatibility@gmail.com'); const msg = data.getMessage('17dad75e63e47f97')!; - const msgText = Buf.fromBase64Str(msg.raw!).toUtfStr(); + const msgText = Buf.fromBase64UrlStr(msg.raw!).toUtfStr(); { const pubkey = /\-\-\-\-\-BEGIN PGP PUBLIC KEY BLOCK\-\-\-\-\-.*\-\-\-\-\-END PGP PUBLIC KEY BLOCK\-\-\-\-\-/s .exec(msgText)![0] diff --git a/test/source/util/parse.ts b/test/source/util/parse.ts index f01f94b12d0..4ba8b8a2f68 100644 --- a/test/source/util/parse.ts +++ b/test/source/util/parse.ts @@ -40,7 +40,7 @@ const parseMixed = async (source: string): Promise => { }; const convertBase64ToMimeMsg = async (base64: string) => { - return await simpleParser(Buffer.from(Buf.fromBase64Str(base64)), { keepCidLinks: true /* #3256 */ }); + return await simpleParser(Buffer.from(Buf.fromBase64UrlStr(base64)), { keepCidLinks: true /* #3256 */ }); }; export default { strictParse, parseMixed, convertBase64ToMimeMsg }; diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index 97054225ffa..6f0eaeab8fe 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -33,7 +33,7 @@ addManifest('firefox-consumer', manifest => { gecko: { id: 'firefox@cryptup.io', update_url: 'https://flowcrypt.com/api/update/firefox', // eslint-disable-line @typescript-eslint/naming-convention - strict_min_version: '112.0', // eslint-disable-line @typescript-eslint/naming-convention + strict_min_version: '134.0', // eslint-disable-line @typescript-eslint/naming-convention }, }; manifest.background = { @@ -41,8 +41,8 @@ addManifest('firefox-consumer', manifest => { scripts: ['/js/service_worker/background.js'], }; // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars - const { service_worker, ...newManifest } = manifest.background as chrome.runtime.ManifestV3; - manifest = newManifest; + const { service_worker, ...newBackground } = manifest.background as chrome.runtime.ManifestV2['background'] & { service_worker?: string }; + manifest.background = newBackground; manifest.permissions = manifest.permissions?.filter((p: string) => p !== 'unlimitedStorage'); delete manifest.minimum_chrome_version; }); @@ -73,7 +73,11 @@ addManifest( default_icon: '/img/logo/flowcrypt-logo-64-64.png', // eslint-disable-line @typescript-eslint/naming-convention }; delete manifest.minimum_chrome_version; - (manifest.browser_specific_settings as messenger._manifest.FirefoxSpecificProperties).strict_min_version = '102.0'; + ( + manifest.browser_specific_settings as { + gecko: messenger._manifest.FirefoxSpecificProperties; + } + ).gecko.strict_min_version = '140.0'; manifest.background = { type: 'module', scripts: ['/js/service_worker/background.js'], diff --git a/tooling/release-firefox-latest-update.json b/tooling/release-firefox-latest-update.json index 319a169706d..1f665d3279e 100644 --- a/tooling/release-firefox-latest-update.json +++ b/tooling/release-firefox-latest-update.json @@ -8,7 +8,7 @@ "update_hash": "sha256:a5cfaf4ac3e0b24ea49f53e3fdd0f12d12d3f33ab73fa91b58c64981f0889a7c", "applications": { "gecko": { - "strict_min_version": "112.0" + "strict_min_version": "134.0" } } } diff --git a/tooling/release-step-2-upload b/tooling/release-step-2-upload index 14d885419dd..fd00f3f97a0 100755 --- a/tooling/release-step-2-upload +++ b/tooling/release-step-2-upload @@ -65,7 +65,7 @@ with open('./tooling/release-firefox-latest-update.json', 'w') as outfile: "update_hash": f"sha256:{get_hash(ff_path_saved)}", "applications": { "gecko": { - "strict_min_version": "112.0" + "strict_min_version": "134.0" } } } @@ -89,4 +89,3 @@ for filepath in replace_version_in_web_files: wp.write(replaced) os.system('cd ../flowcrypt-web && git pull && git checkout -b release-%s && git commit -a --message=\'firefox v%s links updated\' && git push origin release-%s && cd ../flowcrypt-browser' % (dashed_version, version, dashed_version)) - diff --git a/tsconfig.json b/tsconfig.json index 0aded9eb57b..83ec74e467c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2025", + "lib": ["ES2025", "DOM", "DOM.Iterable", "DOM.AsyncIterable", "WebWorker.ImportScripts", "ScriptHost", "ESNext.TypedArrays"], "module": "ES2022", "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true,