@coti-io/coti-sdk-typescript
Version:
A library for encryption, decryption and cryptographic utilities for the COTI blockchain.
882 lines (881 loc) • 39.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildItSignature = exports.prepareSignedIT256 = exports.decryptInt256 = exports.encryptInt256 = exports.prepareIT256 = exports.buildItUint256WithSigner = exports.decryptCtUint256 = exports.isZeroCtUint256 = exports.isCtUint256Shape = exports.normalizeCtPayload = exports.encryptUint256 = exports.encryptUint = exports.prepareIT = exports.encryptNumber = exports.decodeUint = exports.encodeUint = exports.encodeKey = exports.normalizeAesKey = exports.encodeString = exports.binaryStringToBytes = exports.generateRandomAesKeySizeNumber = exports.generateRandomAesKeyBinaryString = exports.decryptString = exports.decryptUint256 = exports.decryptUint = exports.buildStringInputText = exports.buildInputText = exports.signInputText = exports.sign = exports.recoverUserKey = exports.decryptRSA = exports.generateRSAKeyPair = exports.decrypt = exports.encrypt = void 0;
const node_forge_1 = __importDefault(require("node-forge"));
const ethers_1 = require("ethers");
const bytes_1 = require("./bytes");
const BLOCK_SIZE = 16; // AES block size in bytes
const EIGHT_BYTES = 8;
const MAX_PLAINTEXT_BIT_SIZE = 256;
function assertUintInRange(plaintext, maxBits, errorMessage) {
const value = BigInt(plaintext);
if (value < 0n || value >= 1n << BigInt(maxBits)) {
throw new RangeError(errorMessage);
}
return value;
}
function assertIntInRange(plaintext, bits, errorMessage) {
const value = BigInt(plaintext);
const min = -(1n << BigInt(bits - 1));
const max = (1n << BigInt(bits - 1)) - 1n;
if (value < min || value > max) {
throw new RangeError(errorMessage);
}
return value;
}
/** Maps a signed plaintext into unsigned two's-complement bits for AES encrypt. */
function toTwosComplementUint(plaintext, bits) {
return plaintext < 0n ? (1n << BigInt(bits)) + plaintext : plaintext;
}
/** Interprets an unsigned ciphertext plaintext as signed two's-complement. */
function fromTwosComplementUint(unsigned, bits) {
const signBit = 1n << BigInt(bits - 1);
return unsigned >= signBit ? unsigned - (1n << BigInt(bits)) : unsigned;
}
function assertAesKeySize(key) {
if (key.length !== BLOCK_SIZE) {
throw new RangeError("Key size must be 128 bits.");
}
}
function splitCtBlock(block) {
return {
cipher: block.subarray(0, BLOCK_SIZE),
r: block.subarray(BLOCK_SIZE)
};
}
function packEncryptBlocks(...blocks) {
return new Uint8Array(blocks.flatMap(({ ciphertext, r }) => [...ciphertext, ...r]));
}
/**
* Low-level AES block encryption used by the COTI garbled-circuit scheme.
*
* Encrypts up to 16 bytes of plaintext by XORing with AES-ECB(random `r`).
* Returns the 16-byte ciphertext block and the random `r` used (also 16 bytes).
*
* @param key - 128-bit AES key (16 bytes).
* @param plaintext - Up to 16 bytes; shorter inputs are zero-padded on the left.
* @returns The ciphertext block and random `r` (each 16 bytes).
* @throws RangeError if `plaintext` exceeds 16 bytes or `key` is not 16 bytes.
*/
function encrypt(key, plaintext) {
// Ensure plaintext is smaller than 128 bits (16 bytes)
if (plaintext.length > BLOCK_SIZE) {
throw new RangeError("Plaintext size must be 128 bits or smaller.");
}
// Generate a random value 'r' of the same length as the block size
const r = node_forge_1.default.random.getBytesSync(BLOCK_SIZE);
// Get the encrypted random value 'r'
const encryptedR = encryptNumber(r, key);
// Pad the plaintext with zeros if it's smaller than the block size
const plaintextPadded = new Uint8Array([...new Uint8Array(BLOCK_SIZE - plaintext.length), ...plaintext]);
// XOR the encrypted random value 'r' with the plaintext to obtain the ciphertext
const ciphertext = new Uint8Array(BLOCK_SIZE);
for (let i = 0; i < BLOCK_SIZE; i++) {
ciphertext[i] = encryptedR[i] ^ plaintextPadded[i];
}
return {
ciphertext,
r: binaryStringToBytes(r)
};
}
exports.encrypt = encrypt;
// Helper function to validate input sizes
function validateDecryptInputs(key, r, ciphertext) {
if (ciphertext.length !== BLOCK_SIZE) {
throw new RangeError("Ciphertext size must be 128 bits.");
}
assertAesKeySize(key);
if (r.length !== BLOCK_SIZE) {
throw new RangeError("Random size must be 128 bits.");
}
}
// Helper function to validate second block parameters
function validateSecondBlock(r2, ciphertext2) {
if (r2 !== null && r2.length !== BLOCK_SIZE) {
throw new RangeError("Random2 size must be 128 bits, received " + r2.length + " bytes.");
}
if (ciphertext2 !== null && ciphertext2.length !== BLOCK_SIZE) {
throw new RangeError("Ciphertext2 size must be 128 bits, received " + ciphertext2.length + " bytes.");
}
if (r2 !== null && ciphertext2 === null) {
throw new RangeError("Ciphertext2 is required.");
}
if (ciphertext2 !== null && r2 === null) {
throw new RangeError("Random2 is required.");
}
}
// Helper function to decrypt a single block
function decryptBlock(key, r, ciphertext) {
const encryptedR = encryptNumber(r, key);
const plaintext = new Uint8Array(BLOCK_SIZE);
for (let i = 0; i < BLOCK_SIZE; i++) {
plaintext[i] = encryptedR[i] ^ ciphertext[i];
}
return plaintext;
}
/**
* Low-level AES block decryption. Inverts {@link encrypt}.
*
* Decrypts one 16-byte block. When `r2` and `ciphertext2` are provided, decrypts
* a second block and returns the 32-byte concatenation of both plaintext blocks.
*
* @param key - 128-bit AES key (16 bytes).
* @param r - 16-byte random value from encryption.
* @param ciphertext - 16-byte ciphertext block.
* @param r2 - Optional second-block random value (must pair with `ciphertext2`).
* @param ciphertext2 - Optional second-block ciphertext (must pair with `r2`).
* @returns 16-byte plaintext, or 32 bytes when a second block is supplied.
* @throws RangeError if any input has an invalid length or block pairs are mismatched.
*/
function decrypt(key, r, ciphertext, r2 = null, ciphertext2 = null) {
validateDecryptInputs(key, r, ciphertext);
validateSecondBlock(r2, ciphertext2);
const plaintext = decryptBlock(key, r, ciphertext);
// Handle second block if provided
if (r2 !== null && ciphertext2 !== null) {
const plaintext2 = decryptBlock(key, r2, ciphertext2);
return new Uint8Array([...plaintext, ...plaintext2]);
}
return plaintext;
}
exports.decrypt = decrypt;
/**
* Generates a 2048-bit RSA key pair in DER encoding.
*
* @returns Public and private keys as `Uint8Array` DER blobs.
*/
function generateRSAKeyPair() {
// Generate a new RSA key pair
const rsaKeyPair = node_forge_1.default.pki.rsa.generateKeyPair({ bits: 2048 });
// Convert keys to DER format
const privateKey = node_forge_1.default.asn1.toDer(node_forge_1.default.pki.privateKeyToAsn1(rsaKeyPair.privateKey)).data;
const publicKey = node_forge_1.default.asn1.toDer(node_forge_1.default.pki.publicKeyToAsn1(rsaKeyPair.publicKey)).data;
return {
privateKey: binaryStringToBytes(privateKey),
publicKey: binaryStringToBytes(publicKey)
};
}
exports.generateRSAKeyPair = generateRSAKeyPair;
/**
* Decrypts an RSA-OAEP (SHA-256) ciphertext with a DER-encoded private key.
*
* @param privateKey - RSA private key as a DER `Uint8Array`.
* @param ciphertext - Hex-encoded RSA ciphertext (no `0x` prefix required by forge).
* @returns Decrypted payload as a lowercase hex string.
* @throws Error if the key or ciphertext format is invalid.
*/
function decryptRSA(privateKey, ciphertext) {
// Convert privateKey from Uint8Array to PEM format
const privateKeyPEM = node_forge_1.default.pki.privateKeyToPem(node_forge_1.default.pki.privateKeyFromAsn1(node_forge_1.default.asn1.fromDer(node_forge_1.default.util.createBuffer(bytesToBinaryString(privateKey)))));
// Decrypt using RSA-OAEP
const rsaPrivateKey = node_forge_1.default.pki.privateKeyFromPem(privateKeyPEM);
const decrypted = rsaPrivateKey.decrypt(node_forge_1.default.util.hexToBytes(ciphertext), 'RSA-OAEP', {
md: node_forge_1.default.md.sha256.create()
});
const decryptedBytes = binaryStringToBytes(decrypted);
return (0, bytes_1.bytesToHex)(decryptedBytes);
}
exports.decryptRSA = decryptRSA;
/**
* Recovers a user's AES key by XORing two RSA-decrypted key shares.
*
* Each share is RSA-OAEP encrypted; after decryption both shares are parsed as
* hex AES keys and XORed to produce the final 128-bit user key.
*
* @param privateKey - RSA private key used to decrypt both shares.
* @param encryptedKeyShare0 - Hex RSA ciphertext of the first share.
* @param encryptedKeyShare1 - Hex RSA ciphertext of the second share.
* @returns Recovered AES key as a 32-character lowercase hex string.
* @throws Error if RSA decryption or key parsing fails.
*/
function recoverUserKey(privateKey, encryptedKeyShare0, encryptedKeyShare1) {
const decryptedKeyShare0 = decryptRSA(privateKey, encryptedKeyShare0);
const decryptedKeyShare1 = decryptRSA(privateKey, encryptedKeyShare1);
const bufferKeyShare0 = encodeKey(decryptedKeyShare0);
const bufferKeyShare1 = encodeKey(decryptedKeyShare1);
const aesKeyBytes = new Uint8Array(BLOCK_SIZE);
for (let i = 0; i < BLOCK_SIZE; i++) {
aesKeyBytes[i] = bufferKeyShare0[i] ^ bufferKeyShare1[i];
}
return (0, bytes_1.bytesToHex)(aesKeyBytes);
}
exports.recoverUserKey = recoverUserKey;
/**
* Signs a message hash with an Ethereum secp256k1 private key.
*
* @param message - 32-byte message digest as a hex string (e.g. from `solidityPackedKeccak256`).
* @param privateKey - Signer's private key (hex string).
* @returns 65-byte signature (`r` || `s` || `v`) as a `Uint8Array`.
*/
function sign(message, privateKey) {
const key = new ethers_1.SigningKey(privateKey);
const sig = key.sign(message);
return signatureToBytes(sig);
}
exports.sign = sign;
// Computes the COTI IT message hash shared by signInputText and buildItSignature.
function buildItMessageHash(signerAddress, contractAddress, functionSelector, ct) {
return (0, ethers_1.solidityPackedKeccak256)(["address", "address", "bytes4", "uint256"], [signerAddress, contractAddress, functionSelector, ct]);
}
function signatureToBytes(signature) {
return new Uint8Array([...(0, ethers_1.getBytes)(signature.r), ...(0, ethers_1.getBytes)(signature.s), ...(0, ethers_1.getBytes)(`0x0${signature.v - 27}`)]);
}
function signItUintDigest(signerAddress, contractAddress, functionSelector, ct, privateKey) {
return sign(buildItMessageHash(signerAddress, contractAddress, functionSelector, ct), privateKey);
}
/**
* Signs a ctUint ciphertext for COTI input-text (IT) submission.
*
* Hashes `(signer, contract, selector, ciphertext)` with `solidityPackedKeccak256`
* and signs the digest with the sender's wallet private key.
*
* @param sender - Wallet and user AES key (key is not used for signing).
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector (e.g. `"0x11223344"`).
* @param ct - Encrypted value ({@link ctUint} bigint) being signed.
* @returns 65-byte signature as a `Uint8Array`.
*/
function signInputText(sender, contractAddress, functionSelector, ct) {
return signItUintDigest(sender.wallet.address, contractAddress, functionSelector, ct, sender.wallet.privateKey);
}
exports.signInputText = signInputText;
function buildUintInputText(plaintext, sender, contractAddress, functionSelector, maxBits, errorMessage) {
const plaintextBigInt = assertUintInRange(plaintext, maxBits, errorMessage);
const ctInt = encryptUint128Unchecked(plaintextBigInt, sender.userKey);
const signature = signInputText(sender, contractAddress, functionSelector, ctInt);
return {
ciphertext: ctInt,
signature
};
}
/**
* @deprecated Use {@link prepareIT} for unsigned integer input-text values. This
* legacy helper is limited to 64-bit plaintexts and will be removed in a
* future major version.
*
* Encrypts a plaintext, signs the ciphertext, and returns an {@link itUint}
* ready for smart contract submission.
*
* @param plaintext - Unsigned value up to 64 bits.
* @param sender - Wallet and user AES key.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector.
* @returns Signed input text with {@link ctUint} ciphertext.
* @throws RangeError if `plaintext` exceeds 64 bits or is negative.
*/
function buildInputText(plaintext, sender, contractAddress, functionSelector) {
return buildUintInputText(plaintext, sender, contractAddress, functionSelector, 64, "Plaintext size must be 64 bits or smaller.");
}
exports.buildInputText = buildInputText;
/**
* Builds signed input text for a UTF-8 string.
*
* Encodes the string in 8-byte chunks (each stored as a {@link ctUint}),
* encrypts and signs each chunk independently, and returns an {@link itString}.
*
* @param plaintext - UTF-8 string to encrypt.
* @param sender - Wallet and user AES key.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector.
* @returns Signed string input text with one ciphertext/signature pair per chunk.
* @throws RangeError if any chunk exceeds 64 bits after encoding.
*/
function buildStringInputText(plaintext, sender, contractAddress, functionSelector) {
let encoder = new TextEncoder();
// Encode the plaintext string into bytes (UTF-8 encoded)
let encodedStr = encoder.encode(plaintext);
const inputText = {
ciphertext: { value: new Array() },
signature: new Array()
};
// Process the encoded string in chunks of 8 bytes
// We use 8 bytes since we will use ctUint64 to store
// each chunk of 8 characters
for (let startIdx = 0; startIdx < encodedStr.length; startIdx += EIGHT_BYTES) {
const endIdx = Math.min(startIdx + EIGHT_BYTES, encodedStr.length);
const byteArr = new Uint8Array([...encodedStr.slice(startIdx, endIdx), ...new Uint8Array(EIGHT_BYTES - (endIdx - startIdx))]); // pad the end of the string with zeros if needed
const it = buildUintInputText(decodeUint(byteArr), // convert the 8-byte hex string into a number
sender, contractAddress, functionSelector, 64, "Plaintext size must be 64 bits or smaller.");
inputText.ciphertext.value.push(it.ciphertext);
inputText.signature.push(it.signature);
}
return inputText;
}
exports.buildStringInputText = buildStringInputText;
/**
* Decrypts a ctUint ciphertext using the user's AES key.
*
* `ctUint` is stored on-chain as a bigint packing 32 bytes (16-byte AES
* ciphertext + 16-byte random `r`). This function decrypts to a plaintext
* of up to 64 bits. For 128-bit input-text values use `prepareIT` instead.
*
* - A zero ciphertext is short-circuited to `0n` without validating the key,
* since it represents uninitialized/empty on-chain storage. This allows DApps
* to read empty balances before the user has configured their AES key.
* Note: this means `decryptUint(0n, invalidKey)` returns `0n` without throwing.
* - For non-zero ciphertexts, key validation is performed by `encodeKey`
* (strips "0x", lowercases, enforces 128-bit hex). Invalid keys throw
* rather than producing garbage.
*
* @param ciphertext - The ctUint value (32-byte wire format as a bigint).
* @param userKey - The AES key (32 hex chars, optionally "0x"-prefixed).
* @returns The decrypted plaintext as a bigint (up to 64 bits).
* @throws Error if the key is invalid (null, wrong length, non-hex) and ciphertext is non-zero.
*/
function decryptUint(ciphertext, userKey) {
// A zero ciphertext represents uninitialized/empty storage, which decrypts
// to plaintext 0. Short-circuit before touching the key so callers can read
// empty values without a valid key (and to avoid returning AES garbage).
if (ciphertext === 0n) {
return 0n;
}
const { cipher, r } = splitCtBlock((0, bytes_1.ctUintToBytes)(ciphertext));
// encodeKey validates and normalizes the key (strips "0x", enforces 128-bit)
const userKeyBytes = encodeKey(userKey);
// Decrypt the cipher
const decryptedMessage = decrypt(userKeyBytes, r, cipher);
return decodeUint(decryptedMessage);
}
exports.decryptUint = decryptUint;
/**
* Decrypts a canonical {@link ctUint256} ciphertext using the user's AES key.
*
* @param ciphertext - Object with `ciphertextHigh` and `ciphertextLow` parts.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Decrypted plaintext as a bigint (up to 256 bits).
* @throws Error if the key is invalid.
*/
function decryptUint256(ciphertext, userKey) {
const ciphertextBytes = (0, bytes_1.ctUint256ToBytes)(ciphertext);
const { cipher: cipherHigh, r: rHigh } = splitCtBlock(ciphertextBytes.slice(0, bytes_1.CT_SIZE));
const { cipher: cipherLow, r: rLow } = splitCtBlock(ciphertextBytes.slice(bytes_1.CT_SIZE));
const userKeyBytes = encodeKey(userKey);
// Decrypt both parts using the decrypt function
const decryptedMessage = decrypt(userKeyBytes, rHigh, cipherHigh, rLow, cipherLow);
return decodeUint(decryptedMessage);
}
exports.decryptUint256 = decryptUint256;
/**
* Decrypts a {@link ctString} ciphertext back to a UTF-8 string.
*
* Each chunk is decrypted via {@link decryptUint}; trailing zero padding added
* during {@link buildStringInputText} is trimmed before decoding.
*
* @param ciphertext - String ciphertext with an array of {@link ctUint} chunks.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Decrypted UTF-8 string.
* @throws Error if the key is invalid for any non-zero chunk.
*/
function decryptString(ciphertext, userKey) {
const allBytes = [];
for (let i = 0; i < ciphertext.value.length; i++) {
const decrypted = decryptUint(BigInt(ciphertext.value[i]), userKey);
const chunkBytes = encodeUint(decrypted);
// encodeUint returns 16 bytes (BLOCK_SIZE).
// buildStringInputText uses 8-byte chunks (EIGHT_BYTES).
// The relevant 8 bytes are at the end since encodeUint is Big-Endian.
for (let j = BLOCK_SIZE - EIGHT_BYTES; j < BLOCK_SIZE; j++) {
allBytes.push(chunkBytes[j]);
}
}
// Trim trailing zero bytes (padding added by buildStringInputText)
let end = allBytes.length;
while (end > 0 && allBytes[end - 1] === 0) {
end--;
}
const decoder = new TextDecoder();
return decoder.decode(new Uint8Array(allBytes.slice(0, end)));
}
exports.decryptString = decryptString;
/**
* Generates 16 bytes of cryptographically random AES key material as a
* node-forge binary string (each character's code point is one byte 0–255).
*
* @returns 16-character forge binary string (not hex).
* @example
* ```ts
* const hexKey = Buffer.from(generateRandomAesKeyBinaryString(), "binary").toString("hex")
* ```
*/
function generateRandomAesKeyBinaryString() {
return node_forge_1.default.random.getBytesSync(BLOCK_SIZE);
}
exports.generateRandomAesKeyBinaryString = generateRandomAesKeyBinaryString;
/**
* @deprecated Use {@link generateRandomAesKeyBinaryString} instead. Despite the
* name, the return value is a 16-byte binary string, not a numeric or hex value.
*/
function generateRandomAesKeySizeNumber() {
return generateRandomAesKeyBinaryString();
}
exports.generateRandomAesKeySizeNumber = generateRandomAesKeySizeNumber;
/**
* Converts a node-forge binary string into a `Uint8Array`.
*
* In forge binary strings each character's Unicode code point represents one
* byte (0–255). This is **not** UTF-8 encoding of human-readable text.
*
* @param binaryString - Forge binary string (one byte per character code point).
* @returns Byte array with one entry per character.
*/
function binaryStringToBytes(binaryString) {
return new Uint8Array(Array.from(binaryString, (char) => Number.parseInt(char.codePointAt(0)?.toString(bytes_1.HEX_BASE), bytes_1.HEX_BASE)));
}
exports.binaryStringToBytes = binaryStringToBytes;
/**
* @deprecated Use {@link binaryStringToBytes} instead. Despite the name, this does
* not UTF-8-encode a string; it converts a forge binary string to bytes.
*/
function encodeString(str) {
return binaryStringToBytes(str);
}
exports.encodeString = encodeString;
function bytesToBinaryString(bytes) {
return Array.from(bytes, byte => String.fromCodePoint(byte)).join('');
}
function toForgeBinaryString(value) {
return typeof value === 'string' ? value : bytesToBinaryString(value);
}
/**
* Validates and normalizes an AES key: ensures it is present, strips the "0x"
* prefix, and lowercases it. COTI uses a 128-bit AES key, so only 32-character
* hex strings are accepted.
*
* @param aesKey - The AES key, optionally prefixed with "0x".
* @returns The normalized lowercase hex string.
* @throws Error if the key is empty/null/undefined, contains non-hex characters, or is not 32 hex characters.
*/
function normalizeAesKey(aesKey) {
if (!aesKey) {
throw new Error("AES key is required");
}
const trimmed = aesKey.startsWith("0x") ? aesKey.slice(2) : aesKey;
const lowered = trimmed.toLowerCase();
if (!/^[0-9a-f]+$/.test(lowered)) {
throw new Error("Invalid AES key: contains non-hexadecimal characters");
}
if (lowered.length !== 32) {
throw new Error(`Invalid AES key: expected 32 hex characters (128-bit), got ${lowered.length}`);
}
return lowered;
}
exports.normalizeAesKey = normalizeAesKey;
/**
* Parses and validates a user AES key hex string into a 16-byte `Uint8Array`.
*
* Delegates to {@link normalizeAesKey} for validation (strips `0x`, lowercases,
* enforces 128-bit length) before hex decoding.
*
* @param userKey - AES key as a 32-character hex string (optional `0x` prefix).
* @returns 128-bit key as 16 bytes.
* @throws Error if the key is missing, wrong length, or contains non-hex characters.
*/
function encodeKey(userKey) {
const normalizedKey = normalizeAesKey(userKey);
const keyBytes = new Uint8Array(16);
for (let i = 0; i < 32; i += 2) {
keyBytes[i / 2] = Number.parseInt(normalizedKey.slice(i, i + 2), bytes_1.HEX_BASE);
}
return keyBytes;
}
exports.encodeKey = encodeKey;
/**
* Encodes an unsigned integer as a 16-byte big-endian byte array.
*
* @param plaintext - Integer value (typically up to 128 bits).
* @returns 16-byte big-endian representation.
*/
function encodeUint(plaintext) {
return (0, bytes_1.bigintToBytesBE)(plaintext, BLOCK_SIZE);
}
exports.encodeUint = encodeUint;
/**
* Decodes a big-endian byte array into an unsigned bigint.
*
* @param plaintextBytes - Byte array (commonly 16 bytes from {@link encodeUint}).
* @returns Decoded unsigned integer.
* @throws SyntaxError if `plaintextBytes` is empty.
*/
function decodeUint(plaintextBytes) {
return (0, bytes_1.bytesToBigint)(plaintextBytes);
}
exports.decodeUint = decodeUint;
/**
* AES-ECB encrypts a 16-byte random value (the `r` component of COTI encryption).
*
* @param r - Random value as a forge binary string or 16-byte `Uint8Array`.
* @param key - 128-bit AES key (16 bytes).
* @returns Encrypted 16-byte block.
* @throws RangeError if `key` is not 16 bytes.
*/
function encryptNumber(r, key) {
assertAesKeySize(key);
// Create a new AES cipher using the provided key
const cipher = node_forge_1.default.cipher.createCipher('AES-ECB', node_forge_1.default.util.createBuffer(bytesToBinaryString(key)));
// Encrypt the random value 'r' using AES in ECB mode
cipher.start();
cipher.update(node_forge_1.default.util.createBuffer(toForgeBinaryString(r)));
cipher.finish();
// Get the encrypted random value 'r' and ensure it's exactly 16 bytes
const encryptedR = binaryStringToBytes(cipher.output.data).slice(0, BLOCK_SIZE);
return encryptedR;
}
exports.encryptNumber = encryptNumber;
function signIT(sender, contract, hashFunc, ct, signingKey) {
const key = new ethers_1.SigningKey(signingKey);
const message = (0, ethers_1.solidityPackedKeccak256)(["bytes", "bytes", "bytes4", "bytes"], [sender, contract, hashFunc, ct]);
const sig = key.sign(message);
return signatureToBytes(sig);
}
/**
* Prepares a signed input text ({@link itUint}) for an unsigned integer.
*
* Encrypts up to **128-bit** plaintext, signs the resulting {@link ctUint},
* and returns the ciphertext/signature pair for smart contract submission.
* For 256-bit values use {@link prepareIT256} instead.
*
* @param plaintext - Unsigned value up to 128 bits.
* @param sender - Wallet and user AES key.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector.
* @returns Signed input text.
* @throws RangeError if `plaintext` exceeds 128 bits or is negative.
*/
function prepareIT(plaintext, sender, contractAddress, functionSelector) {
return buildUintInputText(plaintext, sender, contractAddress, functionSelector, 128, "Plaintext size must be 128 bits or smaller. To prepare a 256 bit plaintext, use prepareIT256 instead.");
}
exports.prepareIT = prepareIT;
// Helper function to create ciphertext for 128-bit plaintext
function createCiphertext128(plaintextBigInt, userAesKey) {
const plaintextBytes = (0, bytes_1.bigintToBytesBE)(plaintextBigInt, BLOCK_SIZE);
const encrypted = encrypt(userAesKey, plaintextBytes);
const encryptedHigh = encrypt(userAesKey, new Uint8Array(BLOCK_SIZE));
return packEncryptBlocks(encryptedHigh, encrypted);
}
// Helper function to create ciphertext for 256-bit plaintext
function createCiphertext256(plaintextBigInt, userAesKey) {
const plaintextBytes = (0, bytes_1.bigintToBytesBE)(plaintextBigInt, bytes_1.CT_SIZE);
const high = encrypt(userAesKey, plaintextBytes.slice(0, BLOCK_SIZE));
const low = encrypt(userAesKey, plaintextBytes.slice(BLOCK_SIZE));
return packEncryptBlocks(high, low);
}
function encryptUint128Unchecked(plaintext, userKey) {
const encrypted = encrypt(encodeKey(userKey), encodeUint(plaintext));
return decodeUint(packEncryptBlocks(encrypted));
}
function encryptUint256ToBytes(plaintextBigInt, userAesKey) {
const bitSize = plaintextBigInt.toString(2).length;
return bitSize <= MAX_PLAINTEXT_BIT_SIZE / 2
? createCiphertext128(plaintextBigInt, userAesKey)
: createCiphertext256(plaintextBigInt, userAesKey);
}
/**
* Encrypts an unsigned value into a {@link ctUint} without building an IT signature.
*
* Plaintext must fit in **64 bits**. The resulting {@link ctUint} bigint packs
* 32 bytes on the wire (16-byte ciphertext + 16-byte random `r`). For 128-bit
* input-text values use {@link prepareIT} instead.
*
* @param plaintext - Unsigned value up to 64 bits.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Encrypted ctUint ciphertext as a bigint.
* @throws RangeError if `plaintext` exceeds 64 bits or is negative.
* @throws Error if the key is invalid.
*/
function encryptUint(plaintext, userKey) {
return encryptUint128Unchecked(assertUintInRange(plaintext, 64, "Plaintext size must be 64 bits or smaller."), userKey);
}
exports.encryptUint = encryptUint;
/**
* Encrypts an unsigned value into a {@link ctUint256} without building an IT signature.
*
* Values up to 128 bits use compact encoding; larger values up to 256 bits use
* full two-block encoding. For signed submission use {@link prepareIT256},
* {@link prepareSignedIT256}, or {@link buildItUint256WithSigner} instead.
*
* @param plaintext - Unsigned value up to 256 bits.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Encrypted ctUint256 ciphertext.
* @throws RangeError if `plaintext` exceeds 256 bits or is negative.
* @throws Error if the key is invalid.
*/
function encryptUint256(plaintext, userKey) {
const plaintextBigInt = assertUintInRange(plaintext, MAX_PLAINTEXT_BIT_SIZE, "Plaintext size must be 256 bits or smaller.");
return (0, bytes_1.ciphertextBytesToCtUint256)(encryptUint256ToBytes(plaintextBigInt, encodeKey(userKey)));
}
exports.encryptUint256 = encryptUint256;
function toBigInt(value) {
if (value === undefined || value === null) {
throw new Error("Missing bigint value.");
}
if (typeof value === 'bigint') {
return value;
}
if (typeof value === 'number' || typeof value === 'string') {
return BigInt(value);
}
throw new Error("Invalid bigint value.");
}
function asRecord(value) {
return value && typeof value === 'object'
? value
: null;
}
/** Flat ctUint256 wire shapes: named fields or 2-element array. */
function parseFlatCtUint256(value) {
const record = asRecord(value);
if (record) {
const high = record.ciphertextHigh;
const low = record.ciphertextLow;
if (high !== undefined && low !== undefined) {
return { high, low };
}
}
if (Array.isArray(value) && value.length === 2) {
return { high: value[0], low: value[1] };
}
return null;
}
function normalizeCtPayload(value, type) {
if (type === 'ctUint64') {
return toBigInt(value);
}
const flat = parseFlatCtUint256(value);
if (!flat) {
throw new Error("Invalid ctUint256 payload.");
}
return {
ciphertextHigh: toBigInt(flat.high),
ciphertextLow: toBigInt(flat.low)
};
}
exports.normalizeCtPayload = normalizeCtPayload;
function isZeroValue(value) {
try {
return toBigInt(value) === 0n;
}
catch {
return false;
}
}
/** Permissive shape detection for untrusted/on-chain payloads (flat, tuple, nested). */
function parseCtUint256Shape(value) {
const flat = parseFlatCtUint256(value);
if (flat) {
return { kind: 'flat', high: flat.high, low: flat.low };
}
const record = asRecord(value);
if (!record) {
return null;
}
const highObj = asRecord(record.high);
const lowObj = asRecord(record.low);
if (highObj?.high !== undefined &&
highObj?.low !== undefined &&
lowObj?.high !== undefined &&
lowObj?.low !== undefined) {
return {
kind: 'nested',
parts: [highObj.high, highObj.low, lowObj.high, lowObj.low]
};
}
return null;
}
/**
* Returns whether `value` matches a supported {@link ctUint256} wire shape.
*
* Accepts flat objects, two-element tuples, and nested four-limb objects as
* defined by {@link CtUint256Like}. Does not validate ciphertext correctness.
*
* @param value - Value to inspect (e.g. from RPC or contract return data).
* @returns `true` if the shape is recognized.
*/
function isCtUint256Shape(value) {
return parseCtUint256Shape(value) !== null;
}
exports.isCtUint256Shape = isCtUint256Shape;
/**
* Returns whether a ctUint256 value represents zero / uninitialized storage.
*
* Handles scalar zero, flat `{ ciphertextHigh, ciphertextLow }`, tuple, and
* nested four-limb shapes. Returns `false` for unrecognized shapes.
*
* @param ciphertext - Ciphertext value in any supported wire format.
* @returns `true` if all ciphertext limbs are zero.
*/
function isZeroCtUint256(ciphertext) {
if (isZeroValue(ciphertext)) {
return true;
}
const parsed = parseCtUint256Shape(ciphertext);
if (!parsed) {
return false;
}
if (parsed.kind === 'nested') {
return parsed.parts.every(isZeroValue);
}
return isZeroValue(parsed.high) && isZeroValue(parsed.low);
}
exports.isZeroCtUint256 = isZeroCtUint256;
/**
* Decrypts a ctUint256 from any supported wire shape.
*
* Unlike {@link decryptUint256}, accepts flat, tuple, and nested on-chain/RPC
* formats (see {@link isCtUint256Shape}). For strict JSON coercion use
* {@link normalizeCtPayload} first.
*
* @param ciphertext - Flat, tuple, or nested ctUint256 payload.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Decrypted plaintext as a bigint (up to 256 bits).
* @throws Error if the shape is invalid or the key is invalid.
*/
function decryptCtUint256(ciphertext, userKey) {
const parsed = parseCtUint256Shape(ciphertext);
if (!parsed) {
throw new Error("Invalid ctUint256 payload.");
}
if (parsed.kind === 'nested') {
const [highHigh, highLow, lowHigh, lowLow] = parsed.parts;
const d1 = decryptUint(toBigInt(highHigh), userKey);
const d2 = decryptUint(toBigInt(highLow), userKey);
const d3 = decryptUint(toBigInt(lowHigh), userKey);
const d4 = decryptUint(toBigInt(lowLow), userKey);
return (d1 << 192n) + (d2 << 128n) + (d3 << 64n) + d4;
}
return decryptUint256({
ciphertextHigh: toBigInt(parsed.high),
ciphertextLow: toBigInt(parsed.low)
}, userKey);
}
exports.decryptCtUint256 = decryptCtUint256;
/**
* Builds a signed {@link itUint256Signed} using an external wallet signer.
*
* Encrypts the value, then signs an ABI-packed payload of
* `(signer, contract, selector, ciphertextHigh, ciphertextLow)`. Intended for
* browser wallets — **not** interchangeable with {@link prepareIT256}, which
* signs raw ciphertext bytes for private-key callers.
*
* @param params - Value, AES key, addresses, selector, and async sign callback.
* @returns Encrypted ctUint256 and hex signature string from the wallet.
* @throws RangeError if `value` exceeds 256 bits or is negative.
* @throws Error if the AES key is invalid.
*/
async function buildItUint256WithSigner({ value, aesKey, signerAddress, contractAddress, functionSelector, signMessage }) {
const ciphertext = encryptUint256(value, aesKey);
// Browser wallets sign the flat ABI payload. This intentionally differs
// from prepareIT256, which signs the raw 64-byte ciphertext for the
// private-key path used by legacy helpers.
const message = (0, ethers_1.solidityPacked)(["address", "address", "bytes4", "uint256", "uint256"], [
signerAddress,
contractAddress,
functionSelector,
ciphertext.ciphertextHigh,
ciphertext.ciphertextLow
]);
const signature = await signMessage((0, ethers_1.getBytes)(message));
return { ciphertext, signature };
}
exports.buildItUint256WithSigner = buildItUint256WithSigner;
/**
* Prepares a signed 256-bit input text ({@link itUint256}) for smart contract submission.
*
* Encrypts the plaintext (up to 256 bits), signs the raw 64-byte ciphertext bytes
* with the sender's private key, and returns the ciphertext/signature pair.
* For browser-wallet signing use {@link buildItUint256WithSigner} instead.
*
* @param plaintext - Unsigned value up to 256 bits.
* @param sender - Wallet and user AES key.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector.
* @returns Signed 256-bit input text.
* @throws RangeError if `plaintext` exceeds 256 bits or is negative.
*/
function prepareIT256(plaintext, sender, contractAddress, functionSelector) {
const plaintextBigInt = assertUintInRange(plaintext, MAX_PLAINTEXT_BIT_SIZE, "Plaintext size must be 256 bits or smaller.");
const senderBytes = (0, ethers_1.getBytes)(sender.wallet.address);
const contractBytes = (0, ethers_1.getBytes)(contractAddress);
const hashFuncBytes = (0, ethers_1.getBytes)(functionSelector);
const signingKeyBytes = (0, ethers_1.getBytes)(sender.wallet.privateKey);
const ct = encryptUint256ToBytes(plaintextBigInt, encodeKey(sender.userKey));
const signature = signIT(senderBytes, contractBytes, hashFuncBytes, ct, signingKeyBytes);
return {
ciphertext: (0, bytes_1.ciphertextBytesToCtUint256)(ct),
signature
};
}
exports.prepareIT256 = prepareIT256;
/**
* Encrypts a signed int256 into a {@link ctInt256} without building an IT signature.
*
* Negative values are encoded as 256-bit two's complement, then encrypted with
* the same wire format as {@link encryptUint256}.
*
* @param plaintext - Signed value in `[-(2^255), 2^255 - 1]`.
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Encrypted ctInt256 ciphertext.
* @throws RangeError if `plaintext` is outside int256 range.
* @throws Error if the key is invalid.
*/
function encryptInt256(plaintext, userKey) {
const value = assertIntInRange(plaintext, MAX_PLAINTEXT_BIT_SIZE, "Plaintext size must fit in int256.");
return encryptUint256(toTwosComplementUint(value, MAX_PLAINTEXT_BIT_SIZE), userKey);
}
exports.encryptInt256 = encryptInt256;
/**
* Decrypts a {@link ctInt256} ciphertext and returns the signed plaintext.
*
* @param ciphertext - Encrypted int256 (same wire shape as ctUint256).
* @param userKey - AES key (32 hex chars, optionally `0x`-prefixed).
* @returns Signed plaintext in int256 range.
* @throws Error if the key is invalid.
*/
function decryptInt256(ciphertext, userKey) {
return fromTwosComplementUint(decryptUint256(ciphertext, userKey), MAX_PLAINTEXT_BIT_SIZE);
}
exports.decryptInt256 = decryptInt256;
/**
* Prepares a signed int256 input text ({@link itInt256}) for smart contract submission.
*
* Encodes `plaintext` as 256-bit two's complement, then delegates to
* {@link prepareIT256} for encrypt + private-key IT signature.
*
* @param plaintext - Signed value in `[-(2^255), 2^255 - 1]`.
* @param sender - Wallet and user AES key.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector.
* @returns Signed 256-bit input text.
* @throws RangeError if `plaintext` is outside int256 range.
*/
function prepareSignedIT256(plaintext, sender, contractAddress, functionSelector) {
const value = assertIntInRange(plaintext, MAX_PLAINTEXT_BIT_SIZE, "Plaintext size must fit in int256.");
return prepareIT256(toTwosComplementUint(value, MAX_PLAINTEXT_BIT_SIZE), sender, contractAddress, functionSelector);
}
exports.prepareSignedIT256 = prepareSignedIT256;
/**
* Builds a COTI input-text (IT) signature over (signer, contract, selector, ciphertext).
*
* @deprecated Use `signInputText` with an ethers `Wallet` for private-key
* signing. For browser signers, use `buildItUint256WithSigner` for 256-bit
* values. This private-key convenience wrapper will be removed in a future
* major version.
*
* @param signerAddress - Address of the signer; must match the address derived from privateKey.
* @param contractAddress - Target contract address.
* @param functionSelector - 4-byte function selector (e.g. "0x11223344").
* @param ciphertext - The encrypted value being signed.
* @param privateKey - Signer's private key.
* @returns The 65-byte signature as a hex string.
* @throws Error if signerAddress does not match the address derived from privateKey.
*/
function buildItSignature(signerAddress, contractAddress, functionSelector, ciphertext, privateKey) {
const wallet = new ethers_1.Wallet(privateKey);
if (wallet.address.toLowerCase() !== signerAddress.toLowerCase()) {
throw new Error("Invalid signer: signerAddress does not match the address derived from privateKey");
}
return (0, ethers_1.hexlify)(signItUintDigest(signerAddress, contractAddress, functionSelector, ciphertext, privateKey));
}
exports.buildItSignature = buildItSignature;