Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to OpenPGP.js v6 #190

Merged
merged 30 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cfd0370
Upgrade to OpenPGP.js v6
larabr Nov 30, 2023
340a293
Drop BigInt fallback code
larabr May 14, 2024
5f892b2
Update ESLint and plugins, fix errors
larabr Nov 30, 2023
6f02480
Fix/update compatibility test rejecting v6 keys
larabr Nov 29, 2023
bc9bfb3
Test (temp, canary version only): guard against including sha3 in gen…
larabr Dec 11, 2023
bffb3ff
Align web-stream-tools version with OpenPGP.js, fix test-type-definit…
larabr Mar 4, 2024
3ed72ad
Do not automatically generate SEIPDv2 messages when encrypting to pub…
larabr Apr 19, 2024
a4c2b7b
Run npm update
larabr May 17, 2024
ab2a71c
8.0.0-canary.3.patch.0
larabr May 28, 2024
bb43e49
Update to OpenPGP.js v6 beta.2
larabr Jul 5, 2024
aa0cc96
Run npm audit
larabr Jul 5, 2024
be320b2
8.0.0-canary.4
larabr Jul 8, 2024
da102b3
`checkKeyCompatibility`: reject v5 keys (#202)
larabr Jul 15, 2024
18e3269
Update to OpenPGP.js v6 beta.3
larabr Sep 9, 2024
9d7206a
8.0.0-canary.5
larabr Sep 9, 2024
bff29cf
Update to OpenPGP.js v6 beta.3.patch.1
larabr Sep 11, 2024
92bd4cf
Run npm audit
larabr Sep 11, 2024
06b5093
CI: update playwright to test latest browser versions
larabr Sep 11, 2024
52b4467
8.0.0-canary.5.patch.0
larabr Sep 11, 2024
c7f5a82
CI: setup dependabot to automatically update playwright
larabr Sep 11, 2024
3f12e3f
Remove unused AES_CFB module and asmcrypto dependency
larabr Sep 11, 2024
1a65851
Linter: enforce type-only imports and exports
larabr Sep 18, 2024
11342c3
CI: run tests also on macOS
larabr Oct 24, 2024
c9aa085
CI: update Actions to Node 20
larabr Oct 24, 2024
17aed01
Update to OpenPGP.js v6.0.0 (stable)
larabr Nov 6, 2024
0016a46
Test: expect SHA3 to be included in generated key prefs for v6
larabr Nov 6, 2024
a2161b2
`checkKeyCompatibility`: add v6 key support through new `v6KeysAllowe…
larabr Nov 6, 2024
d1cb40f
Set `config.enableParsingV5Entities = true` to log whether v5 keys ar…
larabr Nov 7, 2024
80fc6bb
Update web-stream-tools to v0.1.3 (align with OpenPGP.js)
larabr Nov 7, 2024
04ba4b2
Run npm update
larabr Nov 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"sourceType": "module",
"project": "tsconfig.eslint.json"
},

"settings": {
"import/resolver": {
"typescript": { "alwaysTryTypes": true }
}
},
"globals": {
"window": "readonly",
"btoa": "readonly",
Expand Down Expand Up @@ -82,12 +86,13 @@
"@typescript-eslint/naming-convention": ["error", {
"selector": "typeLike",
"format": ["PascalCase", "UPPER_CASE"]
}],
}],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/indent": ["error", 4],
"@typescript-eslint/comma-dangle": "off"

}
}
9 changes: 9 additions & 0 deletions .github/.dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-name: "playwright"
versioning-strategy: increase
17 changes: 12 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ on:
jobs:
tests:
name: Tests
runs-on: ubuntu-latest

strategy:
fail-fast: false # if tests for one version fail, continue with the rest
matrix:
# run on multiple platforms to test platform-specific code, if present
# (e.g. webkit's WebCrypto API implementation is different in macOS vs Linux)
runner: ['ubuntu-latest', 'macos-latest']
runs-on: ${{ matrix.runner }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

- name: Install dependencies
run: npm ci

- run: npm run lint
- if: ${{ matrix.runner == 'ubuntu-latest' }}
run: npm run lint
- run: npm run test-type-definitions

- name: Install Chrome
Expand Down
174 changes: 174 additions & 0 deletions lib/bigInteger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// These util functions are copied as-is from openpgpjs v6
// Operations are not constant time, but we try and limit timing leakage where we can

const _0n = BigInt(0);
const _1n = BigInt(1);

export function uint8ArrayToBigInt(bytes: Uint8Array) {
const hexAlphabet = '0123456789ABCDEF';
let s = '';
bytes.forEach((v) => {
s += hexAlphabet[v >> 4] + hexAlphabet[v & 15];
});
return BigInt('0x0' + s);
}

export function mod(a: bigint, m: bigint) {
const reduced = a % m;
return reduced < _0n ? reduced + m : reduced;
}

/**
* Compute modular exponentiation using square and multiply
* @param {BigInt} a - Base
* @param {BigInt} e - Exponent
* @param {BigInt} n - Modulo
* @returns {BigInt} b ** e mod n.
*/
export function modExp(b: bigint, e: bigint, n: bigint) {
if (n === _0n) throw Error('Modulo cannot be zero');
if (n === _1n) return BigInt(0);
if (e < _0n) throw Error('Unsopported negative exponent');

let exp = e;
let x = b;

x %= n;
let r = BigInt(1);
while (exp > _0n) {
const lsb = exp & _1n;
exp >>= _1n; // e / 2
// Always compute multiplication step, to reduce timing leakage
const rx = (r * x) % n;
// Update r only if lsb is 1 (odd exponent)
r = lsb ? rx : r;
x = (x * x) % n; // Square
}
return r;
}

function abs(x: bigint) {
return x >= _0n ? x : -x;
}

/**
* Extended Eucleadian algorithm (http://anh.cs.luc.edu/331/notes/xgcd.pdf)
* Given a and b, compute (x, y) such that ax + by = gdc(a, b).
* Negative numbers are also supported.
* @param {BigInt} a - First operand
* @param {BigInt} b - Second operand
* @returns {{ gcd, x, y: bigint }}
*/
function _egcd(aInput: bigint, bInput: bigint) {
let x = BigInt(0);
let y = BigInt(1);
let xPrev = BigInt(1);
let yPrev = BigInt(0);

// Deal with negative numbers: run algo over absolute values,
// and "move" the sign to the returned x and/or y.
// See https://math.stackexchange.com/questions/37806/extended-euclidean-algorithm-with-negative-numbers
let a = abs(aInput);
let b = abs(bInput);
const aNegated = aInput < _0n;
const bNegated = bInput < _0n;

while (b !== _0n) {
const q = a / b;
let tmp = x;
x = xPrev - q * x;
xPrev = tmp;

tmp = y;
y = yPrev - q * y;
yPrev = tmp;

tmp = b;
b = a % b;
a = tmp;
}

return {
x: aNegated ? -xPrev : xPrev,
y: bNegated ? -yPrev : yPrev,
gcd: a
};
}

/**
* Compute the inverse of `a` modulo `n`
* Note: `a` and and `n` must be relatively prime
* @param {BigInt} a
* @param {BigInt} n - Modulo
* @returns {BigInt} x such that a*x = 1 mod n
* @throws {Error} if the inverse does not exist
*/
export function modInv(a: bigint, n: bigint) {
const { gcd, x } = _egcd(a, n);
if (gcd !== _1n) {
throw new Error('Inverse does not exist');
}
return mod(x + n, n);
}

/**
* Compute bit length
*/
export function bitLength(x: bigint) {
// -1n >> -1n is -1n
// 1n >> 1n is 0n
const target = x < _0n ? BigInt(-1) : _0n;
let bitlen = 1;
let tmp = x;
// eslint-disable-next-line no-cond-assign
while ((tmp >>= _1n) !== target) {
bitlen++;
}
return bitlen;
}

/**
* Compute byte length
*/
export function byteLength(x: bigint) {
const target = x < _0n ? BigInt(-1) : _0n;
const _8n = BigInt(8);
let len = 1;
let tmp = x;
// eslint-disable-next-line no-cond-assign
while ((tmp >>= _8n) !== target) {
len++;
}
return len;
}

/**
* Get Uint8Array representation of this number
* @param {String} endian - Endianess of output array (defaults to 'be')
* @param {Number} length - Of output array
* @returns {Uint8Array}
*/
export function bigIntToUint8Array(x: bigint, endian = 'be', length?: number) {
// we get and parse the hex string (https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/)
// this is faster than shift+mod iterations
let hex = x.toString(16);
if (hex.length % 2 === 1) {
hex = '0' + hex;
}

const rawLength = hex.length / 2;
const bytes = new Uint8Array(length || rawLength);
// parse hex
const offset = length ? length - rawLength : 0;
let i = 0;
while (i < rawLength) {
bytes[i + offset] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
i++;
}

if (endian !== 'be') {
bytes.reverse();
}

return bytes;
}
2 changes: 1 addition & 1 deletion lib/crypto/argon2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Argon2S2K, Config, config as defaultConfig } from '../openpgp';
import { Argon2S2K, type Config, config as defaultConfig } from '../openpgp';
import { ARGON2_PARAMS } from '../constants';

type Argon2Params = Config['s2kArgon2Params'] & {
Expand Down
7 changes: 0 additions & 7 deletions lib/crypto/cfb.js

This file was deleted.

12 changes: 6 additions & 6 deletions lib/crypto/hash.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MaybeStream } from '../pmcrypto';
import type { MaybeWebStream } from '../pmcrypto';
import md5 from './_md5';

export const SHA256 = async (data: Uint8Array) => {
Expand All @@ -23,21 +23,21 @@ export const unsafeMD5 = (data: Uint8Array) => md5(data);
* DO NOT USE in contexts where collision resistance is important
* @see openpgp.crypto.hash.sha1
*/
export async function unsafeSHA1(data: MaybeStream<Uint8Array>) {
export async function unsafeSHA1(data: MaybeWebStream<Uint8Array>) {
if (data instanceof Uint8Array) {
const digest = await crypto.subtle.digest('SHA-1', data);
return new Uint8Array(digest);
}

const { Sha1: StreamableSHA1 } = await import('@openpgp/asmcrypto.js/dist_es8/hash/sha1/sha1');
const hashInstance = new StreamableSHA1();
const { sha1 } = await import('@noble/hashes/sha1');
const hashInstance = sha1.create();
const inputReader = data.getReader(); // AsyncInterator is still not widely supported
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await inputReader.read();
if (done) {
return hashInstance.finish().result || new Uint8Array();
return hashInstance.digest();
}
hashInstance.process(value);
hashInstance.update(value);
}
}
34 changes: 23 additions & 11 deletions lib/key/check.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AlgorithmInfo, PublicKey, enums } from '../openpgp';
import { type AlgorithmInfo, type PublicKey, enums } from '../openpgp';

/**
* Checks whether the primary key and the subkeys meet our recommended security requirements.
Expand Down Expand Up @@ -47,9 +47,24 @@ export function checkKeyStrength(publicKey: PublicKey) {
}

/**
* Checks whether the key is compatible with all Proton clients.
* Checks whether the key is compatible with other Proton clients, also based on v6 key support.
*/
export function checkKeyCompatibility(publicKey: PublicKey) {
export function checkKeyCompatibility(publicKey: PublicKey, v6KeysAllowed = false) {
const keyVersion = publicKey.keyPacket.version;
const keyVersionIsSupported = keyVersion === 4 || (v6KeysAllowed && keyVersion === 6);
if (!keyVersionIsSupported) {
throw new Error(`Version ${publicKey.keyPacket.version} keys are currently not supported.`);
}

// These algo are restricted to v6 keys, since they have been added in the same RFC (RFC 9580),
// and they are thus not implemented by clients without v6 support.
const v6OnlyPublicKeyAlgorithms = [
enums.publicKey.ed25519,
enums.publicKey.ed448,
enums.publicKey.x25519,
enums.publicKey.x448
];

const supportedPublicKeyAlgorithms = new Set([
enums.publicKey.dsa,
enums.publicKey.elgamal,
Expand All @@ -58,25 +73,22 @@ export function checkKeyCompatibility(publicKey: PublicKey) {
enums.publicKey.rsaEncrypt,
enums.publicKey.ecdh,
enums.publicKey.ecdsa,
enums.publicKey.eddsaLegacy
enums.publicKey.eddsaLegacy,
...(keyVersion === 6 ? v6OnlyPublicKeyAlgorithms : [])
]);

const supportedCurves: Set<AlgorithmInfo['curve']> = new Set([
enums.curve.ed25519Legacy,
enums.curve.curve25519Legacy,
enums.curve.p256,
enums.curve.p384,
enums.curve.p521,
enums.curve.nistP256,
enums.curve.nistP384,
enums.curve.nistP521,
enums.curve.brainpoolP256r1,
enums.curve.brainpoolP384r1,
enums.curve.brainpoolP512r1,
enums.curve.secp256k1
]);

if (publicKey.keyPacket.version > 5) {
throw new Error(`Version ${publicKey.keyPacket.version} keys are currently not supported.`);
}

publicKey.getKeys().forEach(({ keyPacket }) => {
const keyInfo = keyPacket.getAlgorithmInfo();
// @ts-ignore missing `write` declaration
Expand Down
Loading