use crate::bytes::{as_bytes, gc_bytes};
use crate::fs::{stash, Slot};
use crate::NovaStr;
use ring::rand::SecureRandom;
use ring::{digest, hmac, rand};
pub(crate) const OP_SHA256: i64 = 0;
pub(crate) const OP_SHA512: i64 = 1;
pub(crate) const OP_HMAC_SHA256: i64 = 2;
pub(crate) const OP_HMAC_SHA256_VERIFY: i64 = 3;
const ERR_ENTROPY_UNAVAILABLE: i64 = 1;
const ERR_INVALID_LENGTH: i64 = 2;
const ERR_REQUEST_TOO_LARGE: i64 = 3;
const ERR_INVALID_RANGE: i64 = 4;
const MAX_RANDOM_BYTES: usize = 64 * 1024;
const TAG_MISMATCH: i64 = 1;
fn digest_bytes(op: i64, key: &[u8], data: &[u8]) -> Option<Vec<u8>> {
match op {
OP_SHA256 => Some(digest::digest(&digest::SHA256, data).as_ref().to_vec()),
OP_SHA512 => Some(digest::digest(&digest::SHA512, data).as_ref().to_vec()),
OP_HMAC_SHA256 => {
let k = hmac::Key::new(hmac::HMAC_SHA256, key);
Some(hmac::sign(&k, data).as_ref().to_vec())
}
_ => None,
}
}
fn verify_tag(key: &[u8], data: &[u8], tag: &[u8]) -> bool {
let k = hmac::Key::new(hmac::HMAC_SHA256, key);
hmac::verify(&k, data, tag).is_ok()
}
fn width_of(min: i64, max: i64) -> Option<u64> {
if min > max {
return None;
}
Some((max as i128 - min as i128) as u64)
}
fn reduce(draw: u64, span: u64) -> Option<u64> {
if span <= 1 {
return Some(0);
}
let threshold = (u64::MAX - span + 1) % span;
if draw < threshold {
return None;
}
Some(draw % span)
}
fn place(offset: u64, min: i64) -> i64 {
min.wrapping_add(offset as i64)
}
fn fill(out: &mut [u8]) -> Result<(), ()> {
let sr = rand::SystemRandom::new();
match sr.fill(out) {
Ok(()) => Ok(()),
Err(_) => Err(()),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_crypto_hash(
op: i64,
key: *const NovaStr,
data: *const NovaStr,
tag: *const NovaStr,
) -> i64 {
let k = unsafe { as_bytes(key) };
let d = unsafe { as_bytes(data) };
let t = unsafe { as_bytes(tag) };
if op == OP_HMAC_SHA256_VERIFY {
return if verify_tag(k, d, t) { 0 } else { TAG_MISMATCH };
}
match digest_bytes(op, k, d) {
Some(out) => {
stash(Slot::Buffer, gc_bytes(&out));
0
}
None => crate::task::abort_with("nova_rt_crypto_hash: unknown operation"),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_crypto_random_bytes(n: i64) -> i64 {
let Ok(want) = usize::try_from(n) else {
return -ERR_INVALID_LENGTH;
};
if want > MAX_RANDOM_BYTES {
return -ERR_REQUEST_TOO_LARGE;
}
let mut out = vec![0u8; want];
if fill(&mut out).is_err() {
return -ERR_ENTROPY_UNAVAILABLE;
}
stash(Slot::Buffer, gc_bytes(&out));
0
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_crypto_random_int(min: i64, max: i64) -> i64 {
let Some(width) = width_of(min, max) else {
return -ERR_INVALID_RANGE;
};
let value = if width == u64::MAX {
let mut raw = [0u8; 8];
if fill(&mut raw).is_err() {
return -ERR_ENTROPY_UNAVAILABLE;
}
place(u64::from_le_bytes(raw), min)
} else {
let span = width + 1;
let mut offset = None;
for _ in 0..64 {
let mut raw = [0u8; 8];
if fill(&mut raw).is_err() {
return -ERR_ENTROPY_UNAVAILABLE;
}
if let Some(v) = reduce(u64::from_le_bytes(raw), span) {
offset = Some(v);
break;
}
}
match offset {
Some(v) => place(v, min),
None => return -ERR_ENTROPY_UNAVAILABLE,
}
};
stash(Slot::Buffer, gc_bytes(&value.to_le_bytes()));
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reduce_rejects_the_low_tail_and_is_uniform_above_it() {
assert_eq!(reduce(0, 3), None, "the one biased draw must be rejected");
assert_eq!(reduce(1, 3), Some(1));
assert_eq!(reduce(u64::MAX, 3), Some(u64::MAX % 3));
}
#[test]
fn reduce_accepts_every_draw_when_the_span_divides_the_range() {
assert_eq!(reduce(0, 2), Some(0));
assert_eq!(reduce(u64::MAX, 2), Some(1));
}
#[test]
fn reduce_is_identity_for_a_span_of_one() {
assert_eq!(reduce(0, 1), Some(0));
assert_eq!(reduce(u64::MAX, 1), Some(0));
}
#[test]
fn the_full_signed_range_is_a_span_no_u64_can_hold() {
assert_eq!(width_of(i64::MIN, i64::MAX), Some(u64::MAX));
assert_eq!(width_of(0, 0), Some(0));
assert_eq!(width_of(5, 4), None, "an inverted range has no width");
}
#[test]
fn a_reduced_draw_lands_inside_the_requested_interval() {
assert_eq!(place(0, -10), -10);
assert_eq!(place(20, -10), 10);
assert_eq!(place(0, i64::MIN), i64::MIN);
}
#[test]
fn a_wide_span_reaches_the_rejection_branch_from_the_live_source() {
let span = (1u64 << 63) + 1;
let mut rejected = 0;
let mut accepted = 0;
for _ in 0..40 {
let mut raw = [0u8; 8];
assert!(fill(&mut raw).is_ok(), "the OS entropy source must work");
match reduce(u64::from_le_bytes(raw), span) {
None => rejected += 1,
Some(v) => {
accepted += 1;
assert!(v < span, "a reduced draw must be below the span");
}
}
}
assert!(
rejected > 0,
"a span of 2^63 + 1 rejects half of all draws, so forty should include at least one rejection"
);
assert!(accepted > 0, "and at least one acceptance");
}
#[test]
fn no_crypto_intrinsic_can_panic() {
let source = include_str!("crypto.rs");
let production = source.split("mod tests {").next().unwrap_or(source);
for needle in [
".borrow_mut()",
".borrow()",
"unwrap()",
".expect(",
"panic!",
"format!",
] {
assert!(
!production.contains(needle),
"a std/crypto intrinsic must not panic: `{needle}` found in \
this file's production code, which is reachable from a \
compiled Nova frame with no landing pad to unwind through"
);
}
}
#[test]
fn sha256_matches_a_cross_checked_vector() {
let got = digest_bytes(OP_SHA256, &[], b"abc").expect("sha256 cannot fail");
assert_eq!(
hex(&got),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn sha512_matches_a_cross_checked_vector() {
let got = digest_bytes(OP_SHA512, &[], b"abc").expect("sha512 cannot fail");
assert_eq!(got.len(), 64);
assert!(hex(&got).starts_with("ddaf35a193617aba"));
}
#[test]
fn hmac_sha256_matches_a_cross_checked_vector() {
let key = [0x0bu8; 20];
let got = digest_bytes(OP_HMAC_SHA256, &key, b"Hi There").expect("hmac cannot fail");
assert_eq!(
hex(&got),
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
}
#[test]
fn hmac_verify_accepts_the_right_tag_and_rejects_others() {
let key = [0x0bu8; 20];
let tag = digest_bytes(OP_HMAC_SHA256, &key, b"Hi There").expect("hmac cannot fail");
assert!(verify_tag(&key, b"Hi There", &tag));
assert!(!verify_tag(&key, b"Hi there", &tag), "different data");
assert!(
!verify_tag(&[0x0cu8; 20], b"Hi There", &tag),
"different key"
);
assert!(!verify_tag(&key, b"Hi There", &tag[..31]), "truncated tag");
assert!(!verify_tag(&key, b"Hi There", &[]), "empty tag");
}
fn hex(v: &[u8]) -> String {
let mut s = String::with_capacity(v.len() * 2);
for b in v {
s.push(char::from_digit((b >> 4) as u32, 16).unwrap_or('?'));
s.push(char::from_digit((b & 0x0f) as u32, 16).unwrap_or('?'));
}
s
}
}