UNPKG

@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
"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;