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.

409 lines (322 loc) 15.4 kB
import { assert } from 'chai'; import { decrypt, decryptRSA, encrypt, encryptRSA, generateAesKey, generateECDSAPrivateKey, generateRSAKeyPair, getFuncSig, prepareIT, prepareMessage, signIT } from './crypto.mjs'; import { writeAesKey, loadAesKey } from './utils.mjs'; import { BLOCK_SIZE, ADDRESS_SIZE, FUNC_SIG_SIZE, HEX_BASE } from './crypto.mjs'; import fs from 'fs'; import crypto from 'crypto'; import ethereumjsUtil, {hashPersonalMessage} from 'ethereumjs-util'; import {ethers} from "ethers"; function extractSignatureComponents(signatureBytes) { // Allocate buffers for r, s, and v let rBytes = Buffer.alloc(32); let sBytes = Buffer.alloc(32); let vByte = Buffer.alloc(1); // Copy the corresponding bytes from the signature signatureBytes.copy(rBytes, 0, 0, 32); signatureBytes.copy(sBytes, 0, 32, 64); signatureBytes.copy(vByte, 0, 64); // Return the components as an object return { rBytes, sBytes, vByte }; } function uint8ArrayToBigInt(uint8Array) { let value = BigInt(0); for (let i = 0; i < uint8Array.length; i++) { value = (value << 8n) | BigInt(uint8Array[i]); } return value; } describe('Crypto Tests', () => { // Test case for encrypt and decrypt it('should encrypt and decrypt successfully', () => { // Arrange const plaintextInteger = 100; const plaintextBuffer = Buffer.alloc(1); plaintextBuffer.writeUInt8(plaintextInteger); // Act const key = generateAesKey(); const { ciphertext, r } = encrypt(key, plaintextBuffer); const decryptedBuffer = decrypt(key, r, ciphertext); // Write Buffer to file to later check in Go fs.writeFileSync("test_jsEncryption.txt", key.toString('hex') + "\n" + ciphertext.toString('hex') + "\n" + r.toString('hex')); const decryptedInteger = uint8ArrayToBigInt(decryptedBuffer) // Assert assert.strictEqual(decryptedInteger, BigInt(plaintextInteger)); }); // Test case for load and write AES key it('should load and write AES key successfully', () => { // Arrange const key = generateAesKey(); // Act writeAesKey('key.txt', key); const loadedKey = loadAesKey('key.txt'); // Assert assert.deepStrictEqual(loadedKey, key); // Delete the key file fs.unlinkSync('key.txt', (err) => { if (err) { console.error('Error deleting file:', err); } }); }); // Test case for invalid plaintext size it('should throw error for invalid plaintext size', () => { // Arrange const key = generateAesKey(); const plaintextBuffer = Buffer.alloc(20); // Bigger than 128 bits // Act and Assert assert.throws(() => encrypt(key, plaintextBuffer), RangeError); }); // Test case for invalid ciphertext size it('should throw error for invalid ciphertext size', () => { // Arrange const key = generateAesKey(); const ciphertext = Buffer.from([0x01, 0x02, 0x03]); // Smaller than 128 bits const r = Buffer.alloc(BLOCK_SIZE); // Act and Assert assert.throws(() => decrypt(key, r, ciphertext), RangeError); }); // Test case for invalid random size it('should throw error for invalid random size', () => { // Arrange const key = generateAesKey(); const r = Buffer.from([0x01, 0x02, 0x03]); // Smaller than 128 bits const ciphertext = Buffer.alloc(BLOCK_SIZE); // Act and Assert assert.throws(() => decrypt(key, r, ciphertext), RangeError); }); // Test case for invalid key size it('should throw error for invalid key size', () => { // Arrange const key = Buffer.from([0x01, 0x02, 0x03]); // Smaller than 128 bits // Act and Assert // Test invalid key size when writing key assert.throws(() => writeAesKey('key.txt', key), RangeError); // Test invalid key size when encrypting const plaintextBuffer = Buffer.alloc(BLOCK_SIZE); assert.throws(() => encrypt(key, plaintextBuffer), RangeError); // Test invalid key size when decrypting const ciphertext = Buffer.alloc(BLOCK_SIZE); const r = Buffer.alloc(BLOCK_SIZE); assert.throws(() => decrypt(key, r, ciphertext), RangeError); }); // Test case for verify signature it('should sign and verify the signature', () => { // Arrange // Simulate the generation of random bytes const sender = crypto.randomBytes(ADDRESS_SIZE); const addr = crypto.randomBytes(ADDRESS_SIZE); const funcSig = crypto.randomBytes(FUNC_SIG_SIZE); let key = generateECDSAPrivateKey(); // Create a ciphertext const plaintextBuffer = Buffer.alloc(1); plaintextBuffer.writeUInt8(100); const aeskey = generateAesKey(); const { ciphertext, r } = encrypt(aeskey, plaintextBuffer); let ct = Buffer.concat([ciphertext, r]); // Act // Generate the signature const signatureBytes = signIT(sender, addr, funcSig, ct, key); const {rBytes, sBytes, vByte} = extractSignatureComponents(signatureBytes); // Convert v buffer back to integer let v = vByte.readUInt8(); // JS expects v to be 27 or 28. But in Ethereum, v is either 0 or 1. // In the sign function, 27 is subtracted from v in order to make it work with ethereum. // Now 27 should be added back to v to make it work with JS veification. if (v !== 27 && v !== 28) { v += 27; } // Verify the signature const expectedPublicKey = ethereumjsUtil.privateToPublic(key); const expectedAddress = ethereumjsUtil.toChecksumAddress('0x' + expectedPublicKey.toString('hex')); const message = Buffer.concat([sender, addr, funcSig, ct]); const hash = ethereumjsUtil.keccak256(message); // Recover the public key from the signature const publicKey = ethereumjsUtil.ecrecover(hash, v, rBytes, sBytes); // Derive the Ethereum address from the recovered public key const address = ethereumjsUtil.toChecksumAddress('0x' + publicKey.toString('hex')); // Compare the derived address with the expected signer's address const isVerified = address === expectedAddress; // Assert assert.strictEqual(isVerified, true); }); // Test case for verify signature it('should sign and verify the EIP191 signature', () => { // Arrange // Simulate the generation of random bytes const sender = crypto.randomBytes(ADDRESS_SIZE); const addr = crypto.randomBytes(ADDRESS_SIZE); const funcSig = crypto.randomBytes(FUNC_SIG_SIZE); let key = generateECDSAPrivateKey(); // Create a ciphertext const plaintextBuffer = Buffer.alloc(1); plaintextBuffer.writeUInt8(100); const aeskey = generateAesKey(); const { ciphertext, r } = encrypt(aeskey, plaintextBuffer); let ct = Buffer.concat([ciphertext, r]); // Act // Generate the signature const signatureBytes = signIT(sender, addr, funcSig, ct, key, true); const {rBytes, sBytes, vByte} = extractSignatureComponents(signatureBytes); // Verify the signature const expectedPublicKey = ethereumjsUtil.privateToPublic(key); const expectedAddress = ethereumjsUtil.toChecksumAddress('0x' + expectedPublicKey.toString('hex')); const message = Buffer.concat([sender, addr, funcSig, ct]); const hash = hashPersonalMessage(message); // Recover the public key from the signature const publicKey = ethereumjsUtil.ecrecover(hash, vByte, rBytes, sBytes); // Derive the Ethereum address from the recovered public key const address = ethereumjsUtil.toChecksumAddress('0x' + publicKey.toString('hex')); // Compare the derived address with the expected signer's address const isVerified = address === expectedAddress; // Assert assert.strictEqual(isVerified, true); }); // Test case for verify signature it('should sign a fixed message and write the signature to a file', () => { // Arrange // Simulate the generation of random bytes const sender = Buffer.from('d67fe7792f18fbd663e29818334a050240887c28', 'hex'); const addr = Buffer.from('69413851f025306dbe12c48ff2225016fc5bbe1b', 'hex'); const funcSig = Buffer.from('dc85563d', 'hex'); const ct = Buffer.from('f8765e191e03bf341c1422e0899d092674fc73beb624845199cd6e14b7895882', 'hex'); const key = Buffer.from('3840f44be5805af188e9b42dda56eb99eefc88d7a6db751017ff16d0c5f8143e', 'hex'); // Act // Generate the signature const signature = signIT(sender, addr, funcSig, ct, key); const filename = 'test_jsSignature.txt'; // Name of the file to write to // Convert hexadecimal string to buffer let sigString = signature.toString('hex'); // Write buffer to the file, this simulates the communication between the evm (golang) and the user (python/js) fs.writeFile(filename, sigString, (err) => { if (err) { console.error('Error writing to file:', err); return; } }); }); it('should prepareMessage using fixed data', async () => { // Arrange // Simulate the generation of random bytes const plaintext = BigInt("100"); const userKey = 'b3c3fe73c1bb91862b166a29fe1d63e9'; const senderAddress ='0x8f01160c98e5cdfa625197849c85cf5fc1f76b1b'; const contractAddress = '0x69413851f025306dbe12c48ff2225016fc5bbe1b'; const funcSig = 'test(bytes)'; const signingKey = '0x3840f44be5805af188e9b42dda56eb99eefc88d7a6db751017ff16d0c5f8143e'; // Act // Generate the signature const functionSelector = getFuncSig(funcSig); const { message } = prepareMessage(plaintext, senderAddress, userKey, contractAddress, '0x' + functionSelector.toString('hex')); const wallet = new ethers.Wallet(signingKey); const signature = await wallet.signMessage(message); const recoveredAddress = ethers.verifyMessage(message, signature); // Assert assert.strictEqual(recoveredAddress.toLowerCase(), senderAddress.toLowerCase()); }); // Test case for verify signature it('should prepare IT using fixed data', () => { // Arrange // Simulate the generation of random bytes const plaintext = BigInt("100"); const userKey = Buffer.from('b3c3fe73c1bb91862b166a29fe1d63e9', 'hex'); const sender = new ethereumjsUtil.Address(ethereumjsUtil.toBuffer(Buffer.from('d67fe7792f18fbd663e29818334a050240887c28', 'hex'))); const contract = new ethereumjsUtil.Address(ethereumjsUtil.toBuffer(Buffer.from('69413851f025306dbe12c48ff2225016fc5bbe1b', 'hex'))); const funcSig = 'test(bytes)'; const signingKey = Buffer.from('3840f44be5805af188e9b42dda56eb99eefc88d7a6db751017ff16d0c5f8143e', 'hex'); // Act // Generate the signature const hash_func = getFuncSig(funcSig); const {ctInt, signature} = prepareIT(plaintext, userKey, sender, contract, hash_func, signingKey); const ctHex = ctInt.toString(HEX_BASE); // Create a Buffer to hold the bytes const ctBuffer = Buffer.from(ctHex, 'hex'); // Write Buffer to file to later check in Go fs.writeFileSync("test_jsIT.txt", ctHex + "\n" + signature.toString('hex')); // Decrypt the ct and check the decrypted value is equal to the plaintext const decryptedBuffer = decrypt(userKey, ctBuffer.subarray(BLOCK_SIZE, ctBuffer.length), ctBuffer.subarray(0, BLOCK_SIZE)); // Convert the plaintext to bytes const hexString = plaintext.toString(16); const plaintextBytes = Buffer.from(hexString, 'hex'); // Assert assert.deepStrictEqual(plaintextBytes, decryptedBuffer.subarray(decryptedBuffer.length - plaintextBytes.length, decryptedBuffer.length)); }); // Test case for test rsa encryption scheme it('should encrypt and decrypt a message using RSA scheme', () => { // Arrange const plaintext = Buffer.from('hello world'); const { publicKey, privateKey } = generateRSAKeyPair(); // Act const ciphertext = encryptRSA(publicKey, plaintext); const hexString = privateKey.toString('hex') + "\n" + publicKey.toString('hex'); // Write buffer to the file const filename = 'test_jsRSAEncryption.txt'; // Name of the file to write to fs.writeFile(filename, hexString, (err) => { if (err) { console.error('Error writing to file:', err); return; } }); const decrypted = decryptRSA(privateKey, ciphertext); // Assert assert.deepStrictEqual(plaintext, decrypted); }); function readHexFromFile(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); return; } const lines = data.trim().split('\n'); if (lines.length >= 3) { const hexData1 = lines[0].trim(); const hexData2 = lines[1].trim(); const hexData3 = lines[2].trim(); resolve([hexData1, hexData2, hexData3]); } else { reject(new Error('Not enough lines in the file.')); } }); }); } // Test case for test rsa decryption scheme it.skip('should decrypt a message using RSA scheme', () => { // Arrange const plaintext = Buffer.from('hello world'); // Act // Read private key and ciphertext // Reading from file simulates the communication between the evm (golang) and the user (python/js) readHexFromFile('test_jsRSAEncryption.txt') .then(([hexData1, hexData2, hexData3]) => { const privateKey = Buffer.from(hexData1, 'hex'); const ciphertext = Buffer.from(hexData3, 'hex'); const decrypted = decryptRSA(privateKey, ciphertext); // Assert assert.deepStrictEqual(plaintext, decrypted); }) .catch(error => { console.error("Error reading file:", error); }); fs.unlinkSync('test_jsRSAEncryption.txt'); }); // Test case for test function signature it('should hash a function signature', () => { // Arrange const functionSig = 'sign(bytes)'; // Act const hash = getFuncSig(functionSig); const filename = 'test_jsFunctionKeccak.txt'; // Name of the file to write to // Write Buffer to file fs.writeFileSync(filename, hash.toString('hex')); }); });