Created
March 27, 2023 04:56
-
-
Save HarryR/eb5fd5fb77eda9f509cb94c91fe76fef to your computer and use it in GitHub Desktop.
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
const { expect } = require("chai"); | |
const deoxysii = require('deoxysii'); | |
const { sha512_256 } = require('js-sha512'); | |
const nacl = require('tweetnacl'); | |
// XXX: why isn't this exported in nacl.lowlevel, field inversion is useful! | |
function inv25519(o, i) { | |
const {gf, S, M} = nacl.lowlevel; | |
var c = gf(); | |
var a; | |
for (a = 0; a < 16; a++) c[a] = i[a]; | |
for (a = 253; a >= 0; a--) { | |
S(c, c); | |
if(a !== 2 && a !== 4) M(c, c, i); | |
} | |
for (a = 0; a < 16; a++) o[a] = c[a]; | |
} | |
/** | |
* convert ed25519 public key to its montgomery coordinate | |
* | |
* ed25519 is birationally equiv. to montgomery curve | |
* | |
* (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x) | |
* (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) | |
* | |
* Examples: | |
* - https://github.com/StableLib/stablelib/blob/master/packages/ed25519/ed25519.ts#L861 (convertPublicKeyToX25519) | |
* - https://docs.rs/ed25519_to_curve25519/latest/src/ed25519_to_curve25519/lib.rs.html#1-108 | |
*/ | |
function ed25519_public_to_mont25519(publicKey) { | |
const {gf, pack25519, unpack25519, A, Z, M} = nacl.lowlevel; | |
var AY = gf(); | |
unpack25519(AY, publicKey); | |
var one_minus_y = gf([1]); | |
Z(one_minus_y, one_minus_y, AY); | |
inv25519(one_minus_y, one_minus_y); | |
var x = gf([1]); | |
A(x, x, AY); | |
M(x, x, one_minus_y); | |
var o = new Uint8Array(32); | |
pack25519(o, x); | |
return o; | |
} | |
function buf2hex(buffer) { // buffer is an ArrayBuffer | |
return [...new Uint8Array(buffer)] | |
.map(x => x.toString(16).padStart(2, '0')) | |
.join(''); | |
} | |
function derive_shared_secret(secretKey, peerPublicKey) { | |
const shared = nacl.scalarMult(secretKey, peerPublicKey); | |
return sha512_256.hmac | |
.create('MRAE_Box_Deoxys-II-256-128') | |
.update(shared) | |
.arrayBuffer(); | |
} | |
function ed25519_secret_to_nacl_box_keypair (secretKey) { | |
// ed25519 secret is pre-hashed to derive x25519 keypair | |
var d = new Uint8Array(32); | |
nacl.lowlevel.crypto_hash(d, secretKey, 32); | |
d[0] &= 248; d[31] &= 127; d[31] |= 64; | |
return nacl.box.keyPair.fromSecretKey(d); | |
} | |
describe("E2Example contract", function () { | |
async function deployFixture() { | |
const E2Example = await ethers.getContractFactory("E2Example"); | |
const [owner, addr1, addr2] = await ethers.getSigners(); | |
const ee = await E2Example.deploy(); | |
await ee.deployed(); | |
const pktx = await ee.emitKeys(); | |
const receipt = await pktx.wait(); | |
expect(receipt.events).to.have.lengthOf(2); | |
const pk_event = receipt.events[0]; | |
const ee_publicKey = pk_event.args[0]; | |
expect(ee_publicKey).to.be.a('string'); | |
expect(ee_publicKey).to.have.lengthOf(66); | |
const ee_public_ed25519_bytes = ethers.utils.arrayify(ee_publicKey); | |
const ee_public_x25519_bytes = ed25519_public_to_mont25519(new Uint8Array(ee_public_ed25519_bytes)); | |
const sk_event = receipt.events[1]; | |
const ee_secretKey_bytes = ethers.utils.arrayify(sk_event.args[0]); | |
return { E2Example, ee, owner, addr1, addr2, ee_public_ed25519_bytes, ee_secretKey_bytes, ee_public_x25519_bytes }; | |
} | |
describe("Deployment", function () { | |
it("Emits public key", async function () { | |
const { ee, ee_public_ed25519_bytes, ee_public_x25519_bytes, ee_secretKey_bytes } = await deployFixture(); | |
// Verify ed25519 public can be derived from server secret | |
expect(buf2hex(ee_public_ed25519_bytes)).to.equal(buf2hex(nacl.sign.keyPair.fromSeed(ee_secretKey_bytes).publicKey)); | |
//console.log(''); | |
// Verify conversion of ed25519 public to montgomery repr. and its derivation from the server secret | |
const server_secret_box_keyPair = ed25519_secret_to_nacl_box_keypair(ee_secretKey_bytes); | |
expect(buf2hex(ee_public_x25519_bytes)).to.equal(buf2hex(server_secret_box_keyPair.publicKey)); | |
const client_keypair = nacl.box.keyPair(); | |
const ephem_derived = derive_shared_secret(client_keypair.secretKey, ee_public_x25519_bytes); | |
// Derive shared secret between ephemeral client keypair and server x25519 keypair | |
const result2 = await ee.example_sharedsecret(client_keypair.publicKey); | |
expect(result2).to.equal('0x' + buf2hex(ephem_derived)); | |
// Construct arguments for encrypted session | |
const plaintext = ethers.utils.defaultAbiCoder.encode(["tuple(uint256 a, uint256 b, uint256 c)"], [{a: 1, b: 2, c: 3}]); | |
const authenticated_param = ethers.utils.defaultAbiCoder.encode(['uint256'], [12345]); | |
const nonce = ethers.utils.keccak256(client_keypair.publicKey); | |
var x = new deoxysii.AEAD(new Uint8Array(ephem_derived)); | |
var ciphertext = x.encrypt(ethers.utils.arrayify(nonce).slice(0, deoxysii.NonceSize), ethers.utils.arrayify(plaintext), ethers.utils.arrayify(authenticated_param)); | |
// Submit encrypted arguments | |
var resp = await ee.example(client_keypair.publicKey, ciphertext, 12345); | |
var resp_receipt = await resp.wait(); | |
console.log(resp_receipt); | |
}); | |
}); | |
}); |
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
pragma solidity ^0.8.9; | |
contract E2Example | |
{ | |
event EncryptedResponse(bytes32 nonce, bytes data); | |
event DecryptedInput(uint256 a, uint256 b, uint256 c); | |
event PublicKey(bytes32 x); | |
event SecretKey(bytes32 x); | |
struct EncryptedData { | |
uint256 a; | |
uint256 b; | |
uint256 c; | |
} | |
// ------------------------------------------------------------------ | |
// Keypair to hide client<->contract comms from relayer | |
bytes32 immutable private m_comms_secret; | |
bytes32 immutable private m_comms_secret_x25519; | |
// Note: this is an ed25519 public key, not an x25519 public key | |
bytes32 immutable private m_comms_public; | |
// ------------------------------------------------------------------ | |
constructor() | |
{ | |
bytes32 secret = _random_bytes32(); | |
// If run on EVM without Sapphire builtins, this will fail | |
require( uint256(secret) != 0x0 ); | |
m_comms_secret = secret; | |
(m_comms_public, m_comms_secret_x25519) = _ed25519_publickey(secret); | |
} | |
function emitKeys() | |
external | |
{ | |
emit PublicKey(m_comms_public); | |
emit SecretKey(m_comms_secret); | |
} | |
function example_sharedsecret(bytes32 ephemeral_pubkey) | |
external view | |
returns (bytes32 shared_secret) | |
{ | |
bytes32 tmp; | |
(shared_secret, tmp) = _relay_anti_tamper_secret(ephemeral_pubkey, m_comms_secret_x25519); | |
} | |
function example( | |
bytes32 ephemeral_pubkey, | |
bytes calldata encrypted_data, | |
uint256 authenticated_param | |
) | |
external | |
{ | |
bytes32 comms_secret_x25519 = m_comms_secret_x25519; | |
(bytes32 ephemeral_secret, bytes32 ephemeral_nonce) = _relay_anti_tamper_secret(ephemeral_pubkey, comms_secret_x25519); | |
(EncryptedData memory x) = abi.decode( | |
_decrypt( | |
ephemeral_secret, | |
ephemeral_nonce, | |
encrypted_data, | |
abi.encode(authenticated_param)), | |
(EncryptedData)); | |
emit DecryptedInput(x.a, x.b, x.c); | |
_encrypt_and_emit_response(ephemeral_secret, ephemeral_nonce, abi.encode(uint256(123))); | |
} | |
// ------------------------------------------------------------------ | |
address private constant RANDOM_BYTES = | |
0x0100000000000000000000000000000000000001; | |
address private constant DERIVE_KEY = | |
0x0100000000000000000000000000000000000002; | |
address private constant ENCRYPT = | |
0x0100000000000000000000000000000000000003; | |
address private constant DECRYPT = | |
0x0100000000000000000000000000000000000004; | |
address private constant GENERAGE_SIGNING_KEYPAIR = | |
0x0100000000000000000000000000000000000005; | |
function _random_bytes32() | |
private view | |
returns (bytes32) | |
{ | |
// XXX: is personalization really necessary here? | |
(bool success, bytes memory entropy) = RANDOM_BYTES.staticcall( | |
abi.encode(uint256(32), abi.encodePacked(block.chainid, block.number, block.timestamp, msg.sender, address(this))) | |
); | |
require(success); | |
return bytes32(entropy); | |
} | |
function _encrypt(bytes32 key, bytes32 nonce, bytes memory plaintext, bytes memory additionalData) | |
private view | |
returns (bytes memory) | |
{ | |
(bool success, bytes memory ciphertext) = ENCRYPT.staticcall( | |
abi.encode(key, nonce, plaintext, additionalData) | |
); | |
require(success); | |
return ciphertext; | |
} | |
function _decrypt(bytes32 key, bytes32 nonce, bytes memory ciphertext, bytes memory additionalData) | |
private view | |
returns (bytes memory) | |
{ | |
(bool success, bytes memory plaintext) = DECRYPT.staticcall( | |
abi.encode(key, nonce, ciphertext, additionalData) | |
); | |
require(success); | |
return plaintext; | |
} | |
function _relay_anti_tamper_secret(bytes32 client_ephem_pubkey, bytes32 secret) | |
private view | |
returns (bytes32 client_ephem_secret, bytes32 client_nonce) | |
{ | |
(bool success, bytes memory symmetric) = DERIVE_KEY.staticcall( | |
abi.encode(client_ephem_pubkey, secret) | |
); | |
require(success); | |
client_ephem_secret = bytes32(symmetric); | |
client_nonce = keccak256(abi.encodePacked(client_ephem_pubkey)); | |
} | |
function _encrypt_and_emit_response( | |
bytes32 ephemeral_secret, | |
bytes32 ephemeral_nonce, | |
bytes memory plaintext | |
) | |
private | |
{ | |
bytes32 response_nonce = keccak256(abi.encodePacked(ephemeral_nonce)); | |
bytes memory response = _encrypt(ephemeral_secret, response_nonce, plaintext, new bytes(0)); | |
emit EncryptedResponse(response_nonce, response); | |
} | |
// Doing it this way as generateCurve25519KeyPairs seems to be broken or not implemented | |
// See: https://github.com/oasisprotocol/oasis-sdk/issues/1310 | |
function _ed25519_publickey(bytes32 secret) | |
private view | |
returns (bytes32 publicKey_ed25519, bytes32 secretKey_x25519) | |
{ | |
(bool success, bytes memory keypair) = GENERAGE_SIGNING_KEYPAIR.staticcall(abi.encode(uint256(1), abi.encodePacked(secret))); | |
require(success); | |
(bytes memory publicKey_bytes, bytes memory secretKey_bytes) = abi.decode(keypair, (bytes, bytes)); | |
publicKey_ed25519 = bytes32(publicKey_bytes); | |
assembly { | |
secretKey_x25519 := mload(add(secretKey_bytes, 32)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment