@reclaimprotocol/tls
Version:
TLS 1.2/1.3 for any JavaScript Environment
311 lines (310 loc) • 11.6 kB
JavaScript
import { cbc as aesCbc } from '@noble/ciphers/aes';
import { chacha20poly1305 } from '@noble/ciphers/chacha';
import { ECDSASigValue } from '@peculiar/asn1-ecc';
import { AsnParser } from '@peculiar/asn1-schema';
import { webcrypto } from 'crypto';
import { PKCS1_KEM } from 'micro-rsa-dsa-dh/rsa.js';
import { asciiToUint8Array, concatenateUint8Arrays } from "../utils/generics.js";
import { parseRsaPublicKeyFromAsn1 } from "./common.js";
const subtle = webcrypto.subtle;
const X25519_PRIVATE_KEY_DER_PREFIX = new Uint8Array([
48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 110, 4, 34, 4, 32
]);
const P384_PRIVATE_KEY_DER_PREFIX = new Uint8Array([
0x30, 0x81, 0xb6, 0x02, 0x01, 0x00, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x04, 0x81, 0x9e, 0x30, 0x81, 0x9b, 0x02, 0x01, 0x01, 0x04, 0x30
]);
const P256_PRIVATE_KEY_DER_PREFIX = new Uint8Array([
0x30, 0x81, 0xb6, 0x02, 0x01, 0x00, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x04, 0x81, 0x9e, 0x30, 0x81, 0x9b, 0x02, 0x01, 0x01, 0x04, 0x30
]);
const SHARED_KEY_LEN_MAP = {
'X25519': 32,
'P-384': 48,
'P-256': 32,
};
const AUTH_TAG_BYTE_LENGTH = 16;
export const webcryptoCrypto = {
importKey(alg, raw, ...args) {
let subtleArgs;
let keyUsages;
let keyType = 'raw';
switch (alg) {
case 'AES-256-GCM':
case 'AES-128-GCM':
subtleArgs = {
name: 'AES-GCM',
length: alg === 'AES-256-GCM' ? 256 : 128
};
keyUsages = ['encrypt', 'decrypt'];
break;
case 'AES-128-CBC':
subtleArgs = {
name: 'AES-CBC',
length: 128
};
keyUsages = ['encrypt', 'decrypt'];
break;
case 'CHACHA20-POLY1305':
// chaCha20 is not supported by webcrypto
// so we "fake" create a key
return raw;
case 'SHA-1':
case 'SHA-256':
case 'SHA-384':
subtleArgs = {
name: 'HMAC',
hash: { name: alg }
};
keyUsages = ['sign', 'verify'];
break;
case 'P-384':
case 'P-256':
subtleArgs = {
name: 'ECDH',
namedCurve: alg,
};
keyUsages = [];
if (args[0] === 'private') {
keyUsages = ['deriveBits'];
keyType = 'pkcs8';
const prefix = alg === 'P-256'
? P256_PRIVATE_KEY_DER_PREFIX
: P384_PRIVATE_KEY_DER_PREFIX;
raw = concatenateUint8Arrays([
prefix,
raw
]);
}
break;
case 'X25519':
subtleArgs = { name: 'X25519' };
keyUsages = [];
if (args[0] === 'private') {
keyUsages = ['deriveBits'];
keyType = 'pkcs8';
raw = concatenateUint8Arrays([
X25519_PRIVATE_KEY_DER_PREFIX,
raw
]);
}
break;
case 'RSA-PSS-SHA256':
keyType = 'spki';
keyUsages = ['verify'];
subtleArgs = {
name: 'RSA-PSS',
hash: 'SHA-256'
};
break;
case 'RSA-PKCS1-SHA512':
case 'RSA-PKCS1-SHA256':
case 'RSA-PKCS1-SHA384':
case 'RSA-PKCS1-SHA1':
keyType = 'spki';
keyUsages = ['verify'];
subtleArgs = {
name: 'RSASSA-PKCS1-v1_5',
hash: getHashAlgorithm(alg),
};
break;
case 'RSA-PCKS1_5':
return parseRsaPublicKeyFromAsn1(raw);
case 'ECDSA-SECP256R1-SHA256':
case 'ECDSA-SECP256R1-SHA384':
case 'ECDSA-SECP384R1-SHA384':
case 'ECDSA-SECP384R1-SHA256':
keyType = 'spki';
keyUsages = ['verify'];
subtleArgs = {
name: 'ECDSA',
namedCurve: alg.includes('P256') ? 'P-256' : 'P-384',
};
break;
default:
throw new Error(`Unsupported algorithm ${alg}`);
}
return subtle
.importKey(keyType, raw, subtleArgs, true, keyUsages);
},
async exportKey(key) {
// handle ChaCha20-Poly1305, RSA-PCKS1_5
// as that's already a Uint8Array
if (key instanceof Uint8Array) {
return key;
}
if (key.type === 'private'
&& (key.algorithm.name === 'X25519'
|| key.algorithm.name === 'ECDH')) {
const form = toUint8Array(await subtle.exportKey('pkcs8', key));
const algPrefix = key.algorithm.name === 'X25519'
? X25519_PRIVATE_KEY_DER_PREFIX
: P384_PRIVATE_KEY_DER_PREFIX;
return form.slice(algPrefix.length);
}
return toUint8Array(await subtle.exportKey('raw', key));
},
async generateKeyPair(alg) {
let genKeyArgs;
switch (alg) {
case 'P-384':
case 'P-256':
genKeyArgs = {
name: 'ECDH',
namedCurve: alg,
};
break;
case 'X25519':
genKeyArgs = { name: 'X25519' };
break;
default:
throw new Error(`Unsupported algorithm ${alg}`);
}
const keyPair = await subtle.generateKey(genKeyArgs, true, ['deriveBits']);
return {
pubKey: keyPair.publicKey,
privKey: keyPair.privateKey,
};
},
async calculateSharedSecret(alg, privateKey, publicKey) {
const genKeyName = alg === 'X25519' ? 'X25519' : 'ECDH';
const key = await subtle.deriveBits({ name: genKeyName, public: publicKey }, privateKey, 8 * SHARED_KEY_LEN_MAP[alg]);
return toUint8Array(key);
},
randomBytes(length) {
const buffer = new Uint8Array(length);
return webcrypto.getRandomValues(buffer);
},
asymmetricEncrypt(cipherSuite, { publicKey, data }) {
if (cipherSuite !== 'RSA-PCKS1_5') {
throw new Error(`Unsupported cipher suite ${cipherSuite}`);
}
return PKCS1_KEM.encrypt(publicKey, data);
},
async encrypt(cipherSuite, { iv, data, key }) {
const name = cipherSuite === 'AES-128-CBC' ? 'AES-CBC' : '';
return toUint8Array(await subtle.encrypt({ name, iv }, key, data))
.slice(0, data.length);
},
async decrypt(cipherSuite, { key, iv, data }) {
if (cipherSuite !== 'AES-128-CBC') {
throw new Error(`Unsupported cipher suite: ${cipherSuite}`);
}
const rawKey = key instanceof Uint8Array
? key
: await this.exportKey(key);
const cipher = aesCbc(rawKey, iv, { disablePadding: true });
const decrypted = cipher.decrypt(data);
return decrypted;
},
async authenticatedEncrypt(cipherSuite, { iv, aead, key, data }) {
let ciphertext;
if (cipherSuite === 'CHACHA20-POLY1305') {
const rawKey = key instanceof Uint8Array
? key
: await this.exportKey(key);
const cipher = chacha20poly1305(rawKey, iv, aead);
ciphertext = cipher.encrypt(data);
}
else {
ciphertext = toUint8Array(await subtle.encrypt({ name: 'AES-GCM', iv, additionalData: aead }, key, data));
}
return {
ciphertext: ciphertext.slice(0, -AUTH_TAG_BYTE_LENGTH),
authTag: ciphertext.slice(-AUTH_TAG_BYTE_LENGTH),
};
},
async authenticatedDecrypt(cipherSuite, { iv, aead, key, data, authTag }) {
if (!authTag) {
throw new Error('authTag is required');
}
const ciphertext = concatenateUint8Arrays([data, authTag]);
let plaintext;
if (cipherSuite === 'CHACHA20-POLY1305') {
const rawKey = key instanceof Uint8Array
? key
: await this.exportKey(key);
const cipher = chacha20poly1305(rawKey, iv, aead);
plaintext = cipher.decrypt(ciphertext);
}
else {
plaintext = toUint8Array(await subtle.decrypt({ name: 'AES-GCM', iv, additionalData: aead }, key, ciphertext));
}
return { plaintext };
},
async verify(alg, { data, signature, publicKey }) {
let verifyArgs;
switch (alg) {
case 'RSA-PSS-SHA256':
verifyArgs = { name: 'RSA-PSS', saltLength: 32 };
break;
case 'RSA-PKCS1-SHA512':
case 'RSA-PKCS1-SHA256':
case 'RSA-PKCS1-SHA384':
case 'RSA-PKCS1-SHA1':
verifyArgs = { name: 'RSASSA-PKCS1-v1_5' };
break;
case 'ECDSA-SECP256R1-SHA256':
case 'ECDSA-SECP256R1-SHA384':
case 'ECDSA-SECP384R1-SHA256':
case 'ECDSA-SECP384R1-SHA384':
signature = convertASN1toRS(signature);
verifyArgs = { name: 'ECDSA', hash: getHashAlgorithm(alg) };
break;
default:
throw new Error(`Unsupported algorithm ${alg}`);
}
return subtle.verify(verifyArgs, publicKey, signature, data);
},
async hash(alg, data) {
return toUint8Array(await subtle.digest(alg, data));
},
async hmac(alg, key, data) {
return toUint8Array(await subtle.sign({ name: 'HMAC', hash: alg }, key, data));
},
// extract & expand logic referenced from:
// https://github.com/futoin/util-js-hkdf/blob/master/hkdf.js
async extract(alg, hashLength, ikm, salt) {
salt = typeof salt === 'string' ? asciiToUint8Array(salt) : salt;
if (!salt.length) {
salt = new Uint8Array(hashLength);
}
const key = await this.importKey(alg, salt);
return this.hmac(alg, key, ikm);
},
};
function toUint8Array(buffer) {
return new Uint8Array(buffer);
}
// mostly from ChatGPT
function convertASN1toRS(signatureBytes) {
const data = AsnParser.parse(signatureBytes, ECDSASigValue);
const r = cleanBigNum(new Uint8Array(data.r));
const s = cleanBigNum(new Uint8Array(data.s));
return concatenateUint8Arrays([r, s]);
}
function cleanBigNum(bn) {
if (bn.length > 32 && bn[0] === 0) {
bn = bn.slice(1);
}
else if (bn.length < 32) {
bn = concatenateUint8Arrays([
new Uint8Array(32 - bn.length).fill(0),
bn
]);
}
return bn;
}
function getHashAlgorithm(sig) {
if (sig.endsWith('SHA256')) {
return 'SHA-256';
}
else if (sig.endsWith('SHA384')) {
return 'SHA-384';
}
else if (sig.endsWith('SHA512')) {
return 'SHA-512';
}
else if (sig.endsWith('SHA1')) {
return 'SHA-1';
}
throw new Error(`Unsupported signature algorithm: ${sig}`);
}