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.

458 lines (386 loc) 18.3 kB
import forge from 'node-forge' import {ethers} from "ethers"; export const BLOCK_SIZE = 16; // AES block size in bytes export const ADDRESS_SIZE = 20; // 160-bit is the output of the Keccak-256 algorithm on the sender/contract address export const FUNC_SIG_SIZE = 4; export const CT_SIZE = 32; export const KEY_SIZE = 32; export const 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. */ export function encrypt(key: Uint8Array, plaintext: Uint8Array):{ ciphertext: Buffer; r: Buffer } { if (plaintext.length > BLOCK_SIZE) { throw new RangeError("Plaintext size must be 128 bits or smaller."); } if (key.length !== BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits."); } // Create a new AES cipher using the provided key const r = forge.random.getBytesSync(BLOCK_SIZE); const encryptedR = aesEcbEncrypt(r, key); const plaintext_padded = Buffer.concat([Buffer.alloc(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. */ export function decrypt(key: Uint8Array, r: Uint8Array, ciphertext: Uint8Array): Uint8Array { // Ensure ciphertext size is 128 bits (16 bytes) if (ciphertext.length !== BLOCK_SIZE) { throw new RangeError("Ciphertext size must be 128 bits."); } // Ensure key size is 128 bits (16 bytes) if (key.length !== BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits."); } // Ensure random value size is 128 bits (16 bytes) if (r.length !== BLOCK_SIZE) { throw new RangeError("Random size must be 128 bits."); } const encryptedR = aesEcbEncrypt(r, key); const plaintext = new Uint8Array(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. */ export function generateAesKey(): Buffer { const key = forge.random.getBytesSync(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. */ export function generateECDSAPrivateKey(): Buffer { // Generate a new random wallet const wallet = 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. */ export function signIT(sender:Buffer, addr:Buffer, funcSig:Buffer, ct:Buffer, key:Buffer, eip191 = false) { if (sender.length !== ADDRESS_SIZE) { throw new RangeError(`Invalid sender address length: ${sender.length} bytes, must be ${ADDRESS_SIZE} bytes`); } if (addr.length !== ADDRESS_SIZE) { throw new RangeError(`Invalid contract address length: ${addr.length} bytes, must be ${ADDRESS_SIZE} bytes`); } if (funcSig.length !== FUNC_SIG_SIZE) { throw new RangeError(`Invalid signature size: ${funcSig.length} bytes, must be ${FUNC_SIG_SIZE} bytes`); } if (ct.length !== CT_SIZE) { throw new RangeError(`Invalid ct length: ${ct.length} bytes, must be ${CT_SIZE} bytes`); } if (key.length !== KEY_SIZE) { throw new RangeError(`Invalid key length: ${key.length} bytes, must be ${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. */ export function sign(message: Buffer, key:Buffer):Buffer { const hash = ethers.keccak256(message); const signingKey = new ethers.SigningKey(key); const signature = signingKey.sign(hash); // Concatenate r, s, and v bytes return Buffer.concat([ ethers.getBytes(signature.r), ethers.getBytes(signature.s), 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. */ export function signEIP191(message: Buffer, key: Buffer): Buffer { const hash = ethers.hashMessage(message); const signingKey = new ethers.SigningKey(key); const signature = signingKey.sign(hash); const vBytes = new Uint8Array([signature.v]); // Concatenate r, s, and v bytes return Buffer.concat([ ethers.getBytes(signature.r), 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. */ export function prepareMessage( plaintext: bigint, signerAddress: string, aesKey: string, contractAddress: string, functionSelector: string ): { encryptedInt: bigint; message: string } { // Validate signerAddress (Ethereum address) if (!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.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.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. */ export function prepareIT( plaintext:bigint, userAesKey:Buffer, sender:Buffer, contract:Buffer, hashFunc:Buffer, signingKey:Buffer, eip191 = false ):{ctInt:bigint, signature:Buffer} { // 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. */ export function generateRSAKeyPair():{privateKey:Buffer, publicKey:Buffer} { // Generate a new RSA key pair with 2048 bits const rsaKeyPair = forge.pki.rsa.generateKeyPair({ bits: 2048 }); // Convert the private and public keys to DER format const privateKey = forge.asn1.toDer(forge.pki.privateKeyToAsn1(rsaKeyPair.privateKey)).data; // Convert the public key to DER format const publicKey = forge.asn1.toDer(forge.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. */ export function encryptRSA(publicKeyUint8Array:Uint8Array, plaintext:string):Uint8Array { // 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 = forge.asn1.fromDer(binaryDerString); // Convert the ASN.1 object to an RSA public key const forgePublicKey = forge.pki.publicKeyFromAsn1(asn1PublicKey); // Encrypt the plaintext using RSA-OAEP with SHA-256 as the hash function const encrypted = forgePublicKey.encrypt(plaintext, 'RSA-OAEP', { md: forge.md.sha256.create() // Use SHA-256 for OAEP padding }); // Convert the encrypted binary string to a Uint8Array return new Uint8Array(forge.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. */ export function decryptRSA(privateKey: Uint8Array, ciphertext: string): Uint8Array { // Convert privateKey from Uint8Array to PEM format const privateKeyPEM = forge.pki.privateKeyToPem(forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(forge.util.createBuffer(privateKey)))); // Decrypt using RSA-OAEP const rsaPrivateKey = forge.pki.privateKeyFromPem(privateKeyPEM); const decrypted = rsaPrivateKey.decrypt(forge.util.hexToBytes(ciphertext), 'RSA-OAEP', { md: forge.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. */ export function getFuncSig(functionSig:string):Buffer { const functionSelector = 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. */ export function encodeString(str: string): Uint8Array { return new Uint8Array([...str.split('').map((char) => parseInt(char.codePointAt(0)?.toString(HEX_BASE)!, 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. */ export function reconstructUserKey(privateKey:Buffer, encryptedKeyShare0:string, encryptedKeyShare1:string) { 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. */ export function aesEcbEncrypt(r: string | Uint8Array, key: Uint8Array) { // Ensure key size is 128 bits (16 bytes) if (key.length != BLOCK_SIZE) { throw new RangeError("Key size must be 128 bits.") } // Create a new AES cipher using the provided key const cipher = forge.cipher.createCipher('AES-ECB', forge.util.createBuffer(key)) // Encrypt the random value 'r' using AES in ECB mode cipher.start() cipher.update(forge.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, BLOCK_SIZE) return encryptedR } export function decryptUint(ciphertext: bigint, userKey: string): bigint { // 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, BLOCK_SIZE) const r = ctArray.subarray(BLOCK_SIZE) const userKeyBytes = encodeKey(userKey) // Decrypt the cipher const decryptedMessage = decrypt(userKeyBytes, r, cipher) return decodeUint(decryptedMessage) } export function encodeKey(userKey: string): Uint8Array { const keyBytes = new Uint8Array(16) for (let i = 0; i < 32; i += 2) { keyBytes[i / 2] = parseInt(userKey.slice(i, i + 2), HEX_BASE) } return keyBytes } export function decodeUint(plaintextBytes: Uint8Array): bigint { const plaintext: Array<string> = [] let byte = '' for (let i = 0; i < plaintextBytes.length; i++) { byte = plaintextBytes[i].toString(HEX_BASE).padStart(2, '0') // ensure that the zero byte is represented using two digits plaintext.push(byte) } return BigInt("0x" + plaintext.join("")) }