Skip to content

Commit

Permalink
Merge pull request #108 from influenceth/encryption
Browse files Browse the repository at this point in the history
Encryption helpers
  • Loading branch information
clexmond authored Nov 18, 2024
2 parents 9431084 + f6d2c81 commit 2fec5cc
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 13 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{
"name": "@influenceth/sdk",
"version": "2.3.4",
"version": "2.3.6",
"description": "Influence SDK",
"type": "module",
"module": "./build/index.js",
"exports": {
"import": "./build/index.js",
"require": "./build/index.cjs"
},
"browser": {
"crypto": false
},
"files": [
"build/*"
],
Expand Down
12 changes: 12 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import System from './lib/system.js';

import AdalianOrbit from './utils/AdalianOrbit.js';
import Address from './utils/address.js';
import Encryption from './utils/encryption.js';
import Fixed from './utils/fixed.js';
import Merkle from './utils/MerkleTree.js';
import ProductionJSON from './utils/ProductionJSON.js';
Expand All @@ -39,10 +40,21 @@ import ethereumContracts from './contracts/ethereum_abis.json' assert { type: 'j
import starknetAddresses from './contracts/starknet_addresses.json' assert { type: 'json' };
import starknetContracts from './contracts/starknet_abis.json' assert { type: 'json' };

(async function() {
const isNode = typeof process !== 'undefined' && !!process?.versions?.node;
if (isNode && !globalThis.crypto) {
const { webcrypto } = await import('crypto');
if (webcrypto) {
globalThis.crypto = webcrypto;
}
}
})();

// Utility libs
export {
AdalianOrbit,
Address,
Encryption,
Fixed,
Merkle,
ProductionJSON,
Expand Down
9 changes: 5 additions & 4 deletions src/lib/delivery.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const INSTANT_TRANSPORT_DISTANCE = 5; // instant transport distance in km

const STATUSES = {
PACKAGED: 3, // packaged, controlled by origin
ON_HOLD: 1, // controlled by a system (rather than a user)
SENT: 4, // sent, controlled by destination
COMPLETE: 2 // complete at destination
PACKAGED: 3, // packaged, controlled by origin
ON_HOLD: 1, // controlled by a system (rather than a user)
SENT: 4, // sent, controlled by destination
COMPLETE: 2, // complete at destination
REJECTED: 0 // rejected (i.e. packaged but dismissed)
};

export default {
Expand Down
16 changes: 10 additions & 6 deletions src/lib/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const parseCairoType = (cairoType) => {
let type;
if (['influence::common::types::entity::Entity'].includes(cairoType)) type = 'Entity';
else if (['core::starknet::contract_address::ContractAddress'].includes(cairoType)) type = 'ContractAddress';
else if (['core::integer::u64', 'core::integer::u128', 'core::integer::u256'].includes(cairoType)) type = 'BigNumber';
else if (['core::integer::u64', 'core::integer::u128'].includes(cairoType)) type = 'BigNumber';
else if (['core::integer::u256'].includes(cairoType)) type = 'u256';
else if (['influence::common::types::string::String', 'core::felt252'].includes(cairoType)) type = 'String';
else if (['influence::common::types::inventory_item::InventoryItem'].includes(cairoType)) type = 'InventoryItem';
else if (['influence::interfaces::escrow::Withdrawal'].includes(cairoType)) type = 'Withdrawal';
Expand Down Expand Up @@ -101,9 +102,12 @@ const formatSystemCalldata = (name, vars, limitToVars = false) => {
(isArray ? vars[name] : [vars[name]]).forEach((v) => {
const formattedVar = formatCalldataValue(type, v);
try {
(Array.isArray(formattedVar) ? formattedVar : [formattedVar]).forEach((val) => {
acc.push(val);
});
let parts;
if (Array.isArray(formattedVar)) parts = formattedVar;
else if (typeof formattedVar === 'object') parts = Object.values(formattedVar);
else parts = [formattedVar];

parts.forEach((val) => acc.push(val));
} catch (e) {
console.error(`${name} could not be formatted`, vars[name], e);
}
Expand All @@ -130,7 +134,7 @@ const getApproveErc20Call = (amount, erc20Address, dispatcherAddress) => getForm
'approve',
[
{ value: dispatcherAddress, type: 'ContractAddress' },
{ value: amount, type: 'Ether' }
{ value: amount, type: 'u256' }
]
);

Expand All @@ -139,7 +143,7 @@ const getEscrowDepositCall = (amount, depositHook, withdrawHook, escrowAddress,
'deposit',
[
{ value: swayAddress, type: 'ContractAddress' },
{ value: amount, type: 'Ether' }, // using Ether b/c should match u256
{ value: amount, type: 'u256' },
{ value: withdrawHook, type: 'EscrowHook' },
{ value: depositHook, type: 'EscrowHook' }
]
Expand Down
165 changes: 165 additions & 0 deletions src/utils/encryption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ec } from 'starknet';

// Helper functions
function bytesToHex(bytes) {
return Array.from(bytes)
.map((byte) => ('00' + byte.toString(16)).slice(-2))
.join('');
}

function hexToBytes(hexStr) {
const bytes = new Uint8Array(hexStr.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hexStr.substr(i * 2, 2), 16);
}
return bytes;
}

export function generateSeed() {
const array = crypto.getRandomValues(new Uint8Array(32));
return Array.from(array).map(byte => byte.toString(16).padStart(2, '0')).join('');
}

export function publicKeyToMessagingKeys(publicKey) {
if (publicKey?.length === 130) {
return {
messaging_key_x: BigInt(`0x${publicKey.substr(2, 64)}`),
messaging_key_y: BigInt(`0x${publicKey.substr(66)}`),
};
}
}

export function messagingKeysToPublicKey({ messaging_key_x, messaging_key_y }) {
if (messaging_key_x && messaging_key_y) {
return `04${BigInt(messaging_key_x).toString(16).padStart(64, '0')}${BigInt(messaging_key_y).toString(16).padStart(64, '0')}`;
}
return null;
}

// Function to generate private key from seed
export async function generatePrivateKeyFromSeed(seed) {
// Convert seed to bytes
const encoder = new TextEncoder();
const seedBytes = encoder.encode(seed);
// Hash the seed using SHA-256
const hashBuffer = await crypto.subtle.digest('SHA-256', seedBytes);
const hashArray = new Uint8Array(hashBuffer);
// Convert hash to BigInt
const hashHex = bytesToHex(hashArray);
const hashBigInt = BigInt('0x' + hashHex);
// Reduce modulo curve order n to get a valid private key
const privateKey = hashBigInt % ec.starkCurve.CURVE.n;
// Ensure privateKey != 0
if (privateKey === 0n) {
throw new Error('Invalid seed resulting in zero private key');
}
return `0x${privateKey.toString(16).padStart(64, '0')}`;
}

// Function to get public key from private key
export function getPublicKeyFromPrivateKey(privateKey, returnHex = true) {
const publicKey = ec.starkCurve.getPublicKey(privateKey, false); // Uint8Array
return returnHex ? bytesToHex(publicKey) : publicKey;
}

// Function to encrypt message
export async function encryptContent(recipientPublicKeyHex, message) {
// Convert recipientPublicKeyHex to Uint8Array
const recipientPublicKeyBytes = hexToBytes(recipientPublicKeyHex);

// Generate ephemeral keypair
const ephemeralPrivateKey = ec.starkCurve.utils.randomPrivateKey();
const ephemeralPublicKey = ec.starkCurve.getPublicKey(ephemeralPrivateKey, false); // Uint8Array

// Compute shared secret
const sharedSecret = ec.starkCurve.getSharedSecret(ephemeralPrivateKey, recipientPublicKeyBytes); // Uint8Array

// Derive symmetric key
const hashBuffer = await crypto.subtle.digest('SHA-256', sharedSecret);

// Import symmetric key
const symmetricKey = await crypto.subtle.importKey(
'raw',
hashBuffer,
{ name: 'AES-GCM' },
false,
['encrypt']
);

// Generate IV
const iv = crypto.getRandomValues(new Uint8Array(12));

// Encrypt the message using AES-256-GCM
const encoder = new TextEncoder();
const messageBytes = encoder.encode(message);

const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
symmetricKey,
messageBytes
);

// The encrypted data is an ArrayBuffer
const encryptedBytes = new Uint8Array(encryptedBuffer);

// Return encrypted data: ephemeralPublicKey, iv, encryptedMessage
return {
ephemeralPublicKey: bytesToHex(ephemeralPublicKey),
iv: bytesToHex(iv),
encryptedMessage: bytesToHex(encryptedBytes),
};
}

// Function to decrypt message
export async function decryptContent(privateKey, encryptedData) {
const { ephemeralPublicKey, iv, encryptedMessage } = encryptedData;

// Convert data from hex to Uint8Array
const ephemeralPublicKeyBytes = hexToBytes(ephemeralPublicKey);
const ivBytes = hexToBytes(iv);
const encryptedBytes = hexToBytes(encryptedMessage);

// Compute shared secret
const sharedSecret = ec.starkCurve.getSharedSecret(privateKey, ephemeralPublicKeyBytes); // Uint8Array

// Derive symmetric key
const hashBuffer = await crypto.subtle.digest('SHA-256', sharedSecret);

// Import symmetric key
const symmetricKey = await crypto.subtle.importKey(
'raw',
hashBuffer,
{ name: 'AES-GCM' },
false,
['decrypt']
);

// Decrypt the message using AES-256-GCM
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivBytes,
},
symmetricKey,
encryptedBytes
);

// Convert decryptedBuffer to string
const decoder = new TextDecoder();
const decryptedMessage = decoder.decode(decryptedBuffer);

return decryptedMessage;
}

export default {
generateSeed,
generatePrivateKeyFromSeed,
getPublicKeyFromPrivateKey,
publicKeyToMessagingKeys,
messagingKeysToPublicKey,
encryptContent,
decryptContent,
}
93 changes: 93 additions & 0 deletions test/utils/encryption.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { expect } from 'chai';
import { Encryption } from '../../src/index.js';

const sampleSeed = '2a1ded6e8f66bc48e5872bf9105e80046300308962fe51f0b611f2af6bab60ea';
const samplePrivateKey = '0x0550aeb48829403d98e3ba904acc0217e6dfbd009f0b5ad98569c4b61e373309';
const samplePublicKey = '0404fa8d30182b0f62618a481bf4afb14b815057570a028355fae8da22969840f6042a3c380981faa3ae8e95406ab898b91f124f7f7aced5f3ec4746b99e529c95';
const sampleMessagingKeys = {
messaging_key_x: 2251937603385208024268746691711293108925510072271936097377258983003822506230n,
messaging_key_y: 1883874586592859398548265238594790780856956796067331763265145999321206004885n
};
const sampleDecryptedMessage = 'Hello, world!';
const sampleEncryptedMessage = {
ephemeralPublicKey: '040314053669c042458428babf66fb999d8e489399a3db7d1df9888bd0553b6c1502243a2abf82c8445a6c3b3e8c52fa435cf30554dd10c3b0d7343ed33f792124',
iv: '3cb407c88633f38b1edbf950',
encryptedMessage: '8a7b5550e7f4291a2c762c3dbfe6191b2d61952b19038c0987cef06156'
};

describe('Encryption library', function () {
describe('generateSeed', function () {
it ('should generate a seed', function () {
const seed = Encryption.generateSeed();
expect(seed).to.be.a('string').with.lengthOf(64);
});
});

describe('publicKeyToMessagingKeys', function () {
it ('should convert a public key to message keys', function () {
const messagingKeys = Encryption.publicKeyToMessagingKeys(samplePublicKey);
expect(messagingKeys).to.be.an('object').with.keys('messaging_key_x', 'messaging_key_y');
expect(messagingKeys.messaging_key_x).to.be.a('bigint');
expect(messagingKeys.messaging_key_y).to.be.a('bigint');
expect(messagingKeys).to.deep.equal(sampleMessagingKeys);
});
});

describe('messagingKeysToPublicKey', function () {
it ('should convert message keys to a public key', function () {
const publicKey = Encryption.messagingKeysToPublicKey(sampleMessagingKeys);
expect(publicKey).to.be.a('string').with.lengthOf(130);
expect(publicKey.slice(0, 2)).to.equal('04');
expect(publicKey).to.equal(samplePublicKey);
});
});

describe('getPublicKeyFromPrivateKey', function () {
it('should get a public key (buffer) from a private key', function () {
const publicKey = Encryption.getPublicKeyFromPrivateKey(samplePrivateKey, false);
expect(publicKey).to.be.a('Uint8Array').with.lengthOf(65);
expect(publicKey[0]).to.equal(4);
});

it('should get a public key (hex string) from a private key', function () {
const publicKey = Encryption.getPublicKeyFromPrivateKey(samplePrivateKey);
expect(publicKey).to.be.a('string').with.lengthOf(130);
expect(publicKey.slice(0, 2)).to.equal('04');
expect(publicKey).to.equal(samplePublicKey);
});
});

describe('generatePrivateKeyFromSeed', function () {
it ('should generate a private key from a seed', async function () {
const privateKey = await Encryption.generatePrivateKeyFromSeed(sampleSeed);
expect(privateKey).to.be.a('string').with.lengthOf(66);
expect(privateKey).to.equal(samplePrivateKey);
});
});

describe('encrypt + decrypt', function () {
it ('should encrypt a message', async function () {
const encryptedMessage = await Encryption.encryptContent(samplePublicKey, sampleDecryptedMessage);
expect(encryptedMessage).to.be.a('object').with.keys('ephemeralPublicKey', 'iv', 'encryptedMessage');
});

it ('should decrypt a message', async function () {
const decryptedMessage = await Encryption.decryptContent(samplePrivateKey, sampleEncryptedMessage);
expect(decryptedMessage).to.equal(sampleDecryptedMessage);
});

it ('should encrypt and decrypt a unique message', async function () {
const message = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
${Date.now()}
`;

const encryptedMessage = await Encryption.encryptContent(samplePublicKey, message);
expect(encryptedMessage).to.not.equal(message);
const decryptedMessage = await Encryption.decryptContent(samplePrivateKey, encryptedMessage);
expect(decryptedMessage).to.equal(message);
});
});

});

0 comments on commit 2fec5cc

Please sign in to comment.