use aes::cipher::{KeyIvInit, StreamCipher};
use anyhow::{bail, ensure, Context, Result};
use hmac::{Hmac, Mac};
use rand::RngCore;
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey};
use sha2::{Digest, Sha256};
use sha3::Keccak256;
use std::sync::LazyLock;
pub const ECIES_OVERHEAD: usize = 65 + 16 + 32;
static SECP: LazyLock<Secp256k1<secp256k1::All>> = LazyLock::new(Secp256k1::new);
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
pub fn keccak256(parts: &[&[u8]]) -> [u8; 32] {
let mut h = Keccak256::new();
for p in parts {
h.update(p);
}
h.finalize().into()
}
#[derive(Clone)]
pub struct NodeKey {
secret: SecretKey,
public: PublicKey,
}
impl NodeKey {
pub fn random() -> NodeKey {
loop {
let mut b = [0u8; 32];
rand::thread_rng().fill_bytes(&mut b);
if let Ok(k) = NodeKey::from_bytes(&b) {
return k;
}
}
}
pub fn from_bytes(b: &[u8]) -> Result<NodeKey> {
let secret = SecretKey::from_slice(b).context("not a valid secp256k1 secret key")?;
Ok(NodeKey {
public: PublicKey::from_secret_key(&SECP, &secret),
secret,
})
}
pub fn from_hex(s: &str) -> Result<NodeKey> {
let b =
hex::decode(s.trim().trim_start_matches("0x")).context("the node key is not hex")?;
ensure!(b.len() == 32, "the node key is {} bytes, not 32", b.len());
NodeKey::from_bytes(&b)
}
pub fn id(&self) -> [u8; 64] {
node_id(&self.public)
}
pub fn public(&self) -> &PublicKey {
&self.public
}
pub fn secret(&self) -> &SecretKey {
&self.secret
}
}
pub fn node_id(pk: &PublicKey) -> [u8; 64] {
let full = pk.serialize_uncompressed();
let mut id = [0u8; 64];
id.copy_from_slice(&full[1..]);
id
}
pub fn public_from_id(id: &[u8]) -> Result<PublicKey> {
ensure!(id.len() == 64, "a node id is 64 bytes, not {}", id.len());
let mut full = [0u8; 65];
full[0] = 4;
full[1..].copy_from_slice(id);
PublicKey::from_slice(&full).context("the node id is not a secp256k1 public key")
}
pub fn ecdh_x(secret: &SecretKey, public: &PublicKey) -> [u8; 32] {
let point = secp256k1::ecdh::shared_secret_point(public, secret);
let mut x = [0u8; 32];
x.copy_from_slice(&point[..32]);
x
}
pub fn sign_recoverable(digest: &[u8; 32], secret: &SecretKey) -> [u8; 65] {
let sig = SECP.sign_ecdsa_recoverable(&Message::from_digest(*digest), secret);
let (rec, rs) = sig.serialize_compact();
let mut out = [0u8; 65];
out[..64].copy_from_slice(&rs);
out[64] = rec.to_i32() as u8;
out
}
pub fn recover(digest: &[u8; 32], sig: &[u8]) -> Result<PublicKey> {
ensure!(
sig.len() == 65,
"a signature is 65 bytes, not {}",
sig.len()
);
let rec = RecoveryId::from_i32(i32::from(sig[64])).context("bad recovery id")?;
let sig = RecoverableSignature::from_compact(&sig[..64], rec).context("bad signature")?;
SECP.recover_ecdsa(&Message::from_digest(*digest), &sig)
.context("the signature recovers no key")
}
fn concat_kdf(z: &[u8; 32]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(1u32.to_be_bytes());
h.update(z);
h.finalize().into()
}
fn ecies_keys(z: &[u8; 32]) -> ([u8; 16], [u8; 32]) {
let k = concat_kdf(z);
let mut ke = [0u8; 16];
ke.copy_from_slice(&k[..16]);
let km: [u8; 32] = Sha256::digest(&k[16..]).into();
(ke, km)
}
fn tag(km: &[u8; 32], iv_and_ciphertext: &[u8], shared_mac_data: &[u8]) -> [u8; 32] {
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(km).expect("HMAC takes any key length");
mac.update(iv_and_ciphertext);
mac.update(shared_mac_data);
mac.finalize().into_bytes().into()
}
pub fn ecies_encrypt(remote: &PublicKey, plain: &[u8], shared_mac_data: &[u8]) -> Vec<u8> {
let ephemeral = NodeKey::random();
let z = ecdh_x(ephemeral.secret(), remote);
let (ke, km) = ecies_keys(&z);
let mut iv = [0u8; 16];
rand::thread_rng().fill_bytes(&mut iv);
let mut body = Vec::with_capacity(16 + plain.len());
body.extend_from_slice(&iv);
body.extend_from_slice(plain);
Aes128Ctr::new(&ke.into(), &iv.into()).apply_keystream(&mut body[16..]);
let t = tag(&km, &body, shared_mac_data);
let mut out = Vec::with_capacity(ECIES_OVERHEAD + plain.len());
out.extend_from_slice(&ephemeral.public().serialize_uncompressed());
out.extend_from_slice(&body);
out.extend_from_slice(&t);
out
}
pub fn ecies_decrypt(secret: &SecretKey, data: &[u8], shared_mac_data: &[u8]) -> Result<Vec<u8>> {
ensure!(
data.len() > ECIES_OVERHEAD,
"ECIES message of {} bytes is shorter than its own overhead",
data.len()
);
if data[0] != 4 {
bail!("ECIES ephemeral key is not an uncompressed point");
}
let ephemeral =
PublicKey::from_slice(&data[..65]).context("ECIES ephemeral key is not on the curve")?;
let z = ecdh_x(secret, &ephemeral);
let (ke, km) = ecies_keys(&z);
let body = &data[65..data.len() - 32];
let want = tag(&km, body, shared_mac_data);
let mut diff = 0u8;
for (a, b) in want.iter().zip(&data[data.len() - 32..]) {
diff |= a ^ b;
}
ensure!(diff == 0, "ECIES tag mismatch");
let (iv, ciphertext) = body.split_at(16);
let mut plain = ciphertext.to_vec();
let iv: [u8; 16] = iv.try_into().expect("16 bytes");
Aes128Ctr::new(&ke.into(), &iv.into()).apply_keystream(&mut plain);
Ok(plain)
}