@turnkey/crypto
Version:
Encryption, decryption, and key related utility functions
625 lines (619 loc) • 30.2 kB
JavaScript
'use strict';
var p256 = require('@noble/curves/p256');
var hkdf = require('@noble/hashes/hkdf');
var sha256 = require('@noble/hashes/sha256');
var aes = require('@noble/ciphers/aes');
var utils = require('@noble/hashes/utils');
var borsh = require('borsh');
var encoding = require('@turnkey/encoding');
var math = require('./math.js');
var constants = require('./constants.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var hkdf__namespace = /*#__PURE__*/_interopNamespaceDefault(hkdf);
var borsh__namespace = /*#__PURE__*/_interopNamespaceDefault(borsh);
/// <reference lib="dom" />
// schema for borsh serialization
const EnvelopeSchema = {
struct: {
nonce: { array: { type: "u8", len: constants.QUORUM_ENCRYPT_NONCE_LENGTH_BYTES } },
ephemeralSenderPublic: {
array: { type: "u8", len: constants.UNCOMPRESSED_PUB_KEY_LENGTH_BYTES },
},
encryptedMessage: { array: { type: "u8" } },
},
};
/**
* Get PublicKey function
* Derives public key from Uint8Array or hexstring private key
*
* @param {Uint8Array | string} privateKey - The Uint8Array or hexstring representation of a compressed private key.
* @param {boolean} isCompressed - Specifies whether to return a compressed or uncompressed public key. Defaults to true.
* @returns {Uint8Array} - The public key in Uin8Array representation.
*/
const getPublicKey = (privateKey, isCompressed = true) => {
return p256.p256.getPublicKey(privateKey, isCompressed);
};
/**
* HPKE Encrypt Function
* Encrypts data using Hybrid Public Key Encryption (HPKE) standard https://datatracker.ietf.org/doc/rfc9180/.
*
* @param {HpkeEncryptParams} params - The encryption parameters including plain text, encapsulated key, and sender private key.
* @returns {Uint8Array} - The encrypted data.
*/
const hpkeEncrypt = ({ plainTextBuf, targetKeyBuf, }) => {
try {
// Standard HPKE Mode (Ephemeral Key Pair)
const ephemeralKeyPair = generateP256KeyPair();
const senderPrivBuf = encoding.uint8ArrayFromHexString(ephemeralKeyPair.privateKey);
const senderPubBuf = encoding.uint8ArrayFromHexString(ephemeralKeyPair.publicKeyUncompressed);
const aad = buildAdditionalAssociatedData(senderPubBuf, targetKeyBuf);
// Step 1: Generate Shared Secret
const ss = deriveSS(targetKeyBuf, encoding.uint8ArrayToHexString(senderPrivBuf));
// Step 2: Generate the KEM context
const kemContext = getKemContext(senderPubBuf, encoding.uint8ArrayToHexString(targetKeyBuf));
// Step 3: Build the HKDF inputs for key derivation
let ikm = buildLabeledIkm(constants.LABEL_EAE_PRK, ss, constants.SUITE_ID_1);
let info = buildLabeledInfo(constants.LABEL_SHARED_SECRET, kemContext, constants.SUITE_ID_1, 32);
const sharedSecret = extractAndExpand(new Uint8Array([]), ikm, info, 32);
// Step 4: Derive the AES key
ikm = buildLabeledIkm(constants.LABEL_SECRET, new Uint8Array([]), constants.SUITE_ID_2);
info = constants.AES_KEY_INFO;
const key = extractAndExpand(sharedSecret, ikm, info, 32);
// Step 5: Derive the initialization vector
info = constants.IV_INFO;
const iv = extractAndExpand(sharedSecret, ikm, info, 12);
// Step 6: Encrypt the data using AES-GCM
const encryptedData = aesGcmEncrypt(plainTextBuf, key, iv, aad);
// Step 7: Concatenate the encapsulated key and the encrypted data for output
const compressedSenderBuf = compressRawPublicKey(senderPubBuf);
const result = new Uint8Array(compressedSenderBuf.length + encryptedData.length);
result.set(compressedSenderBuf, 0);
result.set(encryptedData, compressedSenderBuf.length);
return result;
}
catch (error) {
throw new Error(`Unable to perform hpkeEncrypt: ${error}`);
}
};
/**
* HPKE Encrypt Function
* Encrypts data using Authenticated ,Hybrid Public Key Encryption (HPKE) standard https://datatracker.ietf.org/doc/rfc9180/.
*
* @param {HpkeAuthEncryptParams} params - The encryption parameters including plain text, encapsulated key, and sender private key.
* @returns {Uint8Array} - The encrypted data.
*/
const hpkeAuthEncrypt = ({ plainTextBuf, targetKeyBuf, senderPriv, }) => {
try {
// Authenticated HPKE Mode
const senderPrivBuf = encoding.uint8ArrayFromHexString(senderPriv);
const senderPubBuf = getPublicKey(senderPriv, false);
const aad = buildAdditionalAssociatedData(senderPubBuf, targetKeyBuf);
// Step 1: Generate Shared Secret
const ss = deriveSS(targetKeyBuf, encoding.uint8ArrayToHexString(senderPrivBuf));
// Step 2: Generate the KEM context
const kemContext = getKemContext(senderPubBuf, encoding.uint8ArrayToHexString(targetKeyBuf));
// Step 3: Build the HKDF inputs for key derivation
let ikm = buildLabeledIkm(constants.LABEL_EAE_PRK, ss, constants.SUITE_ID_1);
let info = buildLabeledInfo(constants.LABEL_SHARED_SECRET, kemContext, constants.SUITE_ID_1, 32);
const sharedSecret = extractAndExpand(new Uint8Array([]), ikm, info, 32);
// Step 4: Derive the AES key
ikm = buildLabeledIkm(constants.LABEL_SECRET, new Uint8Array([]), constants.SUITE_ID_2);
info = constants.AES_KEY_INFO;
const key = extractAndExpand(sharedSecret, ikm, info, 32);
// Step 5: Derive the initialization vector
info = constants.IV_INFO;
const iv = extractAndExpand(sharedSecret, ikm, info, 12);
// Step 6: Encrypt the data using AES-GCM
const encryptedData = aesGcmEncrypt(plainTextBuf, key, iv, aad);
// Step 7: Concatenate the encapsulated key and the encrypted data for output
const compressedSenderBuf = compressRawPublicKey(senderPubBuf);
const result = new Uint8Array(compressedSenderBuf.length + encryptedData.length);
result.set(compressedSenderBuf, 0);
result.set(encryptedData, compressedSenderBuf.length);
return result;
}
catch (error) {
throw new Error(`Unable to perform hpkeEncrypt: ${error}`);
}
};
/**
* Encrypt a message to a quorum key. Algorithm originally implemented in qos here: https://github.com/tkhq/qos/blob/ae01904c756107f850aea42000137ef124df3fe4/src/qos_p256/src/encrypt.rs#L123
* Returns a borsh serialized encrypted Envelope which is the nonce + ephemeralSenderPublicKey + encryptedMessage
* This function creates an ephemeral key, creates a shared secret with the recipient targetPublicKeyUncompressed
* creates additional associated data which follows the form: sender_public||sender_public_len||receiver_public||receiver_public_len
* encrypts using aes-gcm-256 with a SHA-512 HMAC over the QOS_ENCRYPTION_HMAC_MESSAGE literally: "qos_encryption_hmac_message"
* inserts and returns the necessary information in a borsh serialized envelope as described above
* This encryption function is meant to be used with this decryption function in QOS: https://github.com/tkhq/qos/blob/ae01904c756107f850aea42000137ef124df3fe4/src/qos_p256/src/encrypt.rs#L52
*
* @param {Uint8Array} targetPublicKeyUncompressed - The P256 uncompressed public key to encrypt the message to
* @param {Uint8Array} message - The message to encrypt to targetPublicKeyUncompressed
* @returns {Uint8Array} - A borsh serialized envelope containing the nonce + ephemeralSenderPublicKey + encrypted message
*/
const quorumKeyEncrypt = async (targetPublicKeyUncompressed, message) => {
// generate an ephemeral keypair for this encryption operation
const ephemeralKeyPair = generateP256KeyPair();
const ephemeralSenderPublic = ephemeralKeyPair.publicKeyUncompressed;
// create a shared secret AES-GCM key with the SHA-512 HMAC
let cipher = await createQuorumKeyEncryptCipher(encoding.uint8ArrayFromHexString(ephemeralSenderPublic), encoding.uint8ArrayFromHexString(ephemeralKeyPair.privateKey), targetPublicKeyUncompressed);
// generate a nonce
const nonce = new Uint8Array(constants.QUORUM_ENCRYPT_NONCE_LENGTH_BYTES);
crypto.getRandomValues(nonce);
// create the additional data in the form of sender_public||sender_public_len||receiver_public||receiver_public_len taken from QOS here: https://github.com/tkhq/qos/blob/ae01904c756107f850aea42000137ef124df3fe4/src/qos_p256/src/encrypt.rs#L298
const aad = createAdditionalAssociatedData(encoding.uint8ArrayFromHexString(ephemeralSenderPublic), targetPublicKeyUncompressed);
// algorithm specifications for AES-GCM
const alg = {
name: "AES-GCM",
iv: nonce,
tagLength: 128,
additionalData: aad,
};
// encrypt the message with the shared secret
const encryptedMessageBuf = await crypto.subtle.encrypt(alg, cipher, message);
// create the envelope
let envelope = {
nonce: nonce,
ephemeralSenderPublic: encoding.uint8ArrayFromHexString(ephemeralSenderPublic),
encryptedMessage: new Uint8Array(encryptedMessageBuf),
};
// borsh serialize the envelope
return borsh__namespace.serialize(EnvelopeSchema, envelope);
};
/**
* Format HPKE Buffer Function
* Returns a JSON string of an encrypted bundle, separating out the cipher text and the sender public key
*
* @param {Uint8Array} encryptedBuf - The result of hpkeAuthEncrypt or hpkeEncrypt
* @returns {string} - A JSON string with "encappedPublic" and "ciphertext"
*/
const formatHpkeBuf = (encryptedBuf) => {
const compressedSenderBuf = encryptedBuf.slice(0, 33);
const encryptedData = encryptedBuf.slice(33);
const encappedKeyBufHex = encoding.uint8ArrayToHexString(uncompressRawPublicKey(compressedSenderBuf));
const ciphertextHex = encoding.uint8ArrayToHexString(encryptedData);
return JSON.stringify({
encappedPublic: encappedKeyBufHex,
ciphertext: ciphertextHex,
});
};
/**
* HPKE Decrypt Function
* Decrypts data using Hybrid Public Key Encryption (HPKE) standard https://datatracker.ietf.org/doc/rfc9180/.
*
* @param {HpkeDecryptParams} params - The decryption parameters including ciphertext, encapsulated key, and receiver private key.
* @returns {Uint8Array} - The decrypted data.
*/
const hpkeDecrypt = ({ ciphertextBuf, encappedKeyBuf, receiverPriv, }) => {
try {
let ikm;
let info;
const receiverPubBuf = getPublicKey(encoding.uint8ArrayFromHexString(receiverPriv), false);
const aad = buildAdditionalAssociatedData(encappedKeyBuf, receiverPubBuf); // Eventually we want users to be able to pass in aad as optional
// Step 1: Generate Shared Secret
const ss = deriveSS(encappedKeyBuf, receiverPriv);
// Step 2: Generate the KEM context
const kemContext = getKemContext(encappedKeyBuf, encoding.uint8ArrayToHexString(receiverPubBuf));
// Step 3: Build the HKDF inputs for key derivation
ikm = buildLabeledIkm(constants.LABEL_EAE_PRK, ss, constants.SUITE_ID_1);
info = buildLabeledInfo(constants.LABEL_SHARED_SECRET, kemContext, constants.SUITE_ID_1, 32);
const sharedSecret = extractAndExpand(new Uint8Array([]), ikm, info, 32);
// Step 4: Derive the AES key
ikm = buildLabeledIkm(constants.LABEL_SECRET, new Uint8Array([]), constants.SUITE_ID_2);
info = constants.AES_KEY_INFO;
const key = extractAndExpand(sharedSecret, ikm, info, 32);
// Step 5: Derive the initialization vector
info = constants.IV_INFO;
const iv = extractAndExpand(sharedSecret, ikm, info, 12);
// Step 6: Decrypt the data using AES-GCM
const decryptedData = aesGcmDecrypt(ciphertextBuf, key, iv, aad);
return decryptedData;
}
catch (error) {
throw new Error(`Unable to perform hpkeDecrypt: ${error} `);
}
};
/**
* Generate a P-256 key pair. Contains the hexed privateKey, publicKey, and Uncompressed publicKey
*
* @returns {KeyPair} - The generated key pair.
*/
const generateP256KeyPair = () => {
const privateKey = utils.randomBytes(32);
const publicKey = getPublicKey(privateKey, true);
const publicKeyUncompressed = encoding.uint8ArrayToHexString(uncompressRawPublicKey(publicKey));
return {
privateKey: encoding.uint8ArrayToHexString(privateKey),
publicKey: encoding.uint8ArrayToHexString(publicKey),
publicKeyUncompressed,
};
};
/**
* Create additional associated data (AAD) for AES-GCM decryption.
*
* @param {Uint8Array} senderPubBuf
* @param {Uint8Array} receiverPubBuf
* @return {Uint8Array} - The resulting concatenation of sender and receiver pubkeys.
*/
const buildAdditionalAssociatedData = (senderPubBuf, receiverPubBuf) => {
return new Uint8Array([
...Array.from(senderPubBuf),
...Array.from(receiverPubBuf),
]);
};
/**
* Accepts a private key Uint8Array in the PKCS8 format, and returns the encapsulated private key.
*
* @param {Uint8Array} privateKey - A PKCS#8 private key structured with the key data at a specific position. The actual key starts at byte 36 and is 32 bytes long.
* @return {Uint8Array} - The private key.
*/
const extractPrivateKeyFromPKCS8Bytes = (privateKey) => {
return privateKey.slice(36, 36 + 32);
};
/**
* Accepts a public key Uint8Array, and returns a Uint8Array with the compressed version of the public key.
*
* @param {Uint8Array} rawPublicKey - The raw public key.
* @return {Uint8Array} – The compressed public key.
*/
const compressRawPublicKey = (rawPublicKey) => {
const len = rawPublicKey.byteLength;
// Drop the y coordinate
// Uncompressed key is in the form 0x04||x||y
// `len >>> 1` is a more concise way to write `floor(len/2)`
var compressedBytes = rawPublicKey.slice(0, (1 + len) >>> 1);
// Encode the parity of `y` in first bit
// `BYTE & 0x01` tests for parity and returns 0x00 when even, or 0x01 when odd
// Then `0x02 | <parity test result>` yields either 0x02 (even case) or 0x03 (odd).
compressedBytes[0] = 0x02 | (rawPublicKey[len - 1] & 0x01);
return compressedBytes;
};
/**
* Accepts a public key array buffer, and returns a buffer with the uncompressed version of the public key
* @param {Uint8Array} rawPublicKey - The public key.
* @return {Uint8Array} - The uncompressed public key.
*/
const uncompressRawPublicKey = (rawPublicKey, curve = "CURVE_P256") => {
if (rawPublicKey.length !== 33) {
throw new Error("failed to uncompress raw public key: invalid length");
}
if (!(rawPublicKey[0] === 2 || rawPublicKey[0] === 3)) {
throw new Error("failed to uncompress raw public key: invalid prefix");
}
// point[0] must be 2 (false) or 3 (true).
// this maps to the initial "02" or "03" prefix
const lsb = rawPublicKey[0] === 3;
const x = BigInt("0x" + encoding.uint8ArrayToHexString(rawPublicKey.subarray(1)));
let p, a, b;
if (curve === "CURVE_P256") {
// p-256 domain parameters
// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D).
p = BigInt("115792089210356248762697446949407573530086143415290314195533631308867097853951");
b = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
a = p - BigInt(3);
}
else {
// secp256k1 domain parameters
// https://www.secg.org/sec2-v2.pdf (Section 2.4.1).
p = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");
a = BigInt(0);
b = BigInt(7);
}
// Now compute y based on x
const rhs = ((x * x + a) * x + b) % p;
let y = math.modSqrt(rhs, p);
if (lsb !== math.testBit(y, 0)) {
y = (p - y) % p;
}
if (x < BigInt(0) || x >= p) {
throw new Error("x is out of range");
}
if (y < BigInt(0) || y >= p) {
throw new Error("y is out of range");
}
var uncompressedHexString = "04" + bigIntToHex(x, 64) + bigIntToHex(y, 64);
return encoding.uint8ArrayFromHexString(uncompressedHexString);
};
/**
* Build labeled Initial Key Material (IKM).
*
* @param {Uint8Array} label - The label to use.
* @param {Uint8Array} ikm - The input key material.
* @param {Uint8Array} suiteId - The suite identifier.
* @returns {Uint8Array} - The labeled IKM.
*/
const buildLabeledIkm = (label, ikm, suiteId) => {
const combinedLength = constants.HPKE_VERSION.length + suiteId.length + label.length + ikm.length;
const ret = new Uint8Array(combinedLength);
let offset = 0;
ret.set(constants.HPKE_VERSION, offset);
offset += constants.HPKE_VERSION.length;
ret.set(suiteId, offset);
offset += suiteId.length;
ret.set(label, offset);
offset += label.length;
ret.set(ikm, offset);
return ret;
};
/**
* Build labeled info for HKDF operations.
*
* @param {Uint8Array} label - The label to use.
* @param {Uint8Array} info - Additional information.
* @param {Uint8Array} suiteId - The suite identifier.
* @param {number} len - The output length.
* @returns {Uint8Array} - The labeled info.
*/
const buildLabeledInfo = (label, info, suiteId, len) => {
const suiteIdStartIndex = 9; // first two are reserved for length bytes (unused in this case), the next 7 are for the HPKE_VERSION, then the suiteId starts at 9
const ret = new Uint8Array(suiteIdStartIndex + suiteId.byteLength + label.byteLength + info.byteLength);
ret.set(new Uint8Array([0, len]), 0); // this isn’t an error, we’re starting at index 2 because the first two bytes should be 0. See <https://github.com/dajiaji/hpke-js/blob/1e7fb1372fbcdb6d06bf2f4fa27ff676329d633e/src/kdfs/hkdf.ts#L41> for reference.
ret.set(constants.HPKE_VERSION, 2);
ret.set(suiteId, suiteIdStartIndex);
ret.set(label, suiteIdStartIndex + suiteId.byteLength);
ret.set(info, suiteIdStartIndex + suiteId.byteLength + label.byteLength);
return ret;
};
/**
* Perform HKDF extract and expand operations.
*/
const extractAndExpand = (sharedSecret, ikm, info, len) => {
const prk = hkdf__namespace.extract(sha256.sha256, ikm, sharedSecret);
const resp = hkdf__namespace.expand(sha256.sha256, prk, info, len);
return new Uint8Array(resp);
};
/**
* Derive the Diffie-Hellman shared secret using ECDH.
*/
const deriveSS = (encappedKeyBuf, priv) => {
const ss = p256.p256.getSharedSecret(encoding.uint8ArrayFromHexString(priv), encappedKeyBuf);
return ss.slice(1);
};
/**
* Encrypt data using AES-GCM.
*/
const aesGcmEncrypt = (plainTextData, key, iv, aad) => {
const aes$1 = aes.gcm(key, iv, aad);
const data = aes$1.encrypt(plainTextData);
return data;
};
/**
* Decrypt data using AES-GCM.
*/
const aesGcmDecrypt = (encryptedData, key, iv, aad) => {
const aes$1 = aes.gcm(key, iv, aad);
const data = aes$1.decrypt(encryptedData);
return data;
};
/**
* Generate a Key Encapsulation Mechanism (KEM) context.
*/
const getKemContext = (encappedKeyBuf, publicKey) => {
const encappedKeyArray = new Uint8Array(encappedKeyBuf);
const publicKeyArray = encoding.uint8ArrayFromHexString(publicKey);
const kemContext = new Uint8Array(encappedKeyArray.length + publicKeyArray.length);
kemContext.set(encappedKeyArray);
kemContext.set(publicKeyArray, encappedKeyArray.length);
return kemContext;
};
/**
* Convert a BigInt to a hexadecimal string of a specific length.
*/
const bigIntToHex = (num, length) => {
const hexString = num.toString(16);
if (hexString.length > length) {
throw new Error(`number cannot fit in a hex string of ${length} characters`);
}
return hexString.padStart(length, "0");
};
/**
* Converts an ASN.1 DER-encoded ECDSA signature to the raw format used for verification.
*
* @param {string} derSignature - The DER-encoded signature.
* @returns {Uint8Array} - The raw signature.
*/
const fromDerSignature = (derSignature) => {
const derSignatureBuf = encoding.uint8ArrayFromHexString(derSignature);
// Check minimum length
if (derSignatureBuf.length < 2) {
throw new Error("failed to convert DER-encoded signature: insufficient length");
}
// Check SEQUENCE tag (0x30 at first byte)
if (derSignatureBuf[0] !== 0x30) {
throw new Error("failed to convert DER-encoded signature: invalid format (missing SEQUENCE tag)");
}
// Check second byte, start of length field
let index = 1;
const lengthByte = derSignatureBuf[index];
if (lengthByte <= 0x7f) {
// Short form: single byte length
// directly take the consumed value as length and check against buffer
// buffer length: initial header bytes + claimed remaining length
if (derSignatureBuf.length < 1 + 1 + lengthByte) {
throw new Error("failed to convert DER-encoded signature: inconsistent message length header");
}
// continue parsing
index += 1;
}
else {
// Multi-byte DER length header
// Invalid DER values: lengthByte 0x80 and 0xff
// Valid DER values: lengthByte > 0x80, < 0xff
//
// We do not expect signature data in the Long form notation
// -> reject all such inputs
//
// More complex parsing for longer signature sequences can be implemented once needed
throw new Error("failed to convert DER-encoded signature: unexpectedly large or invalid signature length");
}
// Parse 'r' and check for integer tag (0x02)
if (derSignatureBuf[index] !== 0x02) {
throw new Error("failed to convert DER-encoded signature: invalid tag for r");
}
index++; // Move past the INTEGER tag
const rLength = derSignatureBuf[index];
// Allow up to 32 data bytes + 1 byte 0-padding prefix
if (rLength > 33) {
throw new Error("failed to convert DER-encoded signature: unexpected length for r");
}
index++; // Move past the length byte
const r = derSignatureBuf.slice(index, index + rLength);
index += rLength; // Move to the start of s
// Parse 's' and check for integer tag (0x02)
if (derSignatureBuf[index] !== 0x02) {
throw new Error("failed to convert DER-encoded signature: invalid tag for s");
}
index++; // Move past the INTEGER tag
const sLength = derSignatureBuf[index];
// Allow up to 32 data bytes + 1 byte 0-padding prefix
if (sLength > 33) {
throw new Error("failed to convert DER-encoded signature: unexpected length for s");
}
index++; // Move past the length byte
const s = derSignatureBuf.slice(index, index + sLength);
// Normalize 'r' and 's' to 32 bytes each
const rPadded = encoding.normalizePadding(r, 32);
const sPadded = encoding.normalizePadding(s, 32);
// Concatenate and return the raw signature
return new Uint8Array([...rPadded, ...sPadded]);
};
/**
* Converts a raw ECDSA signature to DER-encoded format.
*
* This function takes a raw ECDSA signature, which is a concatenation of two 32-byte integers (r and s),
* and converts it into the DER-encoded format. DER (Distinguished Encoding Rules) is a binary encoding
* for data structures described by ASN.1.
*
* @param {string} rawSignature - The raw signature in hexadecimal string format.
* @returns {string} - The DER-encoded signature in hexadecimal string format.
*
* @throws {Error} - Throws an error if the input signature is invalid or if the encoding process fails.
*
* @example
* // Example usage:
* const rawSignature = "0x487cdb8a88f2f4044b701cbb116075c4cabe5fe4657a6358b395c0aab70694db3453a8057e442bd1aff0ecabe8a82c831f0edd7f2158b7c1feb3de9b1f20309b1c";
* const derSignature = toDerSignature(rawSignature);
* console.log(derSignature); // Outputs the DER-encoded signature as a hex string
* // "30440220487cdb8a88f2f4044b701cbb116075c4cabe5fe4657a6358b395c0aab70694db02203453a8057e442bd1aff0ecabe8a82c831f0edd7f2158b7c1feb3de9b1f20309b"
*/
const toDerSignature = (rawSignature) => {
const rawSignatureBuf = encoding.uint8ArrayFromHexString(rawSignature);
// Split raw signature into r and s, each 32 bytes
const r = rawSignatureBuf.slice(0, 32);
const s = rawSignatureBuf.slice(32, 64);
// Helper function to encode an integer with DER structure
const encodeDerInteger = (integer) => {
// Check if integer is defined and has at least one byte
if (integer === undefined ||
integer.length === 0 ||
integer[0] === undefined) {
throw new Error("Invalid integer: input is undefined or empty.");
}
// Add a leading zero if the integer's most significant byte is >= 0x80
const needsPadding = integer[0] & 0x80;
const paddedInteger = needsPadding
? new Uint8Array([0x00, ...integer])
: integer;
// Prepend the integer tag (0x02) and length
return new Uint8Array([0x02, paddedInteger.length, ...paddedInteger]);
};
// DER encode r and s
const rEncoded = encodeDerInteger(r);
const sEncoded = encodeDerInteger(s);
// Combine as a DER sequence: 0x30, total length, rEncoded, sEncoded
const derSignature = new Uint8Array([
0x30,
rEncoded.length + sEncoded.length,
...rEncoded,
...sEncoded,
]);
return encoding.uint8ArrayToHexString(derSignature);
};
/**
* Create a shared AES-GCM secret with the quorum key encryption SHA-512 HMAC
*
* This function takes an ephemeral Sender public key generated for each encryption operation
* the corresponding ephemeral private key, and the target public key uncompressed
* for data structures described by ASN.1.
*
* @param {Uint8Array} ephemeralSenderPublic - The ephemeral public key used to create the preImage
* @param {Uint8Array} ephemeralSenderPrivate - The ephemeral private key to create the shared secret with
* @param {Uint8Array} targetPublicKeyUncompressed - The public key to create the shared secret with and encrypt the message to
* @returns {Promise<CryptoKey>} - A shared secret AES-GCM key between ephemeralSenderPrivate and the targetPublicKeyUncompressed
*/
async function createQuorumKeyEncryptCipher(ephemeralSenderPublic, ephemeralSenderPrivate, targetPublicKeyUncompressed) {
// create the shared secret between ephemeralSenderPrivate and targetPublicKeyUncompressed
const sharedSecretUncompressed = p256.p256.getSharedSecret(ephemeralSenderPrivate, targetPublicKeyUncompressed, false);
const sharedSecret = sharedSecretUncompressed.slice(1, 33);
// create the preImage as defined in qos here: https://github.com/tkhq/qos/blob/ae01904c756107f850aea42000137ef124df3fe4/src/qos_p256/src/encrypt.rs#L273-L282
let preImage = new Uint8Array(ephemeralSenderPublic.length +
targetPublicKeyUncompressed.length +
sharedSecret.length);
preImage.set(ephemeralSenderPublic, 0);
preImage.set(targetPublicKeyUncompressed, ephemeralSenderPublic.length);
preImage.set(sharedSecret, ephemeralSenderPublic.length + targetPublicKeyUncompressed.length);
// create the HMAC key and create an HMAC using QOS_ENCRYPTION_HMAC_MESSAGE
const hmacKey = await crypto.subtle.importKey("raw", preImage, {
name: "HMAC",
hash: "SHA-512",
}, false, ["sign"]);
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, constants.QOS_ENCRYPTION_HMAC_MESSAGE));
// Use the first 32 bytes as the AES-GCM key
const aesKeyRaw = mac.slice(0, 32);
return crypto.subtle.importKey("raw", aesKeyRaw, {
name: "AES-GCM",
}, false, ["encrypt"]);
}
/// Helper function to create the additional associated data (AAD). The data is
/// of the form `sender_public||sender_public_len||receiver_public||receiver_public_len`.
/// This is taken from QOS here: https://github.com/tkhq/qos/blob/ae01904c756107f850aea42000137ef124df3fe4/src/qos_p256/src/encrypt.rs#L298
///
/// Note that we append the length to each field as per NIST specs here: <https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar3.pdf/>. See section 5.8.2.
function createAdditionalAssociatedData(ephemeralSenderPublic, receiverPublic) {
// get the length of the sending and receiver public keys
const ephemeralSenderLength = ephemeralSenderPublic.length;
const receiverPublicLength = receiverPublic.length;
// ensure the lengths are under 1 byte
if (ephemeralSenderLength > 255 || receiverPublicLength > 255)
throw new Error("AAD len fields are 1 byte");
// allocate an array the size of both keys + 1 byte for their length
const aad = new Uint8Array(ephemeralSenderLength + 1 + receiverPublicLength + 1);
// keep track of the offset within the array
let offset = 0;
// set the bytes that represent the sender public key + its length
aad.set(ephemeralSenderPublic, offset);
offset += ephemeralSenderLength;
aad[offset++] = ephemeralSenderLength;
// set the bytes that represent the receiver public key + its length
aad.set(receiverPublic, offset);
offset += receiverPublicLength;
aad[offset++] = receiverPublicLength;
return aad;
}
exports.buildAdditionalAssociatedData = buildAdditionalAssociatedData;
exports.compressRawPublicKey = compressRawPublicKey;
exports.extractPrivateKeyFromPKCS8Bytes = extractPrivateKeyFromPKCS8Bytes;
exports.formatHpkeBuf = formatHpkeBuf;
exports.fromDerSignature = fromDerSignature;
exports.generateP256KeyPair = generateP256KeyPair;
exports.getPublicKey = getPublicKey;
exports.hpkeAuthEncrypt = hpkeAuthEncrypt;
exports.hpkeDecrypt = hpkeDecrypt;
exports.hpkeEncrypt = hpkeEncrypt;
exports.quorumKeyEncrypt = quorumKeyEncrypt;
exports.toDerSignature = toDerSignature;
exports.uncompressRawPublicKey = uncompressRawPublicKey;
//# sourceMappingURL=crypto.js.map