├── .browserslistrc ├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── integration-tests-browserstack.yml │ ├── integration-tests-github.yml │ ├── release.yml │ └── virustotal.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── @types ├── buildtimeSettings │ └── index.d.ts ├── inline │ └── index.d.ts ├── json │ └── index.d.ts ├── legacy │ └── index.d.ts └── svelte │ └── index.d.ts ├── AUDITING.md ├── LICENSE ├── README.md ├── SECURITY ├── assets ├── demo.gif └── openpgp_signing_key.asc ├── browserstack.yml ├── closure-externs.js ├── esbuild.ts ├── eslint.config.mjs ├── loader.mjs ├── package-lock.json ├── package.json ├── patches └── postcss-functions+4.0.2.patch ├── scripts ├── ci-tests-launcher.cjs ├── cloudflare-pages.sh ├── new-release.sh └── sign-release.sh ├── src ├── App.css ├── App.svelte ├── components │ ├── DemoBanner.svelte │ ├── Disclaimer.svelte │ ├── Dropzone.svelte │ ├── ErrorModal.svelte │ ├── Footer.svelte │ ├── FullScreenModal.svelte │ ├── Header.svelte │ ├── HumanFileSize.svelte │ ├── Loading.svelte │ ├── Logo.svelte │ ├── OfflineDownload.svelte │ ├── SkipToMainContent.svelte │ └── Spinner.svelte ├── crypto │ ├── constructCmsData.test.ts │ ├── constructCmsData.ts │ ├── deriveKek.test.ts │ ├── deriveKek.ts │ ├── fileDecryptionCms.ts │ ├── fileEncryptionCms.test.ts │ ├── fileEncryptionCms.ts │ ├── index.test.ts │ ├── parseCmsData.ts │ ├── pwriKeyWrapping.test.ts │ ├── pwriKeyWrapping.ts │ ├── sharedBufferConcat.ts │ └── sharedBufferToUint8Array.ts ├── fallbackMessage.inline.ts ├── i18n │ ├── strings.en-tbup.ts │ ├── strings.es.ts │ ├── strings.nb.ts │ └── strings.ts ├── index.ts ├── lib │ ├── Cache.ts │ ├── EFormFields.ts │ ├── blobToBuffer.ts │ ├── bufferEqual.ts │ ├── chunkString.ts │ ├── classNames.ts │ ├── cmsPemToDer.ts │ ├── commentCdataEscapeSequence.ts │ ├── commentCdataExtractor.ts │ ├── downloadArchive.ts │ ├── downloadBlob.ts │ ├── elementIds.ts │ ├── fixBrokenSandboxSecureContext.ts │ ├── generateHtml.ts │ ├── getWrappedCryptoFunctions.ts │ ├── isCI.ts │ ├── isTrustedEvent.ts │ ├── packageInfo.ts │ ├── prepareDownloadableCmsPayload.ts │ ├── sandboxEntrypoints.ts │ ├── setupConstructCmsSandbox.ts │ ├── setupDecryptionSandbox.ts │ ├── setupEncryptionSandbox.ts │ ├── setupParseCmsSandbox.ts │ ├── sharedBufferConcat.ts │ ├── sharedBufferToUint8Array.ts │ ├── tightenCsp.ts │ ├── uint8ArrayToBase64.ts │ └── xmlEscape.ts ├── loader.inline.ts ├── pages │ ├── common.css │ ├── decrypt.svelte │ ├── encrypt.svelte │ └── index.svelte ├── sandbox │ ├── constructCmsData.ts │ ├── deriveKek.ts │ ├── fileDecryptionCms.ts │ ├── fileEncryptionCms.ts │ ├── parseCmsData.ts │ ├── unzip.ts │ └── zip.ts ├── utils │ ├── generateHtml.ts │ └── server.ts └── zip │ ├── crc32.benchmark.ts │ ├── crc32.test.ts │ ├── crc32.ts │ ├── sharedBufferConcat.ts │ ├── sharedBufferToUint8Array.ts │ ├── unzip.test.ts │ ├── unzip.ts │ ├── zip.test.ts │ └── zip.ts ├── test └── e2e │ ├── basicFunctionality.test.ts │ ├── cmsPrimitives.test.ts │ ├── index.test.ts │ └── lib │ ├── dragAndDropFile.ts │ ├── getRandomFileContents.ts │ ├── getRandomFileName.ts │ ├── getRandomIntInRange.ts │ ├── getRandomPassword.ts │ ├── interceptDownload.ts │ ├── navigateToFile.ts │ ├── waitUntilReadyStateComplete.ts │ └── waitUtilNotBusy.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | not dead 3 | >1% 4 | Chrome >= 73 5 | ChromeAndroid >= 73 6 | Edge >= 79 7 | Firefox >= 65 8 | FirefoxAndroid >= 65 9 | iOS >= 15 10 | Opera >= 60 11 | Safari >= 15 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | mangle_table.json 3 | **/*.proto.d.ts 4 | **/*.proto.js 5 | **/*.schema.d.ts 6 | .git 7 | **/.gitignore 8 | **/node_modules 9 | **/obj 10 | **/bin 11 | **/build 12 | **/typings 13 | 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests-browserstack.yml: -------------------------------------------------------------------------------- 1 | name: 'Integration Tests (BrowserStack)' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | branches: 13 | - master 14 | 15 | jobs: 16 | test: 17 | if: ${{ false }} 18 | name: 'BrowserStack Test on Ubuntu' 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | steps: 22 | - name: 'BrowserStack Env Setup' 23 | uses: browserstack/github-actions/setup-env@master 24 | with: 25 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 26 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 27 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 28 | - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 29 | with: 30 | node-version: '22' 31 | - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 32 | with: 33 | distribution: 'microsoft' 34 | java-version: '21' 35 | - name: Setup cloudflared 36 | uses: AnimMouse/setup-cloudflared@625fcfcf71f57a4f00b9656a3b94ee0bbdbd593f 37 | with: 38 | cloudflare_tunnel_credential: ${{ secrets.CLOUDFLARE_TUNNEL_CREDENTIAL }} 39 | cloudflare_tunnel_configuration: ${{ secrets.CLOUDFLARE_TUNNEL_CONFIGURATION }} 40 | cloudflare_tunnel_id: ${{ secrets.CLOUDFLARE_TUNNEL_ID }} 41 | - run: npm ci 42 | - run: npm run lint 43 | - run: npm run build 44 | - run: npm install -g browserstack-node-sdk 45 | - uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 46 | name: 'Starting dev server' 47 | with: 48 | run: npm run start:dev & 49 | wait-on: | 50 | http://localhost:20741/.well-known/time 51 | tail: true 52 | log-output-resume: stderr 53 | wait-for: 1m 54 | log-output: true 55 | log-output-if: failure 56 | - name: 'Running test on BrowserStack' 57 | run: npx browserstack-node-sdk node scripts/ci-tests-launcher.cjs 58 | env: 59 | BASE_URL: ${{ secrets.BASE_URL }} 60 | - name: Shutdown and view logs of cloudflared 61 | if: always() 62 | uses: AnimMouse/setup-cloudflared/shutdown@625fcfcf71f57a4f00b9656a3b94ee0bbdbd593f 63 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests-github.yml: -------------------------------------------------------------------------------- 1 | name: 'Integration Tests (GitHub Actions)' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | branches: 13 | - master 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | os: 20 | - macos-latest 21 | - ubuntu-latest 22 | - windows-latest 23 | browser: 24 | - chrome 25 | - firefox 26 | - MicrosoftEdge 27 | include: 28 | - os: macos-latest 29 | browser: safari 30 | runs-on: ${{ matrix.os }} 31 | timeout-minutes: 5 32 | steps: 33 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 34 | - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 35 | with: 36 | node-version: '22' 37 | - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 38 | with: 39 | distribution: 'microsoft' 40 | java-version: '21' 41 | - run: npm ci 42 | - run: npm run lint 43 | - run: npm run build 44 | - uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 45 | name: 'Starting dev server' 46 | with: 47 | run: npm run start:dev & 48 | wait-on: | 49 | http://localhost:20741/.well-known/time 50 | tail: true 51 | log-output-resume: stderr 52 | wait-for: 1m 53 | log-output: true 54 | log-output-if: failure 55 | - name: 'Starting Xvfb' 56 | if: matrix.os == 'ubuntu-latest' 57 | run: Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & 58 | - name: 'Running test' 59 | run: npm test 60 | env: 61 | BROWSER: ${{ matrix.browser }} 62 | DISPLAY: :99 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Generate GitHub release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | permissions: 13 | contents: write 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 17 | - run: git fetch --tags --force 18 | - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 23 | with: 24 | node-version: '20' 25 | - run: npm ci 26 | - run: npm run build 27 | env: 28 | LC_ALL: C 29 | TZ: UTC 30 | BUILD_TYPE: release 31 | NODE_ENV: production 32 | SIGNATURE_MODE: mandatory 33 | LOCALES: en es nb 34 | - run: | 35 | cd build 36 | cp index.html encrypt.html 37 | cp index.es.html encrypt.es.html 38 | cp index.nb.html encrypt.nb.html 39 | - name: Release 40 | uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 41 | with: 42 | files: | 43 | build/encrypt.html 44 | build/encrypt.es.html 45 | build/encrypt.nb.html 46 | -------------------------------------------------------------------------------- /.github/workflows/virustotal.yml: -------------------------------------------------------------------------------- 1 | name: Upload build assets to VirusTotal 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | release: 8 | types: 9 | - published 10 | 11 | jobs: 12 | virustotal: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | timeout-minutes: 5 17 | steps: 18 | - name: VirusTotal Scan 19 | uses: crazy-max/ghaction-virustotal@92a6081d9aab8f8ef3d9081e8bb264aaccc9e74d 20 | with: 21 | vt_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} 22 | files: | 23 | * 24 | update_release_body: true 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | build 6 | # don't lint nyc coverage output 7 | coverage 8 | package-lock.json 9 | *.schema.d.ts 10 | *.proto.js 11 | *.proto.d.ts 12 | cache 13 | typings/lib.*.d.ts 14 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | module.exports = { 17 | semi: true, 18 | trailingComma: 'all', 19 | singleQuote: true, 20 | printWidth: 80, 21 | tabWidth: 4, 22 | useTabs: true, 23 | plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'], 24 | overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }], 25 | }; 26 | -------------------------------------------------------------------------------- /@types/buildtimeSettings/index.d.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2021 Apeleg Limited. 2 | * 3 | * All rights reserved. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 6 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 7 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 8 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 9 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 10 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 11 | * PERFORMANCE OF THIS SOFTWARE. 12 | */ 13 | 14 | declare namespace __buildtimeSettings__ { 15 | const assetManifest: { 16 | client?: { 17 | scripts?: Record; 18 | styles?: Record; 19 | }; 20 | }; 21 | const publicDir: string; 22 | const buildTarget: 'client' | 'server' | 'worker'; 23 | const ssr: boolean; 24 | const enableFramebusting: boolean; 25 | const gitCommitHash: string | undefined; 26 | const package: Readonly<{ 27 | name?: string; 28 | version?: string; 29 | description?: string; 30 | keywords?: string; 31 | homepage?: string; 32 | bugs?: string; 33 | license?: string; 34 | author?: 35 | | string 36 | | { 37 | name: string; 38 | email?: string; 39 | url?: string; 40 | }; 41 | repository?: 42 | | string 43 | | { 44 | type: string; 45 | url: string; 46 | }; 47 | }>; 48 | } 49 | -------------------------------------------------------------------------------- /@types/inline/index.d.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 4 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 5 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 6 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 7 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 8 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 9 | * PERFORMANCE OF THIS SOFTWARE. 10 | */ 11 | 12 | declare module 'inline:*' { 13 | const content: string; 14 | export const contentBase64: string; 15 | export const path: string; 16 | export const sri: string; 17 | 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /@types/json/index.d.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2021 Apeleg Limited. 2 | * 3 | * All rights reserved. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 6 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 7 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 8 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 9 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 10 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 11 | * PERFORMANCE OF THIS SOFTWARE. 12 | */ 13 | 14 | declare module '*.json' { 15 | const default_: unknown; 16 | export default default_; 17 | } 18 | -------------------------------------------------------------------------------- /@types/legacy/index.d.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 4 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 5 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 6 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 7 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 8 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 9 | * PERFORMANCE OF THIS SOFTWARE. 10 | */ 11 | 12 | declare module 'legacy:*' { 13 | const content: string; 14 | export const contentBase64: string; 15 | export const path: string; 16 | export const sri: string; 17 | 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /@types/svelte/index.d.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2021 Apeleg Limited. 2 | * 3 | * All rights reserved. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 6 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 7 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 8 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 9 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 10 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 11 | * PERFORMANCE OF THIS SOFTWARE. 12 | */ 13 | 14 | import 'svelte'; 15 | -------------------------------------------------------------------------------- /SECURITY: -------------------------------------------------------------------------------- 1 | Security Policy 2 | =============== 3 | 4 | We value your contributions and feedback, and we want to ensure the security and 5 | reliability of our software for all users. As part of our commitment to 6 | security, we have established this security policy to help you report any 7 | vulnerabilities that you may discover. 8 | 9 | If you believe you have found a security vulnerability in our software, please 10 | let us know immediately. You may find our contact information at 11 | . Please include the following 12 | information in your report: 13 | 14 | * A brief description of the vulnerability 15 | * Steps to reproduce the vulnerability 16 | * The version of our software affected by the vulnerability 17 | * Your contact information (email or other means of communication) 18 | 19 | We kindly ask that you do not disclose the vulnerability to anyone else until we 20 | have had the opportunity to investigate and address it. We will respond to your 21 | report as soon as possible and work with you to resolve the issue. We may also 22 | provide credit to you in our release notes or other public acknowledgments. 23 | 24 | Thank you for helping to keep our software secure! 25 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApelegHQ/ts-cms-ep-sfx/63b40d517b887089e04d1600f8d64157bae5d710/assets/demo.gif -------------------------------------------------------------------------------- /assets/openpgp_signing_key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mDMEZoHxgBYJKwYBBAHaRw8BAQdA04cBaC9lu9ryYKQBzZmRYQ1CaPbzBJDD9aMW 4 | qWSipIa0JUFwZWxlZyBMaW1pdGVkIChAYXBlbGVnaHEvY21zLWVwLXNmeCmIlgQT 5 | FggAPgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEFohI6BTFoAIBf8MbgB 6 | Vqxj+96dBQJn6b7gBQkC06tgAAoJELgBVqxj+96dHv4BAOliH0ybJz149wehMsGg 7 | Yu6v79JjorYzclHomVcj9bAaAQDOe9D16wSYqtszrSvCMpeL5yZI1FN7y6vJ75/Y 8 | BvRMBIkCMwQQAQoAHRYhBPinipKccqRuI6UAFLOQEkLDMu6CBQJmso7RAAoJELOQ 9 | EkLDMu6CepMQAM6VGNFbBOdd+UTEt4R5SK57iFoE6PcICntYHUQTOi223jdzs+ik 10 | EjDBa8CpcWi45xBlmsZQuldhb68xQ/B6pd6kvK5isR7wSDetYCDxbGDNzcHcAf9/ 11 | 9aQ0B4rdMfh99yQquD/kebEaHaQvY0EqY26yXHhHQx3rvOQZ6gaqP5uFsU2FNVcv 12 | raHJRWFdUgSnDAaWkzY/PvCOoSsVRGfgznYrm9C13713Tbt+xOYd6rrEZ8Tz4A3I 13 | LXBYP0ostyNjlpBAVLnAOMkJ2RHUkSBQX859w3D/BxtFfxbWLEsQrePuJsXDokhm 14 | kVHGh7a5WmRB6WDwqsAx3rFPQx1Z24ZmPo8detMZ1mlLb2u8qkBgjmaUV57tFJfz 15 | xIXSTEC0pxd/R/FegTveybCp/yFyk67Ae/XrXOPZfFf6w/uL+ThPkX9Z9y55hyVF 16 | n2Pwg2PMbAnaybDw2mmTMOdv0K/EAwp2EI9ujrwUZMdCQU5XPm9kWZHuk+LnAsKo 17 | WiAcXVqPagMik+pzEd1Pn9uU3m5ZtxX9F2pIpXinvsqMhx71pHEQ+MwV1OPRC6IO 18 | hHXPPtxfq3rIncbYs9OKhXIt9rNCSBQIJOUDhdanyr3JEZNCCLosS9rchMPTP3oy 19 | bK9e7r49065rXOXOMizj4YWVdYkveO6Ya4vkZfCobn7cZ4VBckRoA+bmiHUEEBYI 20 | AB0WIQR6zf8ix3O3R11+kKfxiO39gPd0HAUCZrKO0QAKCRDxiO39gPd0HOyeAQDg 21 | 8SRIPyGK/qovmyjqO24hm4JFaK5Nhit13jn6aIBjOgEA6aFiCKCBcIzx0b8RP4ng 22 | 6KvUrkNdAK+Fa8PHnYzqbww= 23 | =qsAU 24 | -----END PGP PUBLIC KEY BLOCK----- 25 | -------------------------------------------------------------------------------- /browserstack.yml: -------------------------------------------------------------------------------- 1 | platforms: 2 | - os: Windows 3 | osVersion: 11 4 | browserName: Edge 5 | browserVersion: latest 6 | - os: Windows 7 | osVersion: 10 8 | browserName: Chrome 9 | browserVersion: latest 10 | - os: OS X 11 | osVersion: Monterey 12 | browserName: Firefox 13 | browserVersion: 115.0 14 | - deviceName: iPhone 14 15 | osVersion: 16 16 | browserName: safari 17 | deviceOrientation: portrait 18 | - deviceName: Google Pixel 8 19 | osVersion: 14.0 20 | browserName: chrome 21 | deviceOrientation: portrait 22 | parallelsPerPlatform: 1 23 | browserstackLocal: false 24 | buildIdentifier: ${BUILD_NUMBER} 25 | debug: true 26 | networkLogs: true 27 | interactiveDebugging: false 28 | consoleLogs: errors 29 | -------------------------------------------------------------------------------- /closure-externs.js: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /** 17 | * @externs 18 | */ 19 | 20 | /** 21 | * @type {string | null | undefined} 22 | */ 23 | Object.prototype.class; 24 | 25 | /** 26 | * @type {Object} 27 | */ 28 | Object.prototype.$$; 29 | 30 | /** 31 | * @param {*} arg1 32 | * @return {undefined} 33 | */ 34 | Object.prototype.$$set = function (arg1) { 35 | void arg1; 36 | }; 37 | 38 | /** 39 | * 40 | * @type {Object} 41 | */ 42 | Object.prototype.$$props; 43 | 44 | /** 45 | * 46 | * @type {Object} 47 | */ 48 | Object.prototype.$$restProps; 49 | 50 | /** 51 | * 52 | * @type {Object} 53 | */ 54 | Object.prototype.$$new_props; 55 | 56 | /** 57 | * 58 | * @type {Object} 59 | */ 60 | Object.prototype.$$scope; 61 | 62 | /** 63 | * 64 | * @type {Object} 65 | */ 66 | Object.prototype.$$slots; 67 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Permission to use, copy, modify, and distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | * PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | 16 | import { FlatCompat } from '@eslint/eslintrc'; 17 | import js from '@eslint/js'; 18 | import plugin from '@typescript-eslint/eslint-plugin'; 19 | import parser from '@typescript-eslint/parser'; 20 | import prettierRecommended from 'eslint-plugin-prettier/recommended'; 21 | import svelte from 'eslint-plugin-svelte'; 22 | import globals from 'globals'; 23 | import path from 'node:path'; 24 | import { fileURLToPath } from 'node:url'; 25 | import svelteParser from 'svelte-eslint-parser'; 26 | 27 | // mimic CommonJS variables -- not needed if using CommonJS 28 | const __filename = fileURLToPath(import.meta.url); 29 | const __dirname = path.dirname(__filename); 30 | 31 | const compat = new FlatCompat({ 32 | baseDirectory: __dirname, 33 | }); 34 | 35 | export default [ 36 | { 37 | ignores: [ 38 | '**/node_modules/*', 39 | '**/.nyc_output/*', 40 | '**/dist/*', 41 | '**/build/*', 42 | '**/coverage/*', 43 | '**/package-lock.json', 44 | ], 45 | }, 46 | js.configs.recommended, 47 | ...compat.extends('plugin:@typescript-eslint/recommended'), 48 | prettierRecommended, 49 | // svelte.configs['flat/recommended'], 50 | // svelte.configs['flat/prettier'], 51 | { 52 | languageOptions: { 53 | parser, 54 | globals: { 55 | ...globals.node, 56 | }, 57 | }, 58 | plugins: { plugin, svelte }, 59 | rules: { 60 | '@typescript-eslint/naming-convention': [ 61 | 'error', 62 | { 63 | selector: 'typeParameter', 64 | format: ['PascalCase'], 65 | prefix: ['T'], 66 | }, 67 | { 68 | selector: 'interface', 69 | format: ['PascalCase'], 70 | prefix: ['I'], 71 | }, 72 | { 73 | selector: 'enumMember', 74 | format: ['UPPER_CASE'], 75 | trailingUnderscore: 'require', 76 | }, 77 | { 78 | selector: 'variable', 79 | modifiers: ['exported'], 80 | format: ['camelCase', 'PascalCase'], 81 | trailingUnderscore: 'require', 82 | }, 83 | { 84 | selector: 'typeProperty', 85 | format: ['camelCase'], 86 | trailingUnderscore: 'require', 87 | }, 88 | { 89 | selector: 'method', 90 | format: ['camelCase'], 91 | trailingUnderscore: 'require', 92 | }, 93 | ], 94 | }, 95 | settings: { 96 | svelte: { 97 | typescript: true, 98 | }, 99 | }, 100 | }, 101 | { 102 | files: ['**/*.js', '**/*.schema.json', '**/package.json', '**/*.d.ts'], 103 | rules: { 104 | '@typescript-eslint/naming-convention': 'off', 105 | }, 106 | }, 107 | { 108 | files: ['**/*.json', '**/closure-externs.js'], 109 | rules: { 110 | '@typescript-eslint/no-unused-expressions': 'off', 111 | }, 112 | }, 113 | { 114 | files: ['**/*.cjs', '**/*.cts'], 115 | rules: { 116 | '@typescript-eslint/no-require-imports': 'off', 117 | }, 118 | }, 119 | { 120 | files: ['**/*.svelte'], 121 | languageOptions: { 122 | parser: svelteParser, 123 | globals: { 124 | ...globals.browser, 125 | }, 126 | parserOptions: { 127 | parser, 128 | tsconfigRootDir: __dirname, 129 | project: ['./tsconfig.json'], 130 | extraFileExtensions: ['.svelte'], 131 | }, 132 | }, 133 | }, 134 | ]; 135 | -------------------------------------------------------------------------------- /loader.mjs: -------------------------------------------------------------------------------- 1 | import { register } from 'node:module'; 2 | import { pathToFileURL } from 'node:url'; 3 | 4 | process.on('uncaughtException', (e) => console.error('Loader error', e)); 5 | register('ts-node/esm', pathToFileURL('./')); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apeleghq/cms-ep-sfx", 3 | "version": "1.1.12", 4 | "description": "Secure File Sharing Utility", 5 | "type": "module", 6 | "main": "-", 7 | "module": "-", 8 | "types": "-", 9 | "exports": { 10 | ".": {} 11 | }, 12 | "devDependencies": { 13 | "@apeleghq/asn1-der": "^1.0.5", 14 | "@apeleghq/cms-classes": "^1.1.0", 15 | "@apeleghq/crypto-oids": "^20250325.0.0", 16 | "@apeleghq/esbuild-plugin-closure-compiler": "^1.0.8", 17 | "@apeleghq/esbuild-plugin-inline-js": "^1.1.11", 18 | "@apeleghq/lot": "^0.0.29", 19 | "@eslint/eslintrc": "^3.3.1", 20 | "@eslint/js": "^9.25.0", 21 | "@tailwindcss/postcss": "^4.1.4", 22 | "@types/node": "^22.14.1", 23 | "@types/postcss-css-variables": "^0.18.3", 24 | "@types/postcss-functions": "^4.0.4", 25 | "@types/selenium-webdriver": "^4.1.28", 26 | "@typescript-eslint/eslint-plugin": "^8.30.1", 27 | "@typescript-eslint/parser": "^8.30.1", 28 | "cssnano": "^7.0.6", 29 | "esbuild": "^0.25.2", 30 | "esbuild-style-plugin": "^1.6.3", 31 | "esbuild-svelte": "^0.9.2", 32 | "eslint": "^9.25.0", 33 | "eslint-config-prettier": "^10.1.2", 34 | "eslint-plugin-prettier": "^5.2.6", 35 | "eslint-plugin-svelte": "^3.5.1", 36 | "globals": "^16.0.0", 37 | "google-closure-compiler": "^20240317.0.0", 38 | "patch-package": "^8.0.0", 39 | "postcss": "^8.5.3", 40 | "postcss-css-variables": "^0.19.0", 41 | "postcss-functions": "^4.0.2", 42 | "prettier": "^3.5.3", 43 | "prettier-plugin-svelte": "^3.3.3", 44 | "prettier-plugin-tailwindcss": "^0.6.11", 45 | "selenium-webdriver": "^4.31.0", 46 | "svelte": "^4.2.19", 47 | "svelte-eslint-parser": "^1.1.3", 48 | "svelte-preprocess": "^6.0.3", 49 | "tailwindcss": "^4.1.4", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.8.3" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/ApelegHQ/ts-cms-ep-sfx.git" 56 | }, 57 | "files": [ 58 | "dist/**/*" 59 | ], 60 | "scripts": { 61 | "test:e2e": "node --import ./loader.mjs test/e2e/index.test.ts", 62 | "test": "npm run test:e2e", 63 | "lint": "eslint . --ext .svelte,.cjs,.cts,.mjs,.mjs,.js,.ts,.json", 64 | "lint:fix": "prettier -w . && eslint . --ext .svelte,.cjs,.cts,.mjs,.mjs,.js,.ts,.json --fix", 65 | "build": "tsc --noEmit && node --import ./loader.mjs esbuild.ts", 66 | "start:dev": "node --import ./loader.mjs src/utils/server.ts", 67 | "prepack": "npm run build", 68 | "prepublishOnly": "npm run build && npm test && npm run lint", 69 | "preversion": "npm run lint", 70 | "version": "npm run lint && git add -A src", 71 | "postversion": "git push && git push --tags", 72 | "postinstall": "patch-package" 73 | }, 74 | "author": "Apeleg Limited", 75 | "license": "Apache-2.0 WITH LLVM-exception", 76 | "keywords": [] 77 | } 78 | -------------------------------------------------------------------------------- /patches/postcss-functions+4.0.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/postcss-functions/dest/lib/transformer.js b/node_modules/postcss-functions/dest/lib/transformer.js 2 | index a13058f..eba4a63 100644 3 | --- a/node_modules/postcss-functions/dest/lib/transformer.js 4 | +++ b/node_modules/postcss-functions/dest/lib/transformer.js 5 | @@ -19,6 +19,13 @@ function transformString(str, functions) { 6 | promises.push(transformNode(part, functions)); 7 | }); 8 | if ((0, _helpers.hasPromises)(promises)) promises = Promise.all(promises); 9 | + promises = promises.forEach((x, i, a) => { 10 | + // Remove ':' in selectors 11 | + if (x.type === 'div' && x.value === ':' && a[i + 1] && a[i + 1].type === 'word' && Array.isArray(a[i + 1].nodes)) { 12 | + x.type = 'word'; 13 | + x.value = ''; 14 | + } 15 | + }) 16 | return (0, _helpers.then)(promises, function () { 17 | return values.toString(); 18 | }); 19 | -------------------------------------------------------------------------------- /scripts/ci-tests-launcher.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 6 | * exceptions; you may not use this file except in compliance with the 7 | * License. You may obtain a copy of the License at 8 | * 9 | * http://llvm.org/foundation/relicensing/LICENSE.txt 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | const { register } = require('node:module'); 19 | const { pathToFileURL } = require('node:url'); 20 | register('ts-node/esm', pathToFileURL('./')); 21 | 22 | import('../test/e2e/basicFunctionality.test.js'); 23 | -------------------------------------------------------------------------------- /scripts/cloudflare-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | git fetch --tags 4 | curl -sSL -o 'OpenJDK21U-jdk_x64_linux_hotspot_21.0.6_7.tar.gz' 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.6%2B7/OpenJDK21U-jdk_x64_linux_hotspot_21.0.6_7.tar.gz' 5 | printf 'a2650fba422283fbed20d936ce5d2a52906a5414ec17b2f7676dddb87201dbae OpenJDK21U-jdk_x64_linux_hotspot_21.0.6_7.tar.gz\n' | sha256sum -c 6 | tar -xzf 'OpenJDK21U-jdk_x64_linux_hotspot_21.0.6_7.tar.gz' 7 | export JAVA_HOME="$(pwd)/jdk-21.0.6+7" 8 | export PATH="$JAVA_HOME/bin:$PATH" 9 | export LC_ALL="C" 10 | export TZ="UTC" 11 | export NODE_ENV="production" 12 | export BUILD_TYPE="release" 13 | export SIGNATURE_MODE="opportunistic" 14 | npm run build 15 | -------------------------------------------------------------------------------- /scripts/new-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | export LC_ALL="C" 5 | export TZ="UTC" 6 | export BUILD_TYPE="release" 7 | export NODE_ENV="production" 8 | export LOCALES="en es nb" 9 | 10 | dir=$(dirname "${0}") 11 | 12 | SIGNATURE_MODE="presign" npm run build 13 | . "$dir/sign-release.sh" 14 | SIGNATURE_MODE="mandatory" npm run build 15 | -------------------------------------------------------------------------------- /scripts/sign-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | export LC_ALL="C" 5 | export TZ="UTC" 6 | 7 | tbs='build/tbs' 8 | user='4168848E814C5A002017FC31B80156AC63FBDE9D' 9 | version=$(jq '--raw-output' '.version' 'package.json') 10 | 11 | if git 'rev-parse' '--quiet' '--verify' 'refs/tags/v'"$version"; then 12 | echo 'Release tag already exists' >&2 13 | exit 1 14 | fi 15 | 16 | signedinfo='' 17 | 18 | for tag in $LOCALES; do 19 | if [ x"$tag" = x'en' ]; then 20 | tag='' 21 | suffix='' 22 | else 23 | suffix=".${tag}" 24 | fi 25 | digest=$(openssl 'dgst' '-binary' '-sha256' "$tbs$suffix" | xxd '-p' '-c' '256') 26 | signature=$(gpg2 '--armor' '--clear-sign' '--local-user' "$user" '--digest-algo' 'SHA256' '--output' '-' "$tbs$suffix" | sed "-n" '/^-----BEGIN PGP SIGNATURE-----/,$p' | sed "-e" "s/^/:/g") 27 | signedinfo="$(printf '%s\n::%s\n:%s\n%s\n' "$signedinfo" "$tag" "$digest" "$signature")" 28 | done 29 | 30 | printf "%s\n\n%s" "v$version" "$signedinfo" | git 'tag' '-s' "v$version" '-F' '-' 31 | echo 'Created tag v'"$version" 32 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | @import 'tailwindcss/preflight.css'; 17 | @import 'tailwindcss/theme.css'; 18 | 19 | @theme { 20 | --font-sans: 'Poppins', 'DejaVu Sans', 'Verdana', 'Noto Sans', 'sans'; 21 | --font-serif: 22 | 'ui-serif', 'Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'; 23 | --font-mono: 24 | 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 25 | 'Liberation Mono', 'Courier New', 'monospace'; 26 | --default-font-family: --theme(--font-sans, initial); 27 | } 28 | 29 | :elementid(LOADING_ELEMENT_ID_) { 30 | display: flex; 31 | flex-direction: column; 32 | position: absolute; 33 | top: 0; 34 | bottom: 0; 35 | left: 0; 36 | right: 0; 37 | background-color: #f2f0f0; 38 | } 39 | 40 | :elementid(LOADING_ANIMATION_ELEMENT_ID_) { 41 | display: flex; 42 | position: relative; 43 | width: auto; 44 | height: 100%; 45 | } 46 | 47 | @media not (writing-mode: tb-lr) { 48 | :elementid(LOADING_ANIMATION_ELEMENT_ID_) { 49 | inline-size: auto; 50 | block-size: 100%; 51 | } 52 | } 53 | 54 | :elementid(LOADING_ANIMATION_ELEMENT_ID_)::after { 55 | content: ''; 56 | font-size: 6rem; 57 | border-radius: 9999px; 58 | border: 0 solid transparent; 59 | padding: 0.75rem; 60 | margin: 0 auto; 61 | background-image: 62 | linear-gradient(white, white), 63 | linear-gradient(to left, #431c01, #a26135); 64 | background-origin: border-box; 65 | background-clip: content-box, border-box; 66 | color: transparent; 67 | position: absolute; 68 | left: 50%; 69 | top: 50%; 70 | width: 1.2em; 71 | height: 1.2em; 72 | transform: translate(-50%, -50%); 73 | animation: 5s ease-out 0s infinite loading-animation-bg-KF_; 74 | } 75 | 76 | @media not (writing-mode: tb-lr) { 77 | :elementid(LOADING_ANIMATION_ELEMENT_ID_)::after { 78 | margin-inline: auto; 79 | margin-block: 0; 80 | } 81 | } 82 | 83 | @keyframes loading-animation-bg-KF_ { 84 | 0% { 85 | transform: translate(-50%, -50%) rotate(0deg); 86 | } 87 | 50% { 88 | transform: translate(-50%, -50%) rotate(270deg); 89 | } 90 | 100% { 91 | transform: translate(-50%, -50%) rotate(360deg); 92 | } 93 | } 94 | 95 | :elementid(LOADING_TEXT_ELEMENT_ID_) { 96 | font-size: 2.5rem; 97 | padding: 2rem 0; 98 | display: block; 99 | margin: 0 auto 2rem auto; 100 | text-align: center; 101 | width: auto; 102 | height: 50%; 103 | text-transform: uppercase; 104 | font-family: 105 | system-ui, 106 | -apple-system, 107 | BlinkMacSystemFont, 108 | 'Segoe UI', 109 | Roboto, 110 | Oxygen, 111 | Ubuntu, 112 | Cantarell, 113 | 'Open Sans', 114 | 'Helvetica Neue', 115 | sans-serif; 116 | color: #111; 117 | } 118 | 119 | @media not (writing-mode: tb-lr) { 120 | :elementid(LOADING_TEXT_ELEMENT_ID_) { 121 | padding-inline: 0; 122 | padding-block: 2rem; 123 | margin-inline: auto; 124 | margin-block: 0 2rem; 125 | inline-size: auto; 126 | block-size: 50%; 127 | } 128 | } 129 | 130 | :elementid(ERROR_WARNING_CONTAINER_ELEMENT_ID_), 131 | :elementid(NOSCRIPT_WARNING_CONTAINER_ELEMENT_ID_) { 132 | display: block; 133 | position: absolute; 134 | width: 100%; 135 | height: 100%; 136 | top: 0; 137 | bottom: 0; 138 | left: 0; 139 | right: 0; 140 | z-index: 9999; 141 | margin: 0; 142 | padding: 0; 143 | background: #ffffff; 144 | } 145 | 146 | :elementid(ERROR_WARNING_TEXT_CONTAINER_ELEMENT_ID_), 147 | :elementid(NOSCRIPT_WARNING_TEXT_CONTAINER_ELEMENT_ID_) { 148 | display: block; 149 | background-color: #ffffde; 150 | border-bottom: 2px solid #8c8475; 151 | margin: 0; 152 | padding: 2rem; 153 | width: 100%; 154 | } 155 | 156 | :elementid(ERROR_WARNING_TEXT_ELEMENT_ID_), 157 | :elementid(NOSCRIPT_WARNING_TEXT_ELEMENT_ID_) { 158 | display: block; 159 | color: #333; 160 | width: 1024px; 161 | height: auto; 162 | max-width: 100%; 163 | max-height: none; 164 | font-size: 2.5rem; 165 | margin: 0 auto; 166 | font-family: 167 | system-ui, 168 | -apple-system, 169 | BlinkMacSystemFont, 170 | 'Segoe UI', 171 | Roboto, 172 | Oxygen, 173 | Ubuntu, 174 | Cantarell, 175 | 'Open Sans', 176 | 'Helvetica Neue', 177 | sans-serif; 178 | } 179 | 180 | @media not (writing-mode: tb-lr) { 181 | :elementid(ERROR_WARNING_TEXT_ELEMENT_ID_), 182 | :elementid(NOSCRIPT_WARNING_TEXT_ELEMENT_ID_) { 183 | inline-size: 1024px; 184 | block-size: auto; 185 | max-inline-size: 100%; 186 | max-block-size: auto; 187 | margin-inline: auto; 188 | margin-block: 0; 189 | } 190 | } 191 | 192 | :elementid(ERROR_ELEMENT_ID_) { 193 | display: none; 194 | } 195 | 196 | html, 197 | :host { 198 | direction: --x-direction(); 199 | text-orientation: --x-text-orientation(); 200 | writing-mode: --x-writing-mode(); 201 | } 202 | 203 | @media (prefers-color-scheme: dark) { 204 | html, 205 | :host { 206 | filter: invert(1) hue-rotate(180deg); 207 | } 208 | } 209 | 210 | @media (prefers-color-scheme: dark) and (forced-colors: active) { 211 | html, 212 | :host { 213 | filter: initial; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 |
23 | 24 |
25 | 26 | 43 | -------------------------------------------------------------------------------- /src/components/DemoBanner.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | {#if isCI} 26 | 31 | {/if} 32 | 33 | 74 | -------------------------------------------------------------------------------- /src/components/Disclaimer.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 37 | 38 | 95 | -------------------------------------------------------------------------------- /src/components/Dropzone.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 109 | 110 |
133 | 134 |

{STRING__DROP_YOUR_FILES_HERE_}

135 |
136 | 150 |
151 | -------------------------------------------------------------------------------- /src/components/ErrorModal.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 41 |
{STRING__ERROR_MODAL_ICON_}
42 |
43 | {STRING__AN_ERROR_OCCURRED_} 44 |
45 | {#if error_ instanceof Error} 46 |
47 |
{STRING__ERROR_NAME_}
48 |
49 | {error_.name ?? '(unknown)'} 50 |
51 | {#if error_.message} 52 |
53 | {STRING__ERROR_MESSAGE_} 54 |
55 |
56 | {error_.message} 57 |
58 | {/if} 59 | {#if error_.stack} 60 |
61 | {STRING__ERROR_STACK_} 62 |
63 |
64 |
{error_.stack}
66 |
67 | {/if} 68 |
69 | {:else} 70 |
{String(error_)}
71 | {/if} 72 |
73 |
74 |
75 | 76 | 118 | -------------------------------------------------------------------------------- /src/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | 48 |
49 |
50 | 54 |

55 | {STRING__COPYRIGHT_YEAR_ALL_RIGHTS_RESERVED_[0]}{STRING__COPYRIGHT_YEAR_ALL_RIGHTS_RESERVED_[1]} 58 |

59 | {#if packageName} 60 |
    61 |
  • 62 | {STRING__BUILD_INFORMATION_VERSION_[0]} 63 | {packageName} 64 | {#if packageVersion}{STRING__BUILD_INFORMATION_VERSION_[1]}v{packageVersion}{/if} 65 | {#if gitCommitHash} 66 | {STRING__BUILD_INFORMATION_VERSION_[2]}({gitCommitHash.slice(0, 7)}) 71 | {/if} 72 | {STRING__BUILD_INFORMATION_VERSION_[3]} 73 |
  • 74 | 75 | {#if packageHomepage} 76 |
  • 77 | {STRING__FOOTER_HOME_LINK_} 82 |
  • 83 | {/if} 84 | {#if repository} 85 |
  • 86 | {STRING__FOOTER_SOURCE_CODE_LINK_} 92 |
  • 93 | {/if} 94 |
95 | {/if} 96 |
97 |
98 | 99 | 175 | -------------------------------------------------------------------------------- /src/components/FullScreenModal.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 28 |
29 |
30 | {#if dismissable_} 31 |
32 | 37 |
38 | {/if} 39 | 40 |
41 |
42 |
43 | 44 | 125 | -------------------------------------------------------------------------------- /src/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 35 |
36 | 37 |
38 |

{STRING__TITLE_HTML_BASED_FILE_UTILITY_}

39 | 45 |
46 |
47 | 48 | 78 | -------------------------------------------------------------------------------- /src/components/HumanFileSize.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | 38 | 39 | {#if value_ >= 1125899906842624} 40 | {(value_ / 1125899906842624).toFixed(2)} {STRING__UNIT_PEBIBYTE_PiB_} 43 | {:else if value_ >= 1099511627776} 44 | {(value_ / 1099511627776).toFixed(2)} {STRING__UNIT_TEBIBYTE_TiB_} 47 | {:else if value_ >= 1073741824} 48 | {(value_ / 1073741824).toFixed(2)} {STRING__UNIT_GIBIBYTE_GiB_} 51 | {:else if value_ >= 1048576} 52 | {(value_ / 1048576).toFixed(2)} {STRING__UNIT_MEBIBYTE_MiB_} 55 | {:else if value_ >= 1024} 56 | {(value_ / 1024).toFixed(2)} {STRING__UNIT_KIBIBYTE_KiB_} 59 | {:else} 60 | {value_} {STRING__UNIT_BYTE_B_} 63 | {/if} 64 | 65 | -------------------------------------------------------------------------------- /src/components/Loading.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 23 |
24 |

25 |
26 | 27 | 41 | -------------------------------------------------------------------------------- /src/components/Logo.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | Apeleg 24 | 29 | -------------------------------------------------------------------------------- /src/components/OfflineDownload.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 51 | 52 | {#if mainScript$_ && mainStylesheet$_ && openPgpSignature$_} 53 | 64 | {/if} 65 | 66 | 81 | -------------------------------------------------------------------------------- /src/components/SkipToMainContent.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | {STRING__SKIP_TO_MAIN_CONTENT_} 23 | 24 | 45 | -------------------------------------------------------------------------------- /src/components/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 56 | -------------------------------------------------------------------------------- /src/crypto/constructCmsData.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { 17 | Asn1ContextSpecific, 18 | Asn1Object, 19 | Asn1Sequence, 20 | } from '@apeleghq/asn1-der'; 21 | import { 22 | AuthEnvelopedData, 23 | ContentEncryptionAlgorithmIdentifier, 24 | ContentType, 25 | EncryptedContent, 26 | EncryptedContentInfo, 27 | EncryptedKey, 28 | KeyDerivationAlgorithmIdentifier, 29 | KeyEncryptionAlgorithmIdentifier, 30 | MessageAuthenticationCode, 31 | PasswordRecipientInfo, 32 | RecipientInfo, 33 | RecipientInfos, 34 | } from '@apeleghq/cms-classes/cms'; 35 | import { 36 | OID_PKCS7_DATA, 37 | OID_PKCS9_SMIME_CT_AUTH_ENVELOPED_DATA, 38 | } from '@apeleghq/crypto-oids'; 39 | 40 | const constructCmsData_ = ( 41 | salt: AllowSharedBufferSource, 42 | iterationCount: number, 43 | ivPWRI: AllowSharedBufferSource, 44 | encryptedKey: AllowSharedBufferSource, 45 | nonceECI: AllowSharedBufferSource, 46 | encryptedContent: AllowSharedBufferSource, 47 | tag: AllowSharedBufferSource, 48 | ): Asn1Sequence => { 49 | return new Asn1Sequence([ 50 | new Asn1Object(OID_PKCS9_SMIME_CT_AUTH_ENVELOPED_DATA), 51 | new Asn1ContextSpecific( 52 | 0, 53 | new AuthEnvelopedData( 54 | new RecipientInfos([ 55 | new RecipientInfo( 56 | new PasswordRecipientInfo( 57 | KeyEncryptionAlgorithmIdentifier.pwriAes256cbc( 58 | ivPWRI, 59 | ), 60 | new EncryptedKey(encryptedKey), 61 | KeyDerivationAlgorithmIdentifier.pbkdf2sha512( 62 | salt, 63 | iterationCount, 64 | ), 65 | ), 66 | ), 67 | ]), 68 | new EncryptedContentInfo( 69 | new ContentType(OID_PKCS7_DATA), 70 | ContentEncryptionAlgorithmIdentifier.aes256gcm( 71 | nonceECI, 72 | tag.byteLength, 73 | ), 74 | new EncryptedContent(encryptedContent), 75 | ), 76 | new MessageAuthenticationCode(tag), 77 | ), 78 | true, 79 | ), 80 | ]); 81 | }; 82 | 83 | export default constructCmsData_; 84 | -------------------------------------------------------------------------------- /src/crypto/deriveKek.test.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as assert from 'node:assert/strict'; 17 | import { describe, it } from 'node:test'; 18 | import deriveKek from './deriveKek.js'; 19 | 20 | describe('deriveKek correctness', () => { 21 | it('should produce a valid KEK and return provided salt and iterationCount', async () => { 22 | const password = 'supersecret'; 23 | const iterationCount = 1000; 24 | const keyUsages: KeyUsage[] = ['encrypt', 'decrypt']; 25 | const salt = new Uint8Array(16); // 16 bytes salt for testing 26 | // fill salt with deterministic values for test predictability 27 | for (let i = 0; i < salt.length; i++) { 28 | salt[i] = i; 29 | } 30 | 31 | const [KEK, retSalt, retIterationCount] = await deriveKek( 32 | password, 33 | iterationCount, 34 | keyUsages, 35 | salt, 36 | ); 37 | 38 | // Verify that a CryptoKey is returned 39 | assert.equal(typeof KEK === 'object', true); 40 | // The returned salt should be the same as provided 41 | assert.deepEqual(retSalt, salt); 42 | // The returned iteration count should be the same 43 | assert.equal(retIterationCount, iterationCount); 44 | 45 | // Optionally check algorithm name of derived key 46 | assert.equal(KEK.algorithm.name, 'AES-CBC'); 47 | }); 48 | 49 | it('should generate a salt if one is not provided', async () => { 50 | const password = 'anotherpassword'; 51 | const iterationCount = 1500; 52 | const keyUsages: KeyUsage[] = ['encrypt']; 53 | 54 | const [KEK, salt, retIterationCount] = await deriveKek( 55 | password, 56 | iterationCount, 57 | keyUsages, 58 | ); 59 | 60 | // Check that salt is present and has a valid length 61 | // (32 as per the function) 62 | assert.ok(salt); 63 | assert.equal(salt.byteLength, 32); 64 | 65 | // Check iteration count 66 | assert.equal(retIterationCount, iterationCount); 67 | 68 | // Check key usages by verifying the derived key's algorithm name 69 | // and key length 70 | assert.equal(KEK.algorithm.name, 'AES-CBC'); 71 | assert.equal((KEK.algorithm as AesKeyAlgorithm).length, 256); 72 | }); 73 | 74 | it('should throw TypeError for empty password', async () => { 75 | const password = ''; 76 | const iterationCount = 1000; 77 | const keyUsages: KeyUsage[] = ['encrypt']; 78 | 79 | await assert.rejects( 80 | () => deriveKek(password, iterationCount, keyUsages), 81 | { 82 | name: 'TypeError', 83 | message: 'Invalid or empty password', 84 | }, 85 | 'Expected error for empty password', 86 | ); 87 | }); 88 | 89 | it('should throw TypeError for non-string password', async () => { 90 | const password = 12345; // not a string 91 | const iterationCount = 1000; 92 | const keyUsages: KeyUsage[] = ['decrypt']; 93 | 94 | await assert.rejects( 95 | () => 96 | deriveKek( 97 | password as unknown as string, 98 | iterationCount, 99 | keyUsages, 100 | ), 101 | { 102 | name: 'TypeError', 103 | message: 'Invalid or empty password', 104 | }, 105 | 'Expected error for non-string password', 106 | ); 107 | }); 108 | 109 | it('should throw TypeError for invalid iteration count', async () => { 110 | const password = 'testpassword'; 111 | const iterationCount = 0; // invalid: less than 1 112 | const keyUsages: KeyUsage[] = ['encrypt']; 113 | 114 | await assert.rejects( 115 | () => deriveKek(password, iterationCount, keyUsages), 116 | { 117 | name: 'TypeError', 118 | message: 'Invalid iteration count', 119 | }, 120 | 'Expected error for invalid iteration count', 121 | ); 122 | }); 123 | 124 | it('should throw TypeError for non-integer iteration count', async () => { 125 | const password = 'testpassword'; 126 | const iterationCount = 1000.5; // non-integer count 127 | const keyUsages: KeyUsage[] = ['decrypt']; 128 | 129 | await assert.rejects( 130 | () => deriveKek(password, iterationCount, keyUsages), 131 | { 132 | name: 'TypeError', 133 | message: 'Invalid iteration count', 134 | }, 135 | 'Expected error for non-integer iteration count', 136 | ); 137 | }); 138 | 139 | it('should throw TypeError for iteration count greater than Number.MAX_SAFE_INTEGER', async () => { 140 | const password = 'testpassword'; 141 | const iterationCount = Number.MAX_SAFE_INTEGER + 1; 142 | const keyUsages: KeyUsage[] = ['encrypt']; 143 | 144 | await assert.rejects( 145 | () => deriveKek(password, iterationCount, keyUsages), 146 | { 147 | name: 'TypeError', 148 | message: 'Invalid iteration count', 149 | }, 150 | 'Expected error for iteration count exceeding safe integer limit', 151 | ); 152 | }); 153 | 154 | it('should throw TypeError for invalid key usage', async () => { 155 | const password = 'testpassword'; 156 | const iterationCount = 1000; 157 | // invalid usage 158 | const keyUsages: KeyUsage[] = ['sign']; 159 | 160 | await assert.rejects( 161 | () => deriveKek(password, iterationCount, keyUsages), 162 | { 163 | name: 'TypeError', 164 | message: 'Invalid key usage', 165 | }, 166 | 'Expected error for invalid key usage', 167 | ); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/crypto/deriveKek.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const deriveKek_ = async ( 17 | password: string, 18 | iterationCount: number, 19 | keyUsages: KeyUsage[], 20 | salt?: AllowSharedBufferSource | undefined, 21 | ): Promise< 22 | [KEK: CryptoKey, salt: AllowSharedBufferSource, iterationCount: number] 23 | > => { 24 | if (typeof password !== 'string' || !password) { 25 | throw new TypeError('Invalid or empty password'); 26 | } 27 | if ( 28 | typeof iterationCount !== 'number' || 29 | iterationCount < 1 || 30 | iterationCount !== (iterationCount | 0) || 31 | iterationCount > Number.MAX_SAFE_INTEGER 32 | ) { 33 | throw new TypeError('Invalid iteration count'); 34 | } 35 | if ( 36 | !Array.isArray(keyUsages) || 37 | keyUsages.some((usage) => usage !== 'encrypt' && usage !== 'decrypt') 38 | ) { 39 | throw new TypeError('Invalid key usage'); 40 | } 41 | 42 | if (!salt) { 43 | const saltBuffer = new Uint8Array(32); 44 | crypto.getRandomValues(saltBuffer); 45 | salt = saltBuffer; 46 | } 47 | 48 | const KEK = await crypto.subtle 49 | .importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [ 50 | 'deriveKey', 51 | ]) 52 | .then((baseKey) => { 53 | return crypto.subtle.deriveKey( 54 | { 55 | ['name']: 'PBKDF2', 56 | ['salt']: salt, 57 | ['iterations']: iterationCount, 58 | ['hash']: 'SHA-512', 59 | }, 60 | baseKey, 61 | { ['name']: 'AES-CBC', ['length']: 256 }, 62 | false, 63 | keyUsages, 64 | ); 65 | }); 66 | 67 | return [KEK, salt, iterationCount]; 68 | }; 69 | 70 | export default deriveKek_; 71 | -------------------------------------------------------------------------------- /src/crypto/fileDecryptionCms.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { pwriKeyUnwrap_ as pwriKeyUnwrap } from './pwriKeyWrapping.js'; 17 | import sharedBufferConcat from './sharedBufferConcat.js'; 18 | 19 | const fileDecryptionCms_ = async ( 20 | deriveKek: { 21 | (): Promise; 22 | }, 23 | ivPWRI: AllowSharedBufferSource, 24 | encryptedKey: AllowSharedBufferSource, 25 | nonceECI: AllowSharedBufferSource, 26 | encryptedContent: AllowSharedBufferSource, 27 | tag: AllowSharedBufferSource, 28 | ): Promise => { 29 | const KEK = await deriveKek(); 30 | 31 | const rawCEK = await pwriKeyUnwrap(KEK, ivPWRI, encryptedKey); 32 | const CEK = await crypto.subtle.importKey( 33 | 'raw', 34 | rawCEK, 35 | { ['name']: 'AES-GCM', ['length']: 256 }, 36 | false, 37 | ['decrypt'], 38 | ); 39 | 40 | const buffer = sharedBufferConcat(encryptedContent, tag); 41 | const data = await crypto.subtle.decrypt( 42 | { 43 | ['name']: 'AES-GCM', 44 | ['iv']: nonceECI, 45 | ['tagLength']: tag.byteLength * 8, 46 | }, 47 | CEK, 48 | buffer, 49 | ); 50 | return data; 51 | }; 52 | 53 | export default fileDecryptionCms_; 54 | -------------------------------------------------------------------------------- /src/crypto/fileEncryptionCms.test.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as assert from 'node:assert/strict'; 17 | import { before, describe, it } from 'node:test'; 18 | import fileEncryptionCms from './fileEncryptionCms.js'; 19 | 20 | describe('fileEncryptionCms', async () => { 21 | let dummyDashedCryptoKey: CryptoKey; 22 | const dummySalt = new Uint8Array([20, 21, 22, 23]); 23 | const dummyIterationCount = 2048; 24 | 25 | const dummyData = new Uint8Array([100, 101, 102, 103, 104, 105]); 26 | 27 | const dummyDeriveKek = async (): Promise< 28 | [KEK: CryptoKey, salt: AllowSharedBufferSource, iterationCount: number] 29 | > => { 30 | return [dummyDashedCryptoKey, dummySalt, dummyIterationCount]; 31 | }; 32 | 33 | before(async () => { 34 | dummyDashedCryptoKey = await crypto.subtle.generateKey( 35 | { ['name']: 'AES-CBC', ['length']: 128 }, 36 | false, 37 | ['encrypt'], 38 | ); 39 | }); 40 | 41 | it('Returns all the expected values', async () => { 42 | const [ 43 | salt, 44 | iterationCount, 45 | ivPWRI, 46 | encryptedKey, 47 | nonceECI, 48 | encryptedContent, 49 | tag, 50 | ] = await fileEncryptionCms(dummyDeriveKek, dummyData); 51 | 52 | // Check that salt and iteration count come from our dummy deriveKek. 53 | assert.deepEqual( 54 | salt, 55 | dummySalt, 56 | 'Salt should be the same as provided by deriveKek', 57 | ); 58 | assert.equal( 59 | iterationCount, 60 | dummyIterationCount, 61 | 'Iteration count should be from deriveKek', 62 | ); 63 | 64 | // ivPWRI: should be a Uint8Array of length 16 that was randomly generated. 65 | assert.ok( 66 | ivPWRI instanceof ArrayBuffer || ArrayBuffer.isView(ivPWRI), 67 | 'ivPWRI should be a buffer', 68 | ); 69 | assert.equal(ivPWRI.byteLength, 16, 'ivPWRI should have 16 bytes'); 70 | 71 | // encryptedKey: provided by our stub pwriKeyWrap. 72 | assert.ok( 73 | encryptedKey instanceof ArrayBuffer || 74 | ArrayBuffer.isView(encryptedKey), 75 | 'encryptedKey should be an ArrayBuffer', 76 | ); 77 | assert.equal( 78 | encryptedKey.byteLength, 79 | 48, 80 | 'encryptedKey should have 48 bytes', 81 | ); 82 | 83 | // nonceECI: should be a randomly-generated Uint8Array of length 12 84 | assert.ok( 85 | nonceECI instanceof ArrayBuffer || ArrayBuffer.isView(nonceECI), 86 | 'nonceECI should be an ArrayBuffer', 87 | ); 88 | assert.equal(nonceECI.byteLength, 12, 'nonceECI should have 12 bytes'); 89 | 90 | assert.ok( 91 | encryptedContent instanceof ArrayBuffer || 92 | ArrayBuffer.isView(encryptedContent), 93 | 'encryptedContent should be an ArrayBuffer', 94 | ); 95 | assert.equal( 96 | encryptedContent.byteLength, 97 | dummyData.byteLength, 98 | 'encryptedContent should have the same length as the input', 99 | ); 100 | 101 | assert.ok( 102 | tag instanceof ArrayBuffer || ArrayBuffer.isView(tag), 103 | 'tag should be an ArrayBuffer', 104 | ); 105 | assert.equal(tag.byteLength, 16, 'tag should have 16 bytes'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/crypto/fileEncryptionCms.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { pwriKeyWrap_ as pwriKeyWrap } from './pwriKeyWrapping.js'; 17 | 18 | const gcmEncrypt = async ( 19 | key: CryptoKey, 20 | nonce: AllowSharedBufferSource, 21 | data: AllowSharedBufferSource, 22 | ): Promise< 23 | [encryptedData: AllowSharedBufferSource, tag: AllowSharedBufferSource] 24 | > => { 25 | const tagLength = 16; 26 | const result = await crypto.subtle.encrypt( 27 | { 28 | ['name']: 'AES-GCM', 29 | ['iv']: nonce, 30 | ['tagLength']: tagLength * 8, 31 | }, 32 | key, 33 | data, 34 | ); 35 | 36 | return [result.slice(0, data.byteLength), result.slice(data.byteLength)]; 37 | }; 38 | 39 | const fileEncryptionCms_ = async ( 40 | deriveKek: { 41 | (): Promise< 42 | [ 43 | KEK: CryptoKey, 44 | salt: AllowSharedBufferSource, 45 | iterationCount: number, 46 | ] 47 | >; 48 | }, 49 | data: AllowSharedBufferSource, 50 | ): Promise< 51 | [ 52 | salt: AllowSharedBufferSource, 53 | iterationCount: number, 54 | ivPWRI: AllowSharedBufferSource, 55 | encryptedKey: AllowSharedBufferSource, 56 | nonceECI: AllowSharedBufferSource, 57 | encryptedContent: AllowSharedBufferSource, 58 | tag: AllowSharedBufferSource, 59 | ] 60 | > => { 61 | let iterationCount: number; 62 | let salt: AllowSharedBufferSource; 63 | 64 | const ivPWRI = new Uint8Array(16); 65 | const nonceECI = new Uint8Array(12); 66 | 67 | crypto.getRandomValues(ivPWRI); 68 | crypto.getRandomValues(nonceECI); 69 | 70 | const [encryptedKey, [encryptedContent, tag]] = await crypto.subtle 71 | .generateKey({ ['name']: 'AES-GCM', ['length']: 256 }, true, [ 72 | 'encrypt', 73 | ]) 74 | .then((CEK) => { 75 | const KEKp = deriveKek().then(([lKEK, lSalt, lIterationCount]) => { 76 | iterationCount = lIterationCount; 77 | salt = lSalt; 78 | return lKEK; 79 | }); 80 | 81 | return Promise.all([ 82 | Promise.all([KEKp, crypto.subtle.exportKey('raw', CEK)]).then( 83 | ([KEK, rawCEK]) => { 84 | return pwriKeyWrap(KEK, ivPWRI, rawCEK); 85 | }, 86 | ), 87 | gcmEncrypt(CEK, nonceECI, data), 88 | ]); 89 | }); 90 | 91 | return [ 92 | salt!, 93 | iterationCount!, 94 | ivPWRI, 95 | encryptedKey, 96 | nonceECI, 97 | encryptedContent, 98 | tag, 99 | ]; 100 | }; 101 | 102 | export default fileEncryptionCms_; 103 | -------------------------------------------------------------------------------- /src/crypto/index.test.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import './constructCmsData.test.js'; 17 | import './deriveKek.test.js'; 18 | import './fileEncryptionCms.test.js'; 19 | import './pwriKeyWrapping.test.js'; 20 | -------------------------------------------------------------------------------- /src/crypto/pwriKeyWrapping.test.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as assert from 'node:assert/strict'; 17 | import { describe, it } from 'node:test'; 18 | import { 19 | pwriKeyUnwrap_ as pwriKeyUnwrap, 20 | pwriKeyWrap_ as pwriKeyWrap, 21 | } from './pwriKeyWrapping.js'; 22 | import sharedBufferToUint8Array from './sharedBufferToUint8Array.js'; 23 | 24 | describe('pwriKeyWrapping', () => { 25 | it('Correctly wraps and unwraps PWRI keys', async () => { 26 | const KEK = await crypto.subtle.generateKey( 27 | { ['name']: 'AES-CBC', ['length']: 256 }, 28 | false, 29 | // Need both encrypt and decrypt to reconstruct padding 30 | ['encrypt', 'decrypt'], 31 | ); 32 | const IV = new Uint8Array(16); 33 | const CEK = new Uint8Array(32); 34 | 35 | const wrapped = await pwriKeyWrap(KEK, IV, CEK); 36 | assert.equal(wrapped.byteLength, 48); 37 | const unwrapped = await pwriKeyUnwrap(KEK, IV, wrapped); 38 | 39 | assert.deepEqual(sharedBufferToUint8Array(unwrapped), CEK); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/crypto/pwriKeyWrapping.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 202 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import sharedBufferConcat from './sharedBufferConcat.js'; 17 | import sharedBufferToUint8Array from './sharedBufferToUint8Array.js'; 18 | 19 | // See RFC 3211, section 2.3 20 | const pwriKeyWrap_ = async ( 21 | KEK: CryptoKey, 22 | IV: AllowSharedBufferSource, 23 | CEK: AllowSharedBufferSource, 24 | ): Promise => { 25 | const CEKu8 = sharedBufferToUint8Array(CEK); 26 | const wrappedLen = 4 + CEK.byteLength; 27 | const paddedLen = Math.max((((wrappedLen - 1) >> 4) + 1) << 4, 32); 28 | const formattedKey = new Uint8Array(paddedLen); 29 | formattedKey[0] = CEK.byteLength; 30 | formattedKey[1] = ~CEKu8[0]; 31 | formattedKey[2] = ~CEKu8[1]; 32 | formattedKey[3] = ~CEKu8[2]; 33 | formattedKey.set(CEKu8, 4); 34 | 35 | if (wrappedLen !== paddedLen) { 36 | crypto.getRandomValues(formattedKey.subarray(wrappedLen)); 37 | } 38 | 39 | const encryptedPaddedKey = await crypto.subtle.encrypt( 40 | { 41 | ['name']: 'AES-CBC', 42 | ['iv']: IV, 43 | }, 44 | KEK, 45 | formattedKey, 46 | ); 47 | 48 | const wrappedKey = await crypto.subtle.encrypt( 49 | { 50 | ['name']: 'AES-CBC', 51 | ['iv']: encryptedPaddedKey.slice(paddedLen - 16, paddedLen), 52 | }, 53 | KEK, 54 | // Get rid of PKCS#7 padding 55 | encryptedPaddedKey.slice(0, paddedLen), 56 | ); 57 | 58 | // Get rid of PKCS#7 padding 59 | return wrappedKey.slice(0, paddedLen); 60 | }; 61 | 62 | // See RFC 3211, section 2.3 63 | const pwriKeyUnwrap_ = async ( 64 | KEK: CryptoKey, 65 | IV: AllowSharedBufferSource, 66 | wrappedCEK: AllowSharedBufferSource, 67 | ): Promise => { 68 | const wrappedCEKu8 = sharedBufferToUint8Array(wrappedCEK); 69 | 70 | // SubtleCrypto expects PKCS#7 padding, so we need to add it. 71 | const reconstructedPkcs7OuterPadding = ( 72 | await crypto.subtle.encrypt( 73 | { 74 | ['name']: 'AES-CBC', 75 | ['iv']: wrappedCEKu8.subarray(wrappedCEKu8.byteLength - 16), 76 | }, 77 | KEK, 78 | new Uint8Array(new Array(16).fill(16)), 79 | ) 80 | ).slice(0, 16); 81 | const outerIv = await crypto.subtle.decrypt( 82 | { 83 | ['name']: 'AES-CBC', 84 | ['iv']: wrappedCEKu8.subarray( 85 | wrappedCEKu8.byteLength - 32, 86 | wrappedCEKu8.byteLength - 16, 87 | ), 88 | }, 89 | KEK, 90 | sharedBufferConcat( 91 | wrappedCEKu8.subarray(wrappedCEKu8.byteLength - 16), 92 | reconstructedPkcs7OuterPadding, 93 | ), 94 | ); 95 | 96 | const encryptedPaddedKey = await crypto.subtle.decrypt( 97 | { 98 | ['name']: 'AES-CBC', 99 | ['iv']: outerIv, 100 | }, 101 | KEK, 102 | sharedBufferConcat(wrappedCEKu8, reconstructedPkcs7OuterPadding), 103 | ); 104 | 105 | const reconstructedPkcs7InnerPadding = ( 106 | await crypto.subtle.encrypt( 107 | { 108 | ['name']: 'AES-CBC', 109 | ['iv']: encryptedPaddedKey.slice( 110 | encryptedPaddedKey.byteLength - 16, 111 | ), 112 | }, 113 | KEK, 114 | new Uint8Array(new Array(16).fill(16)), 115 | ) 116 | ).slice(0, 16); 117 | const formattedKey = await crypto.subtle.decrypt( 118 | { 119 | ['name']: 'AES-CBC', 120 | ['iv']: IV, 121 | }, 122 | KEK, 123 | sharedBufferConcat(encryptedPaddedKey, reconstructedPkcs7InnerPadding), 124 | ); 125 | 126 | const formattedKeyU8 = new Uint8Array(formattedKey); 127 | 128 | if ( 129 | ((formattedKeyU8[1] ^ formattedKeyU8[4]) & 130 | (formattedKeyU8[2] ^ formattedKeyU8[5]) & 131 | (formattedKeyU8[3] ^ formattedKeyU8[6])) !== 132 | 0xff 133 | ) { 134 | throw new Error('Invalid check bytes'); 135 | } 136 | 137 | if ( 138 | formattedKeyU8[0] < 3 || 139 | formattedKeyU8[0] > formattedKeyU8.byteLength - 4 140 | ) { 141 | throw new Error('Invalid key length'); 142 | } 143 | 144 | return formattedKeyU8.subarray(4, 4 + formattedKeyU8[0]); 145 | }; 146 | 147 | export { pwriKeyUnwrap_, pwriKeyWrap_ }; 148 | -------------------------------------------------------------------------------- /src/crypto/sharedBufferConcat.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Simple re-export using `..` to avoid needing a custom resolver to run tests 17 | export * from '../lib/sharedBufferConcat.js'; 18 | export { default } from '../lib/sharedBufferConcat.js'; 19 | -------------------------------------------------------------------------------- /src/crypto/sharedBufferToUint8Array.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Simple re-export using `..` to avoid needing a custom resolver to run tests 17 | export * from '../lib/sharedBufferToUint8Array.js'; 18 | export { default } from '../lib/sharedBufferToUint8Array.js'; 19 | -------------------------------------------------------------------------------- /src/fallbackMessage.inline.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { STRING__ERROR_MESSAGE_UNSUPPORTED_BROWSER_ } from '~/i18n/strings.js'; 17 | import { ERROR_ELEMENT_ID_ } from '~/lib/elementIds.js'; 18 | 19 | type CSSProperty = keyof Omit< 20 | Partial, 21 | | 'length' 22 | | 'parentRule' 23 | | 'getPropertyPriority' 24 | | 'getPropertyValue' 25 | | 'item' 26 | | 'removeProperty' 27 | | 'setProperty' 28 | >; 29 | 30 | self.onerror = function ( 31 | event: ErrorEvent | string, 32 | _document: Document, 33 | _e$: HTMLElement | null, 34 | _paragraph$: HTMLParagraphElement, 35 | _code$: HTMLElement, 36 | ) { 37 | // These are just to avoid declaring variables and unused as arguments 38 | void _document; 39 | void _e$; 40 | void _paragraph$; 41 | void _code$; 42 | 43 | _document = document; 44 | _e$ = _document.getElementById(ERROR_ELEMENT_ID_); 45 | if (_e$) { 46 | _e$.style['display'] = 'block'; 47 | if (event) { 48 | _paragraph$ = _document.createElement('p'); 49 | _code$ = _document.createElement('code'); 50 | _code$.appendChild( 51 | _document.createTextNode( 52 | (event as unknown as ErrorEvent).message || 53 | (event as unknown as string), 54 | ), 55 | ); 56 | _paragraph$.appendChild(_code$); 57 | for (;;) { 58 | if ( 59 | (_e$ as HTMLElement).firstChild && 60 | ((_e$ as HTMLElement).firstChild as HTMLElement) 61 | .nodeType === 1 62 | ) { 63 | _e$ = (_e$ as HTMLElement).firstChild as HTMLElement; 64 | } else { 65 | break; 66 | } 67 | } 68 | (_e$.parentNode as HTMLElement).appendChild(_paragraph$); 69 | } 70 | } 71 | return false; 72 | } as unknown as Window['onerror']; 73 | 74 | if ( 75 | typeof Reflect === [] + [][0] || 76 | typeof globalThis === [] + [][0] || 77 | typeof AbortController !== 'function' || 78 | typeof Promise !== 'function' || 79 | !Promise.prototype || 80 | typeof Promise.prototype.finally !== 'function' || 81 | typeof Blob !== 'function' 82 | // There's also , 83 | // , 84 | // , but 85 | // feature detecting that (message from iframe has isTrusted = false) is 86 | // more involved 87 | // Detecting globalThis might work in this case 88 | ) 89 | self.onload = function ( 90 | _document: Document, 91 | _body$: HTMLElement, 92 | _createElement: Document['createElement'], 93 | _div$: HTMLDivElement, 94 | _divStyles: string[], 95 | _paragraph$: HTMLParagraphElement, 96 | _paragraphStyles: string[], 97 | _applyStyle: ($: HTMLElement, y: string[], i?: number) => void, 98 | ) { 99 | // Unused values as they're there just to help minification by avoiding 100 | // variable declarations 101 | void _document; 102 | void _body$; 103 | void _createElement; 104 | void _div$; 105 | void _divStyles; 106 | void _paragraph$; 107 | void _paragraphStyles; 108 | void _applyStyle; 109 | 110 | _document = document; 111 | _createElement = _document.createElement.bind(_document); 112 | _div$ = _createElement('div'); 113 | _divStyles = 114 | 'position|relative|zIndex|999|width|100%|height|auto|inlineSize|100%|blockSize|auto|margin|0|padding|4px|backgroundColor|#ffffde|color|#333|border|2px none #8c8475|borderBottomStyle|solid|borderInlineStyle|none|borderBlockStyle|none|borderBlockEndStyle|solid|fontSize|12px|fontFamily|Verdana'.split( 115 | '|', 116 | ); 117 | _paragraph$ = _createElement('p'); 118 | _paragraphStyles = 119 | 'maxWidth|1024px|maxHeight|none|maxInlineSize|1024px|maxBlockSize|none|margin|0 auto|marginBlock|0|marginInline|auto'.split( 120 | '|', 121 | ); 122 | _applyStyle = function ($: HTMLElement, y: string[], i?: number) { 123 | for (i = 0; i < y.length; i += 2) 124 | $.style[y[i] as CSSProperty] = y[i + 1]; 125 | }; 126 | _applyStyle(_paragraph$, _paragraphStyles); 127 | _applyStyle(_div$, _divStyles); 128 | _paragraph$.appendChild( 129 | _document.createTextNode( 130 | STRING__ERROR_MESSAGE_UNSUPPORTED_BROWSER_, 131 | ), 132 | ); 133 | _div$.appendChild(_paragraph$); 134 | setTimeout(function () { 135 | _body$ = _document.body; 136 | _body$.insertBefore(_div$, _body$.firstChild); 137 | }, 1500); 138 | } as unknown as Window['onload']; 139 | -------------------------------------------------------------------------------- /src/i18n/strings.en-tbup.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* eslint-disable @typescript-eslint/naming-convention */ 17 | export * from './strings.js'; 18 | export const LANG_TEXT_ORIENTATION_ = 'upright'; 19 | export const LANG_WRITING_MODE_ = 'vertical-rl'; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /// 17 | 18 | import App from '~/App.svelte'; 19 | import { ERROR_ELEMENT_ID_, ROOT_ELEMENT_ID_ } from '~/lib/elementIds.js'; 20 | import isCI from '~/lib/isCI.js'; 21 | import { generateBody_ } from './lib/generateHtml.js'; 22 | import tightenCsp_ from './lib/tightenCsp.js'; 23 | 24 | const onLoad = (handler: { (): void }) => { 25 | if ( 26 | typeof document === 'undefined' || 27 | typeof Document !== 'function' || 28 | !(document instanceof Document) || 29 | typeof self !== 'object' || 30 | typeof top !== 'object' || 31 | typeof window !== 'object' || 32 | typeof Window !== 'function' || 33 | !(window instanceof Window) 34 | ) { 35 | throw new Error('Not executing in a browser context'); 36 | } 37 | 38 | if (__buildtimeSettings__.enableFramebusting && !isCI && self !== top) { 39 | throw new Error('Not executing in a top-level window'); 40 | } 41 | 42 | if (typeof isSecureContext !== 'boolean' || !isSecureContext) { 43 | throw new Error('Not executing in a secure context'); 44 | } 45 | 46 | if ( 47 | typeof crypto === 'undefined' || 48 | typeof Crypto !== 'function' || 49 | !(crypto instanceof Crypto) || 50 | typeof crypto.subtle === 'undefined' || 51 | typeof SubtleCrypto !== 'function' || 52 | !(crypto.subtle instanceof SubtleCrypto) 53 | ) { 54 | throw new Error('Missing required crypto primitives'); 55 | } 56 | 57 | if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { 58 | setTimeout(handler, 0); 59 | } else if (document.addEventListener) { 60 | const eventListener = () => { 61 | document.removeEventListener( 62 | 'DOMContentLoaded', 63 | eventListener, 64 | false, 65 | ); 66 | handler(); 67 | }; 68 | document.addEventListener('DOMContentLoaded', eventListener, false); 69 | } else { 70 | throw new Error('Unsupported browser'); 71 | } 72 | }; 73 | 74 | onLoad(() => { 75 | const ns = 'http://www.w3.org/1999/xhtml'; 76 | const rootId = ROOT_ELEMENT_ID_; 77 | const parser = new DOMParser(); 78 | 79 | const newRoot$ = document.createElementNS(ns, 'div'); 80 | newRoot$.setAttribute('id', rootId); 81 | 82 | // Replace body to reduce the opportunities for tampering with the 83 | // presentational aspects by modifying unsigned parts of the HTML file. 84 | const newBodyDocument = parser.parseFromString( 85 | // Generate no `noscript` tag 86 | '' + generateBody_() + '', 87 | document.contentType as unknown as DOMParserSupportedType, 88 | ); 89 | const root$ = newBodyDocument.getElementById(rootId); 90 | const error$ = newBodyDocument.getElementById(ERROR_ELEMENT_ID_); 91 | const body$ = document.adoptNode(newBodyDocument.body); 92 | 93 | tightenCsp_(); 94 | document.documentElement.replaceChild(body$, document.body); 95 | 96 | // Now, create the App. This needs to be done after replacing body because 97 | // the sandbox attaches elements to the body that shouldn't be removed. 98 | // Otherwise, this would come before replacing body. 99 | void new App({ 100 | ['target']: newRoot$, 101 | }); 102 | 103 | if (root$) { 104 | body$.replaceChild(newRoot$, root$); 105 | } else { 106 | body$.appendChild(newRoot$); 107 | } 108 | if (error$) { 109 | body$.removeChild(error$); 110 | } 111 | window.onerror = null; 112 | }); 113 | -------------------------------------------------------------------------------- /src/lib/Cache.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const newId = () => { 17 | const buf = new Uint8Array(16); 18 | crypto.getRandomValues(buf); 19 | return Array.from(buf) 20 | .map((c) => c.toString(16).padStart(2, '0')) 21 | .join(''); 22 | }; 23 | 24 | class Cache { 25 | keyCache_: Record; 26 | 27 | constructor() { 28 | this.keyCache_ = Object.create(null); 29 | } 30 | 31 | get_(key: string): TV { 32 | if (Object.prototype.hasOwnProperty.call(this.keyCache_, key)) { 33 | return this.keyCache_[key]; 34 | } 35 | throw new RangeError('Non-existent key'); 36 | } 37 | 38 | add_(value: TV): string { 39 | const key = newId(); 40 | this.keyCache_[key] = value; 41 | 42 | return key; 43 | } 44 | } 45 | 46 | export default Cache; 47 | -------------------------------------------------------------------------------- /src/lib/EFormFields.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 5 | * exceptions; you may not use this file except in compliance with the 6 | * License. You may obtain a copy of the License at 7 | * 8 | * http://llvm.org/foundation/relicensing/LICENSE.txt 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | enum EFormFields { 18 | ARCHIVE_FILENAME = '1', 19 | FILE = '2', 20 | FILENAME = '3', 21 | NO_FILENAME = '4', 22 | HINT = '5', 23 | PASSWORD = '6', 24 | PASSWORD_CONFIRM = '7', 25 | PBKDF2_ITERATION_COUNT = '8', 26 | } 27 | 28 | export default EFormFields; 29 | -------------------------------------------------------------------------------- /src/lib/blobToBuffer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const blobToBuffer_ = (blob: Blob) => { 17 | if (typeof blob.arrayBuffer === 'function') { 18 | // More modern API, it also works on iOS Safari in lockdown mode 19 | return blob.arrayBuffer(); 20 | } else if (typeof FileReader === 'function') { 21 | // Older and more widely-supported API 22 | return new Promise((resolve, reject) => { 23 | const fileReader = new FileReader(); 24 | fileReader.onerror = () => { 25 | reject(fileReader.error); 26 | }; 27 | fileReader.onload = () => { 28 | resolve(fileReader.result as ArrayBuffer); 29 | }; 30 | fileReader.readAsArrayBuffer(blob); 31 | }); 32 | } else { 33 | throw new Error('Unable to read file contents'); 34 | } 35 | }; 36 | 37 | export default blobToBuffer_; 38 | -------------------------------------------------------------------------------- /src/lib/bufferEqual.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import sharedBufferToUint8Array from './sharedBufferToUint8Array.js'; 17 | 18 | const bufferEqual_ = ( 19 | a: AllowSharedBufferSource, 20 | b: AllowSharedBufferSource, 21 | ) => { 22 | let r = a.byteLength ^ b.byteLength; 23 | 24 | const u8a = sharedBufferToUint8Array(a); 25 | const u8b = sharedBufferToUint8Array(b); 26 | 27 | const minLength = Math.min(a.byteLength, b.byteLength); 28 | for (let i = 0; i < minLength; i++) { 29 | r |= u8a[i] ^ u8b[i]; 30 | } 31 | 32 | return r === 0; 33 | }; 34 | 35 | export default bufferEqual_; 36 | -------------------------------------------------------------------------------- /src/lib/chunkString.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const chunkString_ = (str: string, chunkSize = 64) => { 17 | const chunks = []; 18 | for (let i = 0; i < str.length; i += chunkSize) { 19 | chunks.push(str.substring(i, i + chunkSize)); 20 | } 21 | return chunks; 22 | }; 23 | 24 | export default chunkString_; 25 | -------------------------------------------------------------------------------- /src/lib/classNames.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* eslint-disable @typescript-eslint/naming-convention */ 17 | 18 | export const CHECKBOX_CLASSNAME_ = '_1'; 19 | export const DECRYPT_DETAIL_NAME_CLASSNAME_ = '_2'; 20 | export const DECRYPT_DETAIL_VALUE_CLASSNAME_ = '_3'; 21 | export const DECRYPT_SUCCESS_ = '_4'; 22 | export const DROPZONE_CLASSNAME_ = '_5'; 23 | export const DROPZONE_ACTIVE_CLASSNAME_ = '_6'; 24 | export const DROPZONE_SELECTED_CLASSNAME_ = '_7'; 25 | export const DROPZONE_ICON_CLASSNAME_ = '_8'; 26 | export const DROPZONE_INNER_CLASSNAME_ = '_9'; 27 | export const DROPZONE_TEXT_CLASSNAME_ = '_10'; 28 | export const ERRORMODAL_ERRORNAME_CLASSNAME_ = '_11'; 29 | export const ERRORMODAL_ICON_CLASSNAME_ = '_12'; 30 | export const ERRORMODAL_MESSAGE_CLASSNAME_ = '_13'; 31 | export const ERRORMODAL_STACK_CLASSNAME_ = '_14'; 32 | export const FIELDSET_CLASSNAME_ = '_15'; 33 | export const INNER_CLASSNAME_ = '_16'; 34 | export const LABEL_CLASSNAME_ = '_17'; 35 | export const MAIN_CLASSNAME_ = '_18'; 36 | export const PAGE_TITLE_CLASSNAME_ = '_19'; 37 | export const PRIMARY_BUTTON_CLASSNAME_ = '_20'; 38 | export const SECONDARY_BUTTON_CLASSNAME_ = '_21'; 39 | export const SR_ONLY_CLASSNAME_ = '_22'; 40 | -------------------------------------------------------------------------------- /src/lib/cmsPemToDer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const cmsPemToDer_ = (s: string): AllowSharedBufferSource => { 17 | const fiveDashes = String.prototype.repeat.call('-', 5); 18 | const cmsBeginMarker = fiveDashes + 'BEGIN CMS' + fiveDashes; 19 | const cmsEndMarker = fiveDashes + 'END CMS' + fiveDashes; 20 | const start = s.indexOf(cmsBeginMarker); 21 | if (start < 0) { 22 | throw new RangeError('Unable to find PEM CMS start'); 23 | } 24 | const end = s.indexOf(cmsEndMarker, start + cmsBeginMarker.length + 1); 25 | if (end < 0) { 26 | throw new RangeError('Unable to find PEM CMS end'); 27 | } 28 | 29 | return new Uint8Array( 30 | atob( 31 | s 32 | .slice(start + cmsBeginMarker.length, end) 33 | .replace(/[^A-Za-z0-9+/=]/g, ''), 34 | ) 35 | .split('') 36 | .map((c) => c.charCodeAt(0)), 37 | ); 38 | }; 39 | 40 | export default cmsPemToDer_; 41 | -------------------------------------------------------------------------------- /src/lib/commentCdataEscapeSequence.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const commentCdataEscapeSequenceStart_ = ''; 18 | -------------------------------------------------------------------------------- /src/lib/commentCdataExtractor.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Delimiters look like: 17 | // * start: `>` (HTML) 19 | const commentCdataExtractor_ = (text: string) => { 20 | const matches = text.match( 21 | /^\s*(?:)?\s*$/, 22 | ); 23 | if (matches) { 24 | return matches[1].trim(); 25 | } 26 | }; 27 | 28 | export default commentCdataExtractor_; 29 | -------------------------------------------------------------------------------- /src/lib/downloadArchive.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import commentCdataExtractor from './commentCdataExtractor.js'; 17 | import downloadBlob from './downloadBlob.js'; 18 | import generateHtml from './generateHtml.js'; 19 | 20 | const downloadArchive_ = async ( 21 | mainScript$: HTMLScriptElement, 22 | mainStylesheet$: HTMLLinkElement, 23 | openPgpSignature$: HTMLScriptElement, 24 | archiveName: string, 25 | encryptedContent?: string | undefined, 26 | hint?: string | undefined, 27 | ) => { 28 | const handleResponseText = (r: Response) => { 29 | if (!r.ok) throw new Error('Invalid response code'); 30 | return r.arrayBuffer(); 31 | }; 32 | 33 | const [scriptSrc, styleSrc] = await Promise.all([ 34 | fetch(mainScript$.src).then(handleResponseText), 35 | fetch(mainStylesheet$.href).then(handleResponseText), 36 | ]); 37 | 38 | const signatureData = commentCdataExtractor(openPgpSignature$.text); 39 | 40 | const htmlDocument = await generateHtml( 41 | scriptSrc, 42 | styleSrc, 43 | signatureData, 44 | encryptedContent, 45 | hint, 46 | ); 47 | 48 | downloadBlob( 49 | new Blob([htmlDocument], { 50 | ['type']: 'text/html', 51 | }), 52 | archiveName, 53 | ); 54 | }; 55 | 56 | export default downloadArchive_; 57 | -------------------------------------------------------------------------------- /src/lib/downloadBlob.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const downloadBlob_ = (blob: Blob, filename?: string) => { 17 | const url = URL.createObjectURL(blob); 18 | const a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); 19 | a.style.setProperty('display', 'none'); 20 | a.style.setProperty('position', 'absolute'); 21 | a.style.setProperty('transform', 'scale(0)'); 22 | a.setAttribute('href', url); 23 | a.setAttribute( 24 | 'download', 25 | filename || (blob instanceof File && blob.name) || '', 26 | ); 27 | document.body.appendChild(a); 28 | const download = () => { 29 | try { 30 | a.click(); 31 | } finally { 32 | a.remove(); 33 | URL.revokeObjectURL(url); 34 | } 35 | }; 36 | // The timeout allows the download to be intercepted by automation systems 37 | setTimeout(download, 0); 38 | }; 39 | 40 | export default downloadBlob_; 41 | -------------------------------------------------------------------------------- /src/lib/elementIds.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /* eslint-disable @typescript-eslint/naming-convention */ 17 | 18 | export const CMS_DATA_ELEMENT_ID_ = 'i1'; 19 | export const CMS_HINT_ELEMENT_ID_ = 'i3'; 20 | export const ENCRYPT_DROPZONE_ELEMENT_ID_ = 'i4'; 21 | export const ERROR_ELEMENT_ID_ = 'i5'; 22 | export const ERROR_WARNING_CONTAINER_ELEMENT_ID_ = 'i6'; 23 | export const ERROR_WARNING_TEXT_CONTAINER_ELEMENT_ID_ = 'i7'; 24 | export const ERROR_WARNING_TEXT_ELEMENT_ID_ = 'i8'; 25 | export const FALLBACK_CONTENT_ELEMENT_ID_ = 'i9'; 26 | export const LOADING_ANIMATION_ELEMENT_ID_ = 'i10'; 27 | export const LOADING_ELEMENT_ID_ = 'i11'; 28 | export const LOADING_TEXT_ELEMENT_ID_ = 'i12'; 29 | export const MAIN_CONTENT_ELEMENT_ID_ = 'i13'; 30 | export const MAIN_SCRIPT_ELEMENT_ID_ = 'i14'; 31 | export const MAIN_SCRIPT_SRC_ELEMENT_ID_ = 'i15'; 32 | export const MAIN_STYLESHEET_ELEMENT_ID_ = 'i16'; 33 | export const NOSCRIPT_WARNING_CONTAINER_ELEMENT_ID_ = 'i17'; 34 | export const NOSCRIPT_WARNING_TEXT_CONTAINER_ELEMENT_ID_ = 'i18'; 35 | export const NOSCRIPT_WARNING_TEXT_ELEMENT_ID_ = 'i19'; 36 | export const OPENPGP_SIGNATURE_ELEMENT_ID_ = 'i20'; 37 | export const ROOT_ELEMENT_ID_ = 'i21'; 38 | -------------------------------------------------------------------------------- /src/lib/fixBrokenSandboxSecureContext.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Needed to address this Chromium issue: 17 | // 18 | 19 | declare const external$decrypt: SubtleCrypto['decrypt'] | undefined; 20 | declare const external$deriveKey: SubtleCrypto['deriveKey'] | undefined; 21 | declare const external$encrypt: SubtleCrypto['encrypt'] | undefined; 22 | declare const external$exportKey: SubtleCrypto['exportKey'] | undefined; 23 | declare const external$generateKey: SubtleCrypto['generateKey'] | undefined; 24 | declare const external$importKey: SubtleCrypto['importKey'] | undefined; 25 | 26 | if (!crypto.subtle) { 27 | console.warn( 28 | 'SubtleCrypto is not available. External (unsandboxed) calls will be used instead.', 29 | ); 30 | const subtlePrototypePolyfill = Object.create(null); 31 | 32 | const assign = (p: PropertyKey, v: T) => { 33 | Object.defineProperty(subtlePrototypePolyfill, p, { 34 | ['writable']: true, 35 | ['enumerable']: true, 36 | ['configurable']: true, 37 | ['value']: v, 38 | }); 39 | }; 40 | 41 | if (typeof external$decrypt === 'function') { 42 | assign('decrypt', external$decrypt); 43 | } 44 | 45 | if (typeof external$deriveKey === 'function') { 46 | assign('deriveKey', external$deriveKey); 47 | } 48 | 49 | if (typeof external$encrypt === 'function') { 50 | assign('encrypt', external$encrypt); 51 | } 52 | 53 | if (typeof external$exportKey === 'function') { 54 | assign('exportKey', external$exportKey); 55 | } 56 | 57 | if (typeof external$generateKey === 'function') { 58 | assign('generateKey', external$generateKey); 59 | } 60 | 61 | if (typeof external$importKey === 'function') { 62 | assign('importKey', external$importKey); 63 | } 64 | 65 | Object.defineProperty(crypto, 'subtle', { 66 | ['configurable']: true, 67 | ['value']: Object.create(subtlePrototypePolyfill), 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/getWrappedCryptoFunctions.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import Cache from './Cache.js'; 17 | 18 | type TIndirectCryptoKey = string; 19 | 20 | const getWrappedCryptoFunctions_ = () => { 21 | const keyCache = new Cache(); 22 | const subtle = crypto.subtle; 23 | 24 | const decrypt = async ( 25 | algorithm: 26 | | AlgorithmIdentifier 27 | | RsaOaepParams 28 | | AesCtrParams 29 | | AesCbcParams 30 | | AesGcmParams, 31 | key: TIndirectCryptoKey, 32 | data: BufferSource, 33 | ): Promise => { 34 | const lookedUpKey = keyCache.get_(key); 35 | return subtle.decrypt(algorithm, lookedUpKey, data); 36 | }; 37 | 38 | const deriveKey = async ( 39 | algorithm: 40 | | AlgorithmIdentifier 41 | | EcdhKeyDeriveParams 42 | | HkdfParams 43 | | Pbkdf2Params, 44 | baseKey: TIndirectCryptoKey, 45 | derivedKeyType: 46 | | AlgorithmIdentifier 47 | | HkdfParams 48 | | Pbkdf2Params 49 | | AesDerivedKeyParams 50 | | HmacImportParams, 51 | extractable: boolean, 52 | keyUsages: KeyUsage[], 53 | ): Promise => { 54 | const lookedUpKey = keyCache.get_(baseKey); 55 | const result = await subtle.deriveKey( 56 | algorithm, 57 | lookedUpKey, 58 | derivedKeyType, 59 | extractable, 60 | keyUsages, 61 | ); 62 | return keyCache.add_(result); 63 | }; 64 | 65 | const encrypt = async ( 66 | algorithm: 67 | | AlgorithmIdentifier 68 | | RsaOaepParams 69 | | AesCtrParams 70 | | AesCbcParams 71 | | AesGcmParams, 72 | key: TIndirectCryptoKey, 73 | data: BufferSource, 74 | ): Promise => { 75 | const lookedUpKey = keyCache.get_(key); 76 | return subtle.encrypt(algorithm, lookedUpKey, data); 77 | }; 78 | 79 | const exportKey = async (format: 'jwk', key: TIndirectCryptoKey) => { 80 | const lookedUpKey = keyCache.get_(key); 81 | return subtle.exportKey(format, lookedUpKey); 82 | }; 83 | 84 | const generateKey = async ( 85 | ...args: Parameters 86 | ): Promise< 87 | | TIndirectCryptoKey 88 | | { 89 | ['privateKey']: TIndirectCryptoKey; 90 | ['publicKey']: TIndirectCryptoKey; 91 | } 92 | > => { 93 | const result = await subtle.generateKey(...args); 94 | if (result instanceof CryptoKey) { 95 | return keyCache.add_(result); 96 | } else { 97 | const keyPair = result; 98 | const privateKeyId = keyCache.add_(keyPair['privateKey']); 99 | const publicKeyId = keyCache.add_(keyPair['publicKey']); 100 | return { 101 | ['privateKey']: privateKeyId, 102 | ['publicKey']: publicKeyId, 103 | }; 104 | } 105 | }; 106 | 107 | const importKey = async ( 108 | ...args: Parameters 109 | ): Promise => { 110 | const result = await subtle.importKey(...args); 111 | return keyCache.add_(result); 112 | }; 113 | 114 | return { 115 | decrypt_: decrypt, 116 | deriveKey_: deriveKey, 117 | encrypt_: encrypt, 118 | exportKey_: exportKey, 119 | generateKey_: generateKey, 120 | importKey_: importKey, 121 | }; 122 | }; 123 | 124 | export default getWrappedCryptoFunctions_; 125 | -------------------------------------------------------------------------------- /src/lib/isCI.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | declare const __CI__: unknown; 17 | 18 | const isCI_ = typeof __CI__ !== [] + [][0] && !!__CI__; 19 | 20 | export default isCI_; 21 | -------------------------------------------------------------------------------- /src/lib/isTrustedEvent.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import isCI from './isCI.js'; 17 | 18 | const isTrustedEvent_ = (e: Event) => { 19 | return isCI || e.isTrusted; 20 | }; 21 | 22 | export default isTrustedEvent_; 23 | -------------------------------------------------------------------------------- /src/lib/packageInfo.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const gitCommitHash_ = __buildtimeSettings__.gitCommitHash; 17 | export const packageName_ = __buildtimeSettings__.package.name; 18 | export const packageVersion_ = __buildtimeSettings__.package.version; 19 | export const packageHomepage_ = __buildtimeSettings__.package.homepage; 20 | export const packageRepository_ = __buildtimeSettings__.package.repository; 21 | -------------------------------------------------------------------------------- /src/lib/prepareDownloadableCmsPayload.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import chunkString from './chunkString.js'; 17 | import { constructCmsData$SEP_ } from './sandboxEntrypoints.js'; 18 | import type setupConstructCmsSandbox from './setupConstructCmsSandbox.js'; 19 | import type setupEncryptionSandbox from './setupEncryptionSandbox.js'; 20 | import sharedBufferToUint8Array from './sharedBufferToUint8Array.js'; 21 | import uint8ArrayToBase64 from './uint8ArrayToBase64.js'; 22 | 23 | const derToPem = (derEncoded: AllowSharedBufferSource) => { 24 | const fiveDashes = String.prototype.repeat.call('-', 5); 25 | const cmsBeginMarker = fiveDashes + 'BEGIN CMS' + fiveDashes + '\r\n'; 26 | const cmsEndMarker = fiveDashes + 'END CMS' + fiveDashes + '\r\n'; 27 | 28 | const buf = sharedBufferToUint8Array(derEncoded); 29 | const base64EncodedBuf = uint8ArrayToBase64(buf); 30 | 31 | const cmsPemData = 32 | cmsBeginMarker + 33 | (base64EncodedBuf 34 | ? chunkString(base64EncodedBuf).join('\r\n') + '\r\n' 35 | : '') + 36 | cmsEndMarker; 37 | 38 | return cmsPemData; 39 | }; 40 | 41 | const prepareDownloadableCmsPayload_ = async ( 42 | cmsSandbox: Awaited>, 43 | encryptionSandbox: Awaited>, 44 | buffer: AllowSharedBufferSource, 45 | filename: string, 46 | ): Promise => { 47 | if ( 48 | typeof cmsSandbox !== 'function' || 49 | typeof encryptionSandbox !== 'function' 50 | ) { 51 | throw new TypeError('sandbox is not a function'); 52 | } 53 | 54 | const data = await encryptionSandbox(filename, buffer); 55 | 56 | return derToPem( 57 | await cmsSandbox( 58 | constructCmsData$SEP_, 59 | data[0], 60 | data[1], 61 | data[2], 62 | data[3], 63 | data[4], 64 | data[5], 65 | data[6], 66 | ), 67 | ); 68 | }; 69 | 70 | export default prepareDownloadableCmsPayload_; 71 | -------------------------------------------------------------------------------- /src/lib/sandboxEntrypoints.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const constructCmsData$SEP_ = 'constructCmsData'; 17 | export const deriveKek$SEP_ = 'deriveKek'; 18 | export const fileDecryptionCms$SEP_ = 'fileDecryptionCms'; 19 | export const fileEncryptionCms$SEP_ = 'fileEncryptionCms'; 20 | export const parseCmsData$SEP_ = 'parseCmsData'; 21 | 22 | export const unzip$SEP_ = 'unzip'; 23 | export const zip$SEP_ = 'zip'; 24 | 25 | export const external$decrypt$SEP_ = 'external$decrypt'; 26 | export const external$deriveKey$SEP_ = 'external$deriveKey'; 27 | export const external$encrypt$SEP_ = 'external$encrypt'; 28 | export const external$exportKey$SEP_ = 'external$exportKey'; 29 | export const external$generateKey$SEP_ = 'external$generateKey'; 30 | export const external$importKey$SEP_ = 'external$importKey'; 31 | -------------------------------------------------------------------------------- /src/lib/setupConstructCmsSandbox.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import browserSandbox from '@apeleghq/lot/browser'; 17 | import * as constructCmsData from 'inline:~/sandbox/constructCmsData.js'; 18 | import type { constructCmsData$SEP_ } from './sandboxEntrypoints.js'; 19 | 20 | const setupConstructCmsDataSandbox_ = (signal?: AbortSignal) => 21 | browserSandbox<{ 22 | [constructCmsData$SEP_]: { 23 | ( 24 | salt: AllowSharedBufferSource, 25 | iterationCount: number, 26 | ivPWRI: AllowSharedBufferSource, 27 | encryptedKey: AllowSharedBufferSource, 28 | nonceECI: AllowSharedBufferSource, 29 | encryptedContent: AllowSharedBufferSource, 30 | tag: AllowSharedBufferSource, 31 | ): AllowSharedBufferSource; 32 | }; 33 | }>(constructCmsData.default, null, null, signal); 34 | 35 | export default setupConstructCmsDataSandbox_; 36 | -------------------------------------------------------------------------------- /src/lib/setupDecryptionSandbox.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import browserSandbox from '@apeleghq/lot/browser'; 17 | import * as deriveKek from 'inline:~/sandbox/deriveKek.js'; 18 | import * as fileDecryptionCms from 'inline:~/sandbox/fileDecryptionCms.js'; 19 | import * as unzip from 'inline:~/sandbox/unzip.js'; 20 | import getWrappedCryptoFunctions from './getWrappedCryptoFunctions.js'; 21 | import { 22 | deriveKek$SEP_, 23 | external$decrypt$SEP_, 24 | external$deriveKey$SEP_, 25 | external$encrypt$SEP_, 26 | external$importKey$SEP_, 27 | fileDecryptionCms$SEP_, 28 | unzip$SEP_, 29 | } from './sandboxEntrypoints.js'; 30 | 31 | const setupDecryptionSandbox_ = async ( 32 | passwordGetter: { (): string }, 33 | iterationCountGetter: { (): number }, 34 | saltGetter: { (): AllowSharedBufferSource }, 35 | signal?: AbortSignal, 36 | ) => { 37 | const wrappedCryptoFunctions = getWrappedCryptoFunctions(); 38 | 39 | const [deriveKekSandbox, unzipSandbox] = await Promise.all([ 40 | browserSandbox<{ 41 | [deriveKek$SEP_]: { 42 | ( 43 | password: string, 44 | iterationCount: number, 45 | keyUsages: KeyUsage[], 46 | salt?: AllowSharedBufferSource | undefined, 47 | ): [ 48 | KEK: CryptoKey, 49 | salt: AllowSharedBufferSource, 50 | iterationCount: number, 51 | ]; 52 | }; 53 | }>( 54 | deriveKek.default, 55 | null, 56 | { 57 | [external$deriveKey$SEP_]: wrappedCryptoFunctions.deriveKey_, 58 | [external$importKey$SEP_]: wrappedCryptoFunctions.importKey_, 59 | }, 60 | signal, 61 | ), 62 | browserSandbox<{ 63 | [unzip$SEP_]: { 64 | ( 65 | data: AllowSharedBufferSource, 66 | ): [name: string, contents: AllowSharedBufferSource]; 67 | }; 68 | }>(unzip.default, null, null, signal), 69 | ]); 70 | 71 | const decryptionSandbox = await browserSandbox<{ 72 | [fileDecryptionCms$SEP_]: { 73 | ( 74 | ivPWRI: AllowSharedBufferSource, 75 | encryptedKey: AllowSharedBufferSource, 76 | nonceECI: AllowSharedBufferSource, 77 | encryptedContent: AllowSharedBufferSource, 78 | tag: AllowSharedBufferSource, 79 | ): AllowSharedBufferSource; 80 | }; 81 | }>( 82 | fileDecryptionCms.default, 83 | null, 84 | { 85 | [deriveKek$SEP_]: async () => { 86 | const [KEK] = await deriveKekSandbox( 87 | deriveKek$SEP_, 88 | passwordGetter(), 89 | iterationCountGetter(), 90 | ['encrypt', 'decrypt'], 91 | saltGetter(), 92 | ); 93 | 94 | return KEK; 95 | }, 96 | [external$encrypt$SEP_]: wrappedCryptoFunctions.encrypt_, 97 | [external$decrypt$SEP_]: wrappedCryptoFunctions.decrypt_, 98 | [external$importKey$SEP_]: wrappedCryptoFunctions.importKey_, 99 | }, 100 | signal, 101 | ); 102 | 103 | const decrypt = async ( 104 | ivPWRI: AllowSharedBufferSource, 105 | encryptedKey: AllowSharedBufferSource, 106 | nonceECI: AllowSharedBufferSource, 107 | encryptedContent: AllowSharedBufferSource, 108 | tag: AllowSharedBufferSource, 109 | ): Promise<[filename: string, contents: AllowSharedBufferSource]> => { 110 | const data = await decryptionSandbox( 111 | fileDecryptionCms$SEP_, 112 | ivPWRI, 113 | encryptedKey, 114 | nonceECI, 115 | encryptedContent, 116 | tag, 117 | ); 118 | 119 | return unzipSandbox(unzip$SEP_, data); 120 | }; 121 | 122 | return decrypt; 123 | }; 124 | export default setupDecryptionSandbox_; 125 | -------------------------------------------------------------------------------- /src/lib/setupEncryptionSandbox.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import browserSandbox from '@apeleghq/lot/browser'; 17 | import * as deriveKek from 'inline:~/sandbox/deriveKek.js'; 18 | import * as fileEncryptionCms from 'inline:~/sandbox/fileEncryptionCms.js'; 19 | import * as zip from 'inline:~/sandbox/zip.js'; 20 | import getWrappedCryptoFunctions from './getWrappedCryptoFunctions.js'; 21 | import { 22 | deriveKek$SEP_, 23 | external$deriveKey$SEP_, 24 | external$encrypt$SEP_, 25 | external$exportKey$SEP_, 26 | external$generateKey$SEP_, 27 | external$importKey$SEP_, 28 | fileEncryptionCms$SEP_, 29 | zip$SEP_, 30 | } from './sandboxEntrypoints.js'; 31 | 32 | const setupEncryptionSandbox_ = async ( 33 | passwordGetter: { (): string }, 34 | iterationCountGetter: { (): number }, 35 | signal?: AbortSignal, 36 | ) => { 37 | const wrappedCryptoFunctions = getWrappedCryptoFunctions(); 38 | 39 | const [deriveKekSandbox, zipSandbox] = await Promise.all([ 40 | browserSandbox<{ 41 | [deriveKek$SEP_]: { 42 | ( 43 | password: string, 44 | iterationCount: number, 45 | keyUsages: KeyUsage[], 46 | salt?: Uint8Array | undefined, 47 | ): [KEK: CryptoKey, salt: Uint8Array, iterationCount: number]; 48 | }; 49 | }>( 50 | deriveKek.default, 51 | null, 52 | { 53 | [external$deriveKey$SEP_]: wrappedCryptoFunctions.deriveKey_, 54 | [external$importKey$SEP_]: wrappedCryptoFunctions.importKey_, 55 | }, 56 | signal, 57 | ), 58 | browserSandbox<{ 59 | [zip$SEP_]: { 60 | ( 61 | name: string, 62 | contents: AllowSharedBufferSource, 63 | ): AllowSharedBufferSource; 64 | }; 65 | }>(zip.default, null, null, signal), 66 | ]); 67 | 68 | const encryptionSandbox = await browserSandbox<{ 69 | [fileEncryptionCms$SEP_]: { 70 | ( 71 | data: AllowSharedBufferSource, 72 | ): [ 73 | salt: AllowSharedBufferSource, 74 | iterationCount: number, 75 | ivPWRI: AllowSharedBufferSource, 76 | encryptedKey: AllowSharedBufferSource, 77 | nonceECI: AllowSharedBufferSource, 78 | encryptedContent: AllowSharedBufferSource, 79 | tag: AllowSharedBufferSource, 80 | ]; 81 | }; 82 | }>( 83 | fileEncryptionCms.default, 84 | null, 85 | { 86 | [deriveKek$SEP_]: () => { 87 | return deriveKekSandbox( 88 | deriveKek$SEP_, 89 | passwordGetter(), 90 | iterationCountGetter(), 91 | ['encrypt'], 92 | ); 93 | }, 94 | [external$encrypt$SEP_]: wrappedCryptoFunctions.encrypt_, 95 | [external$exportKey$SEP_]: wrappedCryptoFunctions.exportKey_, 96 | [external$generateKey$SEP_]: wrappedCryptoFunctions.generateKey_, 97 | }, 98 | signal, 99 | ); 100 | 101 | const encrypt = async ( 102 | name: string, 103 | data: AllowSharedBufferSource, 104 | ): Promise< 105 | [ 106 | salt: AllowSharedBufferSource, 107 | iterationCount: number, 108 | ivPWRI: AllowSharedBufferSource, 109 | encryptedKey: AllowSharedBufferSource, 110 | nonceECI: AllowSharedBufferSource, 111 | encryptedContent: AllowSharedBufferSource, 112 | tag: AllowSharedBufferSource, 113 | ] 114 | > => { 115 | const archive = await zipSandbox(zip$SEP_, name, data); 116 | 117 | return encryptionSandbox(fileEncryptionCms$SEP_, archive); 118 | }; 119 | 120 | return encrypt; 121 | }; 122 | 123 | export default setupEncryptionSandbox_; 124 | -------------------------------------------------------------------------------- /src/lib/setupParseCmsSandbox.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import browserSandbox from '@apeleghq/lot/browser'; 17 | import * as parseCmsData from 'inline:~/sandbox/parseCmsData.js'; 18 | import type { parseCmsData$SEP_ } from './sandboxEntrypoints.js'; 19 | 20 | const setupParseCmsDataSandbox_ = (signal?: AbortSignal) => 21 | browserSandbox<{ 22 | [parseCmsData$SEP_]: { 23 | ( 24 | buf: AllowSharedBufferSource, 25 | ): [ 26 | salt: AllowSharedBufferSource, 27 | iterationCount: number, 28 | ivPWRI: AllowSharedBufferSource, 29 | encryptedKey: AllowSharedBufferSource, 30 | nonceECI: AllowSharedBufferSource, 31 | encryptedContent: AllowSharedBufferSource, 32 | tag: AllowSharedBufferSource, 33 | ]; 34 | }; 35 | }>(parseCmsData.default, null, null, signal); 36 | 37 | export default setupParseCmsDataSandbox_; 38 | -------------------------------------------------------------------------------- /src/lib/sharedBufferConcat.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import sharedBufferToUint8Array from './sharedBufferToUint8Array.js'; 17 | 18 | const sharedBufferConcat_ = ( 19 | ...src: AllowSharedBufferSource[] 20 | ): ArrayBufferLike => { 21 | const result = new Uint8Array( 22 | src.reduce((acc, cv) => acc + cv.byteLength, 0), 23 | ); 24 | void src.reduce((acc, cv) => { 25 | const octets = sharedBufferToUint8Array(cv); 26 | result.set(octets, acc); 27 | return acc + cv.byteLength; 28 | }, 0); 29 | 30 | return result.buffer; 31 | }; 32 | 33 | export default sharedBufferConcat_; 34 | -------------------------------------------------------------------------------- /src/lib/sharedBufferToUint8Array.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const sharedBufferToUint8Array_ = ( 17 | buf: AllowSharedBufferSource, 18 | ): Uint8Array => { 19 | if (ArrayBuffer.isView(buf)) { 20 | return new Uint8Array(buf.buffer).subarray( 21 | buf.byteOffset, 22 | buf.byteOffset + buf.byteLength, 23 | ); 24 | } 25 | return new Uint8Array(buf); 26 | }; 27 | 28 | export default sharedBufferToUint8Array_; 29 | -------------------------------------------------------------------------------- /src/lib/tightenCsp.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2025 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /** 17 | * Tightens the Content Security Policy (CSP) to prevent the loading of any 18 | * new scripts or the dynamic execution of code. 19 | * 20 | * This function sets a strict CSP using a meta tag, ensuring that no new 21 | * scripts can be loaded and code cannot be dynamically executed through 22 | * methods like `Function()`, `eval()` and similar. 23 | * 24 | * Once this strict CSP is applied, it cannot be removed, thus providing 25 | * a robust layer of security against potential code injections or other 26 | * malicious behaviours. 27 | */ 28 | 29 | const tightenCsp_ = () => { 30 | const meta$ = document.createElementNS( 31 | 'http://www.w3.org/1999/xhtml', 32 | 'meta', 33 | ); 34 | meta$.setAttribute('http-equiv', 'content-security-policy'); 35 | // connect-src blob: data: is needed for CI (blob:) and for the 36 | // 'download for offline use' functionality (data:) 37 | meta$.setAttribute( 38 | 'content', 39 | "default-src 'none'; script-src 'self' 'unsafe-eval' blob: data:; script-src-elem blob: data:; script-src-attr 'none'; style-src data:; child-src blob:; connect-src blob: data:; frame-src blob:; worker-src blob:; form-action about:", 40 | ); 41 | const oldMeta$ = document.head.querySelector( 42 | 'meta[http-equiv="content-security-policy"]', 43 | ); 44 | if (oldMeta$) { 45 | document.head.replaceChild(meta$, oldMeta$); 46 | } else { 47 | document.head.appendChild(meta$); 48 | } 49 | }; 50 | 51 | export default tightenCsp_; 52 | -------------------------------------------------------------------------------- /src/lib/uint8ArrayToBase64.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const uint8ArrayToBase64_ = (uint8Array: Uint8Array): string => { 17 | return btoa( 18 | Array.from(uint8Array) 19 | .map((c) => String.fromCharCode(c)) 20 | .join(''), 21 | ); 22 | }; 23 | 24 | export default uint8ArrayToBase64_; 25 | -------------------------------------------------------------------------------- /src/lib/xmlEscape.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const xmlEscape_ = (input: string): string => 17 | input 18 | .split('&') 19 | .join('&') 20 | .split('<') 21 | .join('<') 22 | .split('>') 23 | .join('>'); 24 | 25 | export const xmlEscapeAttr_ = (input: string): string => 26 | input 27 | .split('&') 28 | .join('&') 29 | .split('<') 30 | .join('<') 31 | .split('>') 32 | .join('>') 33 | .split('"') 34 | .join('"') 35 | .split("'") 36 | .join('''); 37 | 38 | export const xmlEscapeJsonScriptCdata_ = (input: string): string => 39 | input 40 | .split(']]>') 41 | .join(']]\\u003e') 42 | .split('') 45 | .join('--\\u003e') 46 | .replace(/<\/(script)/gi, '<\\/$1') 47 | .replace(/<(script)/gi, '\\u003c$1'); 48 | -------------------------------------------------------------------------------- /src/loader.inline.ts: -------------------------------------------------------------------------------- 1 | /* Copyright © 2024 Apeleg Limited. All rights reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License") with LLVM 4 | * exceptions; you may not use this file except in compliance with the 5 | * License. You may obtain a copy of the License at 6 | * 7 | * http://llvm.org/foundation/relicensing/LICENSE.txt 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import { 17 | MAIN_SCRIPT_ELEMENT_ID_, 18 | MAIN_SCRIPT_SRC_ELEMENT_ID_, 19 | } from '~/lib/elementIds.js'; 20 | import commentCdataExtractor from './lib/commentCdataExtractor.js'; 21 | 22 | // The main purpose of this loader is to avoid loading a long `data:` URL, 23 | // which makes stack traces ugly. 24 | (() => { 25 | const ns = 'http://www.w3.org/1999/xhtml'; 26 | 27 | const mainScript$ = document.getElementById(MAIN_SCRIPT_SRC_ELEMENT_ID_); 28 | if (!mainScript$ || !(mainScript$ instanceof HTMLScriptElement)) { 29 | throw new Error('Missing main script element'); 30 | } 31 | mainScript$.parentNode?.removeChild(mainScript$); 32 | const text = atob( 33 | (commentCdataExtractor(mainScript$.text) || '').replace( 34 | /[^a-zA-Z0-9+/=]/g, 35 | '', 36 | ), 37 | ); 38 | const blob = new Blob([text], { ['type']: 'text/javascript' }); 39 | const script$ = document.createElementNS(ns, 'script'); 40 | script$.setAttribute('crossorigin', 'anonymous'); 41 | const integrity = mainScript$.getAttribute('data-integrity'); 42 | if (integrity) { 43 | script$.setAttribute('integrity', integrity); 44 | } 45 | script$.setAttribute('id', MAIN_SCRIPT_ELEMENT_ID_); 46 | script$.setAttribute('src', URL.createObjectURL(blob)); 47 | document.head.appendChild(script$); 48 | })(); 49 | -------------------------------------------------------------------------------- /src/pages/index.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | 65 | 66 |
72 | {#if hasCmsData} 73 | 74 | {:else} 75 | 76 | {/if} 77 | 78 |