├── docs ├── CNAME ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ └── OpenSans-LightItalic-webfont.woff ├── scripts │ ├── linenumber.js │ └── prettify │ │ └── lang-css.js └── styles │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── .eslintignore ├── .mocharc.json ├── .gitignore ├── lightweight └── package.json ├── src ├── crypto │ ├── public_key │ │ ├── post_quantum │ │ │ ├── index.js │ │ │ ├── kem │ │ │ │ ├── index.js │ │ │ │ ├── ecc_kem.js │ │ │ │ ├── kem.js │ │ │ │ └── ml_kem.js │ │ │ ├── signature │ │ │ │ ├── index.js │ │ │ │ ├── ecc_dsa.js │ │ │ │ ├── ml_dsa.js │ │ │ │ └── signature.js │ │ │ └── noble_post_quantum.ts │ │ ├── index.js │ │ ├── elliptic │ │ │ ├── noble_curves.js │ │ │ ├── brainpool │ │ │ │ ├── brainpoolP256r1.ts │ │ │ │ ├── brainpoolP384r1.ts │ │ │ │ └── brainpoolP512r1.ts │ │ │ ├── index.js │ │ │ └── eddsa_legacy.js │ │ ├── hmac.js │ │ └── elgamal.js │ ├── cipher │ │ ├── legacy_ciphers.js │ │ └── index.js │ ├── index.js │ ├── hkdf.js │ ├── hash │ │ ├── noble_hashes.js │ │ ├── md5.ts │ │ └── index.js │ ├── cipherMode │ │ └── index.js │ ├── pkcs5.js │ ├── random.js │ ├── cmac.js │ └── aes_kw.js ├── packet │ ├── index.js │ ├── trust.js │ ├── all_packets.js │ ├── secret_subkey.js │ ├── padding.js │ ├── marker.js │ ├── public_subkey.js │ └── user_attribute.js ├── config │ ├── index.js │ ├── index.d.ts │ └── config.d.ts ├── key │ ├── index.js │ └── public_key.js ├── type │ ├── short_byte_string.js │ ├── s2k │ │ └── index.js │ ├── ecdh_x_symkey.js │ ├── ecdh_symkey.js │ ├── enum.js │ ├── keyid.js │ ├── kdf_params.js │ └── oid.js ├── index.js ├── signature.js └── encoding │ └── base64.js ├── test ├── typescript │ └── tsconfig.test.json ├── worker │ ├── index.js │ ├── application_worker.js │ └── worker_example.js ├── crypto │ ├── hash │ │ ├── index.js │ │ ├── ripemd.js │ │ ├── md5.js │ │ └── sha.js │ ├── cipher │ │ ├── index.js │ │ ├── cast5.js │ │ └── twofish.js │ ├── pkcs5.js │ ├── index.js │ ├── hmac.js │ ├── hkdf.js │ ├── aes_kw.js │ ├── gcm.js │ ├── biginteger.js │ └── brainpool_rfc7027.js ├── security │ ├── index.js │ ├── preferred_algo_mismatch.js │ ├── subkey_trust.js │ ├── unsigned_subpackets.js │ └── message_signature_bypass.js ├── initOpenpgp.js ├── general │ ├── testInputs.js │ ├── index.js │ ├── oid.js │ ├── decompression.js │ └── forwarding.js ├── mockRandom.ts ├── unittests.html ├── web-test-runner.config.js ├── web-test-runner.browserstack.config.js ├── unittests.js └── benchmarks │ └── time.js ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── config.yml ├── workflows │ ├── docs.yml │ ├── benchmark.yml │ └── sop-test-suite.yml ├── test-suite │ ├── prepare_config.sh │ └── config.json.template └── dependabot.yml ├── tsconfig.dist.json ├── update_deps.sh ├── .jsdocrc.cjs ├── SECURITY.md └── package.json /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.openpgpjs.org -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | test/lib/ 3 | test/typescript/ 4 | docs 5 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "node-option": [ 3 | "experimental-specifier-resolution=node", 4 | "loader=ts-node/esm" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm* 4 | test/lib/ 5 | test/typescript/definitions.js 6 | dist/ 7 | openpgp.store/ 8 | coverage 9 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProtonMail/openpgpjs/HEAD/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /lightweight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openpgp-lightweight", 3 | "browser": "../dist/lightweight/openpgp.min.mjs", 4 | "types": "../openpgp.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/index.js: -------------------------------------------------------------------------------- 1 | import * as kem from './kem/index'; 2 | import * as signature from './signature'; 3 | 4 | export { 5 | kem, 6 | signature 7 | }; 8 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/kem/index.js: -------------------------------------------------------------------------------- 1 | export { generate, encrypt, decrypt, validateParams } from './kem'; 2 | export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem'; 3 | -------------------------------------------------------------------------------- /test/typescript/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, // tested by tsx 5 | }, 6 | "files": ["definitions.ts"] 7 | } -------------------------------------------------------------------------------- /test/worker/index.js: -------------------------------------------------------------------------------- 1 | import testApplicationWorker from './application_worker.js'; 2 | 3 | export default () => describe('Web Worker', function () { 4 | testApplicationWorker(); 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /src/packet/index.js: -------------------------------------------------------------------------------- 1 | export * from './all_packets'; 2 | export { default as PacketList } from './packetlist'; 3 | export { UnparseablePacket } from './packet'; 4 | export { GrammarError } from './grammar'; 5 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/signature/index.js: -------------------------------------------------------------------------------- 1 | export { generate, sign, verify, validateParams, isCompatibleHashAlgo } from './signature'; 2 | export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa'; 3 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This object contains global configuration values. 3 | * @see module:config/config 4 | * @module config 5 | */ 6 | 7 | import config from './config'; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /test/crypto/hash/index.js: -------------------------------------------------------------------------------- 1 | import testMD5 from './md5'; 2 | import testRipeMD from './ripemd'; 3 | import testSHA from './sha'; 4 | 5 | export default () => describe('Hash', function () { 6 | testMD5(); 7 | testRipeMD(); 8 | testSHA(); 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es2021", 5 | "module": "esnext", 6 | "moduleResolution": "Bundler", 7 | "allowJs": true, 8 | "paths": { 9 | "@protontech/openpgp": [ "." ] 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/crypto/cipher/index.js: -------------------------------------------------------------------------------- 1 | import testBlowfish from './blowfish'; 2 | import testCAST5 from './cast5'; 3 | import testDES from './des'; 4 | import testTwofish from './twofish'; 5 | 6 | export default () => describe('Cipher', function () { 7 | testBlowfish(); 8 | testCAST5(); 9 | testDES(); 10 | testTwofish(); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue with this library 4 | --- 5 | 6 | 7 | - OpenPGP.js version: 8 | - Affected platform (Browser or Node.js version): 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | // tsconfig taking care of generating declaration files to be released 2 | // under 'dist/types' 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "declarationDir": "./dist/types", 9 | "rootDir": "src" 10 | }, 11 | "files": ["src/index.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/openpgpjs/openpgpjs/discussions/categories/ideas 5 | about: Suggest an idea for this project 6 | - name: Question 7 | url: https://github.com/openpgpjs/openpgpjs/discussions/categories/q-a 8 | about: Please ask any questions here 9 | -------------------------------------------------------------------------------- /src/crypto/public_key/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Asymmetric cryptography functions 3 | * @module crypto/public_key 4 | */ 5 | 6 | export * as rsa from './rsa'; 7 | export * as elgamal from './elgamal'; 8 | export * as elliptic from './elliptic'; 9 | export * as dsa from './dsa'; 10 | export * as hmac from './hmac'; 11 | export * as postQuantum from './post_quantum'; 12 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: JSDoc 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | - run: npm ci --ignore-scripts 19 | - run: npm run docs 20 | -------------------------------------------------------------------------------- /update_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # go to root 4 | cd `dirname $0` 5 | 6 | # clear shrinkwrapped node_modules 7 | rm package-lock.json 8 | rm -rf node_modules/ 9 | 10 | # abort if tests fail 11 | set -e 12 | 13 | # install dependencies 14 | npm install 15 | 16 | # build and test 17 | npm test 18 | 19 | # Add build files to git 20 | git add package-lock.json 21 | git commit -m "Update npm dependencies and package lock" 22 | -------------------------------------------------------------------------------- /.jsdocrc.cjs: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | module.exports = { 4 | plugins: ['plugins/markdown'], 5 | markdown: { 6 | idInHeadings: true 7 | }, 8 | templates: { 9 | default: { 10 | includeDate: false, 11 | outputSourceFiles: false, 12 | externalSourceLinks: { 13 | urlPrefix: `${pkg.repository.url}/blob/v${pkg.version}/src/` 14 | } 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/security/index.js: -------------------------------------------------------------------------------- 1 | import testMessageSignatureBypess from './message_signature_bypass'; 2 | import testUnsignedSubpackets from './unsigned_subpackets'; 3 | import testSubkeyTrust from './subkey_trust'; 4 | import testPreferredAlgoMismatch from './preferred_algo_mismatch'; 5 | 6 | export default () => describe('Security', function () { 7 | testMessageSignatureBypess(); 8 | testUnsignedSubpackets(); 9 | testSubkeyTrust(); 10 | testPreferredAlgoMismatch(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/noble_post_quantum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is needed to dynamic import noble-post-quantum libs. 3 | * Separate dynamic imports are not convenient as they result in multiple chunks, 4 | * which ultimately share a lot of code and need to be imported together 5 | * when it comes to Proton's ML-DSA + ML-KEM keys. 6 | */ 7 | 8 | export { ml_kem768 } from '@noble/post-quantum/ml-kem'; 9 | export { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; 10 | 11 | -------------------------------------------------------------------------------- /test/initOpenpgp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module centralises the openpgp import and ensures that the module is initialised 3 | * at the top of the test bundle, and that the config is initialised before the tests code runs (incl. that outside of `describe`). 4 | */ 5 | 6 | import * as openpgp from '@protontech/openpgp'; 7 | 8 | if (typeof window !== 'undefined') { 9 | window.openpgp = openpgp; 10 | } 11 | 12 | openpgp.config.s2kIterationCountByte = 0; 13 | 14 | export default openpgp; 15 | -------------------------------------------------------------------------------- /src/config/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The config module cannot be written in TS directly for now, 3 | * since our JSDoc compiler does not support TS. 4 | */ 5 | import config, { type Config } from './config'; 6 | 7 | 8 | // PartialConfig has the same properties as Config, but declared as optional. 9 | // This interface is relevant for top-level functions, which accept a subset of configuration options 10 | interface PartialConfig extends Partial {} 11 | 12 | export { Config, PartialConfig }; 13 | export default config; 14 | -------------------------------------------------------------------------------- /test/crypto/pkcs5.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import * as pkcs5 from '../../src/crypto/pkcs5.js'; 4 | 5 | export default () => describe('PKCS5 padding', function() { 6 | it('Add and remove padding', function () { 7 | const m = new Uint8Array([0,1,2,3,4,5,6,7,8]); 8 | const padded = pkcs5.encode(m); 9 | const unpadded = pkcs5.decode(padded); 10 | expect(padded[padded.length - 1]).to.equal(7); 11 | expect(padded.length % 8).to.equal(0); 12 | expect(unpadded).to.deep.equal(m); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.github/test-suite/prepare_config.sh: -------------------------------------------------------------------------------- 1 | CONFIG_TEMPLATE=$1 2 | CONFIG_OUTPUT=$2 3 | OPENPGPJS_BRANCH=$3 4 | OPENPGPJS_MAIN=$4 5 | cat $CONFIG_TEMPLATE \ 6 | | sed "s@__OPENPGPJS_BRANCH__@${OPENPGPJS_BRANCH}@g" \ 7 | | sed "s@__OPENPGPJS_MAIN__@${OPENPGPJS_MAIN}@g" \ 8 | | sed "s@__SQOP__@${SQOP}@g" \ 9 | | sed "s@__GPGME_SOP__@${GPGME_SOP}@g" \ 10 | | sed "s@__GOSOP_V2__@${GOSOP_V2}@g" \ 11 | | sed "s@__SOP_OPENPGPJS__@${SOP_OPENPGPJS_V2}@g" \ 12 | | sed "s@__RNP_SOP__@${RNP_SOP}@g" \ 13 | | sed "s@__RSOP__@${RSOP}@g" \ 14 | > $CONFIG_OUTPUT -------------------------------------------------------------------------------- /test/general/testInputs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a 64 character long javascript string out of the whole utf-8 range. 3 | */ 4 | function createSomeMessage() { 5 | const arr = []; 6 | for (let i = 0; i < 30; i++) { 7 | arr.push(Math.floor(Math.random() * 10174) + 1); 8 | } 9 | for (let i = 0; i < 10; i++) { 10 | arr.push(0x1F600 + Math.floor(Math.random() * (0x1F64F - 0x1F600)) + 1); 11 | } 12 | return '  \t' + String.fromCodePoint(...arr).replace(/[\r\u2028\u2029]/g, '\n') + '  \t\n한국어/조선말'; 13 | } 14 | 15 | export { 16 | createSomeMessage 17 | }; 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting Security Issues 2 | 3 | **Please do not report security vulnerabilities through public GitHub issues.** 4 | 5 | If you believe you have found a security vulnerability in OpenPGP.js, please report it via email to [security@openpgpjs.org](mailto:security@openpgpjs.org). If possible, encrypt your message with our PGP key: it can be downloaded automatically using [WKD](https://wiki.gnupg.org/WKD), or manually on [openpgpjs.org](https://openpgpjs.org/.well-known/openpgpkey/hu/t5s8ztdbon8yzntexy6oz5y48etqsnbb?l=security). 6 | 7 | You should receive a response within 2 working days. -------------------------------------------------------------------------------- /src/crypto/cipher/legacy_ciphers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is needed to dynamic import the legacy ciphers. 3 | * Separate dynamic imports are not convenient as they result in multiple chunks. 4 | */ 5 | 6 | import { TripleDES as tripledes } from './des'; 7 | import cast5 from './cast5'; 8 | import twofish from './twofish'; 9 | import blowfish from './blowfish'; 10 | 11 | // We avoid importing 'enums' as this module is lazy loaded, and doing so could mess up 12 | // chunking for the lightweight build 13 | export const legacyCiphers = new Map(Object.entries({ 14 | tripledes, 15 | cast5, 16 | twofish, 17 | blowfish 18 | })); 19 | -------------------------------------------------------------------------------- /src/crypto/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Provides access to all cryptographic primitives used in OpenPGP.js 3 | * @see module:crypto/crypto 4 | * @see module:crypto/signature 5 | * @see module:crypto/public_key 6 | * @see module:crypto/cipher 7 | * @see module:crypto/random 8 | * @see module:crypto/hash 9 | * @module crypto 10 | */ 11 | 12 | export * from './crypto'; 13 | export { getCipherParams } from './cipher'; 14 | export * from './hash'; 15 | export * as cipherMode from './cipherMode'; 16 | export * as publicKey from './public_key'; 17 | export * as signature from './signature'; 18 | export { getRandomBytes } from './random'; 19 | -------------------------------------------------------------------------------- /src/crypto/hkdf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This module implements HKDF using either the WebCrypto API or Node.js' crypto API. 3 | * @module crypto/hkdf 4 | */ 5 | 6 | import enums from '../enums'; 7 | import util from '../util'; 8 | 9 | export default async function computeHKDF(hashAlgo, inputKey, salt, info, outLen) { 10 | const webCrypto = util.getWebCrypto(); 11 | const hash = enums.read(enums.webHash, hashAlgo); 12 | if (!hash) throw new Error('Hash algo not supported with HKDF'); 13 | 14 | const importedKey = await webCrypto.importKey('raw', inputKey, 'HKDF', false, ['deriveBits']); 15 | const bits = await webCrypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, outLen * 8); 16 | return new Uint8Array(bits); 17 | } 18 | -------------------------------------------------------------------------------- /src/crypto/hash/noble_hashes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is needed to dynamic import the noble-hashes. 3 | * Separate dynamic imports are not convenient as they result in too many chunks, 4 | * which share a lot of code anyway. 5 | */ 6 | 7 | import { sha1 } from '@noble/hashes/sha1'; 8 | import { sha224, sha256 } from '@noble/hashes/sha256'; 9 | import { sha384, sha512 } from '@noble/hashes/sha512'; 10 | import { sha3_256, sha3_512 } from '@noble/hashes/sha3'; 11 | import { ripemd160 } from '@noble/hashes/ripemd160'; 12 | import { md5 } from './md5'; 13 | 14 | export const nobleHashes = new Map(Object.entries({ 15 | md5, 16 | sha1, 17 | sha224, 18 | sha256, 19 | sha384, 20 | sha512, 21 | sha3_256, 22 | sha3_512, 23 | ripemd160 24 | })); 25 | -------------------------------------------------------------------------------- /src/key/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | readKey, 3 | readKeys, 4 | readPrivateKey, 5 | readPrivateKeys, 6 | generate, 7 | reformat 8 | } from './factory'; 9 | 10 | import { 11 | getPreferredHashAlgo, 12 | getPreferredCompressionAlgo, 13 | getPreferredCipherSuite, 14 | createSignaturePacket 15 | } from './helper'; 16 | 17 | import PrivateKey from './private_key'; 18 | import PublicKey from './public_key'; 19 | import Subkey from './subkey'; 20 | 21 | export { 22 | readKey, 23 | readKeys, 24 | readPrivateKey, 25 | readPrivateKeys, 26 | generate, 27 | reformat, 28 | getPreferredHashAlgo, 29 | getPreferredCompressionAlgo, 30 | getPreferredCipherSuite, 31 | createSignaturePacket, 32 | PrivateKey, 33 | PublicKey, 34 | Subkey 35 | }; 36 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /src/type/short_byte_string.js: -------------------------------------------------------------------------------- 1 | import util from '../util'; 2 | 3 | class ShortByteString { 4 | constructor(data) { 5 | if (typeof data === 'undefined') { 6 | data = new Uint8Array([]); 7 | } 8 | if (!util.isUint8Array(data)) { 9 | throw new Error('data must be in the form of a Uint8Array'); 10 | } 11 | this.data = data; 12 | this.length = this.data.byteLength; 13 | } 14 | 15 | write() { 16 | return util.concatUint8Array([new Uint8Array([this.length]), this.data]); 17 | } 18 | 19 | read(input) { 20 | if (input.length >= 1) { 21 | const length = input[0]; 22 | if (input.length >= length + 1) { 23 | this.data = input.subarray(1, 1 + length); 24 | this.length = length; 25 | return 1 + length; 26 | } 27 | } 28 | throw new Error('Invalid octet string'); 29 | } 30 | } 31 | 32 | export default ShortByteString; 33 | -------------------------------------------------------------------------------- /test/mockRandom.ts: -------------------------------------------------------------------------------- 1 | import type { SinonStub } from 'sinon'; 2 | import util from '../src/util'; 3 | 4 | const webcrypto = typeof crypto !== 'undefined' ? crypto : util.nodeRequire('crypto')?.webcrypto; 5 | 6 | type GetRandomValuesFn = typeof crypto.getRandomValues; 7 | let original: GetRandomValuesFn | null = null; 8 | 9 | /** 10 | * Mock `crypto.getRandomValues` using the mocked implementation 11 | */ 12 | export const mockCryptoRandomGenerator = ( 13 | mockedImplementation: GetRandomValuesFn | SinonStub) => { 14 | if (original !== null) { 15 | throw new Error('random mock already initialized'); 16 | } 17 | 18 | original = webcrypto.getRandomValues; 19 | webcrypto.getRandomValues = mockedImplementation; 20 | }; 21 | 22 | export const restoreCryptoRandomGenerator = () => { 23 | if (!original) { 24 | throw new Error('random mock was not initialized'); 25 | } 26 | 27 | webcrypto.getRandomValues = original; 28 | original = null; 29 | }; 30 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/noble_curves.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is needed to dynamic import the noble-curves. 3 | * Separate dynamic imports are not convenient as they result in too many chunks, 4 | * which share a lot of code anyway. 5 | */ 6 | 7 | import { p256 as nistP256 } from '@noble/curves/p256'; 8 | import { p384 as nistP384 } from '@noble/curves/p384'; 9 | import { p521 as nistP521 } from '@noble/curves/p521'; 10 | import { x448, ed448 } from '@noble/curves/ed448'; 11 | import { secp256k1 } from '@noble/curves/secp256k1'; 12 | import { brainpoolP256r1 } from './brainpool/brainpoolP256r1'; 13 | import { brainpoolP384r1 } from './brainpool/brainpoolP384r1'; 14 | import { brainpoolP512r1 } from './brainpool/brainpoolP512r1'; 15 | 16 | export const nobleCurves = new Map(Object.entries({ 17 | nistP256, 18 | nistP384, 19 | nistP521, 20 | brainpoolP256r1, 21 | brainpoolP384r1, 22 | brainpoolP512r1, 23 | secp256k1, 24 | x448, 25 | ed448 26 | })); 27 | 28 | -------------------------------------------------------------------------------- /test/general/index.js: -------------------------------------------------------------------------------- 1 | import testX25519 from './x25519.js'; 2 | import testUtil from './util.js'; 3 | import testArmor from './armor.js'; 4 | import testPacket from './packet.js'; 5 | import testSignature from './signature.js'; 6 | import testKey from './key.js'; 7 | import testForwarding from './forwarding.js'; 8 | import testOpenPGP from './openpgp.js'; 9 | import testConfig from './config.js'; 10 | import testOID from './oid.js'; 11 | import testNistECC from './ecc_nist.js'; 12 | import testSecp256k1 from './ecc_secp256k1.js'; 13 | import testBrainpool from './brainpool.js'; 14 | import testDecompression from './decompression.js'; 15 | import testStreaming from './streaming.js'; 16 | 17 | export default () => describe('General', function () { 18 | testX25519(); 19 | testUtil(); 20 | testArmor(); 21 | testPacket(); 22 | testSignature(); 23 | testKey(); 24 | testForwarding(); 25 | testOpenPGP(); 26 | testConfig(); 27 | testOID(); 28 | testNistECC(); 29 | testSecp256k1(); 30 | testBrainpool(); 31 | testDecompression(); 32 | testStreaming(); 33 | }); 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Export high level API functions. 3 | * Usage: 4 | * 5 | * import { encrypt } from 'openpgp'; 6 | * encrypt({ message, publicKeys }); 7 | */ 8 | export { 9 | encrypt, decrypt, sign, verify, 10 | generateKey, reformatKey, revokeKey, decryptKey, encryptKey, 11 | generateSessionKey, encryptSessionKey, decryptSessionKeys 12 | } from './openpgp'; 13 | 14 | export { PrivateKey, PublicKey, Subkey, readKey, readKeys, readPrivateKey, readPrivateKeys } from './key'; 15 | 16 | export { Signature, readSignature } from './signature'; 17 | 18 | export { Message, readMessage, createMessage } from './message'; 19 | 20 | export { CleartextMessage, readCleartextMessage, createCleartextMessage } from './cleartext'; 21 | 22 | export * from './packet'; 23 | 24 | export { default as KDFParams } from './type/kdf_params'; 25 | export { default as Argon2S2K, Argon2OutOfMemoryError } from './type/s2k/argon2'; 26 | 27 | export * from './encoding/armor'; 28 | 29 | export { default as enums } from './enums'; 30 | 31 | export { default as config } from './config/config'; 32 | -------------------------------------------------------------------------------- /test/crypto/index.js: -------------------------------------------------------------------------------- 1 | import testBigInteger from './biginteger'; 2 | import testCipher from './cipher'; 3 | import testHash from './hash'; 4 | import testCrypto from './crypto'; 5 | import testElliptic from './ecdsa_eddsa'; 6 | import testBrainpoolRFC7027 from './brainpool_rfc7027'; 7 | import testECDH from './ecdh'; 8 | import testPKCS5 from './pkcs5'; 9 | import testAESKW from './aes_kw'; 10 | import testHKDF from './hkdf'; 11 | import testHMAC from './hmac'; 12 | import testGCM from './gcm'; 13 | import testEAX from './eax'; 14 | import testOCB from './ocb'; 15 | import testRSA from './rsa'; 16 | import testValidate from './validate'; 17 | import testPQC from './postQuantum'; 18 | 19 | export default () => describe('Crypto', function () { 20 | testBigInteger(); 21 | testCipher(); 22 | testHash(); 23 | testCrypto(); 24 | testElliptic(); 25 | testBrainpoolRFC7027(); 26 | testECDH(); 27 | testPKCS5(); 28 | testAESKW(); 29 | testHKDF(); 30 | testHMAC(); 31 | testGCM(); 32 | testEAX(); 33 | testOCB(); 34 | testRSA(); 35 | testValidate(); 36 | testPQC(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/crypto/cipher/cast5.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import CAST5 from '../../../src/crypto/cipher/cast5.js'; 4 | import util from '../../../src/util.js'; 5 | 6 | export default () => it('CAST-128 cipher test with test vectors from RFC2144', function (done) { 7 | function test_cast(input, key, output) { 8 | const cast5 = new CAST5(key); 9 | const result = cast5.encrypt(input); 10 | 11 | return util.equalsUint8Array(new Uint8Array(result), new Uint8Array(output)); 12 | } 13 | 14 | const testvectors = [[[0x01,0x23,0x45,0x67,0x12,0x34,0x56,0x78,0x23,0x45,0x67,0x89,0x34,0x56,0x78,0x9A],[0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF],[0x23,0x8B,0x4F,0xE5,0x84,0x7E,0x44,0xB2]]]; 15 | 16 | for (let i = 0; i < testvectors.length; i++) { 17 | const res = test_cast(testvectors[i][1],testvectors[i][0],testvectors[i][2]); 18 | expect(res, 'vector with block ' + util.uint8ArrayToHex(testvectors[i][0]) + 19 | ' and key ' + util.uint8ArrayToHex(testvectors[i][1]) + 20 | ' should be ' + util.uint8ArrayToHex(testvectors[i][2])).to.be.true; 21 | } 22 | done(); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/test-suite/config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "drivers": [ 3 | { 4 | "id": "sop-openpgpjs-branch", 5 | "path": "__SOP_OPENPGPJS__", 6 | "env": { 7 | "OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__", 8 | "OPENPGPJS_CUSTOM_PROFILES": "{\"generate-key\": { \"post-quantum\": { \"description\": \"generate post-quantum v6 keys (relying on ML-DSA + ML-KEM)\", \"options\": { \"type\": \"pqc\", \"config\": { \"v6Keys\": true } } } } }" 9 | } 10 | }, 11 | { 12 | "id": "sop-openpgpjs-main", 13 | "path": "__SOP_OPENPGPJS__", 14 | "env": { 15 | "OPENPGPJS_PATH": "__OPENPGPJS_MAIN__", 16 | "DISABLE_PROFILES": "true" 17 | } 18 | }, 19 | { 20 | "path": "__SQOP__" 21 | }, 22 | { 23 | "path": "__GPGME_SOP__" 24 | }, 25 | { 26 | "id": "gosop-v2", 27 | "path": "__GOSOP_V2__" 28 | }, 29 | { 30 | "path": "__RNP_SOP__" 31 | }, 32 | { 33 | "path": "__RSOP__" 34 | } 35 | ], 36 | "rlimits": { 37 | "DATA": 1073741824 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/crypto/cipherMode/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Cipher modes 3 | * @module crypto/cipherMode 4 | */ 5 | 6 | export * as cfb from './cfb'; 7 | import eax from './eax'; 8 | import ocb from './ocb'; 9 | import gcm from './gcm'; 10 | import enums from '../../enums'; 11 | 12 | /** 13 | * Get implementation of the given AEAD mode 14 | * @param {enums.aead} algo 15 | * @param {Boolean} [acceptExperimentalGCM] - whether to allow the non-standard, legacy `experimentalGCM` algo 16 | * @returns {Object} 17 | * @throws {Error} on invalid algo 18 | */ 19 | export function getAEADMode(algo, acceptExperimentalGCM = false) { 20 | switch (algo) { 21 | case enums.aead.eax: 22 | return eax; 23 | case enums.aead.ocb: 24 | return ocb; 25 | case enums.aead.gcm: 26 | return gcm; 27 | case enums.aead.experimentalGCM: 28 | if (!acceptExperimentalGCM) { 29 | throw new Error('Unexpected non-standard `experimentalGCM` AEAD algorithm provided in `config.preferredAEADAlgorithm`: use `gcm` instead'); 30 | } 31 | return gcm; 32 | default: 33 | throw new Error('Unsupported AEAD mode'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | # The redundant target-branch directive is needed to set two different update schedules for npm, 5 | # working around a dependabot limitation: 6 | # see https://github.com/dependabot/dependabot-core/issues/1778#issuecomment-1988140219 . 7 | target-branch: main 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | allow: 12 | - dependency-name: "playwright" 13 | versioning-strategy: increase 14 | 15 | - package-ecosystem: "npm" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | allow: 20 | - dependency-name: "@noble*" 21 | - dependency-name: "fflate" 22 | versioning-strategy: increase 23 | groups: 24 | # Any packages matching the pattern @noble* where the highest resolvable 25 | # version is minor or patch will be grouped together. 26 | # Grouping rules apply to version updates only. 27 | noble: 28 | applies-to: version-updates 29 | patterns: 30 | - "@noble*" 31 | update-types: 32 | - "minor" 33 | - "patch" 34 | -------------------------------------------------------------------------------- /test/worker/application_worker.js: -------------------------------------------------------------------------------- 1 | /* globals tryTests */ 2 | 3 | import { expect } from 'chai'; 4 | 5 | export default () => tryTests('Application Worker', tests, { 6 | if: typeof window !== 'undefined' && window.Worker && window.MessageChannel 7 | }); 8 | 9 | function tests() { 10 | 11 | it('Should support loading OpenPGP.js from inside a Web Worker', async function() { 12 | const worker = new Worker('./worker/worker_example.js'); 13 | async function delegate(action, message) { 14 | return new Promise((resolve, reject) => { 15 | const channel = new MessageChannel(); 16 | channel.port1.onmessage = function({ data }) { 17 | if (data.error !== undefined) { 18 | reject(new Error(data.error)); 19 | } else { 20 | resolve(data.result); 21 | } 22 | }; 23 | worker.postMessage({ action, message }, [channel.port2]); 24 | }); 25 | } 26 | const encrypted = await delegate('encrypt', 'Hello World!'); 27 | const decrypted = await delegate('decrypt', encrypted); 28 | expect(decrypted).to.equal('Hello World!'); 29 | worker.terminate(); 30 | }); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/brainpool/brainpoolP256r1.ts: -------------------------------------------------------------------------------- 1 | import { createCurve } from '@noble/curves/_shortw_utils'; 2 | import { sha256 } from '@noble/hashes/sha256'; 3 | import { Field } from '@noble/curves/abstract/modular'; 4 | 5 | // brainpoolP256r1: https://datatracker.ietf.org/doc/html/rfc5639#section-3.4 6 | 7 | // eslint-disable-next-line new-cap 8 | const Fp = Field(BigInt('0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377')); 9 | const CURVE_A = Fp.create(BigInt('0x7d5a0975fc2c3057eef67530417affe7fb8055c126dc5c6ce94a4b44f330b5d9')); 10 | const CURVE_B = BigInt('0x26dc5c6ce94a4b44f330b5d9bbd77cbf958416295cf7e1ce6bccdc18ff8c07b6'); 11 | 12 | // prettier-ignore 13 | export const brainpoolP256r1 = createCurve({ 14 | a: CURVE_A, // Equation params: a, b 15 | b: CURVE_B, 16 | Fp, 17 | // Curve order (q), total count of valid points in the field 18 | n: BigInt('0xa9fb57dba1eea9bc3e660a909d838d718c397aa3b561a6f7901e0e82974856a7'), 19 | // Base (generator) point (x, y) 20 | Gx: BigInt('0x8bd2aeb9cb7e57cb2c4b482ffc81b7afb9de27e1e3bd23c23a4453bd9ace3262'), 21 | Gy: BigInt('0x547ef835c3dac4fd97f8461a14611dc9c27745132ded8e545c1d54c72f046997'), 22 | h: BigInt(1), 23 | lowS: false 24 | } as const, sha256); 25 | -------------------------------------------------------------------------------- /test/unittests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenPGPJS Unit Tests 6 | 7 | 8 | 9 |
10 | 11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/crypto/hash/ripemd.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { computeDigest } from '../../../src/crypto/hash'; 4 | import util from '../../../src/util.js'; 5 | import enums from '../../../src/enums.js'; 6 | 7 | export default () => it('RIPE-MD 160 bits with test vectors from https://homes.esat.kuleuven.be/~bosselae/ripemd160.html', async function() { 8 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.ripemd, util.stringToUint8Array('')), 'RMDstring("") = 9c1185a5c5e9fc54612808977ee8f548b2258d31')).to.equal('9c1185a5c5e9fc54612808977ee8f548b2258d31'); 9 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.ripemd, util.stringToUint8Array('a')), 'RMDstring("a") = 0bdc9d2d256b3ee9daae347be6f4dc835a467ffe')).to.equal('0bdc9d2d256b3ee9daae347be6f4dc835a467ffe'); 10 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.ripemd, util.stringToUint8Array('abc')), 'RMDstring("abc") = 8eb208f7e05d987a9b044a8e98c6b087f15a0bfc')).to.equal('8eb208f7e05d987a9b044a8e98c6b087f15a0bfc'); 11 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.ripemd, util.stringToUint8Array('message digest')), 'RMDstring("message digest") = 5d0689ef49d2fae572b881b123a85ffa21595f36')).to.equal('5d0689ef49d2fae572b881b123a85ffa21595f36'); 12 | }); 13 | -------------------------------------------------------------------------------- /test/web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import { playwrightLauncher } from '@web/test-runner-playwright'; 2 | 3 | const sharedPlaywrightCIOptions = { 4 | // createBrowserContext: ({ browser }) => browser.newContext({ ignoreHTTPSErrors: true }), 5 | headless: true 6 | }; 7 | 8 | export default { 9 | nodeResolve: true, // to resolve npm module imports in `unittests.html` 10 | files: './test/unittests.html', 11 | protocol: 'http:', 12 | hostname: '127.0.0.1', 13 | testsStartTimeout: 45000, 14 | browserStartTimeout: 120000, 15 | testsFinishTimeout: 450000, 16 | concurrentBrowsers: 3, 17 | concurrency: 1, // see https://github.com/modernweb-dev/web/issues/2706 18 | coverage: false, 19 | groups: [ 20 | { name: 'local' }, // group meant to be used with either --browser or --manual options via CLI 21 | { 22 | name: 'headless:ci', 23 | browsers: [ 24 | playwrightLauncher({ 25 | ...sharedPlaywrightCIOptions, 26 | product: 'chromium' 27 | }), 28 | playwrightLauncher({ 29 | ...sharedPlaywrightCIOptions, 30 | product: 'firefox' 31 | }), 32 | playwrightLauncher({ 33 | ...sharedPlaywrightCIOptions, 34 | product: 'webkit' 35 | }) 36 | ] 37 | } 38 | ] 39 | }; 40 | -------------------------------------------------------------------------------- /src/packet/trust.js: -------------------------------------------------------------------------------- 1 | import enums from '../enums'; 2 | import { UnsupportedError } from './packet'; 3 | 4 | /** 5 | * Implementation of the Trust Packet (Tag 12) 6 | * 7 | * {@link https://tools.ietf.org/html/rfc4880#section-5.10|RFC4880 5.10}: 8 | * The Trust packet is used only within keyrings and is not normally 9 | * exported. Trust packets contain data that record the user's 10 | * specifications of which key holders are trustworthy introducers, 11 | * along with other information that implementing software uses for 12 | * trust information. The format of Trust packets is defined by a given 13 | * implementation. 14 | * 15 | * Trust packets SHOULD NOT be emitted to output streams that are 16 | * transferred to other users, and they SHOULD be ignored on any input 17 | * other than local keyring files. 18 | */ 19 | class TrustPacket { 20 | static get tag() { 21 | return enums.packet.trust; 22 | } 23 | 24 | /** 25 | * Parsing function for a trust packet (tag 12). 26 | * Currently not implemented as we ignore trust packets 27 | */ 28 | read() { 29 | throw new UnsupportedError('Trust packets are not supported'); 30 | } 31 | 32 | write() { 33 | throw new UnsupportedError('Trust packets are not supported'); 34 | } 35 | } 36 | 37 | export default TrustPacket; 38 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/brainpool/brainpoolP384r1.ts: -------------------------------------------------------------------------------- 1 | import { createCurve } from '@noble/curves/_shortw_utils'; 2 | import { sha384 } from '@noble/hashes/sha512'; 3 | import { Field } from '@noble/curves/abstract/modular'; 4 | 5 | // brainpoolP384 r1: https://datatracker.ietf.org/doc/html/rfc5639#section-3.6 6 | 7 | // eslint-disable-next-line new-cap 8 | const Fp = Field(BigInt('0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b412b1da197fb71123acd3a729901d1a71874700133107ec53')); 9 | const CURVE_A = Fp.create(BigInt('0x7bc382c63d8c150c3c72080ace05afa0c2bea28e4fb22787139165efba91f90f8aa5814a503ad4eb04a8c7dd22ce2826')); 10 | const CURVE_B = BigInt('0x04a8c7dd22ce28268b39b55416f0447c2fb77de107dcd2a62e880ea53eeb62d57cb4390295dbc9943ab78696fa504c11'); 11 | 12 | // prettier-ignore 13 | export const brainpoolP384r1 = createCurve({ 14 | a: CURVE_A, // Equation params: a, b 15 | b: CURVE_B, 16 | Fp, 17 | // Curve order (q), total count of valid points in the field 18 | n: BigInt('0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565'), 19 | // Base (generator) point (x, y) 20 | Gx: BigInt('0x1d1c64f068cf45ffa2a63a81b7c13f6b8847a3e77ef14fe3db7fcafe0cbd10e8e826e03436d646aaef87b2e247d4af1e'), 21 | Gy: BigInt('0x8abe1d7520f9c2a45cb1eb8e95cfd55262b70b29feec5864e19c054ff99129280e4646217791811142820341263c5315'), 22 | h: BigInt(1), 23 | lowS: false 24 | } as const, sha384); 25 | -------------------------------------------------------------------------------- /src/packet/all_packets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Exports all OpenPGP packet types 3 | * @module packet/all_packets 4 | * @private 5 | */ 6 | 7 | export { default as CompressedDataPacket } from './compressed_data'; 8 | export { default as SymEncryptedIntegrityProtectedDataPacket } from './sym_encrypted_integrity_protected_data'; 9 | export { default as AEADEncryptedDataPacket } from './aead_encrypted_data'; 10 | export { default as PublicKeyEncryptedSessionKeyPacket } from './public_key_encrypted_session_key'; 11 | export { default as SymEncryptedSessionKeyPacket } from './sym_encrypted_session_key'; 12 | export { default as LiteralDataPacket } from './literal_data'; 13 | export { default as PublicKeyPacket } from './public_key'; 14 | export { default as SymmetricallyEncryptedDataPacket } from './symmetrically_encrypted_data'; 15 | export { default as MarkerPacket } from './marker'; 16 | export { default as PublicSubkeyPacket } from './public_subkey'; 17 | export { default as UserAttributePacket } from './user_attribute'; 18 | export { default as OnePassSignaturePacket } from './one_pass_signature'; 19 | export { default as SecretKeyPacket } from './secret_key'; 20 | export { default as UserIDPacket } from './userid'; 21 | export { default as SecretSubkeyPacket } from './secret_subkey'; 22 | export { default as SignaturePacket } from './signature'; 23 | export { default as TrustPacket } from './trust'; 24 | export { default as PaddingPacket } from './padding'; 25 | -------------------------------------------------------------------------------- /test/crypto/hmac.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import { sign, verify } from '../../src/crypto/public_key/hmac'; 6 | import enums from '../../src/enums'; 7 | import util from '../../src/util'; 8 | 9 | export default () => describe('HMAC', function () { 10 | it('Test vectors', async function() { 11 | const vectors = [ 12 | { 13 | algo: enums.hash.sha256, 14 | key: util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), 15 | data: util.hexToUint8Array('4869205468657265'), 16 | expected: util.hexToUint8Array('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') 17 | }, 18 | { 19 | algo: enums.hash.sha512, 20 | key: util.hexToUint8Array('4a656665'), 21 | data: util.hexToUint8Array('7768617420646f2079612077616e7420666f72206e6f7468696e673f'), 22 | expected: util.hexToUint8Array('164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737') 23 | } 24 | ]; 25 | 26 | await Promise.all(vectors.map(async vec => { 27 | const res = await sign(vec.algo, vec.key, vec.data); 28 | const verified = await verify(vec.algo, vec.key, vec.expected, vec.data); 29 | expect(util.equalsUint8Array(res, vec.expected)).to.be.true; 30 | expect(verified).to.be.true; 31 | })); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /src/type/s2k/index.js: -------------------------------------------------------------------------------- 1 | import defaultConfig from '../../config'; 2 | import Argon2S2K, { Argon2OutOfMemoryError } from './argon2'; 3 | import GenericS2K from './generic'; 4 | import enums from '../../enums'; 5 | import { UnsupportedError } from '../../packet/packet'; 6 | 7 | const allowedS2KTypesForEncryption = new Set([enums.s2k.argon2, enums.s2k.iterated]); 8 | 9 | /** 10 | * Instantiate a new S2K instance of the given type 11 | * @param {module:enums.s2k} type 12 | * @oaram {Object} [config] 13 | * @returns {Object} New s2k object 14 | * @throws {Error} for unknown or unsupported types 15 | */ 16 | export function newS2KFromType(type, config = defaultConfig) { 17 | switch (type) { 18 | case enums.s2k.argon2: 19 | return new Argon2S2K(config); 20 | case enums.s2k.iterated: 21 | case enums.s2k.gnu: 22 | case enums.s2k.salted: 23 | case enums.s2k.simple: 24 | return new GenericS2K(type, config); 25 | default: 26 | throw new UnsupportedError('Unsupported S2K type'); 27 | } 28 | } 29 | 30 | /** 31 | * Instantiate a new S2K instance based on the config settings 32 | * @oaram {Object} config 33 | * @returns {Object} New s2k object 34 | * @throws {Error} for unknown or unsupported types 35 | */ 36 | export function newS2KFromConfig(config) { 37 | const { s2kType } = config; 38 | 39 | if (!allowedS2KTypesForEncryption.has(s2kType)) { 40 | throw new Error('The provided `config.s2kType` value is not allowed'); 41 | } 42 | 43 | return newS2KFromType(s2kType, config); 44 | } 45 | 46 | export { Argon2OutOfMemoryError }; 47 | -------------------------------------------------------------------------------- /src/type/ecdh_x_symkey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encoded symmetric key for x25519 and x448 3 | * The payload format varies for v3 and v6 PKESK: 4 | * the former includes an algorithm byte preceeding the encrypted session key. 5 | * 6 | * @module type/x25519x448_symkey 7 | */ 8 | 9 | import util from '../util'; 10 | 11 | class ECDHXSymmetricKey { 12 | static fromObject({ wrappedKey, algorithm }) { 13 | const instance = new ECDHXSymmetricKey(); 14 | instance.wrappedKey = wrappedKey; 15 | instance.algorithm = algorithm; 16 | return instance; 17 | } 18 | 19 | /** 20 | * - 1 octect for the length `l` 21 | * - `l` octects of encoded session key data (with optional leading algorithm byte) 22 | * @param {Uint8Array} bytes 23 | * @returns {Number} Number of read bytes. 24 | */ 25 | read(bytes) { 26 | let read = 0; 27 | let followLength = bytes[read++]; 28 | this.algorithm = followLength % 2 ? bytes[read++] : null; // session key size is always even 29 | followLength -= followLength % 2; 30 | this.wrappedKey = util.readExactSubarray(bytes, read, read + followLength); read += followLength; 31 | } 32 | 33 | /** 34 | * Write an MontgomerySymmetricKey as an Uint8Array 35 | * @returns {Uint8Array} Serialised data 36 | */ 37 | write() { 38 | return util.concatUint8Array([ 39 | this.algorithm ? 40 | new Uint8Array([this.wrappedKey.length + 1, this.algorithm]) : 41 | new Uint8Array([this.wrappedKey.length]), 42 | this.wrappedKey 43 | ]); 44 | } 45 | } 46 | 47 | export default ECDHXSymmetricKey; 48 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/brainpool/brainpoolP512r1.ts: -------------------------------------------------------------------------------- 1 | import { createCurve } from '@noble/curves/_shortw_utils'; 2 | import { sha512 } from '@noble/hashes/sha512'; 3 | import { Field } from '@noble/curves/abstract/modular'; 4 | 5 | // brainpoolP512r1: https://datatracker.ietf.org/doc/html/rfc5639#section-3.7 6 | 7 | // eslint-disable-next-line new-cap 8 | const Fp = Field(BigInt('0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3')); 9 | const CURVE_A = Fp.create(BigInt('0x7830a3318b603b89e2327145ac234cc594cbdd8d3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94ca')); 10 | const CURVE_B = BigInt('0x3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94cadc083e67984050b75ebae5dd2809bd638016f723'); 11 | 12 | // prettier-ignore 13 | export const brainpoolP512r1 = createCurve({ 14 | a: CURVE_A, // Equation params: a, b 15 | b: CURVE_B, 16 | Fp, 17 | // Curve order (q), total count of valid points in the field 18 | n: BigInt('0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069'), 19 | // Base (generator) point (x, y) 20 | Gx: BigInt('0x81aee4bdd82ed9645a21322e9c4c6a9385ed9f70b5d916c1b43b62eef4d0098eff3b1f78e2d0d48d50d1687b93b97d5f7c6d5047406a5e688b352209bcb9f822'), 21 | Gy: BigInt('0x7dde385d566332ecc0eabfa9cf7822fdf209f70024a57b1aa000c55b881f8111b2dcde494a5f485e5bca4bd88a2763aed1ca2b2fa8f0540678cd1e0f3ad80892'), 22 | h: BigInt(1), 23 | lowS: false 24 | } as const, sha512); 25 | -------------------------------------------------------------------------------- /test/general/oid.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import OID from '../../src/type/oid.js'; 4 | import util from '../../src/util.js'; 5 | 6 | export default () => describe('Oid tests', function() { 7 | const p256_oid = new Uint8Array([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]); 8 | const p384_oid = new Uint8Array([0x2B, 0x81, 0x04, 0x00, 0x22]); 9 | const p521_oid = new Uint8Array([0x2B, 0x81, 0x04, 0x00, 0x23]); 10 | it('Constructing', function() { 11 | const oids = [p256_oid, p384_oid, p521_oid]; 12 | oids.forEach(function (data) { 13 | const oid = new OID(data); 14 | expect(oid).to.exist; 15 | expect(oid.oid).to.exist; 16 | expect(oid.oid).to.have.length(data.length); 17 | expect(oid.toHex()).to.equal(util.uint8ArrayToHex(data)); 18 | }); 19 | }); 20 | it('Reading and writing', function() { 21 | const oids = [p256_oid, p384_oid, p521_oid]; 22 | oids.forEach(function (data) { 23 | data = util.concatUint8Array([new Uint8Array([data.length]), data]); 24 | const oid = new OID(); 25 | expect(oid.read(data)).to.equal(data.length); 26 | expect(oid.oid).to.exist; 27 | expect(oid.oid).to.have.length(data.length - 1); 28 | expect(oid.toHex()).to.equal(util.uint8ArrayToHex(data.subarray(1))); 29 | const result = oid.write(); 30 | expect(result).to.exist; 31 | expect(result).to.have.length(data.length); 32 | expect(result[0]).to.equal(data.length - 1); 33 | expect( 34 | util.uint8ArrayToHex(result.subarray(1)) 35 | ).to.equal(util.uint8ArrayToHex(data.subarray(1))); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/packet/secret_subkey.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import SecretKeyPacket from './secret_key'; 19 | import enums from '../enums'; 20 | import defaultConfig from '../config'; 21 | 22 | /** 23 | * A Secret-Subkey packet (tag 7) is the subkey analog of the Secret 24 | * Key packet and has exactly the same format. 25 | * @extends SecretKeyPacket 26 | */ 27 | class SecretSubkeyPacket extends SecretKeyPacket { 28 | static get tag() { 29 | return enums.packet.secretSubkey; 30 | } 31 | 32 | /** 33 | * @param {Date} [date] - Creation date 34 | * @param {Object} [config] - Full configuration, defaults to openpgp.config 35 | */ 36 | constructor(date = new Date(), config = defaultConfig) { 37 | super(date, config); 38 | } 39 | } 40 | 41 | export default SecretSubkeyPacket; 42 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/index.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * @fileoverview Functions to access Elliptic Curve Cryptography 20 | * @see module:crypto/public_key/elliptic/curve 21 | * @see module:crypto/public_key/elliptic/ecdh 22 | * @see module:crypto/public_key/elliptic/ecdsa 23 | * @see module:crypto/public_key/elliptic/eddsa 24 | * @module crypto/public_key/elliptic 25 | */ 26 | 27 | import { CurveWithOID, generate, getPreferredHashAlgo } from './oid_curves'; 28 | import * as ecdsa from './ecdsa'; 29 | import * as eddsaLegacy from './eddsa_legacy'; 30 | import * as eddsa from './eddsa'; 31 | import * as ecdh from './ecdh'; 32 | import * as ecdhX from './ecdh_x'; 33 | 34 | export { 35 | CurveWithOID, ecdh, ecdhX, ecdsa, eddsaLegacy, eddsa, generate, getPreferredHashAlgo 36 | }; 37 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/signature/ecc_dsa.js: -------------------------------------------------------------------------------- 1 | import * as eddsa from '../../elliptic/eddsa'; 2 | import enums from '../../../../enums'; 3 | 4 | export async function generate(algo) { 5 | switch (algo) { 6 | case enums.publicKey.pqc_mldsa_ed25519: { 7 | const { A, seed } = await eddsa.generate(enums.publicKey.ed25519); 8 | return { 9 | eccPublicKey: A, 10 | eccSecretKey: seed 11 | }; 12 | } 13 | default: 14 | throw new Error('Unsupported signature algorithm'); 15 | } 16 | } 17 | 18 | export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) { 19 | switch (signatureAlgo) { 20 | case enums.publicKey.pqc_mldsa_ed25519: { 21 | const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest); 22 | 23 | return { eccSignature }; 24 | } 25 | default: 26 | throw new Error('Unsupported signature algorithm'); 27 | } 28 | } 29 | 30 | export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) { 31 | switch (signatureAlgo) { 32 | case enums.publicKey.pqc_mldsa_ed25519: 33 | return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest); 34 | default: 35 | throw new Error('Unsupported signature algorithm'); 36 | } 37 | } 38 | 39 | export async function validateParams(algo, eccPublicKey, eccSecretKey) { 40 | switch (algo) { 41 | case enums.publicKey.pqc_mldsa_ed25519: 42 | return eddsa.validateParams(enums.publicKey.ed25519, eccPublicKey, eccSecretKey); 43 | default: 44 | throw new Error('Unsupported signature algorithm'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/kem/ecc_kem.js: -------------------------------------------------------------------------------- 1 | import * as ecdhX from '../../elliptic/ecdh_x'; 2 | import enums from '../../../../enums'; 3 | 4 | export async function generate(algo) { 5 | switch (algo) { 6 | case enums.publicKey.pqc_mlkem_x25519: { 7 | const { A, k } = await ecdhX.generate(enums.publicKey.x25519); 8 | return { 9 | eccPublicKey: A, 10 | eccSecretKey: k 11 | }; 12 | } 13 | default: 14 | throw new Error('Unsupported KEM algorithm'); 15 | } 16 | } 17 | 18 | export async function encaps(eccAlgo, eccRecipientPublicKey) { 19 | switch (eccAlgo) { 20 | case enums.publicKey.pqc_mlkem_x25519: { 21 | const { ephemeralPublicKey: eccCipherText, sharedSecret: eccKeyShare } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey); 22 | 23 | return { 24 | eccCipherText, 25 | eccKeyShare 26 | }; 27 | } 28 | default: 29 | throw new Error('Unsupported KEM algorithm'); 30 | } 31 | } 32 | 33 | export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) { 34 | switch (eccAlgo) { 35 | case enums.publicKey.pqc_mlkem_x25519: { 36 | const eccKeyShare = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey); 37 | return eccKeyShare; 38 | } 39 | default: 40 | throw new Error('Unsupported KEM algorithm'); 41 | } 42 | } 43 | 44 | export async function validateParams(algo, eccPublicKey, eccSecretKey) { 45 | switch (algo) { 46 | case enums.publicKey.pqc_mlkem_x25519: 47 | return ecdhX.validateParams(enums.publicKey.x25519, eccPublicKey, eccSecretKey); 48 | default: 49 | throw new Error('Unsupported KEM algorithm'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/crypto/hash/md5.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import util from '../../../src/util.js'; 4 | import { computeDigest } from '../../../src/crypto/hash'; 5 | import enums from '../../../src/enums.js'; 6 | 7 | export default () => it('MD5 with test vectors from RFC 1321', async function() { 8 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('')), 'MD5("") = d41d8cd98f00b204e9800998ecf8427e')).to.equal('d41d8cd98f00b204e9800998ecf8427e'); 9 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('abc')), 'MD5("a") = 0cc175b9c0f1b6a831c399e269772661')).to.equal('900150983cd24fb0d6963f7d28e17f72'); 10 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('message digest')), 'MD5("message digest") = f96b697d7cb7938d525a2f31aaf161d0')).to.equal('f96b697d7cb7938d525a2f31aaf161d0'); 11 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('abcdefghijklmnopqrstuvwxyz')), 'MD5("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b')).to.equal('c3fcd3d76192e4007dfb496cca67e13b'); 12 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')), 'MD5("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = d174ab98d277d9f5a5611c2c9f419d9f')).to.equal('d174ab98d277d9f5a5611c2c9f419d9f'); 13 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.md5, util.stringToUint8Array('12345678901234567890123456789012345678901234567890123456789012345678901234567890')), 'MD5("12345678901234567890123456789012345678901234567890123456789012345678901234567890") = 57edf4a22be3c955ac49da2e2107b67a')).to.equal('57edf4a22be3c955ac49da2e2107b67a'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/config/config.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global configuration values. 3 | */ 4 | 5 | import enums from '../enums'; 6 | 7 | export interface Config { 8 | preferredHashAlgorithm: enums.hash; 9 | preferredSymmetricAlgorithm: enums.symmetric; 10 | preferredCompressionAlgorithm: enums.compression; 11 | showVersion: boolean; 12 | showComment: boolean; 13 | aeadProtect: boolean; 14 | ignoreSEIPDv2FeatureFlag: boolean; 15 | allowUnauthenticatedMessages: boolean; 16 | allowUnauthenticatedStream: boolean; 17 | allowForwardedMessages: boolean; 18 | minRSABits: number; 19 | passwordCollisionCheck: boolean; 20 | ignoreUnsupportedPackets: boolean; 21 | ignoreMalformedPackets: boolean; 22 | enforceGrammar: boolean; 23 | additionalAllowedPackets: Array<{ new(): any, tag: enums.packet }>; 24 | versionString: string; 25 | commentString: string; 26 | allowInsecureDecryptionWithSigningKeys: boolean; 27 | allowInsecureVerificationWithReformattedKeys: boolean; 28 | allowMissingKeyFlags: boolean; 29 | constantTimePKCS1Decryption: boolean; 30 | constantTimePKCS1DecryptionSupportedSymmetricAlgorithms: Set; 31 | v6Keys: boolean; 32 | enableParsingV5Entities: boolean; 33 | preferredAEADAlgorithm: enums.aead; 34 | aeadChunkSizeByte: number; 35 | s2kType: enums.s2k.iterated | enums.s2k.argon2; 36 | s2kIterationCountByte: number; 37 | s2kArgon2Params: { passes: number, parallelism: number; memoryExponent: number; }; 38 | maxUserIDLength: number; 39 | knownNotations: string[]; 40 | useEllipticFallback: boolean; 41 | rejectHashAlgorithms: Set; 42 | rejectMessageHashAlgorithms: Set; 43 | rejectPublicKeyAlgorithms: Set; 44 | rejectCurves: Set; 45 | } 46 | 47 | declare const config: Config; 48 | export default config; 49 | -------------------------------------------------------------------------------- /test/web-test-runner.browserstack.config.js: -------------------------------------------------------------------------------- 1 | import { browserstackLauncher } from '@web/test-runner-browserstack'; 2 | import wtrConfig from './web-test-runner.config.js'; 3 | 4 | const sharedBrowserstackCapabilities = { 5 | 'browserstack.user': process.env.BROWSERSTACK_USERNAME, 6 | 'browserstack.key': process.env.BROWSERSTACK_ACCESS_KEY, 7 | 8 | project: `openpgpjs/${process.env.GITHUB_EVENT_NAME || 'push'}${process.env.LIGHTWEIGHT ? '/lightweight' : ''}@${process.env.GITHUB_REF_NAME}`, 9 | name: process.env.GITHUB_WORKFLOW || 'local', 10 | build: process.env.GITHUB_SHA || 'local', 11 | 'browserstack.acceptInsecureCerts': true 12 | }; 13 | 14 | export default { 15 | ...wtrConfig, 16 | protocol: 'https:', 17 | http2: true, 18 | sslKey: './127.0.0.1-key.pem', 19 | sslCert: './127.0.0.1.pem', 20 | testsStartTimeout: 25000, 21 | testsStartTimeoutMaxRetries: 3, // custom config from @openpgp/wtr-test-runner-core 22 | browserStartTimeout: 120000, 23 | testsFinishTimeout: 450000, 24 | concurrentBrowsers: 1, 25 | concurrency: 1, // see https://github.com/modernweb-dev/web/issues/2706 26 | coverage: false, 27 | groups: [], // overwrite the field coming from `wrtConfig` 28 | browsers: [ 29 | browserstackLauncher({ 30 | capabilities: { 31 | ...sharedBrowserstackCapabilities, 32 | browserName: '[Browserstack] Safari iOS 14', 33 | device: 'iPhone 12', 34 | real_mobile: true, 35 | os: 'ios', 36 | os_version: '14' // min supported version (iOS/Safari < 14 does not support native BigInts) 37 | } 38 | }), 39 | browserstackLauncher({ 40 | capabilities: { 41 | ...sharedBrowserstackCapabilities, 42 | browserName: '[Browserstack] Safari iOS latest', 43 | device: 'iPhone 16 Pro', 44 | real_mobile: true, 45 | os: 'ios', 46 | os_version: 'latest' 47 | } 48 | }) 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /test/general/decompression.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import openpgp from '../initOpenpgp.js'; 6 | 7 | const password = 'I am a password'; 8 | 9 | const tests = { 10 | zip: { 11 | input: `-----BEGIN PGP MESSAGE----- 12 | 13 | jA0ECQMC5rhAA7l3jOzk0kwBTMc07y+1NME5RCUQ2EOlSofbh1KARLC5B1NMeBlq 14 | jS917VBeCW3R21xG+0ZJ6Z5iWwdQD7XBtg19doWOqExSmXBWWW/6vSaD81ox 15 | =Gw9+ 16 | -----END PGP MESSAGE-----`, 17 | output: 'Hello world! With zip.' 18 | }, 19 | zlib: { 20 | input: `-----BEGIN PGP MESSAGE----- 21 | 22 | jA0ECQMC8Qfig2+Tygnk0lMB++5JoyZUcpUy5EJqcxBuy93tXw+BSk7OhFhda1Uo 23 | JuQlKv27HlyUaA55tMJsFYPypGBLEXW3k0xi3Cs87RrLqmVGTZSqNhHOVNE28lVe 24 | W40mpQ== 25 | =z0we 26 | -----END PGP MESSAGE-----`, 27 | output: 'Hello world! With zlib.' 28 | }, 29 | bzip2: { 30 | input: `-----BEGIN PGP MESSAGE----- 31 | 32 | jA0ECQMC97w+wp7u9/Xk0oABBfapJBuuxGBiHDfNmVgsRzbjLDBWTJ3LD4UtxEku 33 | qu6hwp5JXB0TgI/XQ3tKobSqHv1wSJ9SVxtWZq6WvWulu+j9GtzIVC3mbDA/qRA3 34 | 41sUEMdAFC6I7BYLYGEiUAVNpjbvGOmJWptDyawjRgEuZeTzKyTI/UcMc/rLy9Pz 35 | Xg== 36 | =6ek1 37 | -----END PGP MESSAGE-----`, 38 | output: 'Hello world! With bzip2.' 39 | } 40 | }; 41 | 42 | export default () => describe('Decrypt and decompress message tests', function () { 43 | 44 | function runTest(key, test) { 45 | it(`Decrypts message compressed with ${key}`, async function () { 46 | const message = await openpgp.readMessage({ armoredMessage: test.input }); 47 | const options = { 48 | passwords: password, 49 | message 50 | }; 51 | return openpgp.decrypt(options).then(function (decrypted) { 52 | expect(decrypted.data).to.equal(test.output + '\n'); 53 | }); 54 | }); 55 | } 56 | 57 | Object.keys(tests).forEach(key => runTest(key, tests[key])); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/crypto/public_key/hmac.js: -------------------------------------------------------------------------------- 1 | import enums from '../../enums'; 2 | import util from '../../util'; 3 | 4 | const supportedHashAlgos = new Set([enums.hash.sha1, enums.hash.sha256, enums.hash.sha512]); 5 | 6 | const webCrypto = util.getWebCrypto(); 7 | const nodeCrypto = util.getNodeCrypto(); 8 | 9 | export async function generate(hashAlgo) { 10 | if (!supportedHashAlgos.has(hashAlgo)) { 11 | throw new Error('Unsupported hash algorithm.'); 12 | } 13 | const hashName = enums.read(enums.webHash, hashAlgo); 14 | 15 | const crypto = webCrypto || nodeCrypto.webcrypto.subtle; 16 | const key = await crypto.generateKey( 17 | { 18 | name: 'HMAC', 19 | hash: { name: hashName } 20 | }, 21 | true, 22 | ['sign', 'verify'] 23 | ); 24 | const exportedKey = await crypto.exportKey('raw', key); 25 | return new Uint8Array(exportedKey); 26 | } 27 | 28 | export async function sign(hashAlgo, key, data) { 29 | if (!supportedHashAlgos.has(hashAlgo)) { 30 | throw new Error('Unsupported hash algorithm.'); 31 | } 32 | const hashName = enums.read(enums.webHash, hashAlgo); 33 | 34 | const crypto = webCrypto || nodeCrypto.webcrypto.subtle; 35 | const importedKey = await crypto.importKey( 36 | 'raw', 37 | key, 38 | { 39 | name: 'HMAC', 40 | hash: { name: hashName } 41 | }, 42 | false, 43 | ['sign'] 44 | ); 45 | const mac = await crypto.sign('HMAC', importedKey, data); 46 | return new Uint8Array(mac); 47 | } 48 | 49 | export async function verify(hashAlgo, key, mac, data) { 50 | if (!supportedHashAlgos.has(hashAlgo)) { 51 | throw new Error('Unsupported hash algorithm.'); 52 | } 53 | const hashName = enums.read(enums.webHash, hashAlgo); 54 | 55 | const crypto = webCrypto || nodeCrypto.webcrypto.subtle; 56 | const importedKey = await crypto.importKey( 57 | 'raw', 58 | key, 59 | { 60 | name: 'HMAC', 61 | hash: { name: hashName } 62 | }, 63 | false, 64 | ['verify'] 65 | ); 66 | return crypto.verify('HMAC', importedKey, mac, data); 67 | } 68 | -------------------------------------------------------------------------------- /src/type/ecdh_symkey.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * Encoded symmetric key for ECDH (incl. legacy x25519) 20 | * 21 | * @module type/ecdh_symkey 22 | */ 23 | 24 | import util from '../util'; 25 | 26 | class ECDHSymmetricKey { 27 | constructor(data) { 28 | if (data) { 29 | this.data = data; 30 | } 31 | } 32 | 33 | /** 34 | * Read an ECDHSymmetricKey from an Uint8Array: 35 | * - 1 octect for the length `l` 36 | * - `l` octects of encoded session key data 37 | * @param {Uint8Array} bytes 38 | * @returns {Number} Number of read bytes. 39 | */ 40 | read(bytes) { 41 | if (bytes.length >= 1) { 42 | const length = bytes[0]; 43 | if (bytes.length >= 1 + length) { 44 | this.data = bytes.subarray(1, 1 + length); 45 | return 1 + this.data.length; 46 | } 47 | } 48 | throw new Error('Invalid symmetric key'); 49 | } 50 | 51 | /** 52 | * Write an ECDHSymmetricKey as an Uint8Array 53 | * @returns {Uint8Array} Serialised data 54 | */ 55 | write() { 56 | return util.concatUint8Array([new Uint8Array([this.data.length]), this.data]); 57 | } 58 | } 59 | 60 | export default ECDHSymmetricKey; 61 | -------------------------------------------------------------------------------- /src/crypto/pkcs5.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import util from '../util'; 19 | 20 | /** 21 | * @fileoverview Functions to add and remove PKCS5 padding 22 | * @see PublicKeyEncryptedSessionKeyPacket 23 | * @module crypto/pkcs5 24 | * @private 25 | */ 26 | 27 | /** 28 | * Add pkcs5 padding to a message 29 | * @param {Uint8Array} message - message to pad 30 | * @returns {Uint8Array} Padded message. 31 | */ 32 | export function encode(message) { 33 | const c = 8 - (message.length % 8); 34 | const padded = new Uint8Array(message.length + c).fill(c); 35 | padded.set(message); 36 | return padded; 37 | } 38 | 39 | /** 40 | * Remove pkcs5 padding from a message 41 | * @param {Uint8Array} message - message to remove padding from 42 | * @returns {Uint8Array} Message without padding. 43 | */ 44 | export function decode(message) { 45 | const len = message.length; 46 | if (len > 0) { 47 | const c = message[len - 1]; 48 | if (c >= 1) { 49 | const provided = message.subarray(len - c); 50 | const computed = new Uint8Array(c).fill(c); 51 | if (util.equalsUint8Array(provided, computed)) { 52 | return message.subarray(0, len - c); 53 | } 54 | } 55 | } 56 | throw new Error('Invalid padding'); 57 | } 58 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /src/packet/padding.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2022 Proton AG 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import { getRandomBytes } from '../crypto'; 19 | import enums from '../enums'; 20 | 21 | /** 22 | * Implementation of the Padding Packet 23 | * 24 | * {@link https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21}: 25 | * Padding Packet 26 | */ 27 | class PaddingPacket { 28 | static get tag() { 29 | return enums.packet.padding; 30 | } 31 | 32 | constructor() { 33 | this.padding = null; 34 | } 35 | 36 | /** 37 | * Read a padding packet 38 | * @param {Uint8Array | ReadableStream} bytes 39 | */ 40 | read(bytes) { // eslint-disable-line @typescript-eslint/no-unused-vars 41 | // Padding packets are ignored, so this function is never called. 42 | } 43 | 44 | /** 45 | * Write the padding packet 46 | * @returns {Uint8Array} The padding packet. 47 | */ 48 | write() { 49 | return this.padding; 50 | } 51 | 52 | /** 53 | * Create random padding. 54 | * @param {Number} length - The length of padding to be generated. 55 | * @throws {Error} if padding generation was not successful 56 | * @async 57 | */ 58 | async createPadding(length) { 59 | this.padding = await getRandomBytes(length); 60 | } 61 | } 62 | 63 | export default PaddingPacket; 64 | -------------------------------------------------------------------------------- /src/crypto/cipher/index.js: -------------------------------------------------------------------------------- 1 | import enums from '../../enums'; 2 | 3 | export async function getLegacyCipher(algo) { 4 | switch (algo) { 5 | case enums.symmetric.aes128: 6 | case enums.symmetric.aes192: 7 | case enums.symmetric.aes256: 8 | throw new Error('Not a legacy cipher'); 9 | case enums.symmetric.cast5: 10 | case enums.symmetric.blowfish: 11 | case enums.symmetric.twofish: 12 | case enums.symmetric.tripledes: { 13 | const { legacyCiphers } = await import('./legacy_ciphers'); 14 | const algoName = enums.read(enums.symmetric, algo); 15 | const cipher = legacyCiphers.get(algoName); 16 | if (!cipher) { 17 | throw new Error('Unsupported cipher algorithm'); 18 | } 19 | return cipher; 20 | } 21 | default: 22 | throw new Error('Unsupported cipher algorithm'); 23 | } 24 | } 25 | 26 | /** 27 | * Get block size for given cipher algo 28 | * @param {module:enums.symmetric} algo - alrogithm identifier 29 | */ 30 | function getCipherBlockSize(algo) { 31 | switch (algo) { 32 | case enums.symmetric.aes128: 33 | case enums.symmetric.aes192: 34 | case enums.symmetric.aes256: 35 | case enums.symmetric.twofish: 36 | return 16; 37 | case enums.symmetric.blowfish: 38 | case enums.symmetric.cast5: 39 | case enums.symmetric.tripledes: 40 | return 8; 41 | default: 42 | throw new Error('Unsupported cipher'); 43 | } 44 | } 45 | 46 | /** 47 | * Get key size for given cipher algo 48 | * @param {module:enums.symmetric} algo - alrogithm identifier 49 | */ 50 | function getCipherKeySize(algo) { 51 | switch (algo) { 52 | case enums.symmetric.aes128: 53 | case enums.symmetric.blowfish: 54 | case enums.symmetric.cast5: 55 | return 16; 56 | case enums.symmetric.aes192: 57 | case enums.symmetric.tripledes: 58 | return 24; 59 | case enums.symmetric.aes256: 60 | case enums.symmetric.twofish: 61 | return 32; 62 | default: 63 | throw new Error('Unsupported cipher'); 64 | } 65 | } 66 | 67 | /** 68 | * Get block and key size for given cipher algo 69 | * @param {module:enums.symmetric} algo - alrogithm identifier 70 | */ 71 | export function getCipherParams(algo) { 72 | return { keySize: getCipherKeySize(algo), blockSize: getCipherBlockSize(algo) }; 73 | } 74 | -------------------------------------------------------------------------------- /src/type/enum.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | 19 | /** 20 | * Wrapper for enums 21 | * 22 | * @requires enums 23 | * @module type/enum.js 24 | */ 25 | 26 | 27 | import enums from '../enums'; 28 | 29 | const type_enum = type => class EnumType { 30 | constructor(data) { 31 | if (typeof data === 'undefined') { 32 | this.data = null; 33 | } else { 34 | this.data = data; 35 | } 36 | } 37 | 38 | /** 39 | * Read an enum entry 40 | * @param {Uint8Array} input Where to read the symmetric algo from 41 | */ 42 | read(input) { 43 | const data = input[0]; 44 | this.data = enums.write(type, data); 45 | return 1; 46 | } 47 | 48 | /** 49 | * Write an enum as an integer 50 | * @returns {Uint8Array} An integer representing the algorithm 51 | */ 52 | write() { 53 | return new Uint8Array([this.data]); 54 | } 55 | 56 | /** 57 | * Get the name of the enum entry 58 | * @returns {string} The name string 59 | */ 60 | getName() { 61 | return enums.read(type, this.data); 62 | } 63 | 64 | /** 65 | * Get the integer value of the enum entry 66 | * @returns {Number} The integer value 67 | */ 68 | getValue() { 69 | return this.data; 70 | } 71 | }; 72 | const AEADEnum = type_enum(enums.aead); 73 | const SymAlgoEnum = type_enum(enums.symmetric); 74 | const HashEnum = type_enum(enums.hash); 75 | 76 | export { AEADEnum, SymAlgoEnum, HashEnum }; 77 | 78 | export default type_enum; 79 | -------------------------------------------------------------------------------- /test/security/preferred_algo_mismatch.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import openpgp from '../initOpenpgp.js'; 6 | 7 | const armoredMessage = `-----BEGIN PGP MESSAGE----- 8 | Version: OpenPGP.js VERSION 9 | Comment: https://openpgpjs.org 10 | 11 | wYwD3eCUoDfD5yoBA/98Ceee8cVOuwZMscnFXzkldJV6Km/Uozcwsx0+Epqb 12 | 31qF6QosSgEBNGet5PXxV3VU5BnjSeMnK3500NFGgLZUYKLqdHmtwj4hIz7S 13 | VpX1fVpp5n8729Fuv9MhRcFrrIrRj5h6Mj8G7xIgCQm+uJTla3X8wRXss8/p 14 | y57epbYHO9JGAZsQl6kFLOsgtlV/NPwAtjsH/AzsQs3Y6WcudHh0XB3E+ncK 15 | BLn6oaBjcnlwdGVk0wJnjV2YZRiZ7V3lUIDdYIMNpL+5qA== 16 | =IoHy 17 | -----END PGP MESSAGE-----`; 18 | 19 | const privateKeyArmor = `-----BEGIN PGP PRIVATE KEY BLOCK----- 20 | Version: OpenPGP.js VERSION 21 | Comment: https://openpgpjs.org 22 | 23 | xcEYBFvbA08BBACl8U5VEY7TNq1PAzwU0f3soqNfFpKtNFt+LY3q5sasouJ7 24 | zE4/TPYrAaAoM5/yOjfvbfJP5myBUCtkdtIRIY2iP2uOPhfaly8U+zH25Qnq 25 | bmgLfvu4ytPAPrKZF8f98cIeJmHD81SPRgDMuB2U9wwgN6stgVBBCUS+lu/L 26 | /4pyuwARAQABAAP+Jz6BIvcrCuJ0bCo8rEPZRHxWHKfO+m1Wcem+FV6Mf8lp 27 | vJNdsfS2hwc0ZC2JVxTTo6kh1CmPYamfCXxcQ7bmsqWkkq/6d17zKE6BqE/n 28 | spW7qTnZ14VPC0iPrBetAWRlCk+m0cEkRnBxqPOVBNd6VPcZyM7GUOGf/kiw 29 | AsHf+nECANkN1tsqLJ3+pH2MRouF7yHevQ9OGg+rwetBO2a8avvcsAuoFjVw 30 | hERpkHv/PQjKAE7KcBzqLLad0QbrQW+sUcMCAMO3to0tSBJrNA9YkrViT76I 31 | siiahSB/FC9JlO+T46xncRleZeBHc0zoVAP+W/PjRo2CR4ydtwjjalrxcKX9 32 | E6kCALfDyhkRNzZLxg2XOGDWyeXqe80VWnMBqTZK73nZlACRcUoXuvjRc15Q 33 | K2c3/nZ7LMyQidj8XsTq4sz1zfWz4Cejj80cVGVzdCBVc2VyIDx0ZXN0QGV4 34 | YW1wbGUuY29tPsK1BBABCAApBQJb2wNPAgsJCRDd4JSgN8PnKgQVCAoCAxYC 35 | AQIZAQIbDwIeBwMiAQIAABGjA/4y6HjthMU03AC3bIUyYPv6EJc9czS5wysa 36 | 5rKuNhzka0Klb0INcX1YZ8usPIIl1rtr8f8xxCdSiqhJpn+uqIPVROHi0XLG 37 | ej3gSJM5i1lIt1jxyJlvVI/7W0vzuE85KDzGXQFNFyO/T9D7T1SDHnS8KbBh 38 | EnxUPL95HuMKoVkf4w== 39 | =oopr 40 | -----END PGP PRIVATE KEY BLOCK-----`; 41 | 42 | export default () => it('Does not accept message encrypted with algo not mentioned in preferred algorithms', async function() { 43 | const message = await openpgp.readMessage({ armoredMessage }); 44 | const privKey = await openpgp.readKey({ armoredKey: privateKeyArmor }); 45 | await expect(openpgp.decrypt({ message, decryptionKeys: [privKey] })).to.be.rejectedWith('A non-preferred symmetric algorithm was used.'); 46 | }); 47 | -------------------------------------------------------------------------------- /src/packet/marker.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import enums from '../enums'; 19 | 20 | /** 21 | * Implementation of the strange "Marker packet" (Tag 10) 22 | * 23 | * {@link https://tools.ietf.org/html/rfc4880#section-5.8|RFC4880 5.8}: 24 | * An experimental version of PGP used this packet as the Literal 25 | * packet, but no released version of PGP generated Literal packets with this 26 | * tag. With PGP 5.x, this packet has been reassigned and is reserved for use as 27 | * the Marker packet. 28 | * 29 | * The body of this packet consists of: 30 | * The three octets 0x50, 0x47, 0x50 (which spell "PGP" in UTF-8). 31 | * 32 | * Such a packet MUST be ignored when received. It may be placed at the 33 | * beginning of a message that uses features not available in PGP 34 | * version 2.6 in order to cause that version to report that newer 35 | * software is necessary to process the message. 36 | */ 37 | class MarkerPacket { 38 | static get tag() { 39 | return enums.packet.marker; 40 | } 41 | 42 | /** 43 | * Parsing function for a marker data packet (tag 10). 44 | * @param {Uint8Array} bytes - Payload of a tag 10 packet 45 | * @returns {Boolean} whether the packet payload contains "PGP" 46 | */ 47 | read(bytes) { 48 | if (bytes[0] === 0x50 && // P 49 | bytes[1] === 0x47 && // G 50 | bytes[2] === 0x50) { // P 51 | return true; 52 | } 53 | return false; 54 | } 55 | 56 | write() { 57 | return new Uint8Array([0x50, 0x47, 0x50]); 58 | } 59 | } 60 | 61 | export default MarkerPacket; 62 | -------------------------------------------------------------------------------- /test/crypto/hkdf.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import computeHKDF from '../../src/crypto/hkdf'; 4 | import enums from '../../src/enums'; 5 | import util from '../../src/util'; 6 | 7 | export default () => describe('HKDF test vectors', function() { 8 | // Vectors from https://www.rfc-editor.org/rfc/rfc5869#appendix-A 9 | it('Test Case 1', async function() { 10 | const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'); 11 | const salt = util.hexToUint8Array('000102030405060708090a0b0c'); 12 | const info = util.hexToUint8Array('f0f1f2f3f4f5f6f7f8f9'); 13 | const outLen = 42; 14 | 15 | const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen); 16 | const expected = util.hexToUint8Array('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865'); 17 | 18 | expect(actual).to.deep.equal(expected); 19 | }); 20 | 21 | it('Test Case 2', async function() { 22 | const inputKey = util.hexToUint8Array('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f'); 23 | const salt = util.hexToUint8Array('606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf'); 24 | const info = util.hexToUint8Array('b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); 25 | const outLen = 82; 26 | 27 | const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen); 28 | const expected = util.hexToUint8Array('b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87'); 29 | 30 | expect(actual).to.deep.equal(expected); 31 | }); 32 | 33 | it('Test Case 3', async function() { 34 | const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'); 35 | const salt = new Uint8Array(); 36 | const info = new Uint8Array(); 37 | const outLen = 42; 38 | 39 | const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen); 40 | const expected = util.hexToUint8Array('8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8'); 41 | 42 | expect(actual).to.deep.equal(expected); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/key/public_key.js: -------------------------------------------------------------------------------- 1 | // This library is free software; you can redistribute it and/or 2 | // modify it under the terms of the GNU Lesser General Public 3 | // License as published by the Free Software Foundation; either 4 | // version 3.0 of the License, or (at your option) any later version. 5 | // 6 | // This library is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 | // Lesser General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU Lesser General Public 12 | // License along with this library; if not, write to the Free Software 13 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 14 | 15 | import { armor } from '../encoding/armor'; 16 | import defaultConfig from '../config'; 17 | import enums from '../enums'; 18 | import Key from './key'; 19 | 20 | /** 21 | * Class that represents an OpenPGP Public Key 22 | */ 23 | class PublicKey extends Key { 24 | /** 25 | * @param {PacketList} packetlist - The packets that form this key 26 | */ 27 | constructor(packetlist) { 28 | super(); 29 | this.keyPacket = null; 30 | this.revocationSignatures = []; 31 | this.directSignatures = []; 32 | this.users = []; 33 | this.subkeys = []; 34 | if (packetlist) { 35 | this.packetListToStructure(packetlist, new Set([enums.packet.secretKey, enums.packet.secretSubkey])); 36 | if (!this.keyPacket) { 37 | throw new Error('Invalid key: missing public-key packet'); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Returns true if this is a private key 44 | * @returns {false} 45 | */ 46 | isPrivate() { 47 | return false; 48 | } 49 | 50 | /** 51 | * Returns key as public key (shallow copy) 52 | * @returns {PublicKey} New public Key 53 | */ 54 | toPublic() { 55 | return this; 56 | } 57 | 58 | /** 59 | * Returns ASCII armored text of key 60 | * @param {Object} [config] - Full configuration, defaults to openpgp.config 61 | * @returns {ReadableStream} ASCII armor. 62 | */ 63 | armor(config = defaultConfig) { 64 | // An ASCII-armored Transferable Public Key packet sequence of a v6 key MUST NOT contain a CRC24 footer. 65 | const emitChecksum = this.keyPacket.version !== 6; 66 | return armor(enums.armor.publicKey, this.toPacketList().write(), undefined, undefined, undefined, emitChecksum, config); 67 | } 68 | } 69 | 70 | export default PublicKey; 71 | 72 | -------------------------------------------------------------------------------- /src/packet/public_subkey.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import PublicKeyPacket from './public_key'; 19 | import enums from '../enums'; 20 | 21 | /** 22 | * A Public-Subkey packet (tag 14) has exactly the same format as a 23 | * Public-Key packet, but denotes a subkey. One or more subkeys may be 24 | * associated with a top-level key. By convention, the top-level key 25 | * provides signature services, and the subkeys provide encryption 26 | * services. 27 | * @extends PublicKeyPacket 28 | */ 29 | class PublicSubkeyPacket extends PublicKeyPacket { 30 | static get tag() { 31 | return enums.packet.publicSubkey; 32 | } 33 | 34 | /** 35 | * @param {Date} [date] - Creation date 36 | * @param {Object} [config] - Full configuration, defaults to openpgp.config 37 | */ 38 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 39 | constructor(date, config) { 40 | super(date, config); 41 | } 42 | 43 | /** 44 | * Create a PublicSubkeyPacket from a SecretSubkeyPacket 45 | * @param {SecretSubkeyPacket} secretSubkeyPacket - subkey packet to convert 46 | * @returns {SecretSubkeyPacket} public key packet 47 | * @static 48 | */ 49 | static fromSecretSubkeyPacket(secretSubkeyPacket) { 50 | const keyPacket = new PublicSubkeyPacket(); 51 | const { version, created, algorithm, publicParams, keyID, fingerprint } = secretSubkeyPacket; 52 | keyPacket.version = version; 53 | keyPacket.created = created; 54 | keyPacket.algorithm = algorithm; 55 | keyPacket.publicParams = publicParams; 56 | keyPacket.keyID = keyID; 57 | keyPacket.fingerprint = fingerprint; 58 | return keyPacket; 59 | } 60 | } 61 | 62 | export default PublicSubkeyPacket; 63 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/kem/kem.js: -------------------------------------------------------------------------------- 1 | import * as eccKem from './ecc_kem'; 2 | import * as mlKem from './ml_kem'; 3 | import * as aesKW from '../../../aes_kw'; 4 | import util from '../../../../util'; 5 | import enums from '../../../../enums'; 6 | import { computeDigest } from '../../../hash'; 7 | 8 | export async function generate(algo) { 9 | const { eccPublicKey, eccSecretKey } = await eccKem.generate(algo); 10 | const { mlkemPublicKey, mlkemSeed, mlkemSecretKey } = await mlKem.generate(algo); 11 | 12 | return { eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed, mlkemSecretKey }; 13 | } 14 | 15 | export async function encrypt(algo, eccPublicKey, mlkemPublicKey, sessioneKeyData) { 16 | const { eccKeyShare, eccCipherText } = await eccKem.encaps(algo, eccPublicKey); 17 | const { mlkemKeyShare, mlkemCipherText } = await mlKem.encaps(algo, mlkemPublicKey); 18 | const kek = await multiKeyCombine(algo, mlkemKeyShare, eccKeyShare, eccCipherText, eccPublicKey); 19 | const wrappedKey = await aesKW.wrap(enums.symmetric.aes256, kek, sessioneKeyData); // C 20 | return { eccCipherText, mlkemCipherText, wrappedKey }; 21 | } 22 | 23 | export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, encryptedSessionKeyData) { 24 | const eccKeyShare = await eccKem.decaps(algo, eccCipherText, eccSecretKey, eccPublicKey); 25 | const mlkemKeyShare = await mlKem.decaps(algo, mlkemCipherText, mlkemSecretKey); 26 | const kek = await multiKeyCombine(algo, mlkemKeyShare, eccKeyShare, eccCipherText, eccPublicKey); 27 | const sessionKey = await aesKW.unwrap(enums.symmetric.aes256, kek, encryptedSessionKeyData); 28 | return sessionKey; 29 | } 30 | 31 | /** 32 | * KEM key combiner 33 | */ 34 | async function multiKeyCombine(algo, mlkemKeyShare, ecdhKeyShare, ecdhCipherText, ecdhPublicKey) { 35 | const domSep = util.encodeUTF8('OpenPGPCompositeKDFv1'); 36 | const encData = util.concatUint8Array([ 37 | mlkemKeyShare, 38 | ecdhKeyShare, 39 | ecdhCipherText, 40 | ecdhPublicKey, 41 | new Uint8Array([algo]), 42 | domSep, 43 | new Uint8Array([domSep.length]) 44 | ]); 45 | 46 | const kek = await computeDigest(enums.hash.sha3_256, encData); 47 | return kek; 48 | } 49 | 50 | export async function validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed) { 51 | const eccValidationPromise = eccKem.validateParams(algo, eccPublicKey, eccSecretKey); 52 | const mlkemValidationPromise = mlKem.validateParams(algo, mlkemPublicKey, mlkemSeed); 53 | const valid = await eccValidationPromise && await mlkemValidationPromise; 54 | return valid; 55 | } 56 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/signature/ml_dsa.js: -------------------------------------------------------------------------------- 1 | import enums from '../../../../enums'; 2 | import util from '../../../../util'; 3 | import { getRandomBytes } from '../../../random'; 4 | 5 | export async function generate(algo) { 6 | switch (algo) { 7 | case enums.publicKey.pqc_mldsa_ed25519: { 8 | const mldsaSeed = getRandomBytes(32); 9 | const { mldsaSecretKey, mldsaPublicKey } = await expandSecretSeed(algo, mldsaSeed); 10 | 11 | return { mldsaSeed, mldsaSecretKey, mldsaPublicKey }; 12 | } 13 | default: 14 | throw new Error('Unsupported signature algorithm'); 15 | } 16 | } 17 | 18 | /** 19 | * Expand ML-DSA secret seed and retrieve the secret and public key material 20 | * @param {module:enums.publicKey} algo - Public key algorithm 21 | * @param {Uint8Array} seed - secret seed to expand 22 | * @returns {Promise<{ mldsaPublicKey: Uint8Array, mldsaSecretKey: Uint8Array }>} 23 | */ 24 | export async function expandSecretSeed(algo, seed) { 25 | switch (algo) { 26 | case enums.publicKey.pqc_mldsa_ed25519: { 27 | const { ml_dsa65 } = await import('../noble_post_quantum'); 28 | const { secretKey: mldsaSecretKey, publicKey: mldsaPublicKey } = ml_dsa65.keygen(seed); 29 | 30 | return { mldsaSecretKey, mldsaPublicKey }; 31 | } 32 | default: 33 | throw new Error('Unsupported signature algorithm'); 34 | } 35 | } 36 | 37 | export async function sign(algo, mldsaSecretKey, dataDigest) { 38 | switch (algo) { 39 | case enums.publicKey.pqc_mldsa_ed25519: { 40 | const { ml_dsa65 } = await import('../noble_post_quantum'); 41 | const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigest); 42 | return { mldsaSignature }; 43 | } 44 | default: 45 | throw new Error('Unsupported signature algorithm'); 46 | } 47 | } 48 | 49 | export async function verify(algo, mldsaPublicKey, dataDigest, mldsaSignature) { 50 | switch (algo) { 51 | case enums.publicKey.pqc_mldsa_ed25519: { 52 | const { ml_dsa65 } = await import('../noble_post_quantum'); 53 | return ml_dsa65.verify(mldsaPublicKey, dataDigest, mldsaSignature); 54 | } 55 | default: 56 | throw new Error('Unsupported signature algorithm'); 57 | } 58 | } 59 | 60 | export async function validateParams(algo, mldsaPublicKey, mldsaSeed) { 61 | switch (algo) { 62 | case enums.publicKey.pqc_mldsa_ed25519: { 63 | const { mldsaPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mldsaSeed); 64 | return util.equalsUint8Array(mldsaPublicKey, expectedPublicKey); 65 | } 66 | default: 67 | throw new Error('Unsupported signature algorithm'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Performance Regression Test 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | benchmark: 9 | name: Time and memory usage benchmark 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # check out pull request branch 14 | - uses: actions/checkout@v4 15 | with: 16 | path: pr 17 | # check out main branch (to compare performance) 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | path: main 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: '>=20.6.0' 25 | 26 | - name: Run pull request time benchmark 27 | run: cd pr && npm install && npm run --silent benchmark-time > benchmarks.txt && cat benchmarks.txt 28 | 29 | - name: Run pull request memory usage benchmark 30 | run: cd pr && npm run --silent benchmark-memory-usage > memory_usage.txt && cat memory_usage.txt 31 | 32 | - name: Run time benchmark on main (baseline) 33 | run: cd main && npm install && npm run --silent benchmark-time > benchmarks.txt && cat benchmarks.txt 34 | 35 | - name: Run memory usage benchmark on main (baseline) 36 | run: cd main && npm run --silent benchmark-memory-usage > memory_usage.txt && cat memory_usage.txt 37 | 38 | - name: Compare time benchmark result 39 | uses: openpgpjs/github-action-pull-request-benchmark@v1 40 | with: 41 | tool: 'benchmarkjs' 42 | name: 'Time benchmark' 43 | pr-benchmark-file-path: pr/benchmarks.txt 44 | base-benchmark-file-path: main/benchmarks.txt 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | # trigger alert comment if 1.3 times slower 47 | alert-threshold: '130%' 48 | comment-on-alert: false 49 | # fail workdlow if 1.5 times slower 50 | fail-threshold: '150%' 51 | fail-on-alert: true 52 | file-to-annotate: ${{ github.workspace }}/test/benchmarks/time.js 53 | 54 | - name: Compare memory usage benchmark result 55 | uses: openpgpjs/github-action-pull-request-benchmark@v1 56 | with: 57 | tool: 'raw' 58 | name: 'Memory usage benchmark' 59 | pr-benchmark-file-path: pr/memory_usage.txt 60 | base-benchmark-file-path: main/memory_usage.txt 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | alert-threshold: '102%' 63 | comment-on-alert: false 64 | fail-threshold: '110%' 65 | fail-on-alert: true 66 | file-to-annotate: ${{ github.workspace }}/test/benchmarks/memory_usage.js 67 | 68 | -------------------------------------------------------------------------------- /src/crypto/random.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | // The GPG4Browsers crypto interface 19 | 20 | /** 21 | * @fileoverview Provides tools for retrieving secure randomness from browsers or Node.js 22 | * @module crypto/random 23 | */ 24 | import { byteLength, mod, uint8ArrayToBigInt } from './biginteger'; 25 | import util from '../util'; 26 | 27 | const nodeCrypto = util.getNodeCrypto(); 28 | 29 | /** 30 | * Retrieve secure random byte array of the specified length 31 | * @param {Integer} length - Length in bytes to generate 32 | * @returns {Uint8Array} Random byte array. 33 | */ 34 | export function getRandomBytes(length) { 35 | const webcrypto = typeof crypto !== 'undefined' ? crypto : nodeCrypto?.webcrypto; 36 | if (webcrypto?.getRandomValues) { 37 | const buf = new Uint8Array(length); 38 | return webcrypto.getRandomValues(buf); 39 | } else { 40 | throw new Error('No secure random number generator available.'); 41 | } 42 | } 43 | 44 | /** 45 | * Create a secure random BigInt that is greater than or equal to min and less than max. 46 | * @param {bigint} min - Lower bound, included 47 | * @param {bigint} max - Upper bound, excluded 48 | * @returns {bigint} Random BigInt. 49 | * @async 50 | */ 51 | export function getRandomBigInteger(min, max) { 52 | if (max < min) { 53 | throw new Error('Illegal parameter value: max <= min'); 54 | } 55 | 56 | const modulus = max - min; 57 | const bytes = byteLength(modulus); 58 | 59 | // Using a while loop is necessary to avoid bias introduced by the mod operation. 60 | // However, we request 64 extra random bits so that the bias is negligible. 61 | // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf 62 | const r = uint8ArrayToBigInt(getRandomBytes(bytes + 8)); 63 | return mod(r, modulus) + min; 64 | } 65 | -------------------------------------------------------------------------------- /test/unittests.js: -------------------------------------------------------------------------------- 1 | import openpgp from './initOpenpgp.js'; 2 | 3 | (typeof window !== 'undefined' ? window : global).globalThis = (typeof window !== 'undefined' ? window : global); 4 | 5 | globalThis.resolves = function(val) { 6 | return new Promise(function(res) { res(val); }); 7 | }; 8 | 9 | globalThis.rejects = function(val) { 10 | return new Promise(function(res, rej) { rej(val); }); 11 | }; 12 | 13 | globalThis.tryTests = function(name, tests, options) { 14 | if (options.if) { 15 | describe(name, function() { 16 | if (options.before) { before(options.before); } 17 | if (options.beforeEach) { beforeEach(options.beforeEach); } 18 | 19 | tests(); 20 | 21 | if (options.afterEach) { afterEach(options.afterEach); } 22 | if (options.after) { after(options.after); } 23 | }); 24 | } else { 25 | describe.skip(name + ' (no support --> skipping tests)', tests); 26 | } 27 | }; 28 | 29 | globalThis.loadStreamsPolyfill = function() { 30 | // do not polyfill Node 31 | const detectNodeWebStreams = () => typeof globalThis.process === 'object' && typeof globalThis.process.versions === 'object' && globalThis.ReadableStream; 32 | 33 | return detectNodeWebStreams() || import('web-streams-polyfill/polyfill'); 34 | }; 35 | 36 | import runWorkerTests from './worker'; 37 | import runCryptoTests from './crypto'; 38 | import runGeneralTests from './general'; 39 | import runSecurityTests from './security'; 40 | 41 | describe('Unit Tests', function () { 42 | if (typeof window !== 'undefined') { 43 | // Safari 14.1.*, 15.* and 16.* seem to have issues handling rejections when their native TransformStream implementation is involved, 44 | // so for now we ignore unhandled rejections for those browser versions. 45 | if (!window.navigator.userAgent.match(/Version\/1(4|5|6)\.\d(\.\d)* Safari/)) { 46 | window.addEventListener('unhandledrejection', function (event) { 47 | throw event.reason; 48 | }); 49 | } 50 | 51 | window.location.search.substr(1).split('&').forEach(param => { 52 | const [key, value] = param.split('='); 53 | if (key && key !== 'grep') { 54 | openpgp.config[key] = decodeURIComponent(value); 55 | try { 56 | openpgp.config[key] = window.eval(openpgp.config[key]); // eslint-disable-line no-eval 57 | } catch (e) {} 58 | } 59 | }); 60 | } else { 61 | process.on('unhandledRejection', error => { 62 | console.error(error); // eslint-disable-line no-console 63 | process.exit(1); // eslint-disable-line no-process-exit 64 | }); 65 | } 66 | 67 | runWorkerTests(); 68 | runCryptoTests(); 69 | runGeneralTests(); 70 | runSecurityTests(); 71 | }); 72 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/kem/ml_kem.js: -------------------------------------------------------------------------------- 1 | import enums from '../../../../enums'; 2 | import util from '../../../../util'; 3 | import { getRandomBytes } from '../../../random'; 4 | 5 | export async function generate(algo) { 6 | switch (algo) { 7 | case enums.publicKey.pqc_mlkem_x25519: { 8 | const mlkemSeed = getRandomBytes(64); 9 | const { mlkemSecretKey, mlkemPublicKey } = await expandSecretSeed(algo, mlkemSeed); 10 | 11 | return { mlkemSeed, mlkemSecretKey, mlkemPublicKey }; 12 | } 13 | default: 14 | throw new Error('Unsupported KEM algorithm'); 15 | } 16 | } 17 | 18 | /** 19 | * Expand ML-KEM secret seed and retrieve the secret and public key material 20 | * @param {module:enums.publicKey} algo - Public key algorithm 21 | * @param {Uint8Array} seed - secret seed to expand 22 | * @returns {Promise<{ mlkemPublicKey: Uint8Array, mlkemSecretKey: Uint8Array }>} 23 | */ 24 | export async function expandSecretSeed(algo, seed) { 25 | switch (algo) { 26 | case enums.publicKey.pqc_mlkem_x25519: { 27 | const { ml_kem768 } = await import('../noble_post_quantum'); 28 | const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed); 29 | 30 | return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey }; 31 | } 32 | default: 33 | throw new Error('Unsupported KEM algorithm'); 34 | } 35 | } 36 | 37 | export async function encaps(algo, mlkemRecipientPublicKey) { 38 | switch (algo) { 39 | case enums.publicKey.pqc_mlkem_x25519: { 40 | const { ml_kem768 } = await import('../noble_post_quantum'); 41 | const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey); 42 | 43 | return { mlkemCipherText, mlkemKeyShare }; 44 | } 45 | default: 46 | throw new Error('Unsupported KEM algorithm'); 47 | } 48 | } 49 | 50 | export async function decaps(algo, mlkemCipherText, mlkemSecretKey) { 51 | switch (algo) { 52 | case enums.publicKey.pqc_mlkem_x25519: { 53 | const { ml_kem768 } = await import('../noble_post_quantum'); 54 | const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey); 55 | 56 | return mlkemKeyShare; 57 | } 58 | default: 59 | throw new Error('Unsupported KEM algorithm'); 60 | } 61 | } 62 | 63 | export async function validateParams(algo, mlkemPublicKey, mlkemSeed) { 64 | switch (algo) { 65 | case enums.publicKey.pqc_mlkem_x25519: { 66 | const { mlkemPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mlkemSeed); 67 | return util.equalsUint8Array(mlkemPublicKey, expectedPublicKey); 68 | } 69 | default: 70 | throw new Error('Unsupported KEM algorithm'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/crypto/public_key/post_quantum/signature/signature.js: -------------------------------------------------------------------------------- 1 | import enums from '../../../../enums'; 2 | import * as mldsa from './ml_dsa'; 3 | import * as eccdsa from './ecc_dsa'; 4 | import { getHashByteLength } from '../../../hash'; 5 | 6 | export async function generate(algo) { 7 | switch (algo) { 8 | case enums.publicKey.pqc_mldsa_ed25519: { 9 | const { eccSecretKey, eccPublicKey } = await eccdsa.generate(algo); 10 | const { mldsaSeed, mldsaSecretKey, mldsaPublicKey } = await mldsa.generate(algo); 11 | return { eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }; 12 | } 13 | default: 14 | throw new Error('Unsupported signature algorithm'); 15 | } 16 | } 17 | 18 | export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, dataDigest) { 19 | switch (signatureAlgo) { 20 | case enums.publicKey.pqc_mldsa_ed25519: { 21 | const { eccSignature } = await eccdsa.sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest); 22 | const { mldsaSignature } = await mldsa.sign(signatureAlgo, mldsaSecretKey, dataDigest); 23 | 24 | return { eccSignature, mldsaSignature }; 25 | } 26 | default: 27 | throw new Error('Unsupported signature algorithm'); 28 | } 29 | } 30 | 31 | export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicKey, dataDigest, { eccSignature, mldsaSignature }) { 32 | switch (signatureAlgo) { 33 | case enums.publicKey.pqc_mldsa_ed25519: { 34 | const eccVerifiedPromise = eccdsa.verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature); 35 | const mldsaVerifiedPromise = mldsa.verify(signatureAlgo, mldsaPublicKey, dataDigest, mldsaSignature); 36 | const verified = await eccVerifiedPromise && await mldsaVerifiedPromise; 37 | return verified; 38 | } 39 | default: 40 | throw new Error('Unsupported signature algorithm'); 41 | } 42 | } 43 | 44 | export function isCompatibleHashAlgo(signatureAlgo, hashAlgo) { 45 | // The signature hash algo MUST have digest larger than 256 bits 46 | // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4 47 | switch (signatureAlgo) { 48 | case enums.publicKey.pqc_mldsa_ed25519: 49 | return getHashByteLength(hashAlgo) >= 32; 50 | default: 51 | throw new Error('Unsupported signature algorithm'); 52 | } 53 | } 54 | 55 | export async function validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed) { 56 | const eccValidationPromise = eccdsa.validateParams(algo, eccPublicKey, eccSecretKey); 57 | const mldsaValidationPromise = mldsa.validateParams(algo, mldsaPublicKey, mldsaSeed); 58 | const valid = await eccValidationPromise && await mldsaValidationPromise; 59 | return valid; 60 | } 61 | -------------------------------------------------------------------------------- /test/crypto/aes_kw.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import * as aesKW from '../../src/crypto/aes_kw.js'; 4 | import util from '../../src/util.js'; 5 | import enums from '../../src/enums.js'; 6 | 7 | export default () => describe('AES Key Wrap and Unwrap', function () { 8 | const test_vectors = [ 9 | [ 10 | '128 bits of Key Data with a 128-bit KEK', 11 | enums.symmetric.aes128, 12 | '000102030405060708090A0B0C0D0E0F', 13 | '00112233445566778899AABBCCDDEEFF', 14 | '1FA68B0A8112B447 AEF34BD8FB5A7B82 9D3E862371D2CFE5' 15 | ], 16 | [ 17 | '128 bits of Key Data with a 192-bit KEK', 18 | enums.symmetric.aes192, 19 | '000102030405060708090A0B0C0D0E0F1011121314151617', 20 | '00112233445566778899AABBCCDDEEFF', 21 | '96778B25AE6CA435 F92B5B97C050AED2 468AB8A17AD84E5D' 22 | ], 23 | [ 24 | '128 bits of Key Data with a 256-bit KEK', 25 | enums.symmetric.aes256, 26 | '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', 27 | '00112233445566778899AABBCCDDEEFF', 28 | '64E8C3F9CE0F5BA2 63E9777905818A2A 93C8191E7D6E8AE7' 29 | ], 30 | [ 31 | '192 bits of Key Data with a 192-bit KEK', 32 | enums.symmetric.aes192, 33 | '000102030405060708090A0B0C0D0E0F1011121314151617', 34 | '00112233445566778899AABBCCDDEEFF0001020304050607', 35 | '031D33264E15D332 68F24EC260743EDC E1C6C7DDEE725A93 6BA814915C6762D2' 36 | ], 37 | [ 38 | '192 bits of Key Data with a 256-bit KEK', 39 | enums.symmetric.aes256, 40 | '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', 41 | '00112233445566778899AABBCCDDEEFF0001020304050607', 42 | 'A8F9BC1612C68B3F F6E6F4FBE30E71E4 769C8B80A32CB895 8CD5D17D6B254DA1' 43 | ], 44 | [ 45 | '256 bits of Key Data with a 256-bit KEK', 46 | enums.symmetric.aes256, 47 | '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', 48 | '00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F', 49 | '28C9F404C4B810F4 CBCCB35CFB87F826 3F5786E2D80ED326 CBC7F0E71A99F43B FB988B9B7A02DD21' 50 | ] 51 | ]; 52 | 53 | test_vectors.forEach(function(test) { 54 | it(test[0], async function() { 55 | const algo = test[1]; 56 | const kek = util.hexToUint8Array(test[2]); 57 | const input = test[3].replace(/\s/g, ''); 58 | const input_bin = util.hexToUint8Array(input); 59 | const output = test[4].replace(/\s/g, ''); 60 | const output_bin = util.hexToUint8Array(output); 61 | expect(util.uint8ArrayToHex(await aesKW.wrap(algo, kek, input_bin)).toUpperCase()).to.equal(output); 62 | expect(util.uint8ArrayToHex(await aesKW.unwrap(algo, kek, output_bin)).toUpperCase()).to.equal(input); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/crypto/hash/md5.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/paulmillr/noble-hashes/blob/main/test/misc/md5.ts 2 | 3 | import { HashMD } from '@noble/hashes/_md'; 4 | import { rotl, wrapConstructor } from '@noble/hashes/utils'; 5 | 6 | // Per-round constants 7 | const K = Array.from({ length: 64 }, (_, i) => Math.floor(2 ** 32 * Math.abs(Math.sin(i + 1)))); 8 | // Choice: a ? b : c 9 | const Chi = (a: number, b: number, c: number) => (a & b) ^ (~a & c); 10 | // Initial state (same as sha1, but 4 u32 instead of 5) 11 | const IV = /* @__PURE__ */ new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]); 12 | // Temporary buffer, not used to store anything between runs 13 | // Named this way for SHA1 compat 14 | const MD5_W = /* @__PURE__ */ new Uint32Array(16); 15 | class MD5 extends HashMD { 16 | private A = IV[0] | 0; 17 | private B = IV[1] | 0; 18 | private C = IV[2] | 0; 19 | private D = IV[3] | 0; 20 | constructor() { 21 | super(64, 16, 8, true); 22 | } 23 | protected get(): [number, number, number, number] { 24 | const { A, B, C, D } = this; 25 | return [A, B, C, D]; 26 | } 27 | protected set(A: number, B: number, C: number, D: number) { 28 | this.A = A | 0; 29 | this.B = B | 0; 30 | this.C = C | 0; 31 | this.D = D | 0; 32 | } 33 | protected process(view: DataView, offset: number): void { 34 | for (let i = 0; i < 16; i++, offset += 4) MD5_W[i] = view.getUint32(offset, true); 35 | // Compression function main loop, 64 rounds 36 | let { A, B, C, D } = this; 37 | for (let i = 0; i < 64; i++) { 38 | // eslint-disable-next-line one-var, one-var-declaration-per-line 39 | let F, g, s; 40 | if (i < 16) { 41 | // eslint-disable-next-line new-cap 42 | F = Chi(B, C, D); 43 | g = i; 44 | s = [7, 12, 17, 22]; 45 | } else if (i < 32) { 46 | // eslint-disable-next-line new-cap 47 | F = Chi(D, B, C); 48 | g = (5 * i + 1) % 16; 49 | s = [5, 9, 14, 20]; 50 | } else if (i < 48) { 51 | F = B ^ C ^ D; 52 | g = (3 * i + 5) % 16; 53 | s = [4, 11, 16, 23]; 54 | } else { 55 | F = C ^ (B | ~D); 56 | g = (7 * i) % 16; 57 | s = [6, 10, 15, 21]; 58 | } 59 | F = F + A + K[i] + MD5_W[g]; 60 | A = D; 61 | D = C; 62 | C = B; 63 | B = B + rotl(F, s[i % 4]); 64 | } 65 | // Add the compressed chunk to the current hash value 66 | A = (A + this.A) | 0; 67 | B = (B + this.B) | 0; 68 | C = (C + this.C) | 0; 69 | D = (D + this.D) | 0; 70 | this.set(A, B, C, D); 71 | } 72 | protected roundClean() { 73 | MD5_W.fill(0); 74 | } 75 | destroy() { 76 | this.set(0, 0, 0, 0); 77 | this.buffer.fill(0); 78 | } 79 | } 80 | export const md5 = /* @__PURE__ */ wrapConstructor(() => new MD5()); 81 | -------------------------------------------------------------------------------- /test/security/subkey_trust.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import openpgp from '../initOpenpgp.js'; 6 | 7 | const { readKey, PublicKey, readCleartextMessage, createCleartextMessage, enums, PacketList, SignaturePacket } = openpgp; 8 | 9 | async function generateTestData() { 10 | const { privateKey: victimPrivKey } = await openpgp.generateKey({ 11 | userIDs: [{ name: 'Victim', email: 'victim@example.com' }], 12 | type: 'rsa', 13 | rsaBits: 2048, 14 | subkeys: [{ sign: true }], 15 | format: 'object' 16 | }); 17 | 18 | const { privateKey: attackerPrivKey } = await openpgp.generateKey({ 19 | userIDs: [{ name: 'Attacker', email: 'attacker@example.com' }], 20 | type: 'rsa', 21 | rsaBits: 2048, 22 | subkeys: [], 23 | format: 'object' 24 | }); 25 | 26 | const signed = await openpgp.sign({ 27 | message: await createCleartextMessage({ text: 'I am batman' }), 28 | signingKeys: victimPrivKey 29 | }); 30 | return { 31 | victimPubKey: victimPrivKey.toPublic(), 32 | attackerPrivKey, 33 | signed 34 | }; 35 | } 36 | 37 | export default () => it('Does not trust subkeys without Primary Key Binding Signature', async function() { 38 | // attacker only has his own private key, 39 | // the victim's public key and a signed message 40 | const { victimPubKey, attackerPrivKey, signed } = await generateTestData(); 41 | 42 | const pktPubVictim = victimPubKey.toPacketList(); 43 | const pktPubAttacker = attackerPrivKey.toPublic().toPacketList(); 44 | const dataToSign = { 45 | key: attackerPrivKey.toPublic().keyPacket, 46 | bind: pktPubVictim[3] // victim subkey 47 | }; 48 | const fakeBindingSignature = new SignaturePacket(); 49 | fakeBindingSignature.signatureType = enums.signature.subkeyBinding; 50 | fakeBindingSignature.publicKeyAlgorithm = attackerPrivKey.keyPacket.algorithm; 51 | fakeBindingSignature.hashAlgorithm = enums.hash.sha256; 52 | fakeBindingSignature.keyFlags = [enums.keyFlags.signData]; 53 | await fakeBindingSignature.sign(attackerPrivKey.keyPacket, dataToSign, undefined, undefined, openpgp.config); 54 | const newList = new PacketList(); 55 | newList.push( 56 | pktPubAttacker[0], // attacker private key 57 | pktPubAttacker[1], // attacker user 58 | pktPubAttacker[2], // attacker self signature 59 | pktPubVictim[3], // victim subkey 60 | fakeBindingSignature // faked key binding 61 | ); 62 | let fakeKey = new PublicKey(newList); 63 | fakeKey = await readKey({ armoredKey: await fakeKey.toPublic().armor() }); 64 | const verifyAttackerIsBatman = await openpgp.verify({ 65 | message: await readCleartextMessage({ cleartextMessage: signed }), 66 | verificationKeys: fakeKey 67 | }); 68 | // expect the signature to have the expected keyID, but be invalid due to fake key binding signature in the subkey 69 | expect(verifyAttackerIsBatman.signatures[0].keyID.equals(victimPubKey.subkeys[0].getKeyID())).to.be.true; 70 | await expect(verifyAttackerIsBatman.signatures[0].verified).to.be.rejectedWith(/Could not find valid signing key packet/); 71 | }); 72 | -------------------------------------------------------------------------------- /test/worker/worker_example.js: -------------------------------------------------------------------------------- 1 | /* globals openpgp */ 2 | 3 | importScripts('../../dist/openpgp.js'); 4 | 5 | const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- 6 | Version: OpenPGP.js v4.7.2 7 | Comment: https://openpgpjs.org 8 | 9 | xjMEXh8OhhYJKwYBBAHaRw8BAQdAgfwsqplEv19tUU/CoJOsGiWhssumaO5p 10 | aFHmyl5hHpbNIURhbmllbCA8ZC5odWlnZW5zQHByb3Rvbm1haWwuY29tPsJ4 11 | BBAWCgAgBQJeHw6GBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQ33Rm 12 | ygBzJWpArgEA7EG2cf40B92+ohh5+r6G/YBzwgy0JxhdYeI6VeTLjwABAIMO 13 | 45Nn00opO7gI7nqu0VHkWWDREKH3zHcVkitrpXcNzjgEXh8OhhIKKwYBBAGX 14 | VQEFAQEHQK8Z7Zeg4qap2g8+axIMWaHmn+dbsBjMjssfRlkZRx1oAwEIB8Jh 15 | BBgWCAAJBQJeHw6GAhsMAAoJEN90ZsoAcyVqjigA/0q+C3cX2cVRFOWq1xKt 16 | aKsRWgxXiPCDD1SP6nqS9dIiAP9bl5iix1Wo1eTSV1f+nqGmTkFaZbnvcfZy 17 | Q9eY5AnnBg== 18 | =jTkZ 19 | -----END PGP PUBLIC KEY BLOCK----- 20 | `; 21 | 22 | const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- 23 | Version: OpenPGP.js v4.7.2 24 | Comment: https://openpgpjs.org 25 | 26 | xYYEXh8OhhYJKwYBBAHaRw8BAQdAgfwsqplEv19tUU/CoJOsGiWhssumaO5p 27 | aFHmyl5hHpb+CQMIQGjCBRmnRL1g1x10ygKW8a29+2V7pGbRFShEi+92Y6Xa 28 | js3SmduC5K9T2Jnn3Mn1esoCA0gliMpakkWZm3B65J2axI8qo8RTGRnRP1Yp 29 | w80hRGFuaWVsIDxkLmh1aWdlbnNAcHJvdG9ubWFpbC5jb20+wngEEBYKACAF 30 | Al4fDoYGCwkHCAMCBBUICgIEFgIBAAIZAQIbAwIeAQAKCRDfdGbKAHMlakCu 31 | AQDsQbZx/jQH3b6iGHn6vob9gHPCDLQnGF1h4jpV5MuPAAEAgw7jk2fTSik7 32 | uAjueq7RUeRZYNEQoffMdxWSK2uldw3HiwReHw6GEgorBgEEAZdVAQUBAQdA 33 | rxntl6DipqnaDz5rEgxZoeaf51uwGMyOyx9GWRlHHWgDAQgH/gkDCNKsGYh3 34 | rcY5YKc2PzxhFexONEmwJ6Cq3KJ+nW9RbRDYb78aitaacLmWfuxNYu12OhKr 35 | DLwUsgyr8vXKg6yZcmNnpi0P1VYElfb4ECZABq/CYQQYFggACQUCXh8OhgIb 36 | DAAKCRDfdGbKAHMlao4oAP9Kvgt3F9nFURTlqtcSrWirEVoMV4jwgw9Uj+p6 37 | kvXSIgD/W5eYosdVqNXk0ldX/p6hpk5BWmW573H2ckPXmOQJ5wY= 38 | =lOCw 39 | -----END PGP PRIVATE KEY BLOCK----- 40 | `; 41 | 42 | onmessage = async function({ data: { action, message }, ports: [port] }) { 43 | try { 44 | let result; 45 | switch (action) { 46 | case 'encrypt': { 47 | const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); 48 | const privateKey = await openpgp.decryptKey({ 49 | privateKey: await openpgp.readKey({ armoredKey: privateKeyArmored }), 50 | passphrase: 'test' 51 | }); 52 | const data = await openpgp.encrypt({ 53 | message: await openpgp.createMessage({ text: message }), 54 | encryptionKeys: publicKey, 55 | signingKeys: privateKey 56 | }); 57 | result = data; 58 | break; 59 | } 60 | case 'decrypt': { 61 | const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); 62 | const privateKey = await openpgp.decryptKey({ 63 | privateKey: await openpgp.readKey({ armoredKey: privateKeyArmored }), 64 | passphrase: 'test' 65 | }); 66 | const { data, signatures } = await openpgp.decrypt({ 67 | message: await openpgp.readMessage({ armoredMessage: message }), 68 | verificationKeys: publicKey, 69 | decryptionKeys: privateKey 70 | }); 71 | await signatures[0].verified; 72 | result = data; 73 | break; 74 | } 75 | } 76 | port.postMessage({ result }); 77 | } catch (e) { 78 | console.error(e); // eslint-disable-line no-console 79 | port.postMessage({ error: e.message }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/type/keyid.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * @module type/keyid 20 | */ 21 | 22 | import util from '../util'; 23 | 24 | /** 25 | * Implementation of type key id 26 | * 27 | * {@link https://tools.ietf.org/html/rfc4880#section-3.3|RFC4880 3.3}: 28 | * A Key ID is an eight-octet scalar that identifies a key. 29 | * Implementations SHOULD NOT assume that Key IDs are unique. The 30 | * section "Enhanced Key Formats" below describes how Key IDs are 31 | * formed. 32 | */ 33 | class KeyID { 34 | constructor() { 35 | this.bytes = ''; 36 | } 37 | 38 | /** 39 | * Parsing method for a key id 40 | * @param {Uint8Array} bytes - Input to read the key id from 41 | */ 42 | read(bytes) { 43 | this.bytes = util.uint8ArrayToString(bytes.subarray(0, 8)); 44 | return this.bytes.length; 45 | } 46 | 47 | /** 48 | * Serializes the Key ID 49 | * @returns {Uint8Array} Key ID as a Uint8Array. 50 | */ 51 | write() { 52 | return util.stringToUint8Array(this.bytes); 53 | } 54 | 55 | /** 56 | * Returns the Key ID represented as a hexadecimal string 57 | * @returns {String} Key ID as a hexadecimal string. 58 | */ 59 | toHex() { 60 | return util.uint8ArrayToHex(util.stringToUint8Array(this.bytes)); 61 | } 62 | 63 | /** 64 | * Checks equality of Key ID's 65 | * @param {KeyID} keyID 66 | * @param {Boolean} matchWildcard - Indicates whether to check if either keyID is a wildcard 67 | */ 68 | equals(keyID, matchWildcard = false) { 69 | return (matchWildcard && (keyID.isWildcard() || this.isWildcard())) || this.bytes === keyID.bytes; 70 | } 71 | 72 | /** 73 | * Checks to see if the Key ID is unset 74 | * @returns {Boolean} True if the Key ID is null. 75 | */ 76 | isNull() { 77 | return this.bytes === ''; 78 | } 79 | 80 | /** 81 | * Checks to see if the Key ID is a "wildcard" Key ID (all zeros) 82 | * @returns {Boolean} True if this is a wildcard Key ID. 83 | */ 84 | isWildcard() { 85 | return /^0+$/.test(this.toHex()); 86 | } 87 | 88 | static mapToHex(keyID) { 89 | return keyID.toHex(); 90 | } 91 | 92 | static fromID(hex) { 93 | const keyID = new KeyID(); 94 | keyID.read(util.hexToUint8Array(hex)); 95 | return keyID; 96 | } 97 | 98 | static wildcard() { 99 | const keyID = new KeyID(); 100 | keyID.read(new Uint8Array(8)); 101 | return keyID; 102 | } 103 | } 104 | 105 | export default KeyID; 106 | -------------------------------------------------------------------------------- /test/crypto/cipher/twofish.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import TF from '../../../src/crypto/cipher/twofish.js'; 4 | import util from '../../../src/util.js'; 5 | 6 | export default () => it('Twofish with test vectors from https://www.schneier.com/code/ecb_ival.txt', function(done) { 7 | function tfencrypt(block, key) { 8 | const tf = new TF(util.stringToUint8Array(key)); 9 | 10 | return tf.encrypt(block); 11 | } 12 | 13 | const start = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; 14 | const start_short = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; 15 | const testvectors = [[0x57,0xFF,0x73,0x9D,0x4D,0xC9,0x2C,0x1B,0xD7,0xFC,0x01,0x70,0x0C,0xC8,0x21,0x6F], 16 | [0xD4,0x3B,0xB7,0x55,0x6E,0xA3,0x2E,0x46,0xF2,0xA2,0x82,0xB7,0xD4,0x5B,0x4E,0x0D], 17 | [0x90,0xAF,0xE9,0x1B,0xB2,0x88,0x54,0x4F,0x2C,0x32,0xDC,0x23,0x9B,0x26,0x35,0xE6], 18 | [0x6C,0xB4,0x56,0x1C,0x40,0xBF,0x0A,0x97,0x05,0x93,0x1C,0xB6,0xD4,0x08,0xE7,0xFA], 19 | [0x30,0x59,0xD6,0xD6,0x17,0x53,0xB9,0x58,0xD9,0x2F,0x47,0x81,0xC8,0x64,0x0E,0x58], 20 | [0xE6,0x94,0x65,0x77,0x05,0x05,0xD7,0xF8,0x0E,0xF6,0x8C,0xA3,0x8A,0xB3,0xA3,0xD6], 21 | [0x5A,0xB6,0x7A,0x5F,0x85,0x39,0xA4,0xA5,0xFD,0x9F,0x03,0x73,0xBA,0x46,0x34,0x66], 22 | [0xDC,0x09,0x6B,0xCD,0x99,0xFC,0x72,0xF7,0x99,0x36,0xD4,0xC7,0x48,0xE7,0x5A,0xF7], 23 | [0xC5,0xA3,0xE7,0xCE,0xE0,0xF1,0xB7,0x26,0x05,0x28,0xA6,0x8F,0xB4,0xEA,0x05,0xF2], 24 | [0x43,0xD5,0xCE,0xC3,0x27,0xB2,0x4A,0xB9,0x0A,0xD3,0x4A,0x79,0xD0,0x46,0x91,0x51]]; 25 | testvectors[47] = [0x43,0x10,0x58,0xF4,0xDB,0xC7,0xF7,0x34,0xDA,0x4F,0x02,0xF0,0x4C,0xC4,0xF4,0x59]; 26 | testvectors[48] = [0x37,0xFE,0x26,0xFF,0x1C,0xF6,0x61,0x75,0xF5,0xDD,0xF4,0xC3,0x3B,0x97,0xA2,0x05]; 27 | 28 | for (let i = 0; i < 49; i++) { 29 | let res; 30 | let exp; 31 | let blk; 32 | let key; 33 | let ct; 34 | 35 | if (i === 0) { 36 | blk = start_short; 37 | key = util.uint8ArrayToString(start); 38 | ct = testvectors[0]; 39 | res = util.uint8ArrayToString(tfencrypt(blk,key)); 40 | exp = util.uint8ArrayToString(ct); 41 | } else if (i === 1) { 42 | blk = testvectors[0]; 43 | key = util.uint8ArrayToString(start); 44 | ct = testvectors[1]; 45 | res = util.uint8ArrayToString(tfencrypt(blk,key)); 46 | exp = util.uint8ArrayToString(ct); 47 | } else if (i === 2) { 48 | blk = testvectors[i - 1]; 49 | key = util.uint8ArrayToString(testvectors[i - 2].concat(start_short)); 50 | ct = testvectors[i]; 51 | res = util.uint8ArrayToString(tfencrypt(blk,key)); 52 | exp = util.uint8ArrayToString(ct); 53 | } else if (i < 10 || i > 46) { 54 | blk = testvectors[i - 1]; 55 | key = util.uint8ArrayToString(testvectors[i - 2].concat(testvectors[i - 3])); 56 | ct = testvectors[i]; 57 | res = util.uint8ArrayToString(tfencrypt(blk,key)); 58 | exp = util.uint8ArrayToString(ct); 59 | } else { 60 | testvectors[i] = tfencrypt(testvectors[i - 1],util.uint8ArrayToString(testvectors[i - 2].concat(testvectors[i - 3]))); 61 | continue; 62 | } 63 | expect(res, 'vector with block ' + util.uint8ArrayToHex(blk) + 64 | ' with key ' + util.uint8ArrayToHex(util.stringToUint8Array(key)) + 65 | ' should be ' + util.uint8ArrayToHex(ct) + 66 | ' but is ' + util.uint8ArrayToHex(tfencrypt(blk,key))).to.equal(exp); 67 | } 68 | done(); 69 | }); 70 | -------------------------------------------------------------------------------- /src/packet/user_attribute.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import { readSimpleLength, writeSimpleLength } from './packet'; 19 | import enums from '../enums'; 20 | import util from '../util'; 21 | 22 | /** 23 | * Implementation of the User Attribute Packet (Tag 17) 24 | * 25 | * The User Attribute packet is a variation of the User ID packet. It 26 | * is capable of storing more types of data than the User ID packet, 27 | * which is limited to text. Like the User ID packet, a User Attribute 28 | * packet may be certified by the key owner ("self-signed") or any other 29 | * key owner who cares to certify it. Except as noted, a User Attribute 30 | * packet may be used anywhere that a User ID packet may be used. 31 | * 32 | * While User Attribute packets are not a required part of the OpenPGP 33 | * standard, implementations SHOULD provide at least enough 34 | * compatibility to properly handle a certification signature on the 35 | * User Attribute packet. A simple way to do this is by treating the 36 | * User Attribute packet as a User ID packet with opaque contents, but 37 | * an implementation may use any method desired. 38 | */ 39 | class UserAttributePacket { 40 | static get tag() { 41 | return enums.packet.userAttribute; 42 | } 43 | 44 | constructor() { 45 | this.attributes = []; 46 | } 47 | 48 | /** 49 | * parsing function for a user attribute packet (tag 17). 50 | * @param {Uint8Array} input - Payload of a tag 17 packet 51 | */ 52 | read(bytes) { 53 | let i = 0; 54 | while (i < bytes.length) { 55 | const len = readSimpleLength(bytes.subarray(i, bytes.length)); 56 | i += len.offset; 57 | 58 | this.attributes.push(util.uint8ArrayToString(bytes.subarray(i, i + len.len))); 59 | i += len.len; 60 | } 61 | } 62 | 63 | /** 64 | * Creates a binary representation of the user attribute packet 65 | * @returns {Uint8Array} String representation. 66 | */ 67 | write() { 68 | const arr = []; 69 | for (let i = 0; i < this.attributes.length; i++) { 70 | arr.push(writeSimpleLength(this.attributes[i].length)); 71 | arr.push(util.stringToUint8Array(this.attributes[i])); 72 | } 73 | return util.concatUint8Array(arr); 74 | } 75 | 76 | /** 77 | * Compare for equality 78 | * @param {UserAttributePacket} usrAttr 79 | * @returns {Boolean} True if equal. 80 | */ 81 | equals(usrAttr) { 82 | if (!usrAttr || !(usrAttr instanceof UserAttributePacket)) { 83 | return false; 84 | } 85 | return this.attributes.every(function(attr, index) { 86 | return attr === usrAttr.attributes[index]; 87 | }); 88 | } 89 | } 90 | 91 | export default UserAttributePacket; 92 | -------------------------------------------------------------------------------- /.github/workflows/sop-test-suite.yml: -------------------------------------------------------------------------------- 1 | name: SOP interoperability test suite 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | test-suite: 10 | name: Run interoperability test suite 11 | runs-on: ubuntu-latest 12 | container: 13 | image: ghcr.io/protonmail/openpgp-interop-test-docker:v1.1.12 14 | credentials: 15 | username: ${{ github.actor }} 16 | password: ${{ secrets.github_token }} 17 | steps: 18 | # check out repo for scripts 19 | - uses: actions/checkout@v4 20 | # check out pull request branch 21 | - name: Checkout openpgpjs-branch 22 | uses: actions/checkout@v4 23 | with: 24 | path: openpgpjs-branch 25 | - name: Install openpgpjs-branch 26 | run: cd openpgpjs-branch && npm install 27 | - name: Print openpgpjs-branch version 28 | run: $SOP_OPENPGPJS_V2 version --extended 29 | env: 30 | OPENPGPJS_PATH: ${{ github.workspace }}/openpgpjs-branch 31 | # check out main branch 32 | - name: Checkout openpgpjs-main 33 | uses: actions/checkout@v4 34 | with: 35 | ref: main 36 | path: openpgpjs-main 37 | - name: Install openpgpjs-main 38 | run: cd openpgpjs-main && npm install 39 | - name: Print openpgpjs-main version 40 | run: $SOP_OPENPGPJS_V2 version --extended 41 | env: 42 | OPENPGPJS_PATH: ${{ github.workspace }}/openpgpjs-main 43 | # Run test suite 44 | - name: Prepare test configuration 45 | run: ./.github/test-suite/prepare_config.sh $CONFIG_TEMPLATE $CONFIG_OUTPUT $GITHUB_WORKSPACE/openpgpjs-branch $GITHUB_WORKSPACE/openpgpjs-main 46 | env: 47 | CONFIG_TEMPLATE: .github/test-suite/config.json.template 48 | CONFIG_OUTPUT: .github/test-suite/config.json 49 | - name: Display configuration 50 | run: cat .github/test-suite/config.json 51 | - name: Run interoperability test suite 52 | run: cd $TEST_SUITE_DIR && $TEST_SUITE --config $GITHUB_WORKSPACE/$CONFIG --json-out $GITHUB_WORKSPACE/$RESULTS_JSON --html-out $GITHUB_WORKSPACE/$RESULTS_HTML 53 | env: 54 | CONFIG: .github/test-suite/config.json 55 | RESULTS_JSON: .github/test-suite/test-suite-results.json 56 | RESULTS_HTML: .github/test-suite/test-suite-results.html 57 | # Upload results 58 | - name: Upload test results json artifact 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: test-suite-results.json 62 | path: .github/test-suite/test-suite-results.json 63 | - name: Upload test results html artifact 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: test-suite-results.html 67 | path: .github/test-suite/test-suite-results.html 68 | 69 | compare-with-main: 70 | name: Compare with main 71 | runs-on: ubuntu-latest 72 | needs: test-suite 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v4 76 | - name: Download test results json artifact 77 | id: download-test-results 78 | uses: actions/download-artifact@v4 79 | with: 80 | name: test-suite-results.json 81 | - name: Compare with baseline 82 | uses: ProtonMail/openpgp-interop-test-analyzer@v2 83 | with: 84 | results: ${{ steps.download-test-results.outputs.download-path }}/test-suite-results.json 85 | output: baseline-comparison.json 86 | baseline: sop-openpgpjs-main 87 | target: sop-openpgpjs-branch 88 | -------------------------------------------------------------------------------- /test/crypto/gcm.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { use as chaiUse, expect } from 'chai'; 3 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 4 | chaiUse(chaiAsPromised); 5 | 6 | import openpgp from '../initOpenpgp.js'; 7 | import * as crypto from '../../src/crypto'; 8 | import util from '../../src/util.js'; 9 | 10 | export default () => describe('Symmetric AES-GCM (experimental)', function() { 11 | let sinonSandbox; 12 | let getWebCryptoStub; 13 | let getNodeCryptoStub; 14 | 15 | beforeEach(function () { 16 | sinonSandbox = sinon.createSandbox(); 17 | enableNative(); 18 | }); 19 | 20 | afterEach(function () { 21 | sinonSandbox.restore(); 22 | }); 23 | 24 | const disableNative = () => { 25 | enableNative(); 26 | // stubbed functions return undefined 27 | getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto'); 28 | getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); 29 | }; 30 | const enableNative = () => { 31 | getWebCryptoStub && getWebCryptoStub.restore(); 32 | getNodeCryptoStub && getNodeCryptoStub.restore(); 33 | }; 34 | 35 | function testAESGCM(plaintext, nativeEncrypt, nativeDecrypt) { 36 | const aesAlgoNames = Object.keys(openpgp.enums.symmetric).filter( 37 | algoName => algoName.substr(0,3) === 'aes' 38 | ); 39 | aesAlgoNames.forEach(function(algoName) { 40 | it(algoName, async function() { 41 | const nodeCrypto = util.getNodeCrypto(); 42 | const webCrypto = util.getWebCrypto(); 43 | if (!nodeCrypto && !webCrypto) { 44 | this.skip(); // eslint-disable-line no-invalid-this 45 | } 46 | const algo = openpgp.enums.write(openpgp.enums.symmetric, algoName); 47 | const key = crypto.generateSessionKey(algo); 48 | const gcmMode = crypto.cipherMode.getAEADMode(openpgp.enums.aead.gcm); 49 | const iv = crypto.getRandomBytes(gcmMode.ivLength); 50 | 51 | const nativeEncryptSpy = nodeCrypto ? sinonSandbox.spy(nodeCrypto, 'createCipheriv') : sinonSandbox.spy(webCrypto, 'encrypt'); 52 | const nativeDecryptSpy = nodeCrypto ? sinonSandbox.spy(nodeCrypto, 'createDecipheriv') : sinonSandbox.spy(webCrypto, 'decrypt'); 53 | 54 | nativeEncrypt || disableNative(); 55 | let modeInstance = await gcmMode(algo, key); 56 | const ciphertext = await modeInstance.encrypt(util.stringToUint8Array(plaintext), iv); 57 | enableNative(); 58 | 59 | nativeDecrypt || disableNative(); 60 | modeInstance = await gcmMode(algo, key); 61 | const decrypted = await modeInstance.decrypt(util.stringToUint8Array(util.uint8ArrayToString(ciphertext)), iv); 62 | enableNative(); 63 | 64 | const decryptedStr = util.uint8ArrayToString(decrypted); 65 | expect(decryptedStr).to.equal(plaintext); 66 | 67 | if (algo !== openpgp.enums.symmetric.aes192) { // not implemented by webcrypto 68 | // sanity check: native crypto was indeed on/off 69 | expect(nativeEncryptSpy.called).to.equal(nativeEncrypt); 70 | expect(nativeDecryptSpy.called).to.equal(nativeDecrypt); 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | describe('Symmetric AES-GCM (native)', function() { 77 | testAESGCM('12345678901234567890123456789012345678901234567890', true, true); 78 | }); 79 | 80 | describe('Symmetric AES-GCM (non-native)', function() { 81 | testAESGCM('12345678901234567890123456789012345678901234567890', false, false); 82 | }); 83 | 84 | describe('Symmetric AES-GCM (native encrypt, non-native decrypt)', function() { 85 | testAESGCM('12345678901234567890123456789012345678901234567890', true, false); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/crypto/cmac.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This module implements AES-CMAC on top of 3 | * native AES-CBC using either the WebCrypto API or Node.js' crypto API. 4 | * @module crypto/cmac 5 | */ 6 | 7 | import { cbc as nobleAesCbc } from '@noble/ciphers/aes'; 8 | import util from '../util'; 9 | 10 | const webCrypto = util.getWebCrypto(); 11 | const nodeCrypto = util.getNodeCrypto(); 12 | 13 | 14 | /** 15 | * This implementation of CMAC is based on the description of OMAC in 16 | * http://web.cs.ucdavis.edu/~rogaway/papers/eax.pdf. As per that 17 | * document: 18 | * 19 | * We have made a small modification to the OMAC algorithm as it was 20 | * originally presented, changing one of its two constants. 21 | * Specifically, the constant 4 at line 85 was the constant 1/2 (the 22 | * multiplicative inverse of 2) in the original definition of OMAC [14]. 23 | * The OMAC authors indicate that they will promulgate this modification 24 | * [15], which slightly simplifies implementations. 25 | */ 26 | 27 | const blockLength = 16; 28 | 29 | 30 | /** 31 | * xor `padding` into the end of `data`. This function implements "the 32 | * operation xor→ [which] xors the shorter string into the end of longer 33 | * one". Since data is always as least as long as padding, we can 34 | * simplify the implementation. 35 | * @param {Uint8Array} data 36 | * @param {Uint8Array} padding 37 | */ 38 | function rightXORMut(data, padding) { 39 | const offset = data.length - blockLength; 40 | for (let i = 0; i < blockLength; i++) { 41 | data[i + offset] ^= padding[i]; 42 | } 43 | return data; 44 | } 45 | 46 | function pad(data, padding, padding2) { 47 | // if |M| in {n, 2n, 3n, ...} 48 | if (data.length && data.length % blockLength === 0) { 49 | // then return M xor→ B, 50 | return rightXORMut(data, padding); 51 | } 52 | // else return (M || 10^(n−1−(|M| mod n))) xor→ P 53 | const padded = new Uint8Array(data.length + (blockLength - (data.length % blockLength))); 54 | padded.set(data); 55 | padded[data.length] = 0b10000000; 56 | return rightXORMut(padded, padding2); 57 | } 58 | 59 | const zeroBlock = new Uint8Array(blockLength); 60 | 61 | export default async function CMAC(key) { 62 | const cbc = await CBC(key); 63 | 64 | // L ← E_K(0^n); B ← 2L; P ← 4L 65 | const padding = util.double(await cbc(zeroBlock)); 66 | const padding2 = util.double(padding); 67 | 68 | return async function(data) { 69 | // return CBC_K(pad(M; B, P)) 70 | return (await cbc(pad(data, padding, padding2))).subarray(-blockLength); 71 | }; 72 | } 73 | 74 | async function CBC(key) { 75 | if (util.getNodeCrypto()) { // Node crypto library 76 | return async function(pt) { 77 | const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock); 78 | const ct = en.update(pt); 79 | return new Uint8Array(ct); 80 | }; 81 | } 82 | 83 | if (util.getWebCrypto()) { 84 | try { 85 | key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']); 86 | return async function(pt) { 87 | const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt); 88 | return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength); 89 | }; 90 | } catch (err) { 91 | // no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support 92 | if (err.name !== 'NotSupportedError' && 93 | !(key.length === 24 && err.name === 'OperationError')) { 94 | throw err; 95 | } 96 | util.printDebugError('Browser did not support operation: ' + err.message); 97 | } 98 | } 99 | 100 | return async function(pt) { 101 | return nobleAesCbc(key, zeroBlock, { disablePadding: true }).encrypt(pt); 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/crypto/public_key/elliptic/eddsa_legacy.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2018 Proton Technologies AG 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * @fileoverview Implementation of legacy EdDSA following RFC4880bis-03 for OpenPGP. 20 | * This key type has been deprecated by the crypto-refresh RFC. 21 | * @module crypto/public_key/elliptic/eddsa_legacy 22 | */ 23 | 24 | import util from '../../../util'; 25 | import enums from '../../../enums'; 26 | import { CurveWithOID, checkPublicPointEnconding } from './oid_curves'; 27 | import { sign as eddsaSign, verify as eddsaVerify, validateParams as eddsaValidateParams } from './eddsa'; 28 | 29 | /** 30 | * Sign a message using the provided legacy EdDSA key 31 | * @param {module:type/oid} oid - Elliptic curve object identifier 32 | * @param {module:enums.hash} hashAlgo - Hash algorithm used to sign (must be sha256 or stronger) 33 | * @param {Uint8Array} message - Message to sign 34 | * @param {Uint8Array} publicKey - Public key 35 | * @param {Uint8Array} privateKey - Private key used to sign the message 36 | * @param {Uint8Array} hashed - The hashed message 37 | * @returns {Promise<{ 38 | * r: Uint8Array, 39 | * s: Uint8Array 40 | * }>} Signature of the message 41 | * @async 42 | */ 43 | export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed) { 44 | const curve = new CurveWithOID(oid); 45 | checkPublicPointEnconding(curve, publicKey); 46 | const { RS: signature } = await eddsaSign(enums.publicKey.ed25519, hashAlgo, message, publicKey.subarray(1), privateKey, hashed); 47 | // EdDSA signature params are returned in little-endian format 48 | return { 49 | r: signature.subarray(0, 32), 50 | s: signature.subarray(32) 51 | }; 52 | } 53 | 54 | /** 55 | * Verifies if a legacy EdDSA signature is valid for a message 56 | * @param {module:type/oid} oid - Elliptic curve object identifier 57 | * @param {module:enums.hash} hashAlgo - Hash algorithm used in the signature 58 | * @param {{r: Uint8Array, 59 | s: Uint8Array}} signature Signature to verify the message 60 | * @param {Uint8Array} m - Message to verify 61 | * @param {Uint8Array} publicKey - Public key used to verify the message 62 | * @param {Uint8Array} hashed - The hashed message 63 | * @returns {Boolean} 64 | * @async 65 | */ 66 | export async function verify(oid, hashAlgo, { r, s }, m, publicKey, hashed) { 67 | const curve = new CurveWithOID(oid); 68 | checkPublicPointEnconding(curve, publicKey); 69 | const RS = util.concatUint8Array([r, s]); 70 | return eddsaVerify(enums.publicKey.ed25519, hashAlgo, { RS }, m, publicKey.subarray(1), hashed); 71 | } 72 | /** 73 | * Validate legacy EdDSA parameters 74 | * @param {module:type/oid} oid - Elliptic curve object identifier 75 | * @param {Uint8Array} Q - EdDSA public point 76 | * @param {Uint8Array} k - EdDSA secret seed 77 | * @returns {Promise} Whether params are valid. 78 | * @async 79 | */ 80 | export async function validateParams(oid, Q, k) { 81 | // Check whether the given curve is supported 82 | if (oid.getName() !== enums.curve.ed25519Legacy) { 83 | return false; 84 | } 85 | 86 | // First byte is relevant for encoding purposes only 87 | if (Q.length < 1 || Q[0] !== 0x40) { 88 | return false; 89 | } 90 | return eddsaValidateParams(enums.publicKey.ed25519, Q.subarray(1), k); 91 | } 92 | -------------------------------------------------------------------------------- /src/type/kdf_params.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import { UnsupportedError } from '../packet/packet'; 19 | 20 | /** 21 | * Implementation of type KDF parameters 22 | * 23 | * {@link https://tools.ietf.org/html/rfc6637#section-7|RFC 6637 7}: 24 | * A key derivation function (KDF) is necessary to implement the EC 25 | * encryption. The Concatenation Key Derivation Function (Approved 26 | * Alternative 1) [NIST-SP800-56A] with the KDF hash function that is 27 | * SHA2-256 [FIPS-180-3] or stronger is REQUIRED. 28 | * @module type/kdf_params 29 | */ 30 | import util from '../util'; 31 | 32 | const VERSION_FORWARDING = 0xFF; 33 | 34 | class KDFParams { 35 | /** 36 | * @param {Integer} version Version, defaults to 1 37 | * @param {enums.hash} hash Hash algorithm 38 | * @param {enums.symmetric} cipher Symmetric algorithm 39 | * @param {Uint8Array} replacementFingerprint (forwarding only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) 40 | */ 41 | constructor(data) { 42 | if (data) { 43 | const { version, hash, cipher, replacementFingerprint } = data; 44 | this.version = version || 1; 45 | this.hash = hash; 46 | this.cipher = cipher; 47 | 48 | this.replacementFingerprint = replacementFingerprint; 49 | } else { 50 | this.version = null; 51 | this.hash = null; 52 | this.cipher = null; 53 | this.replacementFingerprint = null; 54 | } 55 | } 56 | 57 | /** 58 | * Read KDFParams from an Uint8Array 59 | * @param {Uint8Array} input - Where to read the KDFParams from 60 | * @returns {Number} Number of read bytes. 61 | */ 62 | read(input) { 63 | if (input.length < 4 || (input[1] !== 1 && input[1] !== VERSION_FORWARDING)) { 64 | throw new UnsupportedError('Cannot read KDFParams'); 65 | } 66 | const totalBytes = input[0]; 67 | this.version = input[1]; 68 | this.hash = input[2]; 69 | this.cipher = input[3]; 70 | let readBytes = 4; 71 | 72 | if (this.version === VERSION_FORWARDING) { 73 | const fingerprintLength = totalBytes - readBytes + 1; // acount for length byte 74 | this.replacementFingerprint = input.slice(readBytes, readBytes + fingerprintLength); 75 | readBytes += fingerprintLength; 76 | } 77 | return readBytes; 78 | } 79 | 80 | /** 81 | * Write KDFParams to an Uint8Array 82 | * @param {Boolean} [forReplacementParams] - forwarding only: whether to serialize data to use for replacement params 83 | * @returns {Uint8Array} Array with the KDFParams value 84 | */ 85 | write(forReplacementParams) { 86 | if (!this.version || this.version === 1 || forReplacementParams) { 87 | return new Uint8Array([3, 1, this.hash, this.cipher]); 88 | } 89 | 90 | const forwardingFields = util.concatUint8Array([ 91 | new Uint8Array([ 92 | 3 + this.replacementFingerprint.length, 93 | this.version, 94 | this.hash, 95 | this.cipher 96 | ]), 97 | this.replacementFingerprint 98 | ]); 99 | 100 | return forwardingFields; 101 | } 102 | } 103 | 104 | export default KDFParams; 105 | -------------------------------------------------------------------------------- /test/security/unsigned_subpackets.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import openpgp from '../initOpenpgp.js'; 6 | 7 | const { readKey, PrivateKey, createMessage, enums, PacketList, SignaturePacket } = openpgp; 8 | 9 | /* 10 | * This key is long expired and cannot be used for encryption. 11 | */ 12 | const INVALID_KEY = ` 13 | -----BEGIN PGP PRIVATE KEY BLOCK----- 14 | Version: OpenPGP.js VERSION 15 | Comment: https://openpgpjs.org 16 | 17 | xcMGBDhtQ4ABB/9uAfnjiE8HLfFrk4AzYIoxISvIbqDlItn3Mk2RK4iGTaAL 18 | h+hN8BrqOopgdHj5c3pTo6VDvJLieHwymdZ3d296L55zt2ichhVIgRxh20Tv 19 | j0dYLKGIEWDMBKvQNoDi83eGrIeHGNjRDOipr/PD251LzwaeiNVyw8ce2Fpd 20 | 1ORbC2MJU57C2appZqeMJsWPCnsHNkhxPyRGdp+vifgizi/lt2DcQ6C6EiJx 21 | HV0jFDPJnb69LxKLUelRH+l/b2ZHTONu2pZwUXcFpjA5yTrSzO/kaUtGu/Cz 22 | 3euQ3scEtvMXgO2R9H7halxYwyXL/PPLmgaUt1RNXGC7BZjkUW4n8qd/ABEB 23 | AAH+CQMITYNkFGQHMiJgt2s69CHTfwUUZg1Yfcq8alY7GpqeH4CayWCMPI+v 24 | l7kIJdl2b9N/xGnpaUMmaXJts6AtlIBLwzxg0syIfgRv4/wfrVeruJ9TfCFC 25 | NbKP3lk3FZCGF0I4T1FSNvyPJ//ee1cX7U/gM7A2g5xyBFnH5d8LTUDlQjXb 26 | a+BwYN2TZaFrvlWwMIU+NQa+EOiyAwXsgyQbVn2d7JsUUs/lyEG2xkWNTeqe 27 | FWKJJvyDwixsxd7oobBSM6Krt2TreuelPBFQxKyaYyv81gASga9wxyfbIuTG 28 | 7wAKW9i4pFMgrrIABcnNKOyeAgMDcAYXAW3eNbYDCIDL9/AuOUotPR+0pEun 29 | WssAZGBM78ZlJZ1Qnbg9nT0rn4pHrFQHnBxlWyPEqj1mZ0Svc0vXHVH+8JgN 30 | pwOGxo7DiF5lL/bphdFVMF2e+UPoc1efO4cpW+ZH/BOug14dJROfkrPhrUTp 31 | nYu6VF9N723YVT9PDTg79E4kIzjMDvhV1odHSaxfl4VtgueYv+Bt3n2nXdME 32 | XZVBXbp7jO7pTS5HsOBcModos8ZYS5RcaHPJ6H8807hFyva4GThZ744ryV8b 33 | XnROoC+d/xR4ShA4f/f9QszMXZ+Xlh4IU3Ccz5PF5UiZ/nC5ho5KzJphBB53 34 | c78gjRIXeUK1Rgj2AquF3KDOjCm60oazKzXv8316ZODNJr+HVvGSKeq85z9Z 35 | z/BfXUtn+PrmzHxegusZfFCpB6YAJCILsHgJ2gT8v26QF+1CJ3ngHVnSkghR 36 | z64zJexeqA8ChTZnhPbHVhh5qx2hlNTofBV29LJGa/EpMykO5pZiuaSEkmZx 37 | RpU+iKNYKa3U516O8f9yj+UZ5/t2SJRpT+9fro3RB4lUnt/RdkY8q2R+3owo 38 | xr4sYaInfvrs3eCsmh5UtygUVARKrK84zR1UZXN0aSBUZXN0IDx0ZXN0QGV4 39 | YW1wbGUuY29tPsLAewQQAQgALwUCOG1DgAUJAAAACgYLCQcIAwIJEMwSTBo3 40 | j0N5BBUICgIDFgIBAhkBAhsDAh4BAAD2TQf+KQbrX2zO9SL5ffCK5qu2VigM 41 | 0E3uF763II9vRYfXHdZtXY/8K/uBLbu2rdZHwwb/jAHEe60Qf5VjcbIMtCfA 42 | khPB5JuCvW+JEsYhXplNxYka27svfWI75/cYVc/0OharKEaaPOv2F8C1k2jL 43 | Sk7Az01IAJkdwmBkG6fUwupevuvpO+kUQjsHg35q8Lm7G8roCYiK7K7/JQi3 44 | K+e0ovVFvunFSORaG8jR37uT7X7KA0LHD3S7XYO0o2OJi0QKB1wN3H3FEll0 45 | bFznfdIzKKIDzGwC/zhpUMGMwsqVLb8sw/H9cr82yPoM6pXVUqnstKDlLce8 46 | Dc2vwS83Aja9iWrIEg== 47 | =dvRO 48 | -----END PGP PRIVATE KEY BLOCK-----`; 49 | 50 | async function getInvalidKey() { 51 | return readKey({ armoredKey: INVALID_KEY }); 52 | } 53 | async function makeKeyValid() { 54 | /** 55 | * Checks if a key can be used for encryption. 56 | */ 57 | async function encryptFails(k) { 58 | try { 59 | await openpgp.encrypt({ 60 | message: await createMessage({ text: 'Hello', filename: 'hello.txt' }), 61 | encryptionKeys: k 62 | }); 63 | return false; 64 | } catch (e) { 65 | return true; 66 | } 67 | } 68 | const invalidkey = await getInvalidKey(); 69 | // deconstruct invalid key 70 | const [pubkey, puser, pusersig] = invalidkey.toPacketList().map(i => i); 71 | // create a fake signature 72 | const fake = new SignaturePacket(); 73 | Object.assign(fake, pusersig); 74 | // extend expiration times 75 | fake.keyExpirationTime = 0x7FFFFFFF; 76 | fake.signatureExpirationTime = 0x7FFFFFFF; 77 | // add key capability 78 | fake.keyFlags[0] |= enums.keyFlags.encryptCommunication; 79 | // create modified subpacket data 80 | pusersig.readSubPackets(fake.writeHashedSubPackets(), false); 81 | // reconstruct the modified key 82 | const newlist = new PacketList(); 83 | newlist.push(pubkey, puser, pusersig); 84 | let modifiedkey = new PrivateKey(newlist); 85 | // re-read the message to eliminate any 86 | // behaviour due to cached values. 87 | modifiedkey = await readKey({ armoredKey: await modifiedkey.armor() }); 88 | 89 | expect(await encryptFails(invalidkey)).to.be.true; 90 | expect(await encryptFails(modifiedkey)).to.be.true; 91 | } 92 | 93 | export default () => it('Does not accept unsigned subpackets', makeKeyValid); 94 | -------------------------------------------------------------------------------- /src/crypto/aes_kw.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * @fileoverview Implementation of RFC 3394 AES Key Wrap & Key Unwrap funcions 20 | * @see module:crypto/public_key/elliptic/ecdh 21 | * @module crypto/aes_kw 22 | */ 23 | 24 | import { aeskw as nobleAesKW } from '@noble/ciphers/aes'; 25 | import { getCipherParams } from './cipher'; 26 | import util from '../util'; 27 | 28 | const webCrypto = util.getWebCrypto(); 29 | /** 30 | * AES key wrap 31 | * @param {enums.symmetric.aes128|enums.symmetric.aes256|enums.symmetric.aes192} algo - AES algo 32 | * @param {Uint8Array} key - wrapping key 33 | * @param {Uint8Array} dataToWrap 34 | * @returns {Uint8Array} wrapped key 35 | */ 36 | export async function wrap(algo, key, dataToWrap) { 37 | const { keySize } = getCipherParams(algo); 38 | // sanity checks, since WebCrypto does not use the `algo` input 39 | if (!util.isAES(algo) || key.length !== keySize) { 40 | throw new Error('Unexpected algorithm or key size'); 41 | } 42 | 43 | try { 44 | const wrappingKey = await webCrypto.importKey('raw', key, { name: 'AES-KW' }, false, ['wrapKey']); 45 | // Import data as HMAC key, as it has no key length requirements 46 | const keyToWrap = await webCrypto.importKey('raw', dataToWrap, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']); 47 | const wrapped = await webCrypto.wrapKey('raw', keyToWrap, wrappingKey, { name: 'AES-KW' }); 48 | return new Uint8Array(wrapped); 49 | } catch (err) { 50 | // no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support 51 | if (err.name !== 'NotSupportedError' && 52 | !(key.length === 24 && err.name === 'OperationError')) { 53 | throw err; 54 | } 55 | util.printDebugError('Browser did not support operation: ' + err.message); 56 | } 57 | 58 | return nobleAesKW(key).encrypt(dataToWrap); 59 | } 60 | 61 | /** 62 | * AES key unwrap 63 | * @param {enums.symmetric.aes128|enums.symmetric.aes256|enums.symmetric.aes192} algo - AES algo 64 | * @param {Uint8Array} key - wrapping key 65 | * @param {Uint8Array} wrappedData 66 | * @returns {Uint8Array} unwrapped data 67 | */ 68 | export async function unwrap(algo, key, wrappedData) { 69 | const { keySize } = getCipherParams(algo); 70 | // sanity checks, since WebCrypto does not use the `algo` input 71 | if (!util.isAES(algo) || key.length !== keySize) { 72 | throw new Error('Unexpected algorithm or key size'); 73 | } 74 | 75 | let wrappingKey; 76 | try { 77 | wrappingKey = await webCrypto.importKey('raw', key, { name: 'AES-KW' }, false, ['unwrapKey']); 78 | } catch (err) { 79 | // no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support 80 | if (err.name !== 'NotSupportedError' && 81 | !(key.length === 24 && err.name === 'OperationError')) { 82 | throw err; 83 | } 84 | util.printDebugError('Browser did not support operation: ' + err.message); 85 | return nobleAesKW(key).decrypt(wrappedData); 86 | } 87 | 88 | try { 89 | const unwrapped = await webCrypto.unwrapKey('raw', wrappedData, wrappingKey, { name: 'AES-KW' }, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']); 90 | return new Uint8Array(await webCrypto.exportKey('raw', unwrapped)); 91 | } catch (err) { 92 | if (err.name === 'OperationError') { 93 | throw new Error('Key Data Integrity failed'); 94 | } 95 | throw err; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/type/oid.js: -------------------------------------------------------------------------------- 1 | // OpenPGP.js - An OpenPGP implementation in javascript 2 | // Copyright (C) 2015-2016 Decentral 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * Wrapper to an OID value 20 | * 21 | * {@link https://tools.ietf.org/html/rfc6637#section-11|RFC6637, section 11}: 22 | * The sequence of octets in the third column is the result of applying 23 | * the Distinguished Encoding Rules (DER) to the ASN.1 Object Identifier 24 | * with subsequent truncation. The truncation removes the two fields of 25 | * encoded Object Identifier. The first omitted field is one octet 26 | * representing the Object Identifier tag, and the second omitted field 27 | * is the length of the Object Identifier body. For example, the 28 | * complete ASN.1 DER encoding for the NIST P-256 curve OID is "06 08 2A 29 | * 86 48 CE 3D 03 01 07", from which the first entry in the table above 30 | * is constructed by omitting the first two octets. Only the truncated 31 | * sequence of octets is the valid representation of a curve OID. 32 | * @module type/oid 33 | */ 34 | 35 | import util from '../util'; 36 | import enums from '../enums'; 37 | 38 | const knownOIDs = { 39 | '2a8648ce3d030107': enums.curve.nistP256, 40 | '2b81040022': enums.curve.nistP384, 41 | '2b81040023': enums.curve.nistP521, 42 | '2b8104000a': enums.curve.secp256k1, 43 | '2b06010401da470f01': enums.curve.ed25519Legacy, 44 | '2b060104019755010501': enums.curve.curve25519Legacy, 45 | '2b2403030208010107': enums.curve.brainpoolP256r1, 46 | '2b240303020801010b': enums.curve.brainpoolP384r1, 47 | '2b240303020801010d': enums.curve.brainpoolP512r1 48 | }; 49 | 50 | class OID { 51 | constructor(oid) { 52 | if (oid instanceof OID) { 53 | this.oid = oid.oid; 54 | } else if (util.isArray(oid) || 55 | util.isUint8Array(oid)) { 56 | oid = new Uint8Array(oid); 57 | if (oid[0] === 0x06) { // DER encoded oid byte array 58 | if (oid[1] !== oid.length - 2) { 59 | throw new Error('Length mismatch in DER encoded oid'); 60 | } 61 | oid = oid.subarray(2); 62 | } 63 | this.oid = oid; 64 | } else { 65 | this.oid = ''; 66 | } 67 | } 68 | 69 | /** 70 | * Method to read an OID object 71 | * @param {Uint8Array} input - Where to read the OID from 72 | * @returns {Number} Number of read bytes. 73 | */ 74 | read(input) { 75 | if (input.length >= 1) { 76 | const length = input[0]; 77 | if (input.length >= 1 + length) { 78 | this.oid = input.subarray(1, 1 + length); 79 | return 1 + this.oid.length; 80 | } 81 | } 82 | throw new Error('Invalid oid'); 83 | } 84 | 85 | /** 86 | * Serialize an OID object 87 | * @returns {Uint8Array} Array with the serialized value the OID. 88 | */ 89 | write() { 90 | return util.concatUint8Array([new Uint8Array([this.oid.length]), this.oid]); 91 | } 92 | 93 | /** 94 | * Serialize an OID object as a hex string 95 | * @returns {string} String with the hex value of the OID. 96 | */ 97 | toHex() { 98 | return util.uint8ArrayToHex(this.oid); 99 | } 100 | 101 | /** 102 | * If a known curve object identifier, return the canonical name of the curve 103 | * @returns {enums.curve} String with the canonical name of the curve 104 | * @throws if unknown 105 | */ 106 | getName() { 107 | const name = knownOIDs[this.toHex()]; 108 | if (!name) { 109 | throw new Error('Unknown curve object identifier.'); 110 | } 111 | 112 | return name; 113 | } 114 | } 115 | 116 | export default OID; 117 | -------------------------------------------------------------------------------- /src/signature.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import { armor, unarmor } from './encoding/armor'; 19 | import { PacketList, SignaturePacket } from './packet'; 20 | import enums from './enums'; 21 | import util from './util'; 22 | import defaultConfig from './config'; 23 | 24 | // A Signature can contain the following packets 25 | const allowedPackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); 26 | 27 | /** 28 | * Class that represents an OpenPGP signature. 29 | */ 30 | export class Signature { 31 | /** 32 | * @param {PacketList} packetlist - The signature packets 33 | */ 34 | constructor(packetlist) { 35 | this.packets = packetlist || new PacketList(); 36 | } 37 | 38 | /** 39 | * Returns binary encoded signature 40 | * @returns {ReadableStream} Binary signature. 41 | */ 42 | write() { 43 | return this.packets.write(); 44 | } 45 | 46 | /** 47 | * Returns ASCII armored text of signature 48 | * @param {Object} [config] - Full configuration, defaults to openpgp.config 49 | * @returns {ReadableStream} ASCII armor. 50 | */ 51 | armor(config = defaultConfig) { 52 | // An ASCII-armored sequence of Signature packets that only includes v6 Signature packets MUST NOT contain a CRC24 footer. 53 | const emitChecksum = this.packets.some(packet => packet.constructor.tag === SignaturePacket.tag && packet.version !== 6); 54 | return armor(enums.armor.signature, this.write(), undefined, undefined, undefined, emitChecksum, config); 55 | } 56 | 57 | /** 58 | * Returns an array of KeyIDs of all of the issuers who created this signature 59 | * @returns {Array} The Key IDs of the signing keys 60 | */ 61 | getSigningKeyIDs() { 62 | return this.packets.map(packet => packet.issuerKeyID); 63 | } 64 | } 65 | 66 | /** 67 | * reads an (optionally armored) OpenPGP signature and returns a signature object 68 | * @param {Object} options 69 | * @param {String} [options.armoredSignature] - Armored signature to be parsed 70 | * @param {Uint8Array} [options.binarySignature] - Binary signature to be parsed 71 | * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} 72 | * @returns {Promise} New signature object. 73 | * @async 74 | * @static 75 | */ 76 | export async function readSignature({ armoredSignature, binarySignature, config, ...rest }) { 77 | config = { ...defaultConfig, ...config }; 78 | let input = armoredSignature || binarySignature; 79 | if (!input) { 80 | throw new Error('readSignature: must pass options object containing `armoredSignature` or `binarySignature`'); 81 | } 82 | if (armoredSignature && !util.isString(armoredSignature)) { 83 | throw new Error('readSignature: options.armoredSignature must be a string'); 84 | } 85 | if (binarySignature && !util.isUint8Array(binarySignature)) { 86 | throw new Error('readSignature: options.binarySignature must be a Uint8Array'); 87 | } 88 | const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`); 89 | 90 | if (armoredSignature) { 91 | const { type, data } = await unarmor(input, config); 92 | if (type !== enums.armor.signature) { 93 | throw new Error('Armored text not of type signature'); 94 | } 95 | input = data; 96 | } 97 | const packetlist = await PacketList.fromBinary(input, allowedPackets, config); 98 | return new Signature(packetlist); 99 | } 100 | -------------------------------------------------------------------------------- /src/encoding/base64.js: -------------------------------------------------------------------------------- 1 | /* OpenPGP radix-64/base64 string encoding/decoding 2 | * Copyright 2005 Herbert Hanewinkel, www.haneWIN.de 3 | * version 1.0, check www.haneWIN.de for the latest version 4 | * 5 | * This software is provided as-is, without express or implied warranty. 6 | * Permission to use, copy, modify, distribute or sell this software, with or 7 | * without fee, for any purpose and by any individual or organization, is hereby 8 | * granted, provided that the above copyright notice and this paragraph appear 9 | * in all copies. Distribution as a part of an application or binary must 10 | * include the above copyright notice in the documentation and/or other materials 11 | * provided with the application or distribution. 12 | */ 13 | 14 | /** 15 | * @module encoding/base64 16 | */ 17 | 18 | import { transform as streamTransform } from '@openpgp/web-stream-tools'; 19 | import util from '../util'; 20 | 21 | const Buffer = util.getNodeBuffer(); 22 | 23 | let encodeChunk; 24 | let decodeChunk; 25 | if (Buffer) { 26 | encodeChunk = buf => Buffer.from(buf).toString('base64'); 27 | decodeChunk = str => { 28 | const b = Buffer.from(str, 'base64'); 29 | return new Uint8Array(b.buffer, b.byteOffset, b.byteLength); 30 | }; 31 | } else { 32 | encodeChunk = buf => btoa(util.uint8ArrayToString(buf)); 33 | decodeChunk = str => util.stringToUint8Array(atob(str)); 34 | } 35 | 36 | /** 37 | * Convert binary array to radix-64 38 | * @param {Uint8Array | ReadableStream} data - Uint8Array to convert 39 | * @returns {String | ReadableStream} Radix-64 version of input string. 40 | * @static 41 | */ 42 | export function encode(data) { 43 | let buf = new Uint8Array(); 44 | return streamTransform(data, value => { 45 | buf = util.concatUint8Array([buf, value]); 46 | const r = []; 47 | const bytesPerLine = 45; // 60 chars per line * (3 bytes / 4 chars of base64). 48 | const lines = Math.floor(buf.length / bytesPerLine); 49 | const bytes = lines * bytesPerLine; 50 | const encoded = encodeChunk(buf.subarray(0, bytes)); 51 | for (let i = 0; i < lines; i++) { 52 | r.push(encoded.substr(i * 60, 60)); 53 | r.push('\n'); 54 | } 55 | buf = buf.subarray(bytes); 56 | return r.join(''); 57 | }, () => (buf.length ? encodeChunk(buf) + '\n' : '')); 58 | } 59 | 60 | /** 61 | * Convert radix-64 to binary array 62 | * @param {String | ReadableStream} data - Radix-64 string to convert 63 | * @returns {Uint8Array | ReadableStream} Binary array version of input string. 64 | * @static 65 | */ 66 | export function decode(data) { 67 | let buf = ''; 68 | return streamTransform(data, value => { 69 | buf += value; 70 | 71 | // Count how many whitespace characters there are in buf 72 | let spaces = 0; 73 | const spacechars = [' ', '\t', '\r', '\n']; 74 | for (let i = 0; i < spacechars.length; i++) { 75 | const spacechar = spacechars[i]; 76 | for (let pos = buf.indexOf(spacechar); pos !== -1; pos = buf.indexOf(spacechar, pos + 1)) { 77 | spaces++; 78 | } 79 | } 80 | 81 | // Backtrack until we have 4n non-whitespace characters 82 | // that we can safely base64-decode 83 | let length = buf.length; 84 | for (; length > 0 && (length - spaces) % 4 !== 0; length--) { 85 | if (spacechars.includes(buf[length])) spaces--; 86 | } 87 | 88 | const decoded = decodeChunk(buf.substr(0, length)); 89 | buf = buf.substr(length); 90 | return decoded; 91 | }, () => decodeChunk(buf)); 92 | } 93 | 94 | /** 95 | * Convert a Base-64 encoded string an array of 8-bit integer 96 | * 97 | * Note: accepts both Radix-64 and URL-safe strings 98 | * @param {String} base64 - Base-64 encoded string to convert 99 | * @returns {Uint8Array} An array of 8-bit integers. 100 | */ 101 | export function b64ToUint8Array(base64) { 102 | return decode(base64.replace(/-/g, '+').replace(/_/g, '/')); 103 | } 104 | 105 | /** 106 | * Convert an array of 8-bit integer to a Base-64 encoded string 107 | * @param {Uint8Array} bytes - An array of 8-bit integers to convert 108 | * @param {bool} url - If true, output is URL-safe 109 | * @returns {String} Base-64 encoded string. 110 | */ 111 | export function uint8ArrayToB64(bytes, url) { 112 | let encoded = encode(bytes).replace(/[\r\n]/g, ''); 113 | if (url) { 114 | encoded = encoded.replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/[=]/g, ''); 115 | } 116 | return encoded; 117 | } 118 | -------------------------------------------------------------------------------- /src/crypto/hash/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Provides an interface to hashing functions available in Node.js or external libraries. 3 | * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} 4 | * @see {@link https://github.com/indutny/hash.js|hash.js} 5 | * @module crypto/hash 6 | */ 7 | 8 | import { transform as streamTransform, isArrayStream, readToEnd as streamReadToEnd } from '@openpgp/web-stream-tools'; 9 | import util from '../../util'; 10 | import enums from '../../enums'; 11 | 12 | const webCrypto = util.getWebCrypto(); 13 | const nodeCrypto = util.getNodeCrypto(); 14 | const nodeCryptoHashes = nodeCrypto && nodeCrypto.getHashes(); 15 | 16 | function nodeHash(type) { 17 | if (!nodeCrypto || !nodeCryptoHashes.includes(type)) { 18 | return; 19 | } 20 | return async function (data) { 21 | const shasum = nodeCrypto.createHash(type); 22 | return streamTransform(data, value => { 23 | shasum.update(value); 24 | }, () => new Uint8Array(shasum.digest())); 25 | }; 26 | } 27 | 28 | function nobleHash(nobleHashName, webCryptoHashName) { 29 | const getNobleHash = async () => { 30 | const { nobleHashes } = await import('./noble_hashes'); 31 | const hash = nobleHashes.get(nobleHashName); 32 | if (!hash) throw new Error('Unsupported hash'); 33 | return hash; 34 | }; 35 | 36 | return async function(data) { 37 | if (isArrayStream(data)) { 38 | data = await streamReadToEnd(data); 39 | } 40 | if (util.isStream(data)) { 41 | const hash = await getNobleHash(); 42 | 43 | const hashInstance = hash.create(); 44 | return streamTransform(data, value => { 45 | hashInstance.update(value); 46 | }, () => hashInstance.digest()); 47 | } else if (webCrypto && webCryptoHashName) { 48 | return new Uint8Array(await webCrypto.digest(webCryptoHashName, data)); 49 | } else { 50 | const hash = await getNobleHash(); 51 | 52 | return hash(data); 53 | } 54 | }; 55 | } 56 | 57 | const md5 = nodeHash('md5') || nobleHash('md5'); 58 | const sha1 = nodeHash('sha1') || nobleHash('sha1', 'SHA-1'); 59 | const sha224 = nodeHash('sha224') || nobleHash('sha224'); 60 | const sha256 = nodeHash('sha256') || nobleHash('sha256', 'SHA-256'); 61 | const sha384 = nodeHash('sha384') || nobleHash('sha384', 'SHA-384'); 62 | const sha512 = nodeHash('sha512') || nobleHash('sha512', 'SHA-512'); 63 | const ripemd = nodeHash('ripemd160') || nobleHash('ripemd160'); 64 | const sha3_256 = nodeHash('sha3-256') || nobleHash('sha3_256'); 65 | const sha3_512 = nodeHash('sha3-512') || nobleHash('sha3_512'); 66 | 67 | /** 68 | * Create a hash on the specified data using the specified algorithm 69 | * @param {module:enums.hash} algo - Hash algorithm type (see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4}) 70 | * @param {Uint8Array} data - Data to be hashed 71 | * @returns {Promise} Hash value. 72 | */ 73 | export function computeDigest(algo, data) { 74 | switch (algo) { 75 | case enums.hash.md5: 76 | return md5(data); 77 | case enums.hash.sha1: 78 | return sha1(data); 79 | case enums.hash.ripemd: 80 | return ripemd(data); 81 | case enums.hash.sha256: 82 | return sha256(data); 83 | case enums.hash.sha384: 84 | return sha384(data); 85 | case enums.hash.sha512: 86 | return sha512(data); 87 | case enums.hash.sha224: 88 | return sha224(data); 89 | case enums.hash.sha3_256: 90 | return sha3_256(data); 91 | case enums.hash.sha3_512: 92 | return sha3_512(data); 93 | default: 94 | throw new Error('Unsupported hash function'); 95 | } 96 | } 97 | 98 | /** 99 | * Returns the hash size in bytes of the specified hash algorithm type 100 | * @param {module:enums.hash} algo - Hash algorithm type (See {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4}) 101 | * @returns {Integer} Size in bytes of the resulting hash. 102 | */ 103 | export function getHashByteLength(algo) { 104 | switch (algo) { 105 | case enums.hash.md5: 106 | return 16; 107 | case enums.hash.sha1: 108 | case enums.hash.ripemd: 109 | return 20; 110 | case enums.hash.sha256: 111 | return 32; 112 | case enums.hash.sha384: 113 | return 48; 114 | case enums.hash.sha512: 115 | return 64; 116 | case enums.hash.sha224: 117 | return 28; 118 | case enums.hash.sha3_256: 119 | return 32; 120 | case enums.hash.sha3_512: 121 | return 64; 122 | default: 123 | throw new Error('Invalid hash algorithm.'); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/security/message_signature_bypass.js: -------------------------------------------------------------------------------- 1 | import { use as chaiUse, expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import 3 | chaiUse(chaiAsPromised); 4 | 5 | import openpgp from '../initOpenpgp.js'; 6 | import util from '../../src/util.js'; 7 | 8 | const { readKey, readCleartextMessage, SignaturePacket } = openpgp; 9 | 10 | /** 11 | * public key of another user. 12 | */ 13 | const OTHERPUBKEY = ` 14 | -----BEGIN PGP PUBLIC KEY BLOCK----- 15 | Version: OpenPGP.js VERSION 16 | Comment: https://openpgpjs.org 17 | 18 | xsBNBFuqNY0BCADFUCnl03vimEQRs7mtDIp0g6tItuguhJJu1/QjXwTXUHZg 19 | pZosOPkGOR1EubydjYz4kvAnZ5r9cWA4xQ96rBdvj/kIaP+oJKLB1jXwh4Ft 20 | +8YT4mVU2yWLu7U2p4tSyRoM5VCDEqG64OcbZMwEdDKf8t6JTjYTtEfPfW5R 21 | 4hy8NjPYOx0Jw8MG+U0aP4WA1xsMXFP/VWF1IseEcVIWKs/VroJc5Xe80QDN 22 | hRtKTRVJV/wTnkao2MLcq/hgOfhO28NjnxVlX06O/XTWdElA7CCi1Zg1/BZ+ 23 | r2XuuE1J2DjERfTokFzkKnMlGK9zXn0LxPnAJAIfu33/SFuAZcVu4UEJABEB 24 | AAHNHVRlc3RpIFRlc3QgPHRlc3RAZXhhbXBsZS5jb20+wsB1BBABCAApBQJb 25 | qjWcBgsJBwgDAgkQVSCLLRis484EFQgKAgMWAgECGQECGwMCHgEAAGNXB/4g 26 | DX082p83RfMmBv8hRN1V9ruPAvlxDWNBHb5dc1Y67yrBXOLMtaSauSZKrbf1 27 | moPDHT2eoLl7cV3BQbXWp+hiMZ4W53ZFJt26Kwwwf1yVRAZME7VRNwqW0aJv 28 | FKgCq7XTgJ61UYNhc31bLH0eVcfCkAExfwqZlwTWRzRSCqr0NL0XZVakJE6F 29 | al1Y+uN7CEr0/vbc6uSuo0hyZwxAw+Iynd5cO9PRXSssAm4IaulSnYUd96r2 30 | l8jsa+p6ooBYPotnLQ9fdd457JMoc8jDHf4m+P9/ZiWpycCB0DgUtNw1wH2T 31 | DHYf+2lfGGoA3osuHeJJfZfJujbKW5L7ZMNJ23tSzsBNBFuqNY0BB/9XKYzS 32 | PdHC/dXoBC9un3YLCcUX6LMNnaQMryVONYKFE1Rt0/si9XtnIDqyBrTr3LRi 33 | D+GIR+b7zCXOGkvmjztblD2P3SweCudPIbVLxePI+SfyjRs9EsMOrEPymN7U 34 | u/CU7jefvNBKvvMHi1m1Ibqg/A+ZheqJ+xBjSQM88dWsY/XB/jh7PGAM0QEu 35 | ezafNwlUUNnXyYRuC3P4h66OIJcDPcfaao3uAuJ/C81E8ttuws3c08kudd/A 36 | szIGpPtxAakimiWVHa0ceKi3exXXjRDrufroPcV3+Gbn4J8NqcUPRhB3L3CD 37 | rCivRme8qGEYh+ADPLy88SytdtCr+6W4hiQVABEBAAHCwF8EGAEIABMFAluq 38 | NZ0JEFUgiy0YrOPOAhsMAABebggAxANqkwS/Ag3NQLUu/wNZMMifZAxpFIWo 39 | CQQrCOU94OSsUKz8Q11yoOvsQN3T4CSL8dG5DbIucnHsx39jVeTniG6P3p9f 40 | NE/lq7RtLnXjVgGYpPNNUbLcOfXaCDhmS4GEunwTsVlsmEqyfLniKLG8to5Q 41 | 6f/wGPJRvYB8rgLfVGV3DCvILg/CMzkceM9ia6jDQeHHwnoFVXnlsRAgQefJ 42 | rT5hVim4Wzg/5Lxt9Efry0k1ZhT0kondF1qNMv0wKxIJ+/gDNT2ZP4RIr1Kl 43 | eu6a8CH841yfF0+r5RV9xOky0jxwGgcxT29c8DBoawjXu6TtJ/SP8UrscttA 44 | bVLYdBmWLw== 45 | =nMyV 46 | -----END PGP PUBLIC KEY BLOCK-----`; 47 | 48 | /** 49 | * an original unmodified message as a template. 50 | */ 51 | const ORIGINAL = ` 52 | -----BEGIN PGP SIGNED MESSAGE----- 53 | Hash: SHA256 54 | 55 | You owe me € 10 56 | -----BEGIN PGP SIGNATURE----- 57 | Version: OpenPGP.js VERSION 58 | Comment: https://openpgpjs.org 59 | 60 | wsBcBAEBCAAQBQJbq0iICRBVIIstGKzjzgAAeV0H/3ZxWuEV+2PNXHR+PdxX 61 | WRxjk6Zu+jjpb/iRS8IynRoe3iDaai3+iiAHM1GsHvOIBVJU6Bjx1ZyyEI0a 62 | dDg/yj3LBqBW9U3AiGpsXPfuyLKYIHfPbrygEleRIQKh7+iwNmn9ScVvzJrl 63 | hUurlZxx1mWbERAchwsrcZpwFCdfjJ/C9sblTxgnsm1YlYZNkf95DFtRnVO5 64 | prUuOjqJ0bA7bxg5GA4FQskRPIQ0ioZ6DyDi2IU3rdVEOs2Pc8S0EsD9K7af 65 | vO5oXKiJsyUN5EXEI8kYRulP1l0kvEWVTlnY2ek1qS637RkBI+DHLcXV5Hcu 66 | fhGyl7nA7UCwgsqf7ZPBhRg= 67 | =nbjQ 68 | -----END PGP SIGNATURE-----`; 69 | async function getOtherPubKey() { 70 | return readKey({ armoredKey: OTHERPUBKEY }); 71 | } 72 | 73 | /** 74 | * The "standalone" signature signed by the victim. 75 | */ 76 | const STANDALONE_PKT = util.hexToUint8Array('04020108001005025bab730a091055208b2d18ace3ce000059da0800b823eceba1bae016afa584bc67ef931dde167fe683bea58761dac31abaaf223afa4cd41fe609b06f7809f2e01ae8792e08591419e591d652d7580af3b7cdfa27e63dd4838fc7ec2aa485757d6c1c6c33bf305cb8fb7eaa1b47ac00825b08a20606a320e988733294957e03012064b61c74a3d41bfebddd4fdd739ab9e220ae48d32a9edf8ff5aec1e13807fc76cd84b9bba914926a14e6f5aacb0c584fa306b4d11280ff107e6aeee9f68c419c7084dc5504990aa7e31d3e042fa745fdb9ae8207fbc15fc440b5df148252e9c65cccaf3a5d6d6919a5c12912ef41761afde4561ca70696bba37452b32584684fa2d50e4f138e101f13dab6125aa5680bd9658c'); 77 | async function fakeSignature() { 78 | // read the template and modify the text to 79 | // invalidate the signature. 80 | let fake = await readCleartextMessage({ 81 | cleartextMessage: ORIGINAL.replace( 82 | 'You owe me', 83 | 'I owe you') 84 | }); 85 | // read the standalone signature packet 86 | const tmp = new SignaturePacket(); 87 | await tmp.read(STANDALONE_PKT); 88 | 89 | // replace the "text" signature with the 90 | // "standalone" signature 91 | fake.signature.packets[0] = tmp; 92 | const faked_armored = await fake.armor(); 93 | // re-read the message to eliminate any 94 | // behaviour due to cached values. 95 | fake = await readCleartextMessage({ cleartextMessage: faked_armored }); 96 | // faked message now verifies correctly 97 | const res = await openpgp.verify({ 98 | message: fake, 99 | verificationKeys: await getOtherPubKey() 100 | }); 101 | const { signatures } = res; 102 | expect(signatures).to.have.length(0); 103 | } 104 | 105 | export default () => it('Does not accept non-binary/text signatures', fakeSignature); 106 | -------------------------------------------------------------------------------- /test/benchmarks/time.js: -------------------------------------------------------------------------------- 1 | import Benchmark from 'benchmark'; 2 | import { readToEnd } from '@openpgp/web-stream-tools'; 3 | import * as openpgp from '@protontech/openpgp'; 4 | 5 | const wrapAsync = func => ({ 6 | fn: async deferred => { 7 | await func().catch(onError); 8 | deferred.resolve(); 9 | }, 10 | defer: true 11 | }); 12 | 13 | const onError = err => { 14 | // eslint-disable-next-line no-console 15 | console.error('The time benchmark tests failed by throwing the following error:'); 16 | // eslint-disable-next-line no-console 17 | console.error(err); 18 | // eslint-disable-next-line no-process-exit 19 | process.exit(1); 20 | }; 21 | 22 | /** 23 | * Time benchmark tests. 24 | * NB: each test will be run multiple times, so any input must be consumable multiple times. 25 | */ 26 | (async () => { 27 | const suite = new Benchmark.Suite(); 28 | const { armoredKey, privateKey, publicKey, armoredEncryptedMessage, armoredSignedMessage } = await getTestData(); 29 | function* largeDataGenerator({ chunk, numberOfChunks }) { 30 | for (let chunkNumber = 0; chunkNumber < numberOfChunks; chunkNumber++) { 31 | yield chunk; 32 | } 33 | } 34 | 35 | const streamFromGenerator = it => new ReadableStream({ 36 | pull: controller => { 37 | const { value, done } = it.next(); 38 | if (done) { 39 | controller.close(); 40 | } else { 41 | controller.enqueue(value); 42 | } 43 | } 44 | }); 45 | 46 | suite.add('openpgp.readKey', wrapAsync(async () => { 47 | await openpgp.readKey({ armoredKey }); 48 | })); 49 | 50 | suite.add('openpgp.readMessage', wrapAsync(async () => { 51 | await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage }); 52 | })); 53 | 54 | suite.add('openpgp.generateKey', wrapAsync(async () => { 55 | await openpgp.generateKey({ userIDs: { email: 'test@test.it' } }); 56 | })); 57 | 58 | suite.add('openpgp.encrypt', wrapAsync(async () => { 59 | const message = await openpgp.createMessage({ text: 'plaintext' }); 60 | await openpgp.encrypt({ message, encryptionKeys: publicKey }); 61 | })); 62 | 63 | suite.add('openpgp.sign', wrapAsync(async () => { 64 | const message = await openpgp.createMessage({ text: 'plaintext' }); 65 | await openpgp.sign({ message, signingKeys: privateKey }); 66 | })); 67 | 68 | suite.add('openpgp.sign (stream)', wrapAsync(async () => { 69 | const inputStream = streamFromGenerator(largeDataGenerator({ chunk: new Uint8Array(10000), numberOfChunks: 10 })); 70 | const message = await openpgp.createMessage({ binary: inputStream }); 71 | const signed = await openpgp.sign({ message, signingKeys: privateKey }); 72 | 73 | await readToEnd(signed); 74 | })); 75 | 76 | suite.add('openpgp.decrypt', wrapAsync(async () => { 77 | const message = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage }); 78 | await openpgp.decrypt({ message, decryptionKeys: privateKey }); 79 | })); 80 | 81 | suite.add('openpgp.verify', wrapAsync(async () => { 82 | const message = await openpgp.readMessage({ armoredMessage: armoredSignedMessage }); 83 | await openpgp.verify({ message, verificationKeys: publicKey, expectSigned: true }); 84 | })); 85 | 86 | suite.on('cycle', event => { 87 | // Output benchmark result by converting benchmark result to string 88 | // eslint-disable-next-line no-console 89 | console.log(String(event.target)); 90 | }); 91 | 92 | suite.run({ 'async': true }); 93 | })(); 94 | 95 | async function getTestData() { 96 | const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- 97 | 98 | xVgEYS4KIRYJKwYBBAHaRw8BAQdAOl5Ij0p8llEOLqalwRM8+YWKXELm+Zl1 99 | arT2orL/42MAAP9SQBdl+A/i4AtIOr33rn6OKzmXQ2EQH0xoSPJcVxX7BA5U 100 | zRR0ZXN0IDx0ZXN0QHRlc3QuY29tPsKMBBAWCgAdBQJhLgohBAsJBwgDFQgK 101 | BBYAAgECGQECGwMCHgEAIQkQ2RFo4G/cGHQWIQRL9hTrZduw8+42e1rZEWjg 102 | b9wYdEi3AP91NftBKXLfcMRz/g540cQ/0+ax8pvsiqFSb+Sqz87YPwEAkoYK 103 | 8I9rVAlVABIhy/g7ZStHu/u0zsPbiquZFKoVLgPHXQRhLgohEgorBgEEAZdV 104 | AQUBAQdAqY5VZYX6axscpfVN3EED83T3WO3+Hzxfq31dXJXKrRkDAQgHAAD/ 105 | an6zziN/Aw0ruIxuZTjmkYriDW34hys8F2nRR23PO6gPjsJ4BBgWCAAJBQJh 106 | LgohAhsMACEJENkRaOBv3Bh0FiEES/YU62XbsPPuNnta2RFo4G/cGHQjlgEA 107 | gbOEmauiq2avut4e7pSJ98t50zai2dzNies1OpqTU58BAM1pWI99FxM6thX9 108 | aDa+Qhz0AxhA9P+3eQCXYTZR7CEE 109 | =LPl8 110 | -----END PGP PRIVATE KEY BLOCK-----`; 111 | 112 | const privateKey = await openpgp.readKey({ armoredKey }); 113 | const publicKey = privateKey.toPublic(); 114 | const plaintextMessage = await openpgp.createMessage({ text: 'plaintext' }); 115 | const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, encryptionKeys: publicKey }); 116 | const armoredSignedMessage = await openpgp.sign({ message: await openpgp.createMessage({ text: 'plaintext' }), signingKeys: privateKey }); 117 | 118 | return { 119 | armoredKey, 120 | privateKey, 121 | publicKey, 122 | armoredEncryptedMessage, 123 | armoredSignedMessage 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /test/crypto/biginteger.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import BN from 'bn.js'; 4 | import { bigIntToUint8Array, bitLength, byteLength, gcd, getBit, modExp, modInv } from '../../src/crypto/biginteger'; 5 | import { getRandomBytes } from '../../src/crypto/random'; 6 | 7 | async function getRandomBN(min, max) { 8 | if (max.cmp(min) <= 0) { 9 | throw new Error('Illegal parameter value: max <= min'); 10 | } 11 | 12 | const modulus = max.sub(min); 13 | const bytes = modulus.byteLength(); 14 | const r = new BN(getRandomBytes(bytes + 8)); 15 | return r.mod(modulus).add(min); 16 | } 17 | 18 | 19 | export default () => describe('BigInt', () => { 20 | it('bitLength is correct', function() { 21 | const n = BigInt(127); 22 | const expected = 7; 23 | expect(bitLength(n)).to.equal(expected); 24 | expect(bitLength(n + BigInt(1))).to.equal(expected + 1); 25 | }); 26 | 27 | it('byteLength is correct', function() { 28 | const n = BigInt(65535); 29 | const expected = 2; 30 | expect(byteLength(n)).to.equal(expected); 31 | expect(byteLength(n + BigInt(1))).to.equal(expected + 1); 32 | }); 33 | 34 | it('toUint8Array is correct', function() { 35 | const nString = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; 36 | const n = BigInt(nString); 37 | const paddedSize = Number(byteLength(n)) + 1; 38 | // big endian, unpadded 39 | let expected = new BN(nString).toArrayLike(Uint8Array); 40 | expect(bigIntToUint8Array(n)).to.deep.equal(expected); 41 | // big endian, padded 42 | expected = new BN(nString).toArrayLike(Uint8Array, 'be', paddedSize); 43 | expect(bigIntToUint8Array(n, 'be', paddedSize)).to.deep.equal(expected); 44 | // little endian, unpadded 45 | expected = new BN(nString).toArrayLike(Uint8Array, 'le'); 46 | expect(bigIntToUint8Array(n, 'le')).to.deep.equal(expected); 47 | //little endian, padded 48 | expected = new BN(nString).toArrayLike(Uint8Array, 'le', paddedSize); 49 | expect(bigIntToUint8Array(n, 'le', paddedSize)).to.deep.equal(expected); 50 | }); 51 | 52 | it('modExp is correct (large values)', function() { 53 | const stringX = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; 54 | const stringE = '21139356010872569239159922781526379521587348169074209285187910481667533072168468011617194695181255483288792585413365359733692097084373249198758148704369207793873998901870577262254971784191473102265830193058813215898765238784670469696574407580179153118937858890572095234316482449291777882525949871374961971753'; 55 | const stringN = '129189808515414783602892982235788912674846062846614219472827821758734760420002631653235573915244294540972376140705505703576175711417114803419704967903726436285518767606681184247119430411311152556442947708732584954518890222684529678365388350886907287414896703685680210648760841628375425909680236584021041565183'; 56 | const x = BigInt(stringX); 57 | const e = BigInt(stringE); 58 | const n = BigInt(stringN); 59 | 60 | const got = modExp(x, e, n); 61 | const expected = new BN(stringX).toRed(BN.red(new BN(stringN))).redPow(new BN(stringE)); 62 | // different formats, it's easier to compare strings 63 | expect(got.toString(), expected.toString()); 64 | }); 65 | 66 | it('gcd is correct', async function() { 67 | const aBN = await getRandomBN(new BN(2), new BN(200)); 68 | const bBN = await getRandomBN(new BN(2), new BN(200)); 69 | if (aBN.isEven()) aBN.iaddn(1); 70 | const a = BigInt(aBN.toString()); 71 | const b = BigInt(bBN.toString()); 72 | const expected = aBN.gcd(bBN); 73 | expect(gcd(a, b).toString()).to.equal(expected.toString()); 74 | }); 75 | 76 | it('modular inversion is correct', async function() { 77 | const moduloBN = new BN(229); // this is a prime 78 | const baseBN = await getRandomBN(new BN(2), moduloBN); 79 | const a = BigInt(baseBN.toString()); 80 | const n = BigInt(moduloBN.toString()); 81 | const expected = baseBN.invm(moduloBN); 82 | expect(modInv(a, n).toString()).to.equal(expected.toString()); 83 | // test negative operand 84 | const expectedNegated = baseBN.neg().invm(moduloBN); 85 | expect(modInv(-a, n).toString()).to.equal(expectedNegated.toString()); 86 | expect(() => modInv(a * n, n)).to.throw(/Inverse does not exist/); 87 | }); 88 | 89 | it('getBit is correct', async function() { 90 | const i = 5; 91 | const nBN = await getRandomBN(new BN(2), new BN(200)); 92 | const n = BigInt(nBN.toString()); 93 | const expected = nBN.testn(5) ? 1 : 0; 94 | expect(getBit(n, i)).to.equal(expected); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@protontech/openpgp", 3 | "description": "OpenPGP.js is a Javascript implementation of the OpenPGP protocol. This is defined in RFC 4880.", 4 | "version": "6.2.2", 5 | "license": "LGPL-3.0+", 6 | "homepage": "https://openpgpjs.org/", 7 | "engines": { 8 | "node": ">= 18.0.0" 9 | }, 10 | "keywords": [ 11 | "crypto", 12 | "pgp", 13 | "gpg", 14 | "openpgp" 15 | ], 16 | "main": "dist/node/openpgp.min.cjs", 17 | "module": "dist/node/openpgp.min.mjs", 18 | "browser": { 19 | "./dist/node/openpgp.min.cjs": "./dist/openpgp.min.js", 20 | "./dist/node/openpgp.min.mjs": "./dist/openpgp.min.mjs" 21 | }, 22 | "exports": { 23 | ".": { 24 | "types": "./dist/types/index.d.ts", 25 | "browser": "./dist/openpgp.min.mjs", 26 | "import": "./dist/node/openpgp.mjs", 27 | "require": "./dist/node/openpgp.min.cjs" 28 | }, 29 | "./lightweight": { 30 | "types": "./dist/types/index.d.ts", 31 | "browser": "./dist/lightweight/openpgp.min.mjs" 32 | } 33 | }, 34 | "types": "dist/types/index.d.ts", 35 | "type": "module", 36 | "directories": { 37 | "lib": "src" 38 | }, 39 | "files": [ 40 | "dist/", 41 | "lightweight/" 42 | ], 43 | "scripts": { 44 | "build": "rollup --config", 45 | "build-types": "rm -rf dist/types && tsc --project tsconfig.dist.json && cpy 'src/**/*.d.ts' dist/types", 46 | "build-test": "npm run build -- --config-build-only=test", 47 | "prepare": "npm run build && npm run build-types", 48 | "test": "mocha --timeout 120000 test/unittests.js", 49 | "test-type-definitions": "npm run build-types && tsc --project test/typescript/tsconfig.test.json && tsx test/typescript/definitions.ts", 50 | "benchmark-time": "node test/benchmarks/time.js", 51 | "benchmark-memory-usage": "node test/benchmarks/memory_usage.js", 52 | "prebrowsertest": "npm run build-test", 53 | "browsertest": "web-test-runner --config test/web-test-runner.config.js --group local --manual --open", 54 | "test-browser": "web-test-runner --config test/web-test-runner.config.js --group local --playwright --browsers chromium firefox webkit", 55 | "test-browser:ci": "web-test-runner --config test/web-test-runner.config.js --group headless:ci", 56 | "test-browserstack": "web-test-runner --config test/web-test-runner.browserstack.config.js", 57 | "coverage": "c8 npm test", 58 | "lint": "eslint .", 59 | "docs": "jsdoc --configure .jsdocrc.cjs --destination docs --recurse README.md src && printf '%s' 'docs.openpgpjs.org' > docs/CNAME", 60 | "preversion": "rm -rf dist docs node_modules && npm ci && npm test", 61 | "version": "npm run docs && git add -A docs", 62 | "postversion": "git push --follow-tags && npm publish" 63 | }, 64 | "devDependencies": { 65 | "@noble/ciphers": "^1.3.0", 66 | "@noble/curves": "^1.9.6", 67 | "@noble/hashes": "^1.8.0", 68 | "@noble/post-quantum": "^0.2.1", 69 | "@openpgp/jsdoc": "^3.6.11", 70 | "@openpgp/seek-bzip": "^1.0.5-git", 71 | "@openpgp/tweetnacl": "^1.0.4-2", 72 | "@openpgp/web-stream-tools": "~0.1.3", 73 | "@protontech/eslint-plugin-enforce-uint8array-arraybuffer": "^1.0.0", 74 | "@rollup/plugin-alias": "^5.1.1", 75 | "@rollup/plugin-commonjs": "^28.0.6", 76 | "@rollup/plugin-node-resolve": "^16.0.1", 77 | "@rollup/plugin-replace": "^6.0.2", 78 | "@rollup/plugin-terser": "^0.4.4", 79 | "@rollup/plugin-typescript": "^12.1.4", 80 | "@rollup/plugin-wasm": "^6.2.2", 81 | "@types/chai": "^4.3.20", 82 | "@types/node": "^22.18.0", 83 | "@types/sinon": "^17.0.4", 84 | "@typescript-eslint/parser": "^7.18.0", 85 | "@web/test-runner": "^0.19.0", 86 | "@web/test-runner-browserstack": "^0.8.0", 87 | "@web/test-runner-mocha": "^0.9.0", 88 | "@web/test-runner-playwright": "^0.11.1", 89 | "argon2id": "^1.0.1", 90 | "benchmark": "^2.1.4", 91 | "bn.js": "^5.2.2", 92 | "c8": "^10.1.3", 93 | "chai": "^4.5.0", 94 | "chai-as-promised": "^7.1.2", 95 | "cpy-cli": "^6.0.0", 96 | "eckey-utils": "^0.7.14", 97 | "eslint": "^8.57.1", 98 | "eslint-config-airbnb": "^19.0.4", 99 | "eslint-config-airbnb-base": "^15.0.0", 100 | "eslint-config-airbnb-typescript": "^18.0.0", 101 | "eslint-import-resolver-typescript": "^3.10.1", 102 | "eslint-plugin-chai-friendly": "^0.7.4", 103 | "eslint-plugin-import": "^2.32.0", 104 | "eslint-plugin-unicorn": "^48.0.1", 105 | "fflate": "^0.8.2", 106 | "mocha": "^11.7.1", 107 | "playwright": "^1.55.0", 108 | "rollup": "^4.48.1", 109 | "sinon": "^20.0.0", 110 | "ts-node": "^10.9.2", 111 | "tslib": "^2.8.1", 112 | "tsx": "^4.20.5", 113 | "typescript": "^5.9.2", 114 | "web-streams-polyfill": "^4.1.0" 115 | }, 116 | "overrides": { 117 | "@web/dev-server-core": "npm:@openpgp/wtr-dev-server-core@0.7.3-patch.1", 118 | "@web/test-runner-core": "npm:@openpgp/wtr-test-runner-core@0.13.4-patch.2" 119 | }, 120 | "repository": { 121 | "type": "git", 122 | "url": "https://github.com/ProtonMail/openpgpjs" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/crypto/brainpool_rfc7027.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { brainpoolP256r1 } from '../../src/crypto/public_key/elliptic/brainpool/brainpoolP256r1'; 4 | import { brainpoolP384r1 } from '../../src/crypto/public_key/elliptic/brainpool/brainpoolP384r1'; 5 | import { brainpoolP512r1 } from '../../src/crypto/public_key/elliptic/brainpool/brainpoolP512r1'; 6 | import util from '../../src/util'; 7 | 8 | const rfc7027 = [ 9 | { 10 | 'curve': 'brainpoolP256r1', 11 | 'dA': '81DB1EE100150FF2EA338D708271BE38300CB54241D79950F77B063039804F1D', 12 | 'QAx': '44106E913F92BC02A1705D9953A8414DB95E1AAA49E81D9E85F929A8E3100BE5', 13 | 'QAy': '8AB4846F11CACCB73CE49CBDD120F5A900A69FD32C272223F789EF10EB089BDC', 14 | 'dB': '55E40BC41E37E3E2AD25C3C6654511FFA8474A91A0032087593852D3E7D76BD3', 15 | 'QBx': '8D2D688C6CF93E1160AD04CC4429117DC2C41825E1E9FCA0ADDD34E6F1B39F7B', 16 | 'QBy': '990C57520812BE512641E47034832106BC7D3E8DD0E4C7F1136D7006547CEC6A', 17 | 'Zx': '89AFC39D41D3B327814B80940B042590F96556EC91E6AE7939BCE31F3A18BF2B', 18 | 'Zy': '49C27868F4ECA2179BFD7D59B1E3BF34C1DBDE61AE12931648F43E59632504DE' 19 | }, 20 | { 21 | 'curve': 'brainpoolP384r1', 22 | 'dA': '1E20F5E048A5886F1F157C74E91BDE2B98C8B52D58E5003D57053FC4B0BD65D6F15EB5D1EE1610DF870795143627D042', 23 | 'QAx': '68B665DD91C195800650CDD363C625F4E742E8134667B767B1B476793588F885AB698C852D4A6E77A252D6380FCAF068', 24 | 'QAy': '55BC91A39C9EC01DEE36017B7D673A931236D2F1F5C83942D049E3FA20607493E0D038FF2FD30C2AB67D15C85F7FAA59', 25 | 'dB': '032640BC6003C59260F7250C3DB58CE647F98E1260ACCE4ACDA3DD869F74E01F8BA5E0324309DB6A9831497ABAC96670', 26 | 'QBx': '4D44326F269A597A5B58BBA565DA5556ED7FD9A8A9EB76C25F46DB69D19DC8CE6AD18E404B15738B2086DF37E71D1EB4', 27 | 'QBy': '62D692136DE56CBE93BF5FA3188EF58BC8A3A0EC6C1E151A21038A42E9185329B5B275903D192F8D4E1F32FE9CC78C48', 28 | 'Zx': '0BD9D3A7EA0B3D519D09D8E48D0785FB744A6B355E6304BC51C229FBBCE239BBADF6403715C35D4FB2A5444F575D4F42', 29 | 'Zy': '0DF213417EBE4D8E40A5F76F66C56470C489A3478D146DECF6DF0D94BAE9E598157290F8756066975F1DB34B2324B7BD' 30 | }, 31 | { 32 | 'curve': 'brainpoolP512r1', 33 | 'dA': '16302FF0DBBB5A8D733DAB7141C1B45ACBC8715939677F6A56850A38BD87BD59B09E80279609FF333EB9D4C061231FB26F92EEB04982A5F1D1764CAD57665422', 34 | 'QAx': '0A420517E406AAC0ACDCE90FCD71487718D3B953EFD7FBEC5F7F27E28C6149999397E91E029E06457DB2D3E640668B392C2A7E737A7F0BF04436D11640FD09FD', 35 | 'QAy': '72E6882E8DB28AAD36237CD25D580DB23783961C8DC52DFA2EC138AD472A0FCEF3887CF62B623B2A87DE5C588301EA3E5FC269B373B60724F5E82A6AD147FDE7', 36 | 'dB': '230E18E1BCC88A362FA54E4EA3902009292F7F8033624FD471B5D8ACE49D12CFABBC19963DAB8E2F1EBA00BFFB29E4D72D13F2224562F405CB80503666B25429', 37 | 'QBx': '9D45F66DE5D67E2E6DB6E93A59CE0BB48106097FF78A081DE781CDB31FCE8CCBAAEA8DD4320C4119F1E9CD437A2EAB3731FA9668AB268D871DEDA55A5473199F', 38 | 'QBy': '2FDC313095BCDD5FB3A91636F07A959C8E86B5636A1E930E8396049CB481961D365CC11453A06C719835475B12CB52FC3C383BCE35E27EF194512B71876285FA', 39 | 'Zx': 'A7927098655F1F9976FA50A9D566865DC530331846381C87256BAF3226244B76D36403C024D7BBF0AA0803EAFF405D3D24F11A9B5C0BEF679FE1454B21C4CD1F', 40 | 'Zy': '7DB71C3DEF63212841C463E881BDCF055523BD368240E6C3143BD8DEF8B3B3223B95E0F53082FF5E412F4222537A43DF1C6D25729DDB51620A832BE6A26680A2' 41 | } 42 | ]; 43 | 44 | const hexToBigint = hex => BigInt(`0x${hex}`); 45 | 46 | // prettier-ignore 47 | const BRAINPOOL = { 48 | brainpoolP256r1, 49 | brainpoolP384r1, 50 | brainpoolP512r1 51 | }; 52 | 53 | export default () => describe('Brainpool curves (RFC7027)', () => { 54 | it('Field orders', () => { 55 | const vectors = { 56 | brainpoolP256r1: BigInt('0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377'), 57 | brainpoolP384r1: BigInt('0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b412b1da197fb71123acd3a729901d1a71874700133107ec53'), 58 | brainpoolP512r1: BigInt('0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3') 59 | }; 60 | for (const n of Object.keys(vectors)) { expect(BRAINPOOL[n].CURVE.Fp.ORDER).to.deep.equal(vectors[n]); } 61 | }); 62 | 63 | for (const v of rfc7027) { 64 | it(v.curve, () => { 65 | const curve = BRAINPOOL[v.curve]; 66 | const secKeyA = util.hexToUint8Array(v.dA); 67 | const pubKeyA = curve.getPublicKey(secKeyA); 68 | const pubPointA = curve.ProjectivePoint.fromHex(pubKeyA); 69 | expect(pubPointA.x).to.equal(hexToBigint(v.QAx)); 70 | expect(pubPointA.y).to.equal(hexToBigint(v.QAy)); 71 | const secKeyB = hexToBigint(v.dB); 72 | const pubKeyB = curve.getPublicKey(secKeyB); 73 | const pubPointB = curve.ProjectivePoint.fromHex(pubKeyB); 74 | expect(pubPointB.x).to.equal(hexToBigint(v.QBx)); 75 | expect(pubPointB.y).to.equal(hexToBigint(v.QBy)); 76 | const shared = curve.getSharedSecret(secKeyA, pubKeyB); 77 | const sharedPoint = curve.ProjectivePoint.fromHex(shared); 78 | expect(sharedPoint.x).to.equal(hexToBigint(v.Zx)); 79 | expect(sharedPoint.y).to.equal(hexToBigint(v.Zy)); 80 | expect(shared).to.deep.equal(curve.getSharedSecret(secKeyB, pubKeyA)); 81 | }); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /src/crypto/public_key/elgamal.js: -------------------------------------------------------------------------------- 1 | // GPG4Browsers - An OpenPGP implementation in javascript 2 | // Copyright (C) 2011 Recurity Labs GmbH 3 | // 4 | // This library is free software; you can redistribute it and/or 5 | // modify it under the terms of the GNU Lesser General Public 6 | // License as published by the Free Software Foundation; either 7 | // version 3.0 of the License, or (at your option) any later version. 8 | // 9 | // This library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | // Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with this library; if not, write to the Free Software 16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | /** 19 | * @fileoverview ElGamal implementation 20 | * @module crypto/public_key/elgamal 21 | */ 22 | import { getRandomBigInteger } from '../random'; 23 | import { emeEncode, emeDecode } from '../pkcs1'; 24 | import { bigIntToUint8Array, bitLength, byteLength, mod, modExp, modInv, uint8ArrayToBigInt } from '../biginteger'; 25 | 26 | const _1n = BigInt(1); 27 | 28 | /** 29 | * ElGamal Encryption function 30 | * Note that in OpenPGP, the message needs to be padded with PKCS#1 (same as RSA) 31 | * @param {Uint8Array} data - To be padded and encrypted 32 | * @param {Uint8Array} p 33 | * @param {Uint8Array} g 34 | * @param {Uint8Array} y 35 | * @returns {Promise<{ c1: Uint8Array, c2: Uint8Array }>} 36 | * @async 37 | */ 38 | export async function encrypt(data, p, g, y) { 39 | p = uint8ArrayToBigInt(p); 40 | g = uint8ArrayToBigInt(g); 41 | y = uint8ArrayToBigInt(y); 42 | 43 | const padded = emeEncode(data, byteLength(p)); 44 | const m = uint8ArrayToBigInt(padded); 45 | 46 | // OpenPGP uses a "special" version of ElGamal where g is generator of the full group Z/pZ* 47 | // hence g has order p-1, and to avoid that k = 0 mod p-1, we need to pick k in [1, p-2] 48 | const k = getRandomBigInteger(_1n, p - _1n); 49 | return { 50 | c1: bigIntToUint8Array(modExp(g, k, p)), 51 | c2: bigIntToUint8Array(mod(modExp(y, k, p) * m, p)) 52 | }; 53 | } 54 | 55 | /** 56 | * ElGamal Encryption function 57 | * @param {Uint8Array} c1 58 | * @param {Uint8Array} c2 59 | * @param {Uint8Array} p 60 | * @param {Uint8Array} x 61 | * @param {Uint8Array} randomPayload - Data to return on unpadding error, instead of throwing 62 | * (needed for constant-time processing) 63 | * @returns {Promise} Unpadded message. 64 | * @throws {Error} on decryption error, unless `randomPayload` is given 65 | * @async 66 | */ 67 | export async function decrypt(c1, c2, p, x, randomPayload) { 68 | c1 = uint8ArrayToBigInt(c1); 69 | c2 = uint8ArrayToBigInt(c2); 70 | p = uint8ArrayToBigInt(p); 71 | x = uint8ArrayToBigInt(x); 72 | 73 | const padded = mod(modInv(modExp(c1, x, p), p) * c2, p); 74 | return emeDecode(bigIntToUint8Array(padded, 'be', byteLength(p)), randomPayload); 75 | } 76 | 77 | /** 78 | * Validate ElGamal parameters 79 | * @param {Uint8Array} p - ElGamal prime 80 | * @param {Uint8Array} g - ElGamal group generator 81 | * @param {Uint8Array} y - ElGamal public key 82 | * @param {Uint8Array} x - ElGamal private exponent 83 | * @returns {Promise} Whether params are valid. 84 | * @async 85 | */ 86 | export async function validateParams(p, g, y, x) { 87 | p = uint8ArrayToBigInt(p); 88 | g = uint8ArrayToBigInt(g); 89 | y = uint8ArrayToBigInt(y); 90 | 91 | // Check that 1 < g < p 92 | if (g <= _1n || g >= p) { 93 | return false; 94 | } 95 | 96 | // Expect p-1 to be large 97 | const pSize = BigInt(bitLength(p)); 98 | const _1023n = BigInt(1023); 99 | if (pSize < _1023n) { 100 | return false; 101 | } 102 | 103 | /** 104 | * g should have order p-1 105 | * Check that g ** (p-1) = 1 mod p 106 | */ 107 | if (modExp(g, p - _1n, p) !== _1n) { 108 | return false; 109 | } 110 | 111 | /** 112 | * Since p-1 is not prime, g might have a smaller order that divides p-1 113 | * We want to make sure that the order is large enough to hinder a small subgroup attack 114 | * 115 | * We just check g**i != 1 for all i up to a threshold 116 | */ 117 | let res = g; 118 | let i = BigInt(1); 119 | const _2n = BigInt(2); 120 | const threshold = _2n << BigInt(17); // we want order > threshold 121 | while (i < threshold) { 122 | res = mod(res * g, p); 123 | if (res === _1n) { 124 | return false; 125 | } 126 | i++; 127 | } 128 | 129 | /** 130 | * Re-derive public key y' = g ** x mod p 131 | * Expect y == y' 132 | * 133 | * Blinded exponentiation computes g**{r(p-1) + x} to compare to y 134 | */ 135 | x = uint8ArrayToBigInt(x); 136 | const r = getRandomBigInteger(_2n << (pSize - _1n), _2n << pSize); // draw r of same size as p-1 137 | const rqx = (p - _1n) * r + x; 138 | if (y !== modExp(g, rqx, p)) { 139 | return false; 140 | } 141 | 142 | return true; 143 | } 144 | -------------------------------------------------------------------------------- /test/crypto/hash/sha.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { computeDigest } from '../../../src/crypto/hash'; 4 | import util from '../../../src/util.js'; 5 | import enums from '../../../src/enums.js'; 6 | 7 | export default () => it('SHA* with test vectors from NIST FIPS 180-2', async function() { 8 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha1, util.stringToUint8Array('abc')), 'hash.sha1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d')).to.equal('a9993e364706816aba3e25717850c26c9cd0d89d'); 9 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha1, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 84983e441c3bd26ebaae4aa1f95129e5e54670f1')).to.equal('84983e441c3bd26ebaae4aa1f95129e5e54670f1'); 10 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha224, util.stringToUint8Array('abc')), 'hash.sha224("abc") = 23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7')).to.equal('23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7'); 11 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha224, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha224("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525')).to.equal('75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525'); 12 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha256, util.stringToUint8Array('abc')), 'hash.sha256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')).to.equal('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); 13 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha256, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1')).to.equal('248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1'); 14 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha384, util.stringToUint8Array('abc')), 'hash.sha384("abc") = cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7')).to.equal('cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7'); 15 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha384, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha384("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b')).to.equal('3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b'); 16 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha512, util.stringToUint8Array('abc')), 'hash.sha512("abc") = ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f')).to.equal('ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'); 17 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha512, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445')).to.equal('204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445'); 18 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha3_256, util.stringToUint8Array('abc')), 'hash.sha3_256("abc") = 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532')).to.equal('3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532'); 19 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha3_256, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha3_256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376')).to.equal('41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376'); 20 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha3_512, util.stringToUint8Array('abc')), 'hash.sha3_512("abc") = b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0')).to.equal('b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0'); 21 | expect(util.uint8ArrayToHex(await computeDigest(enums.hash.sha3_512, util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha3_512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 04a371e84ecfb5b8b77cb48610fca8182dd457ce6f326a0fd3d7ec2f1e91636dee691fbe0c985302ba1b0d8dc78c086346b533b49c030d99a27daf1139d6e75e')).to.equal('04a371e84ecfb5b8b77cb48610fca8182dd457ce6f326a0fd3d7ec2f1e91636dee691fbe0c985302ba1b0d8dc78c086346b533b49c030d99a27daf1139d6e75e'); 22 | }); 23 | -------------------------------------------------------------------------------- /test/general/forwarding.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import openpgp from '../initOpenpgp.js'; 4 | 5 | const charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- 6 | 7 | xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 8 | R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy 9 | bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW 10 | IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA 11 | /2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ 12 | LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW 13 | TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF 14 | EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC 15 | ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS 16 | zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS 17 | 3t9mIZPc+zRJtCHzQYmhDg== 18 | =lESj 19 | -----END PGP PRIVATE KEY BLOCK-----`; 20 | 21 | const fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- 22 | 23 | wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw 24 | yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E 25 | 0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC 26 | 7lM8r1DumNnO8srssko2qIja 27 | =pVRa 28 | -----END PGP MESSAGE-----`; 29 | 30 | export default () => describe('Forwarding', function() { 31 | it('can decrypt forwarded ciphertext', async function() { 32 | const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); 33 | 34 | await expect(openpgp.decrypt({ 35 | message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), 36 | decryptionKeys: charlieKey 37 | })).to.be.rejectedWith(/Error decrypting message/); 38 | 39 | const result = await openpgp.decrypt({ 40 | message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), 41 | decryptionKeys: charlieKey, 42 | config: { allowForwardedMessages: true } 43 | }); 44 | 45 | expect(result.data).to.equal('Message for Bob'); 46 | }); 47 | 48 | it('supports serialising key with KDF params for forwarding', async function() { 49 | const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); 50 | 51 | const serializedKey = charlieKey.write(); 52 | const { data: expectedSerializedKey } = await openpgp.unarmor(charlieKeyArmored); 53 | expect(serializedKey).to.deep.equal(expectedSerializedKey); 54 | }); 55 | 56 | it('generates subkey with forwarding flag (0x40)', async function() { 57 | const { privateKey: armoredKey } = await openpgp.generateKey({ userIDs: { email: 'test@forwarding.it' }, subkeys: [{ forwarding: true }, {}] }); 58 | const privateKey = await openpgp.readKey({ armoredKey }); 59 | 60 | expect(privateKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); 61 | expect(privateKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); 62 | }); 63 | 64 | it('reformatting a key preserves its forwarding flags (0x40)', async function() { 65 | // two subkeys, the first with forwarding flag, the second with standard encryption ones 66 | const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- 67 | 68 | xVgEZPhkahYJKwYBBAHaRw8BAQdARUPOBft22XPObTCYNRD2VB8ESYHOZsII 69 | XrpUHn2AstUAAQCl30ZHts8cyRRXw7B2595L8RIovkwxhnCRTqe+V92+2BFK 70 | zRQ8dGVzdEBmb3J3YXJkaW5nLml0PsKMBBAWCgA+BYJk+GRqBAsJBwgJkLvy 71 | KUWO/JamAxUICgQWAAIBAhkBApsDAh4BFiEEM00dF5bOjezdbhYlu/IpRY78 72 | lqYAAP6uAQDt7Xxoh+VUB/xkOX1cj7at7U7zrKAxq7Xh1YbGM+RHKgEAgRoz 73 | UGXKsQigC2KyXGW0nObT8RfUcQIUyrkVdImWiAjHXQRk+GRqEgorBgEEAZdV 74 | AQUBAQdA1E/PrQHG7g8UW7v7fKwgc0x+jTHp8cOa3SGAqd3Pc3gDAQgHAAD/ 75 | TY0mClFVWkDM/W6CnN7pOO36baJ0o1LJAVHucDTbxOgSMMJ4BBgWCAAqBYJk 76 | +GRqCZC78ilFjvyWpgKbQBYhBDNNHReWzo3s3W4WJbvyKUWO/JamAABzegEA 77 | mP3WSG1pceOppv5ncSoZJ9GZoaiXxnkk2TyLvmBQi7kA/1MoAjQDjF3XbX8y 78 | ScSjs3juhSAQ/MnFj8RsDaI7XdIBx10EZPhkahIKKwYBBAGXVQEFAQEHQEyC 79 | E9n5Jo23u9OfoVcUwEfQj4yAMhNBII3j5ePRDaYXAwEIBwAA/2M7YfJN9jV4 80 | LuiY7ldrWsd875xA5s6I6/8aOtUHuJcYEmPCeAQYFggAKgWCZPhkagmQu/Ip 81 | RY78lqYCmwwWIQQzTR0Xls6N7N1uFiW78ilFjvyWpgAA5KEBAKaoHbyi3wpr 82 | jt2m75fdx10rDOxJDR9H6ilI5ygLWeLsAPoCozX/3KhXLx8WbTe7MFcGl47J 83 | YdgLdgXl0dn/xdXjCQ== 84 | =eC8z 85 | -----END PGP PRIVATE KEY BLOCK-----` }); 86 | 87 | const { privateKey: reformattedKey } = await openpgp.reformatKey({ privateKey, userIDs: { email: 'test@forwarding.it' }, format: 'object' }); 88 | 89 | expect(reformattedKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); 90 | expect(reformattedKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); 91 | }); 92 | 93 | it('refuses to encrypt using encryption key with forwarding flag (0x40)', async function() { 94 | const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); 95 | 96 | await expect(openpgp.encrypt({ 97 | message: await openpgp.createMessage({ text: 'abc' }), 98 | encryptionKeys: charlieKey 99 | })).to.be.rejectedWith(/Could not find valid encryption key packet/); 100 | }); 101 | }); 102 | --------------------------------------------------------------------------------