-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
contracts: calldata encryption function (Solidity)
- Loading branch information
Showing
5 changed files
with
265 additions
and
7 deletions.
There are no files selected for viewing
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,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) | ||
); | ||
} |
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,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); | ||
} | ||
} |
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,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)); | ||
} | ||
}); | ||
}); |