use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as B64;
use ring::signature::Ed25519KeyPair;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureKind {
HmacSha256,
Ed25519,
}
enum Imp {
Hmac(ring::hmac::Key),
Ed25519(Box<Ed25519KeyPair>),
}
pub struct Signer {
imp: Imp,
}
impl std::fmt::Debug for Signer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Signer")
.field("kind", &self.kind())
.finish()
}
}
const MAX_ENCODED: usize = 3 * 88;
impl Signer {
#[must_use]
pub fn hmac_sha256(secret: &[u8]) -> Self {
Self {
imp: Imp::Hmac(ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret)),
}
}
pub fn ed25519_pkcs8(der: &[u8]) -> Result<Self> {
let kp = Ed25519KeyPair::from_pkcs8_maybe_unchecked(der)
.map_err(|e| Error::Config(format!("Ed25519 key: {e}")))?;
Ok(Self {
imp: Imp::Ed25519(Box::new(kp)),
})
}
pub fn ed25519_pem(pem: &str) -> Result<Self> {
use rustls::pki_types::PrivatePkcs8KeyDer;
use rustls::pki_types::pem::PemObject;
let der = PrivatePkcs8KeyDer::from_pem_slice(pem.as_bytes())
.map_err(|e| Error::Config(format!("Ed25519 PEM: {e}")))?;
Self::ed25519_pkcs8(der.secret_pkcs8_der())
}
#[must_use]
pub const fn kind(&self) -> SignatureKind {
match self.imp {
Imp::Hmac(_) => SignatureKind::HmacSha256,
Imp::Ed25519(_) => SignatureKind::Ed25519,
}
}
#[must_use]
pub fn ed25519_public_key(&self) -> Option<&[u8]> {
match &self.imp {
Imp::Ed25519(kp) => Some(ring::signature::KeyPair::public_key(kp.as_ref()).as_ref()),
Imp::Hmac(_) => None,
}
}
pub fn append_signature(&self, payload: &[u8], out: &mut Vec<u8>) {
let mut buf = [0u8; MAX_ENCODED];
let n = self.encode_signature(payload, &mut buf);
out.extend_from_slice(&buf[..n]);
}
fn encode_signature(&self, payload: &[u8], buf: &mut [u8; MAX_ENCODED]) -> usize {
match &self.imp {
Imp::Hmac(key) => {
let tag = ring::hmac::sign(key, payload);
hex_into(tag.as_ref(), buf)
}
Imp::Ed25519(kp) => {
let sig = kp.sign(payload);
let mut b64 = [0u8; 88];
let n = B64.encode_slice(sig.as_ref(), &mut b64).unwrap_or_default();
percent_encode_into(&b64[..n], buf)
}
}
}
}
pub fn append_signed_params(out: &mut Vec<u8>, params: &str, timestamp_ms: u64, signer: &Signer) {
let start = out.len();
out.extend_from_slice(params.as_bytes());
if !params.is_empty() {
out.push(b'&');
}
out.extend_from_slice(b"timestamp=");
push_u64(out, timestamp_ms);
let mut buf = [0u8; MAX_ENCODED];
let n = signer.encode_signature(&out[start..], &mut buf);
out.extend_from_slice(b"&signature=");
out.extend_from_slice(&buf[..n]);
}
pub fn validate_params(params: &str) -> Result<()> {
let ok = |b: u8| b.is_ascii_alphanumeric() || b"-._~!$'()*+,;=:@/?&%".contains(&b);
if params.bytes().all(ok) {
Ok(())
} else {
Err(Error::Http("parameters must be percent-encoded".into()))
}
}
fn hex_into(bytes: &[u8], buf: &mut [u8]) -> usize {
const HEX: &[u8; 16] = b"0123456789abcdef";
for (i, b) in bytes.iter().enumerate() {
buf[2 * i] = HEX[usize::from(b >> 4)];
buf[2 * i + 1] = HEX[usize::from(b & 0x0F)];
}
2 * bytes.len()
}
fn percent_encode_into(src: &[u8], buf: &mut [u8]) -> usize {
let mut n = 0;
for &b in src {
let esc: &[u8] = match b {
b'+' => b"%2B",
b'/' => b"%2F",
b'=' => b"%3D",
_ => {
buf[n] = b;
n += 1;
continue;
}
};
buf[n..n + 3].copy_from_slice(esc);
n += 3;
}
n
}
fn push_u64(out: &mut Vec<u8>, mut v: u64) {
let mut digits = [0u8; 20];
let mut i = digits.len();
loop {
i -= 1;
#[allow(clippy::cast_possible_truncation)]
{
digits[i] = b'0' + (v % 10) as u8;
}
v /= 10;
if v == 0 {
break;
}
}
out.extend_from_slice(&digits[i..]);
}