UNPKG

soda-sdk

Version:

This SDK provides functionalities for AES and RSA encryption schemes, ECDSA signature scheme and some functionalities used for working with sodalabs blockchain.

393 lines (392 loc) 19.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HEX_BASE = exports.KEY_SIZE = exports.CT_SIZE = exports.FUNC_SIG_SIZE = exports.ADDRESS_SIZE = exports.BLOCK_SIZE = void 0; exports.encrypt = encrypt; exports.decrypt = decrypt; exports.generateAesKey = generateAesKey; exports.generateECDSAPrivateKey = generateECDSAPrivateKey; exports.signIT = signIT; exports.sign = sign; exports.signEIP191 = signEIP191; exports.prepareMessage = prepareMessage; exports.prepareIT = prepareIT; exports.generateRSAKeyPair = generateRSAKeyPair; exports.encryptRSA = encryptRSA; exports.decryptRSA = decryptRSA; exports.getFuncSig = getFuncSig; exports.encodeString = encodeString; exports.reconstructUserKey = reconstructUserKey; exports.aesEcbEncrypt = aesEcbEncrypt; exports.decryptUint = decryptUint; exports.encodeKey = encodeKey; exports.decodeUint = decodeUint; const node_forge_1 = __importDefault(require("node-forge")); const ethers_1 = require("ethers"); exports.BLOCK_SIZE = 16; // AES block size in bytes exports.ADDRESS_SIZE = 20; // 160-bit is the output of the Keccak-256 algorithm on the sender/contract address exports.FUNC_SIG_SIZE = 4; exports.CT_SIZE = 32; exports.KEY_SIZE = 32; exports.HEX_BASE = 16; /** * Encrypts a plaintext using AES encryption with a given key. * @param {Buffer} key - The AES key (16 bytes). * @param {Buffer} plaintext - The plaintext to encrypt (must be 16 bytes or smaller). * @returns {Object} - An object containing the ciphertext and the random value 'r' used during encryption. * @throws {RangeError} - Throws if plaintext is larger than 16 bytes or if the key size is not 16 bytes. */ function encrypt(key, plaintext) { if (plaintext.length > exports.BLOCK_SIZE) { throw new RangeError("Plaintext size must be 128 bits or smaller."); } if (key.length !== exports.BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits."); } // Create a new AES cipher using the provided key const r = node_forge_1.default.random.getBytesSync(exports.BLOCK_SIZE); const encryptedR = aesEcbEncrypt(r, key); const plaintext_padded = Buffer.concat([Buffer.alloc(exports.BLOCK_SIZE - plaintext.length), plaintext]); const ciphertext = Buffer.alloc(encryptedR.length); for (let i = 0; i < encryptedR.length; i++) { ciphertext[i] = encryptedR[i] ^ plaintext_padded[i]; } const uint8ArrayR = new Uint8Array(r.split('').map(c => c.charCodeAt(0))); return { ciphertext, r: Buffer.from(uint8ArrayR) }; } /** * Decrypts a ciphertext using AES decryption with a given key and random value 'r'. * @param {Buffer} key - The AES key (16 bytes). * @param {Buffer} r - The random value used during encryption (16 bytes). * @param {Buffer} ciphertext - The ciphertext to decrypt (16 bytes). * @returns {Uint8Array} - The decrypted plaintext. * @throws {RangeError} - Throws if any input size is incorrect. */ function decrypt(key, r, ciphertext) { // Ensure ciphertext size is 128 bits (16 bytes) if (ciphertext.length !== exports.BLOCK_SIZE) { throw new RangeError("Ciphertext size must be 128 bits."); } // Ensure key size is 128 bits (16 bytes) if (key.length !== exports.BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits."); } // Ensure random value size is 128 bits (16 bytes) if (r.length !== exports.BLOCK_SIZE) { throw new RangeError("Random size must be 128 bits."); } const encryptedR = aesEcbEncrypt(r, key); const plaintext = new Uint8Array(exports.BLOCK_SIZE); for (let i = 0; i < encryptedR.length; i++) { plaintext[i] = encryptedR[i] ^ ciphertext[i]; } return plaintext; } /** * Generates a random 128-bit AES key. * @returns {Buffer} - A Buffer containing a random 16-byte AES key. */ function generateAesKey() { const key = node_forge_1.default.random.getBytesSync(exports.BLOCK_SIZE); const uint8ArrayKey = new Uint8Array(key.split('').map(c => c.charCodeAt(0))); return Buffer.from(uint8ArrayKey); } /** * Generates a new ECDSA private key using the secp256k1 curve. * @returns {Buffer} - A Buffer containing a 32-byte private key. */ function generateECDSAPrivateKey() { // Generate a new random wallet const wallet = ethers_1.ethers.Wallet.createRandom(); const privateKeyHex = wallet.privateKey; // Return the private key as a Buffer without the '0x' prefix return Buffer.from(privateKeyHex.slice(2), 'hex'); } /** * Signs a message using the provided parameters and a given key. * Supports optional EIP-191 signing. * @param {Buffer} sender - The sender's address (20 bytes). * @param {Buffer} addr - The contract address (20 bytes). * @param {Buffer} funcSig - The function signature (4 bytes). * @param {Buffer} ct - The ciphertext (32 bytes). * @param {Buffer} key - The signing key (32 bytes). * @param {boolean} eip191 - Whether to use EIP-191 signing (default: false). * @returns {Buffer} - The signature as a Buffer. * @throws {RangeError} - Throws if input sizes are incorrect. */ function signIT(sender, addr, funcSig, ct, key, eip191 = false) { if (sender.length !== exports.ADDRESS_SIZE) { throw new RangeError(`Invalid sender address length: ${sender.length} bytes, must be ${exports.ADDRESS_SIZE} bytes`); } if (addr.length !== exports.ADDRESS_SIZE) { throw new RangeError(`Invalid contract address length: ${addr.length} bytes, must be ${exports.ADDRESS_SIZE} bytes`); } if (funcSig.length !== exports.FUNC_SIG_SIZE) { throw new RangeError(`Invalid signature size: ${funcSig.length} bytes, must be ${exports.FUNC_SIG_SIZE} bytes`); } if (ct.length !== exports.CT_SIZE) { throw new RangeError(`Invalid ct length: ${ct.length} bytes, must be ${exports.CT_SIZE} bytes`); } if (key.length !== exports.KEY_SIZE) { throw new RangeError(`Invalid key length: ${key.length} bytes, must be ${exports.KEY_SIZE} bytes`); } // Create the message to be signed by concatenating all inputs let message = Buffer.concat([sender, addr, funcSig, ct]); if (eip191) { return signEIP191(message, key); } else { return sign(message, key); } } /** * Signs a message using the standard signing process. * @param {Buffer} message - The message to sign. * @param {Buffer} key - The signing key (32 bytes). * @returns {Buffer} - The signature as a concatenation of r, s, and v values. */ function sign(message, key) { const hash = ethers_1.ethers.keccak256(message); const signingKey = new ethers_1.ethers.SigningKey(key); const signature = signingKey.sign(hash); // Concatenate r, s, and v bytes return Buffer.concat([ ethers_1.ethers.getBytes(signature.r), ethers_1.ethers.getBytes(signature.s), ethers_1.ethers.getBytes(`0x0${signature.v - 27}`) ]); } /** * Signs a message using EIP-191. * @param {Buffer} message - The message to sign. * @param {Buffer} key - The signing key (32 bytes). * @returns {Buffer} - The signature as a concatenation of r, s, and v values. */ function signEIP191(message, key) { const hash = ethers_1.ethers.hashMessage(message); const signingKey = new ethers_1.ethers.SigningKey(key); const signature = signingKey.sign(hash); const vBytes = new Uint8Array([signature.v]); // Concatenate r, s, and v bytes return Buffer.concat([ ethers_1.ethers.getBytes(signature.r), ethers_1.ethers.getBytes(signature.s), vBytes ]); } /** * Prepares a message by encrypting the given plaintext and constructing the message. This message needs to be signed to create an IT. * @param {bigint} plaintext - The plaintext value to be encrypted as a BigInt. * @param {string} signerAddress - The address of the signer (Ethereum address). * @param {string} aesKey - The AES key used for encryption (32 bytes as a hex string). * @param {string} contractAddress - The address of the contract (Ethereum address). * @param {string} functionSelector - The function selector (4 bytes as a hex string, e.g., '0x12345678'). * @returns {Object} - An object containing the encrypted integer and the message. * @throws {TypeError} - Throws if any of the input parameters are of invalid types or have incorrect lengths. */ function prepareMessage(plaintext, signerAddress, aesKey, contractAddress, functionSelector) { // Validate signerAddress (Ethereum address) if (!ethers_1.ethers.isAddress(signerAddress)) { throw new TypeError("Invalid signer address"); } // Validate aesKey (32 bytes as hex string) if (typeof aesKey !== "string" || aesKey.length !== 32) { throw new TypeError("Invalid AES key length. Expected 32 bytes."); } // Validate contractAddress (Ethereum address) if (typeof contractAddress !== "string" || !ethers_1.ethers.isAddress(signerAddress)) { throw new TypeError("Invalid contract address"); } // Validate functionSelector (4 bytes as hex string) if (typeof functionSelector !== "string" || functionSelector.length !== 10 || !functionSelector.startsWith('0x')) { throw new TypeError("Invalid function selector"); } // Convert the plaintext to bytes const plaintextBytes = Buffer.alloc(8); // Allocate a buffer of size 8 bytes plaintextBytes.writeBigUInt64BE(plaintext); // Write the uint64 value to the buffer as little-endian // Encrypt the plaintext using AES key const { ciphertext, r } = encrypt(Buffer.from(aesKey, 'hex'), plaintextBytes); const ct = Buffer.concat([ciphertext, r]); // Create the packed message const message = ethers_1.ethers.solidityPacked(["address", "address", "bytes4", "uint256"], [signerAddress, contractAddress, functionSelector, BigInt("0x" + ct.toString("hex"))]); // Convert the ciphertext to BigInt const encryptedInt = BigInt("0x" + ct.toString("hex")); return { encryptedInt, message }; } /** * Prepares an IT by encrypting the plaintext, signing the encrypted message, * and packaging the resulting data. This data represents encrypted data that can be sent to the contract. * @param {bigint} plaintext - The plaintext value to be encrypted as a BigInt. * @param {Buffer} userAesKey - The AES key used for encryption (16 bytes). * @param {Buffer} sender - The sender's address as a Buffer. * @param {Buffer} contract - The contract's address as a Buffer. * @param {Buffer} hashFunc - The function signature (4 bytes). * @param {Buffer} signingKey - The ECDSA signing key (32 bytes). * @param {boolean} [eip191=false] - Whether to use EIP-191 signing (default: false). * @returns {Object} - An object containing the encrypted integer (as `ctInt`) and the signature. */ function prepareIT(plaintext, userAesKey, sender, contract, hashFunc, signingKey, eip191 = false) { // Get the bytes of the sender, contract, and function signature // todo: check if sender and contract are already in bytes const senderBytes = sender; const contractBytes = contract; // Convert the plaintext to bytes const plaintextBytes = Buffer.alloc(8); // Allocate a buffer of size 8 bytes plaintextBytes.writeBigUInt64BE(BigInt(plaintext)); // Write the uint64 value to the buffer as little-endian // Encrypt the plaintext using AES key const { ciphertext, r } = encrypt(userAesKey, plaintextBytes); let ct = Buffer.concat([ciphertext, r]); // Sign the message const signature = signIT(senderBytes, contractBytes, hashFunc, ct, signingKey, eip191); // Convert the ciphertext to BigInt const ctInt = BigInt('0x' + ct.toString('hex')); return { ctInt, signature }; } /** * Generates a new RSA key pair. * @returns {Object} - An object containing the private key and public key as Buffers. */ function generateRSAKeyPair() { // Generate a new RSA key pair with 2048 bits const rsaKeyPair = node_forge_1.default.pki.rsa.generateKeyPair({ bits: 2048 }); // Convert the private and public keys to DER format const privateKey = node_forge_1.default.asn1.toDer(node_forge_1.default.pki.privateKeyToAsn1(rsaKeyPair.privateKey)).data; // Convert the public key to DER format const publicKey = node_forge_1.default.asn1.toDer(node_forge_1.default.pki.publicKeyToAsn1(rsaKeyPair.publicKey)).data; // Return the private and public keys as Buffers return { privateKey: Buffer.from(encodeString(privateKey)), publicKey: Buffer.from(encodeString(publicKey)) }; } /** * Encrypts plaintext using RSA with the provided public key. * @param {Uint8Array} publicKeyUint8Array - The RSA public key in Uint8Array format. * @param {string} plaintext - The plaintext to be encrypted. * @returns {Uint8Array} - The encrypted data as a Uint8Array. * @throws {Error} - Throws if the encryption fails or if the input format is incorrect. */ function encryptRSA(publicKeyUint8Array, plaintext) { // Convert the Uint8Array to a binary string for forge const binaryDerString = String.fromCharCode(...publicKeyUint8Array); // Decode the binary DER string into an ASN.1 object const asn1PublicKey = node_forge_1.default.asn1.fromDer(binaryDerString); // Convert the ASN.1 object to an RSA public key const forgePublicKey = node_forge_1.default.pki.publicKeyFromAsn1(asn1PublicKey); // Encrypt the plaintext using RSA-OAEP with SHA-256 as the hash function const encrypted = forgePublicKey.encrypt(plaintext, 'RSA-OAEP', { md: node_forge_1.default.md.sha256.create() // Use SHA-256 for OAEP padding }); // Convert the encrypted binary string to a Uint8Array return new Uint8Array(node_forge_1.default.util.createBuffer(encrypted, 'raw').bytes().split('').map(c => c.charCodeAt(0))); } /** * Decrypts RSA-encrypted data using the provided private key. * @param {Uint8Array} privateKey - The RSA private key in Uint8Array format. * @param {Uint8Array|string} ciphertext - The encrypted data to decrypt (Uint8Array or hex string). * @returns {Uint8Array} - The decrypted plaintext as a Uint8Array. * @throws {Error} - Throws if the decryption fails or if the input format is incorrect. */ 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(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() }); return encodeString(decrypted); } /** * Generates the function selector for a given function signature. * @param {string} functionSig - The function signature (e.g., 'test(bytes)'). * @returns {Buffer} - A Buffer containing the first 4 bytes of the Keccak-256 hash of the function signature. */ function getFuncSig(functionSig) { const functionSelector = ethers_1.ethers.id(functionSig).slice(0, 10); return Buffer.from(functionSelector.slice(2, 10), 'hex'); } /** * Encodes a string into a Uint8Array of hexadecimal values. * @param {string} str - The input string to encode. * @returns {Uint8Array} - A Uint8Array representing the encoded hexadecimal values of the input string. */ function encodeString(str) { return new Uint8Array([...str.split('').map((char) => parseInt(char.codePointAt(0)?.toString(exports.HEX_BASE), exports.HEX_BASE))]); } /** * This function recovers a user's key by decrypting two encrypted key shares with the given private key, * and then XORing the two key shares together. * * @param {Buffer} privateKey - The private key used to decrypt the key shares. * @param {string} encryptedKeyShare0 - The first encrypted key share. * @param {string} encryptedKeyShare1 - The second encrypted key share. * * @returns {Buffer} - The recovered user key. */ function reconstructUserKey(privateKey, encryptedKeyShare0, encryptedKeyShare1) { const decryptedKeyShare0 = decryptRSA(privateKey, encryptedKeyShare0); const decryptedKeyShare1 = decryptRSA(privateKey, encryptedKeyShare1); const aesKey = Buffer.alloc(decryptedKeyShare0.length); for (let i = 0; i < decryptedKeyShare0.length; i++) { aesKey[i] = decryptedKeyShare0[i] ^ decryptedKeyShare1[i]; } return aesKey; } /** * Encrypts a random value 'r' using AES in ECB mode with the provided key. * @param {string} r - The random value to be encrypted (16 bytes). * @param {Buffer} key - The AES key (16 bytes). * @returns {Uint8Array} - A Uint8Array containing the encrypted random value. * @throws {RangeError} - Throws if the key size is not 16 bytes. */ function aesEcbEncrypt(r, key) { // Ensure key size is 128 bits (16 bytes) if (key.length != exports.BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits."); } // 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(key)); // Encrypt the random value 'r' using AES in ECB mode cipher.start(); cipher.update(node_forge_1.default.util.createBuffer(r)); cipher.finish(); // Get the encrypted random value 'r' as a Buffer and ensure it's exactly 16 bytes const encryptedR = encodeString(cipher.output.data).slice(0, exports.BLOCK_SIZE); return encryptedR; } function decryptUint(ciphertext, userKey) { // Convert ciphertext to Uint8Array let ctArray = new Uint8Array(); while (ciphertext > 0) { const temp = new Uint8Array([Number(ciphertext & BigInt(255))]); ctArray = new Uint8Array([...temp, ...ctArray]); ciphertext >>= BigInt(8); } ctArray = new Uint8Array([...new Uint8Array(32 - ctArray.length), ...ctArray]); // Split CT into two 128-bit arrays r and cipher const cipher = ctArray.subarray(0, exports.BLOCK_SIZE); const r = ctArray.subarray(exports.BLOCK_SIZE); const userKeyBytes = encodeKey(userKey); // Decrypt the cipher const decryptedMessage = decrypt(userKeyBytes, r, cipher); return decodeUint(decryptedMessage); } function encodeKey(userKey) { const keyBytes = new Uint8Array(16); for (let i = 0; i < 32; i += 2) { keyBytes[i / 2] = parseInt(userKey.slice(i, i + 2), exports.HEX_BASE); } return keyBytes; } function decodeUint(plaintextBytes) { const plaintext = []; let byte = ''; for (let i = 0; i < plaintextBytes.length; i++) { byte = plaintextBytes[i].toString(exports.HEX_BASE).padStart(2, '0'); // ensure that the zero byte is represented using two digits plaintext.push(byte); } return BigInt("0x" + plaintext.join("")); }