use std::convert::TryInto;
use anyhow::anyhow;
use byteorder::{BigEndian, WriteBytesExt};
use oasis_core_runtime::consensus::beacon;
use rand_core::{OsRng, RngCore};
use crate::{
context::Context,
core::common::crypto::{mrae::deoxysii, x25519},
crypto::signature::context::get_chain_context_for,
keymanager, module,
modules::core::Error,
state::CurrentState,
types::{
self,
transaction::{Call, CallFormat, CallResult},
},
};
const MAX_EPHEMERAL_KEY_AGE: beacon::EpochTime = 5;
pub enum Metadata {
Empty,
EncryptedX25519DeoxysII {
pk: x25519::PublicKey,
sk: x25519::PrivateKey,
index: usize,
},
}
impl std::fmt::Debug for Metadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => f.debug_struct("Metadata::Empty").finish(),
Self::EncryptedX25519DeoxysII { pk, index, .. } => f
.debug_struct("Metadata::EncryptedX25519DeoxysII")
.field("pk", pk)
.field("index", index)
.finish_non_exhaustive(),
}
}
}
pub fn get_key_pair_id(epoch: beacon::EpochTime) -> keymanager::KeyPairId {
keymanager::get_key_pair_id([
get_chain_context_for(types::callformat::CALL_DATA_KEY_PAIR_ID_CONTEXT_BASE).as_slice(),
&epoch.to_be_bytes(),
])
}
fn verify_epoch<C: Context>(ctx: &C, epoch: beacon::EpochTime) -> Result<(), Error> {
if epoch > ctx.epoch() {
return Err(Error::InvalidCallFormat(anyhow!("epoch in the future")));
}
if epoch < ctx.epoch().saturating_sub(MAX_EPHEMERAL_KEY_AGE) {
return Err(Error::InvalidCallFormat(anyhow!(
"epoch too far in the past"
)));
}
Ok(())
}
pub fn decode_call<C: Context>(
ctx: &C,
call: Call,
index: usize,
) -> Result<Option<(Call, Metadata)>, Error> {
decode_call_ex(ctx, call, index, false )
}
pub fn decode_call_ex<C: Context>(
ctx: &C,
call: Call,
index: usize,
assume_km_reachable: bool,
) -> Result<Option<(Call, Metadata)>, Error> {
match call.format {
CallFormat::Plain => Ok(Some((call, Metadata::Empty))),
CallFormat::EncryptedX25519DeoxysII => {
if !call.method.is_empty() {
return Err(Error::InvalidCallFormat(anyhow!("non-empty method")));
}
let envelope: types::callformat::CallEnvelopeX25519DeoxysII =
cbor::from_value(call.body)
.map_err(|_| Error::InvalidCallFormat(anyhow!("bad call envelope")))?;
let pk = envelope.pk;
let key_manager = ctx
.key_manager()
.ok_or_else(|| Error::InvalidCallFormat(anyhow!("confidential txs unavailable")))?;
if !assume_km_reachable && CurrentState::with_env(|env| !env.is_execute()) {
return Ok(None);
}
let decrypt = |epoch: beacon::EpochTime| {
let keypair = key_manager
.get_or_create_ephemeral_keys(get_key_pair_id(epoch), epoch)
.map_err(|err| match err {
keymanager::KeyManagerError::InvalidEpoch(..) => {
Error::InvalidCallFormat(anyhow!("invalid epoch"))
}
_ => Error::Abort(err.into()),
})?;
let sk = keypair.input_keypair.sk;
deoxysii::box_open(
&envelope.nonce,
envelope.data.clone(),
vec![],
&envelope.pk.0,
&sk.0,
)
.map(|data| (data, sk))
};
let (data, sk) = if envelope.epoch > 0 {
verify_epoch(ctx, envelope.epoch)?;
decrypt(envelope.epoch)
} else {
decrypt(ctx.epoch()).or_else(|_| decrypt(ctx.epoch() - 1))
}
.map_err(Error::InvalidCallFormat)?;
let read_only = call.read_only;
let call: Call = cbor::from_slice(&data)
.map_err(|_| Error::InvalidCallFormat(anyhow!("malformed call")))?;
if call.read_only != read_only {
return Err(Error::InvalidCallFormat(anyhow!("read-only flag mismatch")));
}
Ok(Some((
call,
Metadata::EncryptedX25519DeoxysII { pk, sk, index },
)))
}
}
}
#[cfg(any(test, feature = "test"))]
pub fn encode_call<C: Context>(
ctx: &C,
mut call: Call,
client_keypair: &(x25519_dalek::PublicKey, x25519_dalek::StaticSecret),
) -> Result<Call, Error> {
match call.format {
CallFormat::Plain => Ok(call),
CallFormat::EncryptedX25519DeoxysII => {
let key_manager = ctx.key_manager().ok_or_else(|| {
Error::InvalidCallFormat(anyhow!("confidential transactions not available"))
})?;
let epoch = ctx.epoch();
let runtime_keypair = key_manager
.get_or_create_ephemeral_keys(get_key_pair_id(epoch), epoch)
.map_err(|err| Error::Abort(err.into()))?;
let runtime_pk = runtime_keypair.input_keypair.pk;
let nonce = [0u8; deoxysii::NONCE_SIZE];
Ok(Call {
format: call.format,
method: std::mem::take(&mut call.method),
body: cbor::to_value(types::callformat::CallEnvelopeX25519DeoxysII {
pk: client_keypair.0.into(),
nonce,
epoch,
data: deoxysii::box_seal(
&nonce,
cbor::to_vec(call),
vec![],
&runtime_pk.0,
&client_keypair.1,
)
.unwrap(),
}),
..Default::default()
})
}
}
}
pub fn encode_result<C: Context>(
ctx: &C,
result: module::CallResult,
metadata: Metadata,
) -> CallResult {
encode_result_ex(ctx, result, metadata, false )
}
pub fn encode_result_ex<C: Context>(
ctx: &C,
result: module::CallResult,
metadata: Metadata,
expose_failure: bool,
) -> CallResult {
match metadata {
Metadata::Empty => result.into(),
Metadata::EncryptedX25519DeoxysII { pk, sk, index } => {
let result: CallResult = result.into();
if expose_failure {
if result.is_success() {
return CallResult::Ok(encrypt_result_x25519_deoxysii(
ctx, result, pk, sk, index,
));
}
return result;
}
CallResult::Unknown(encrypt_result_x25519_deoxysii(ctx, result, pk, sk, index))
}
}
}
pub fn encrypt_result_x25519_deoxysii<C: Context>(
ctx: &C,
result: types::transaction::CallResult,
pk: x25519::PublicKey,
sk: x25519::PrivateKey,
index: usize,
) -> cbor::Value {
let mut nonce = Vec::with_capacity(deoxysii::NONCE_SIZE);
if CurrentState::with_env(|env| env.is_execute()) {
nonce
.write_u64::<BigEndian>(ctx.runtime_header().round)
.unwrap();
nonce
.write_u32::<BigEndian>(index.try_into().unwrap())
.unwrap();
nonce.extend(&[0, 0, 0]);
} else {
nonce.resize(deoxysii::NONCE_SIZE, 0);
OsRng.fill_bytes(&mut nonce);
}
let nonce = nonce.try_into().unwrap();
let result = cbor::to_vec(result);
let data = deoxysii::box_seal(&nonce, result, vec![], &pk.0, &sk.0).unwrap();
cbor::to_value(types::callformat::ResultEnvelopeX25519DeoxysII { nonce, data })
}
#[cfg(any(test, feature = "test"))]
pub fn decode_result<C: Context>(
ctx: &C,
format: CallFormat,
result: CallResult,
client_keypair: &(x25519_dalek::PublicKey, x25519_dalek::StaticSecret),
) -> Result<module::CallResult, Error> {
if matches!(format, CallFormat::Plain) {
return Ok(result.into_call_result().expect("CallResult was Unknown"));
}
let envelope_value = match result {
CallResult::Ok(v) | CallResult::Unknown(v) => v,
CallResult::Failed {
module,
code,
message,
} => {
return Ok(module::CallResult::Failed {
module,
code,
message,
})
}
};
match format {
CallFormat::Plain => unreachable!("checked above"),
CallFormat::EncryptedX25519DeoxysII => {
let envelope: types::callformat::ResultEnvelopeX25519DeoxysII =
cbor::from_value(envelope_value)
.map_err(|_| Error::InvalidCallFormat(anyhow!("bad result envelope")))?;
let key_manager = ctx
.key_manager()
.ok_or_else(|| Error::InvalidCallFormat(anyhow!("confidential txs unavailable")))?;
let keypair = key_manager
.get_or_create_ephemeral_keys(get_key_pair_id(ctx.epoch()), ctx.epoch())
.map_err(|err| Error::Abort(err.into()))?;
let runtime_pk = keypair.input_keypair.pk;
let data = deoxysii::box_open(
&envelope.nonce,
envelope.data,
vec![],
&runtime_pk.0,
&client_keypair.1,
)
.map_err(Error::InvalidCallFormat)?;
let call_result: CallResult = cbor::from_slice(&data)
.map_err(|_| Error::InvalidCallFormat(anyhow!("malformed call")))?;
Ok(call_result
.into_call_result()
.expect("CallResult was Unknown"))
}
}
}