Skip to content

Instantly share code, notes, and snippets.

@HarryR
Created March 27, 2023 04:56
Show Gist options
  • Save HarryR/eb5fd5fb77eda9f509cb94c91fe76fef to your computer and use it in GitHub Desktop.
Save HarryR/eb5fd5fb77eda9f509cb94c91fe76fef to your computer and use it in GitHub Desktop.
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);
});
});
});
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