mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-06 23:39:38 +01:00
feat(protocol): add THP cryptography
Co-authored-by: Ondřej Vejpustek <ondrej.vejpustek@satoshilabs.com> following: https://github.com/trezor/trezor-firmware/pull/3435
This commit is contained in:
committed by
Szymon Lesisz
parent
84b3a67442
commit
b3056f54d2
25
packages/protocol/src/protocol-thp/crypto/aesgcm.ts
Normal file
25
packages/protocol/src/protocol-thp/crypto/aesgcm.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export const aesgcm = (key: Buffer, iv: Buffer) => {
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||
|
||||
return {
|
||||
auth: (authData: Buffer) => {
|
||||
cipher.setAAD(authData);
|
||||
decipher.setAAD(authData);
|
||||
},
|
||||
encrypt: (plainText: Buffer) => {
|
||||
const encrypted = cipher.update(plainText);
|
||||
|
||||
return Buffer.concat([encrypted, cipher.final()]);
|
||||
},
|
||||
decrypt: (cipherText: Buffer, authTag: Buffer) => {
|
||||
decipher.setAuthTag(authTag);
|
||||
const decrypted = decipher.update(cipherText);
|
||||
|
||||
return Buffer.concat([decrypted, decipher.final()]);
|
||||
},
|
||||
finish: () => cipher.getAuthTag(),
|
||||
};
|
||||
};
|
||||
260
packages/protocol/src/protocol-thp/crypto/curve25519.ts
Normal file
260
packages/protocol/src/protocol-thp/crypto/curve25519.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
// TypeScript implementation of elligator2 https://www.rfc-editor.org/rfc/rfc9380.html#ell2-opt
|
||||
|
||||
let constants: ReturnType<typeof getConstants> | undefined;
|
||||
|
||||
const getConstants = (): {
|
||||
p: bigint;
|
||||
J: bigint;
|
||||
c3: bigint;
|
||||
c4: bigint;
|
||||
a24: bigint;
|
||||
} => {
|
||||
if (constants) {
|
||||
return constants;
|
||||
}
|
||||
|
||||
if (typeof BigInt === 'undefined') {
|
||||
throw new Error('curve25519: BigInt not supported');
|
||||
}
|
||||
|
||||
const p = 2n ** 255n - 19n;
|
||||
const J = 486662n;
|
||||
|
||||
const c3 = BigInt(
|
||||
'19681161376707505956807079304988542015446066515923890162744021073123829784752',
|
||||
); // sqrt(-1)
|
||||
|
||||
const c4 = (p - 5n) / 8n;
|
||||
const a24 = (J + 2n) / 4n;
|
||||
|
||||
const ctx = {
|
||||
p,
|
||||
J,
|
||||
c3,
|
||||
c4,
|
||||
a24,
|
||||
};
|
||||
|
||||
constants = ctx;
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// python int.from_bytes(array, "little")
|
||||
function littleEndianBytesToBigInt(bytes: Uint8Array): bigint {
|
||||
let result = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result += BigInt(bytes[i]) << (8n * BigInt(i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// python int.to_bytes(32, "little")
|
||||
function bigintToLittleEndianBytes(value: bigint, length: number = 32): Uint8Array {
|
||||
const byteArray = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
byteArray[i] = Number(value & 0xffn);
|
||||
value >>= 8n;
|
||||
}
|
||||
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
// python pow(a, b, c)
|
||||
function pow(base: bigint, exp: bigint, mod: bigint): bigint {
|
||||
let result = 1n;
|
||||
base = base % mod;
|
||||
while (exp > 0) {
|
||||
if (exp % 2n === 1n) {
|
||||
result = (result * base) % mod;
|
||||
}
|
||||
exp = exp >> 1n;
|
||||
base = (base * base) % mod;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// decodeScalar25519 from
|
||||
// https://datatracker.ietf.org/doc/html/rfc7748#section-5
|
||||
function decodeScalar(scalar: Uint8Array): bigint {
|
||||
if (scalar.length !== 32) {
|
||||
throw new Error('Invalid length of scalar');
|
||||
}
|
||||
|
||||
const array = new Uint8Array(scalar);
|
||||
array[0] &= 248;
|
||||
array[31] &= 127;
|
||||
array[31] |= 64;
|
||||
|
||||
return littleEndianBytesToBigInt(array);
|
||||
}
|
||||
|
||||
// decodeUCoordinate from
|
||||
// https://datatracker.ietf.org/doc/html/rfc7748#section-5
|
||||
function decodeCoordinate(coordinate: Uint8Array): bigint {
|
||||
if (coordinate.length !== 32) {
|
||||
throw new Error('Invalid length of coordinate');
|
||||
}
|
||||
|
||||
const array = new Uint8Array(coordinate);
|
||||
array[array.length - 1] &= 0x7f;
|
||||
|
||||
return littleEndianBytesToBigInt(array);
|
||||
}
|
||||
|
||||
// encodeUCoordinate from
|
||||
// https://datatracker.ietf.org/doc/html/rfc7748#section-5
|
||||
function encodeCoordinate(coordinate: bigint): Uint8Array {
|
||||
return bigintToLittleEndianBytes(coordinate);
|
||||
}
|
||||
|
||||
// Returns (second, first) if condition is true and (first, second) otherwise
|
||||
function conditionalSwap(a: bigint, b: bigint, condition: boolean): [bigint, bigint] {
|
||||
// Must be implemented in a way that it is constant time
|
||||
// const mask = condition ? -1n : 0n;
|
||||
// const newA = (a & ~mask) | (b & mask);
|
||||
// const newB = (a & mask) | (b & ~mask);
|
||||
// return [newA, newB];
|
||||
// NOTE: typescript doesn't guarantee constant time. leaving the code just for reference
|
||||
|
||||
return condition ? [b, a] : [a, b];
|
||||
}
|
||||
|
||||
// https://hyperelliptic.org/EFD/g1p/auto-montgom-xz.html#ladder-ladd-1987-m-3
|
||||
// (x4, z4) = 2 * (x2, z2)
|
||||
// (x5, z5) = (x2, z2) + (x3, z3)
|
||||
// where (x1, 1) = (x3, z3) - (x2, z2)
|
||||
function ladderOperation(
|
||||
{ p, a24 }: ReturnType<typeof getConstants>,
|
||||
x1: bigint,
|
||||
x2: bigint,
|
||||
z2: bigint,
|
||||
x3: bigint,
|
||||
z3: bigint,
|
||||
): [bigint, bigint, bigint, bigint] {
|
||||
const a = (x2 + z2) % p;
|
||||
const aa = (a * a) % p;
|
||||
const b = (x2 - z2 + p) % p;
|
||||
const bb = (b * b) % p;
|
||||
const e = (aa - bb + p) % p;
|
||||
const c = (x3 + z3) % p;
|
||||
const d = (x3 - z3 + p) % p;
|
||||
const da = (d * a) % p;
|
||||
const cb = (c * b) % p;
|
||||
const t0 = (da + cb) % p;
|
||||
const x5 = (t0 * t0) % p;
|
||||
const t1 = (da - cb + p) % p;
|
||||
const t2 = (t1 * t1) % p;
|
||||
const z5 = (x1 * t2) % p;
|
||||
const x4 = (aa * bb) % p;
|
||||
const t3 = (a24 * e) % p;
|
||||
const t4 = (bb + t3) % p;
|
||||
const z4 = (e * t4) % p;
|
||||
|
||||
return [x4, z4, x5, z5];
|
||||
}
|
||||
|
||||
// X25519 from
|
||||
// https://datatracker.ietf.org/doc/html/rfc7748#section-5
|
||||
export function curve25519(privateKey: Uint8Array, publicKey: Uint8Array): Buffer {
|
||||
const ctx = getConstants();
|
||||
const { p } = ctx;
|
||||
const k = decodeScalar(privateKey);
|
||||
const u = decodeCoordinate(publicKey) % p;
|
||||
|
||||
const x1 = u;
|
||||
let x2 = 1n;
|
||||
let z2 = 0n;
|
||||
let x3 = u;
|
||||
let z3 = 1n;
|
||||
let swap = 0;
|
||||
|
||||
for (let i = 255; i >= 0; i--) {
|
||||
const bit = Number((k >> BigInt(i)) & 1n);
|
||||
swap ^= bit;
|
||||
[x2, x3] = conditionalSwap(x2, x3, Boolean(swap));
|
||||
[z2, z3] = conditionalSwap(z2, z3, Boolean(swap));
|
||||
swap = bit;
|
||||
[x2, z2, x3, z3] = ladderOperation(ctx, x1, x2, z2, x3, z3);
|
||||
}
|
||||
|
||||
[x2, x3] = conditionalSwap(x2, x3, Boolean(swap));
|
||||
[z2, z3] = conditionalSwap(z2, z3, Boolean(swap));
|
||||
|
||||
const x = (pow(z2, p - 2n, p) * x2) % p;
|
||||
|
||||
return Buffer.from(encodeCoordinate(x));
|
||||
}
|
||||
|
||||
// Returns second if condition is true and first otherwise
|
||||
function conditionalMove(first: bigint, second: bigint, condition: boolean): bigint {
|
||||
// Must be implemented in a way that it is constant time
|
||||
// const trueMask = condition ? -1n : 0n;
|
||||
// const falseMask = ~trueMask;
|
||||
// return (first & falseMask) | (second & trueMask);
|
||||
// NOTE: typescript doesn't guarantee constant time. leaving the code just for reference
|
||||
|
||||
return condition ? second : first;
|
||||
}
|
||||
|
||||
// map_to_curve_elligator2_curve25519 from
|
||||
// https://www.rfc-editor.org/rfc/rfc9380.html#ell2-opt
|
||||
export function elligator2(point: Uint8Array): Uint8Array {
|
||||
const ctx = getConstants();
|
||||
const { p, J, c4, c3 } = ctx;
|
||||
|
||||
const u = decodeCoordinate(point) % p;
|
||||
|
||||
let tv1 = (u * u) % p;
|
||||
tv1 = (2n * tv1) % p;
|
||||
const xd = (tv1 + 1n) % p;
|
||||
const x1n = (-J + p) % p;
|
||||
let tv2 = (xd * xd) % p;
|
||||
const gxd = (tv2 * xd) % p;
|
||||
let gx1 = (J * tv1) % p;
|
||||
gx1 = (gx1 * x1n) % p;
|
||||
gx1 = (gx1 + tv2) % p;
|
||||
gx1 = (gx1 * x1n) % p;
|
||||
|
||||
let tv3 = (gxd * gxd) % p;
|
||||
tv2 = (tv3 * tv3) % p;
|
||||
tv3 = (tv3 * gxd) % p;
|
||||
tv3 = (tv3 * gx1) % p;
|
||||
tv2 = (tv2 * tv3) % p;
|
||||
|
||||
let y11 = pow(tv2, c4, p);
|
||||
y11 = (y11 * tv3) % p;
|
||||
const y12 = (y11 * c3) % p;
|
||||
tv2 = (y11 * y11) % p;
|
||||
tv2 = (tv2 * gxd) % p;
|
||||
|
||||
const e1 = tv2 == gx1;
|
||||
const y1 = conditionalMove(y12, y11, e1);
|
||||
const x2n = (x1n * tv1) % p;
|
||||
|
||||
tv2 = (y1 * y1) % p;
|
||||
tv2 = (tv2 * gxd) % p;
|
||||
const e3 = tv2 == gx1;
|
||||
const xn = conditionalMove(x2n, x1n, e3);
|
||||
const x = (xn * pow(xd, p - 2n, p)) % p;
|
||||
|
||||
return encodeCoordinate(x);
|
||||
}
|
||||
|
||||
// https://cr.yp.to/ecdh.html
|
||||
// Computing secret keys
|
||||
export const getCurve25519KeyPair = (randomPriv: Buffer) => {
|
||||
randomPriv[0] &= 248;
|
||||
randomPriv[31] &= 127;
|
||||
randomPriv[31] |= 64;
|
||||
|
||||
const basepoint = Buffer.alloc(32).fill(0);
|
||||
basepoint[0] = 0x09;
|
||||
|
||||
return {
|
||||
publicKey: curve25519(randomPriv, basepoint),
|
||||
privateKey: randomPriv,
|
||||
};
|
||||
};
|
||||
2
packages/protocol/src/protocol-thp/crypto/index.ts
Normal file
2
packages/protocol/src/protocol-thp/crypto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { aesgcm } from './aesgcm';
|
||||
export { crc32 } from './crc32';
|
||||
53
packages/protocol/src/protocol-thp/crypto/tools.ts
Normal file
53
packages/protocol/src/protocol-thp/crypto/tools.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export const hmacSHA256 = (key: Buffer, data: Buffer) =>
|
||||
crypto.createHmac('sha256', key).update(data).digest();
|
||||
|
||||
export const sha256 = (buffer: Buffer) => crypto.createHash('sha256').update(buffer).digest();
|
||||
|
||||
export const hkdf = (chainingKey: Buffer, input: Buffer) => {
|
||||
const tempKey = hmacSHA256(chainingKey, input);
|
||||
const output1 = hmacSHA256(tempKey, Buffer.from([0x01]));
|
||||
|
||||
const ctxOutput2 = crypto.createHmac('sha256', tempKey).update(output1);
|
||||
ctxOutput2.update(Buffer.from([0x02]));
|
||||
const output2 = ctxOutput2.digest();
|
||||
|
||||
return [output1, output2];
|
||||
};
|
||||
|
||||
export const hashOfTwo = (hash1: Buffer, hash2: Buffer) =>
|
||||
crypto.createHash('sha256').update(hash1).update(hash2).digest();
|
||||
|
||||
export const getIvFromNonce = (nonce: number): Buffer => {
|
||||
const iv = new Uint8Array(12);
|
||||
const nonceBytes = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
nonceBytes[7 - i] = nonce & 0xff;
|
||||
nonce = nonce >> 8;
|
||||
}
|
||||
iv.set(nonceBytes, 4);
|
||||
|
||||
return Buffer.from(iv);
|
||||
};
|
||||
|
||||
// python int.from_bytes(array, "big")
|
||||
export const bigEndianBytesToBigInt = (bytes: Uint8Array): bigint => {
|
||||
const result: bigint[] = [];
|
||||
const { length } = bytes;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(BigInt(bytes[i] * 256 ** (length - (1 + i))));
|
||||
}
|
||||
|
||||
return result.reduce((prev, curr) => prev + curr, BigInt(0));
|
||||
};
|
||||
|
||||
// python int.from_bytes(array, "little")
|
||||
export const littleEndianBytesToBigInt = (bytes: Uint8Array): bigint => {
|
||||
let result = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result += BigInt(bytes[i]) << (8n * BigInt(i));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
27
packages/protocol/tests/protocol-thp/aesgcm.test.ts
Normal file
27
packages/protocol/tests/protocol-thp/aesgcm.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { aesgcm } from '../../src/protocol-thp/crypto/aesgcm';
|
||||
|
||||
it('AESGCM encode/decode', () => {
|
||||
const key = Buffer.from(
|
||||
'ccbf529fc8dd4662d4d1d1fa66368b8758c0b6673a1bb9d532d95ca607cbf729',
|
||||
'hex',
|
||||
);
|
||||
const iv1 = Buffer.alloc(12).fill(0);
|
||||
const plaintext = Buffer.from(
|
||||
'd28c57e2c61ccddf449fc65a585cbe98f061e0fa99911763423440ee84710c2a',
|
||||
'hex',
|
||||
);
|
||||
const authData = Buffer.from(
|
||||
'152fd53e7dcee02d6f30b80371674b0a777441ca035919724c2f6bbfad6ed7eb',
|
||||
'hex',
|
||||
);
|
||||
|
||||
const aesCtx = aesgcm(key, iv1);
|
||||
aesCtx.auth(authData);
|
||||
const staticPubKey = aesCtx.encrypt(plaintext);
|
||||
const tag = aesCtx.finish();
|
||||
|
||||
const aesCtx2 = aesgcm(key, iv1);
|
||||
aesCtx2.auth(authData);
|
||||
const decryptedData = aesCtx2.decrypt(staticPubKey, tag);
|
||||
expect(decryptedData.toString('hex')).toEqual(plaintext.toString('hex'));
|
||||
});
|
||||
3679
packages/protocol/tests/protocol-thp/curve25519.fixtures.ts
Normal file
3679
packages/protocol/tests/protocol-thp/curve25519.fixtures.ts
Normal file
File diff suppressed because it is too large
Load Diff
21
packages/protocol/tests/protocol-thp/curve25519.test.ts
Normal file
21
packages/protocol/tests/protocol-thp/curve25519.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { curve25519fixtures, elligator2fixtures } from './curve25519.fixtures';
|
||||
import { curve25519, elligator2 } from '../../src/protocol-thp/crypto/curve25519';
|
||||
|
||||
describe('curve25519', () => {
|
||||
it('elligator2', () => {
|
||||
elligator2fixtures.forEach(([input, output]) => {
|
||||
const point = Uint8Array.from(Buffer.from(input, 'hex'));
|
||||
const result = elligator2(point);
|
||||
expect(Buffer.from(result).toString('hex')).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
curve25519fixtures.forEach(f => {
|
||||
it(`curve25519 ${f.description}`, () => {
|
||||
const publicKey = Buffer.from(f.public, 'hex');
|
||||
const privateKey = Buffer.from(f.private, 'hex');
|
||||
const secret = curve25519(privateKey, publicKey);
|
||||
expect(secret.toString('hex')).toEqual(f.shared);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
"outDir": "lib",
|
||||
"target": "es2022",
|
||||
"lib": ["es2022", "esnext"]
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": []
|
||||
|
||||
Reference in New Issue
Block a user