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:
Szymon Lesisz
2024-08-28 12:32:42 +02:00
committed by Szymon Lesisz
parent 84b3a67442
commit b3056f54d2
8 changed files with 4070 additions and 1 deletions

View 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(),
};
};

View 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,
};
};

View File

@@ -0,0 +1,2 @@
export { aesgcm } from './aesgcm';
export { crc32 } from './crc32';

View 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;
};

View 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'));
});

File diff suppressed because it is too large Load Diff

View 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);
});
});
});

View File

@@ -1,7 +1,9 @@
{
"extends": "../../tsconfig.lib.json",
"compilerOptions": {
"outDir": "lib"
"outDir": "lib",
"target": "es2022",
"lib": ["es2022", "esnext"]
},
"include": ["./src"],
"references": []