@coti-io/coti-sdk-typescript
Version:
A library for encryption, decryption and cryptographic utilities for the COTI blockchain.
236 lines (235 loc) • 10.8 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.encryptNumber = exports.decodeUint = exports.encodeUint = exports.encodeKey = exports.encodeString = exports.generateRandomAesKeySizeNumber = exports.decryptString = 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 BLOCK_SIZE = 16; // AES block size in bytes
const HEX_BASE = 16;
const EIGHT_BYTES = 8;
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: encodeString(r)
};
}
exports.encrypt = encrypt;
function decrypt(key, r, ciphertext) {
if (ciphertext.length !== BLOCK_SIZE) {
throw new RangeError("Ciphertext size must be 128 bits.");
}
// Ensure random size is 128 bits (16 bytes)
if (r.length != BLOCK_SIZE) {
throw new RangeError("Random size must be 128 bits.");
}
// Get the encrypted random value 'r'
const encryptedR = encryptNumber(r, key);
// XOR the encrypted random value 'r' with the ciphertext to obtain the plaintext
const plaintext = new Uint8Array(BLOCK_SIZE);
for (let i = 0; i < encryptedR.length; i++) {
plaintext[i] = encryptedR[i] ^ ciphertext[i];
}
return plaintext;
}
exports.decrypt = decrypt;
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: encodeString(privateKey),
publicKey: encodeString(publicKey)
};
}
exports.generateRSAKeyPair = generateRSAKeyPair;
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()
});
const decryptedBytes = encodeString(decrypted);
const userKey = [];
for (let i = 0; i < decryptedBytes.length; i++) {
userKey.push(decryptedBytes[i]
.toString(16)
.padStart(2, '0') // make sure each cell is one byte
);
}
return userKey.join("");
}
exports.decryptRSA = decryptRSA;
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];
}
const aesKey = [];
let byte = '';
for (let i = 0; i < aesKeyBytes.length; i++) {
byte = aesKeyBytes[i].toString(HEX_BASE).padStart(2, '0'); // ensure that the zero byte is represented using two digits
aesKey.push(byte);
}
return aesKey.join("");
}
exports.recoverUserKey = recoverUserKey;
function sign(message, privateKey) {
const key = new ethers_1.SigningKey(privateKey);
const sig = key.sign(message);
return new Uint8Array([...(0, ethers_1.getBytes)(sig.r), ...(0, ethers_1.getBytes)(sig.s), ...(0, ethers_1.getBytes)(`0x0${sig.v - 27}`)]);
}
exports.sign = sign;
function signInputText(sender, contractAddress, functionSelector, ct) {
const message = (0, ethers_1.solidityPackedKeccak256)(["address", "address", "bytes4", "uint256"], [sender.wallet.address, contractAddress, functionSelector, ct]);
return sign(message, sender.wallet.privateKey);
}
exports.signInputText = signInputText;
function buildInputText(plaintext, sender, contractAddress, functionSelector) {
if (plaintext >= BigInt(2) ** BigInt(64)) {
throw new RangeError("Plaintext size must be 64 bits or smaller.");
}
// Convert the plaintext to bytes
const plaintextBytes = encodeUint(plaintext);
// Convert user key to bytes
const keyBytes = encodeKey(sender.userKey);
// Encrypt the plaintext using AES key
const { ciphertext, r } = encrypt(keyBytes, plaintextBytes);
const ct = new Uint8Array([...ciphertext, ...r]);
// Convert the ciphertext to BigInt
const ctInt = decodeUint(ct);
const signature = signInputText(sender, contractAddress, functionSelector, ctInt);
return {
ciphertext: ctInt,
signature: signature
};
}
exports.buildInputText = buildInputText;
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 = buildInputText(decodeUint(byteArr), // convert the 8-byte hex string into a number
sender, contractAddress, functionSelector);
inputText.ciphertext.value.push(it.ciphertext);
inputText.signature.push(it.signature);
}
return inputText;
}
exports.buildStringInputText = buildStringInputText;
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, BLOCK_SIZE);
const r = ctArray.subarray(BLOCK_SIZE);
const userKeyBytes = encodeKey(userKey);
// Decrypt the cipher
const decryptedMessage = decrypt(userKeyBytes, r, cipher);
return decodeUint(decryptedMessage);
}
exports.decryptUint = decryptUint;
function decryptString(ciphertext, userKey) {
let encodedStr = new Uint8Array();
for (let i = 0; i < ciphertext.value.length; i++) {
const decrypted = decryptUint(BigInt(ciphertext.value[i]), userKey);
encodedStr = new Uint8Array([...encodedStr, ...encodeUint(decrypted)]);
}
const decoder = new TextDecoder();
return decoder
.decode(encodedStr)
.replace(/\0/g, '');
}
exports.decryptString = decryptString;
function generateRandomAesKeySizeNumber() {
return node_forge_1.default.random.getBytesSync(BLOCK_SIZE);
}
exports.generateRandomAesKeySizeNumber = generateRandomAesKeySizeNumber;
function encodeString(str) {
return new Uint8Array([...str.split('').map((char) => parseInt(char.codePointAt(0)?.toString(HEX_BASE), HEX_BASE))]);
}
exports.encodeString = encodeString;
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), HEX_BASE);
}
return keyBytes;
}
exports.encodeKey = encodeKey;
function encodeUint(plaintext) {
// Convert the plaintext to bytes in little-endian format
const plaintextBytes = new Uint8Array(BLOCK_SIZE); // Allocate a buffer of size 16 bytes
for (let i = 15; i >= 0; i--) {
plaintextBytes[i] = Number(plaintext & BigInt(255));
plaintext >>= BigInt(8);
}
return plaintextBytes;
}
exports.encodeUint = encodeUint;
function decodeUint(plaintextBytes) {
const plaintext = [];
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(""));
}
exports.decodeUint = decodeUint;
function encryptNumber(r, key) {
// 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 = 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, BLOCK_SIZE);
return encryptedR;
}
exports.encryptNumber = encryptNumber;