Skip to content

Commit

Permalink
Merge pull request #190 from ProtonMail/openpgpjs-v6
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr authored Nov 7, 2024
2 parents 33e35ca + 04ba4b2 commit 7d12b22
Show file tree
Hide file tree
Showing 27 changed files with 3,457 additions and 6,577 deletions.
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

0 comments on commit 7d12b22

Please sign in to comment.