Skip to content

Commit

Permalink
contracts: calldata encryption function (Solidity)
Browse files Browse the repository at this point in the history
  • Loading branch information
CedarMist committed Oct 31, 2024
1 parent be334e2 commit 4a32e74
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 7 deletions.
20 changes: 15 additions & 5 deletions clients/js/src/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ export abstract class Cipher {
public abstract publicKey: Uint8Array;
public abstract epoch?: number;

public abstract encrypt(plaintext: Uint8Array): {
public abstract encrypt(
plaintext: Uint8Array,
nonce?: Uint8Array,
): {
ciphertext: Uint8Array;
nonce: Uint8Array;
};
Expand All @@ -93,7 +96,10 @@ export abstract class Cipher {
): Uint8Array;

/** Encrypts the plaintext and encodes it for sending. */
public encryptCall(calldata?: BytesLike | null): BytesLike {
public encryptCall(
calldata?: BytesLike | null,
nonce?: Uint8Array,
): BytesLike {
// Txs without data are just balance transfers, and all data in those is public.
if (calldata === undefined || calldata === null || calldata.length === 0)
return '';
Expand All @@ -104,7 +110,8 @@ export abstract class Cipher {

const innerEnvelope = cborEncode({ body: getBytes(calldata) });

const { ciphertext, nonce } = this.encrypt(innerEnvelope);
let ciphertext: Uint8Array;
({ ciphertext, nonce } = this.encrypt(innerEnvelope, nonce));

const envelope: Envelope = {
format: this.kind,
Expand Down Expand Up @@ -251,11 +258,14 @@ export class X25519DeoxysII extends Cipher {
this.cipher = new deoxysii.AEAD(new Uint8Array(this.key)); // deoxysii owns the input
}

public encrypt(plaintext: Uint8Array): {
public encrypt(
plaintext: Uint8Array,
nonce?: Uint8Array,
): {
ciphertext: Uint8Array;
nonce: Uint8Array;
} {
const nonce = randomBytes(deoxysii.NonceSize);
nonce = nonce ?? randomBytes(deoxysii.NonceSize);
const ciphertext = this.cipher.encrypt(nonce, plaintext);
return { nonce, ciphertext };
}
Expand Down
6 changes: 4 additions & 2 deletions clients/js/test/cipher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import nacl from 'tweetnacl';
import { hexlify, getBytes } from 'ethers';
import { X25519DeoxysII } from '@oasisprotocol/sapphire-paratime';
import { isCalldataEnveloped, X25519DeoxysII } from '@oasisprotocol/sapphire-paratime';

describe('X25519DeoxysII', () => {
it('key derivation', () => {
Expand Down Expand Up @@ -30,7 +30,9 @@ describe('X25519DeoxysII', () => {
const cipher = X25519DeoxysII.ephemeral(nacl.box.keyPair().publicKey);
for (let i = 1; i < 512; i += 30) {
const expected = nacl.randomBytes(i);
const decoded = cipher.decryptCall(cipher.encryptCall(expected));
const encrypted = cipher.encryptCall(expected);
expect(isCalldataEnveloped(encrypted)).toStrictEqual(true);
const decoded = cipher.decryptCall(encrypted);
expect(hexlify(decoded)).toEqual(hexlify(expected));
}
});
Expand Down
102 changes: 102 additions & 0 deletions contracts/contracts/CalldataEncryption.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import {Subcall} from "./Subcall.sol";
import {Sapphire} from "./Sapphire.sol";
import "./CBOR.sol" as CBOR;

function _deriveKey(
bytes32 in_peerPublicKey,
Sapphire.Curve25519SecretKey in_x25519_secret
) view returns (bytes32) {
return
Sapphire.deriveSymmetricKey(
Sapphire.Curve25519PublicKey.wrap(in_peerPublicKey),
in_x25519_secret
);
}

function _encryptInner(
bytes memory in_data,
Sapphire.Curve25519SecretKey in_x25519_secret,
bytes15 nonce,
bytes32 peerPublicKey
) view returns (bytes memory out_encrypted) {
bytes memory plaintextEnvelope = abi.encodePacked(
hex"a1", // map(1)
hex"64", // text(4) "body"
"body",
CBOR.encodeBytes(in_data)
);

out_encrypted = Sapphire.encrypt(
_deriveKey(peerPublicKey, in_x25519_secret),
nonce,
plaintextEnvelope,
""
);
}

function encryptCallData(bytes memory in_data)
view
returns (bytes memory out_encrypted)
{
if (in_data.length == 0) {
return "";
}

Sapphire.Curve25519PublicKey myPublic;
Sapphire.Curve25519SecretKey mySecret;

(myPublic, mySecret) = Sapphire.generateCurve25519KeyPair("");

bytes15 nonce = bytes15(Sapphire.randomBytes(15, ""));

Subcall.CallDataPublicKey memory cdpk;
uint256 epoch;

(epoch, cdpk) = Subcall.coreCallDataPublicKey();

return encryptCallData(in_data, myPublic, mySecret, nonce, epoch, cdpk.key);
}

function encryptCallData(
bytes memory in_data,
Sapphire.Curve25519PublicKey myPublic,
Sapphire.Curve25519SecretKey mySecret,
bytes15 nonce,
uint256 epoch,
bytes32 peerPublicKey
) view returns (bytes memory out_encrypted) {
if (in_data.length == 0) {
return "";
}

bytes memory inner = _encryptInner(in_data, mySecret, nonce, peerPublicKey);

return
abi.encodePacked(
hex"a2", // map(2)
hex"64", // text(4) "body"
"body",
hex"a4", // map(4)
hex"62", // text(2) "pk"
"pk",
hex"5820", // bytes(32)
myPublic,
hex"64", // text(4) "data"
"data",
CBOR.encodeBytes(inner), // bytes(n) inner
hex"65", // text(5) "epoch"
"epoch",
CBOR.encodeUint(epoch), // unsigned(epoch)
hex"65", // text(5) "nonce"
"nonce",
hex"4f", // bytes(15) nonce
nonce,
hex"66", // text(6) "format"
"format",
hex"01" // unsigned(1)
);
}
52 changes: 52 additions & 0 deletions contracts/contracts/tests/TestCalldataEncryption.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import { Sapphire } from "../Sapphire.sol";
import { encryptCallData } from "../CalldataEncryption.sol";
import { EIP155Signer } from "../EIP155Signer.sol";

contract TestCalldataEncryption {
function testEncryptCallData(
bytes memory in_data,
Sapphire.Curve25519PublicKey myPublic,
Sapphire.Curve25519SecretKey mySecret,
bytes15 nonce,
uint256 epoch,
bytes32 peerPublicKey
) external view returns (bytes memory) {
return encryptCallData(in_data, myPublic, mySecret, nonce, epoch, peerPublicKey);
}

function makeExampleCall(
bytes calldata in_data,
uint64 nonce,
uint256 gasPrice,
uint64 gasLimit,
address myAddr,
bytes32 myKey
)
external view
returns (bytes memory)
{
EIP155Signer.EthTx memory theTx = EIP155Signer.EthTx({
nonce: nonce,
gasPrice: gasPrice,
gasLimit: gasLimit,
value: 0,
to: address(this),
chainId: block.chainid,
data: encryptCallData(abi.encodeCall(this.example, in_data))
});

return EIP155Signer.sign(myAddr, myKey, theTx);
}

event ExampleEvent(bytes);

function example(bytes calldata in_calldata)
external
{
emit ExampleEvent(in_calldata);
}
}
92 changes: 92 additions & 0 deletions contracts/test/calldata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0

import { expect } from 'chai';
import { ethers } from 'hardhat';
import { TestCalldataEncryption } from '../typechain-types/contracts/tests';
import {
boxKeyPairFromSecretKey,
crypto_box_SECRETKEYBYTES,
isCalldataEnveloped,
X25519DeoxysII,
} from '@oasisprotocol/sapphire-paratime';
import { hexlify, parseUnits, randomBytes } from 'ethers';
import { randomInt } from 'crypto';

describe('CalldataEncryption', () => {
let contract: TestCalldataEncryption;

before(async () => {
const factory = await ethers.getContractFactory('TestCalldataEncryption');
contract = await factory.deploy();
await contract.waitForDeployment();
});

// Ensures that the JS library provides the same results as Solidity
it('testEncryptCallData', async () => {
for (let i = 1; i < 1024; i += 1 + i / 5) {
const peerKeypair = boxKeyPairFromSecretKey(
randomBytes(crypto_box_SECRETKEYBYTES),
);
const myKeypair = boxKeyPairFromSecretKey(
randomBytes(crypto_box_SECRETKEYBYTES),
);
const calldata = randomBytes(i);
const epoch = randomInt(1 << 32);
const nonce = randomBytes(15);
const cipher = X25519DeoxysII.fromSecretKey(
myKeypair.secretKey,
peerKeypair.publicKey,
epoch,
);
const encryptedCall = cipher.encryptCall(calldata, nonce);
const result = await contract.testEncryptCallData(
calldata,
myKeypair.publicKey,
myKeypair.secretKey,
nonce,
epoch,
peerKeypair.publicKey,
);
expect(result).eq(hexlify(encryptedCall));
}
});

it('roundtrip encryption', async () => {
// Tests must be submitted from an account which has a balance
// But can't get access to the signer private key from here
// So, assume the 0xf39F address is being used to run tests
const myAddr = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const myKey =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const [signerAddr] = await ethers.getSigners();
expect(signerAddr).eq(myAddr);

for (let i = 1; i < 1024; i += 250) {
// Have the contract sign an encrypted transaction for us
const bytes = randomBytes(i);
const nonce = await ethers.provider.getTransactionCount(myAddr);
const gasPrice = parseUnits('100', 'gwei');
const gasLimit = 200000;
const tx = await contract.makeExampleCall(
bytes,
nonce,
gasPrice,
gasLimit,
myAddr,
myKey,
);

// Then broadcast transaction and make sure the result is given back to us
// Making sure the tx was encrypted, and data is passed correctly
const response = await ethers.provider.broadcastTransaction(tx);
expect(isCalldataEnveloped(response.data)).eq(true);
const receipt = await response.wait();
expect(receipt?.status).eq(1);
const parsed = contract.interface.parseLog({
topics: receipt!.logs[0].topics as string[],
data: receipt!.logs[0].data,
});
expect(parsed!.args[0]).eq(hexlify(bytes));
}
});
});

0 comments on commit 4a32e74

Please sign in to comment.