oasisprotocol / sapphire-paratime

Oasis Sapphire - the confidential EVM-compatible ParaTime for the Oasis Network
https://oasisprotocol.org/sapphire
Apache License 2.0
37 stars 27 forks source link

Support generating encrypted transactions on-chain #428

Open matevz opened 3 weeks ago

matevz commented 3 weeks ago

The current EIP155Signer expects arbitrary calldata, e.g. https://docs.oasis.io/dapp/sapphire/gasless#gasless-proxy-contract.

Add a helper to Sapphire.sol or other contract for on-chain Deoxys-II encryption of the call data prior to signing the transaction. See how to fetch the ephemeral public key here.

Update the Gasless chapter with the new encrypted version of the tx.

CedarMist commented 3 weeks ago

https://github.com/oasisprotocol/sapphire-paratime/blob/f708f3912424c84a6f03f87b9f6f928093da036a/contracts/contracts/Subcall.sol#L718-L722

https://github.com/oasisprotocol/sapphire-paratime/blob/f708f3912424c84a6f03f87b9f6f928093da036a/contracts/contracts/Sapphire.sol#L156-L160

https://github.com/oasisprotocol/sapphire-paratime/blob/f708f3912424c84a6f03f87b9f6f928093da036a/contracts/contracts/Sapphire.sol#L189-L202

https://github.com/oasisprotocol/sapphire-paratime/blob/f708f3912424c84a6f03f87b9f6f928093da036a/contracts/contracts/Sapphire.sol#L240-L245

CBOR encoding the encrypted calldata packet may be a little annoying, but there are many examples of CBOR encoding in Sapphire contracts library.

CedarMist commented 3 weeks ago

It may be worth extracting the CBOR related functions into a helper library, although I'd be wary of modifying or refactoring of the existing functions.

CedarMist commented 3 weeks ago

Made a quick sketch so far, will test it

// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import { HMAC_sha512_256 } from './HMAC_sha512_256.sol';
import { Subcall } from '@oasisprotocol/sapphire-contracts/contracts/Subcall.sol';
import { Sapphire } from '@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol';

library EncryptedTx {
    function internal_deriveSharedSecret(
        bytes32 peerPublicKey,
        Sapphire.Curve25519SecretKey mySecretKey
    )
        internal view
        returns (bytes32)
    {
        return HMAC_sha512_256(
            "MRAE_Box_Deoxys-II-256-128",
            abi.encodePacked(
                Sapphire.deriveSymmetricKey(
                    Sapphire.Curve25519PublicKey.wrap(peerPublicKey),
                    mySecretKey)));
    }

    error CBORBytesTooLong();

    function _cborEncodeBytes(bytes memory in_bytes)
        internal pure
        returns (bytes memory out_cbor)
    {
        /*
        0x40..0x57  byte string (0x00..0x17 bytes follow)
        0x58    byte string (one-byte uint8_t for n, and then n bytes follow)
        0x59    byte string (two-byte uint16_t for n, and then n bytes follow)
        0x5a    byte string (four-byte uint32_t for n, and then n bytes follow)
        0x5b    byte string (eight-byte uint64_t for n, and then n bytes follow)
        */
        if( in_bytes.length <= 0x17 ) {
            return abi.encodePacked(uint8(0x40 + 0x17), in_bytes);
        }
        if( in_bytes.length <= 0xFF ) {
            return abi.encodePacked(uint8(0x58), uint8(in_bytes.length), in_bytes);
        }
        if( in_bytes.length <= 0xFFFF ) {
            return abi.encodePacked(uint8(0x59), uint16(in_bytes.length), in_bytes);
        }
        // We assume Solidity won't be encoding anything larger than 64kb
        revert CBORBytesTooLong();
    }

    function _cborEncodeUint(uint256 value) public pure returns (bytes memory) {
        if (value < 24) {
            return abi.encodePacked(uint8(value));
        } else if (value <= type(uint8).max) {
            return abi.encodePacked(uint8(24), uint8(value));
        } else if (value <= type(uint16).max) {
            return abi.encodePacked(uint8(25), uint16(value));
        } else if (value <= type(uint32).max) {
            return abi.encodePacked(uint8(26), uint32(value));
        } else {
            return abi.encodePacked(uint8(27), uint64(value));
        }
    }

    function encryptCalldata(bytes memory in_data)
        internal
        returns (bytes memory out_encrypted)
    {
        (uint256 epoch, Subcall.CallDataPublicKey memory cdpk) = Subcall.coreCallDataPublicKey();

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

        bytes32 sharedSecret = internal_deriveSharedSecret(cdpk.key, mySecret);

        bytes memory plaintextEnvelope = abi.encodePacked(
            hex"a1",            // map(1)
                hex"64",        //   text(4)
                    "body",     //     "body"
                    _cborEncodeBytes(in_data));

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

        bytes memory encryptedInner = Sapphire.encrypt(sharedSecret, nonce, plaintextEnvelope, "");

        return abi.encodePacked(
            hex"a2",                    //  map(2)
                hex"64", "body",        //      text(4) "body"
                    hex"a4",                //      map(4)
                        hex"62", "pk",      //          text(2) "pk"
                            hex"5820", myPublic, //         bytes(32) pubkey
                        hex"64", "data",    //          text(4) "data"
                            encryptedInner, //              bytes(n) inner
                        hex"65", "nonce",   //          text(4) "nonce"
                            hex"580f", nonce,   //          bytes(15) nonce
                        hex"65", "epoch",   //          text(4) "epoch"
                            _cborEncodeUint(epoch),
                hex"66", "format",      //      text(6) "format"
                    hex"01"                 //      unsigned(1)
        );
    }
}