-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #108 from influenceth/encryption
Encryption helpers
- Loading branch information
Showing
7 changed files
with
291 additions
and
13 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
|
||
}); |