feat(keystore): sealed real/duress slots (argon2id 16MiB/3, ChaCha20-Poly1305), torn-write-safe 2-copy record, wipe state; virtio-rng driver + Entropy port; RustCrypto deps
This commit is contained in:
216
Cargo.lock
generated
216
Cargo.lock
generated
@@ -17,6 +17,16 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alarm-server"
|
name = "alarm-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -28,6 +38,17 @@ dependencies = [
|
|||||||
"manifest",
|
"manifest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "audio-capture-server"
|
name = "audio-capture-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -36,6 +57,30 @@ version = "0.1.0"
|
|||||||
name = "audio-playback-server"
|
name = "audio-playback-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bluetooth-server"
|
name = "bluetooth-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -56,6 +101,47 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20poly1305"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"chacha20",
|
||||||
|
"cipher",
|
||||||
|
"poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-server"
|
name = "clipboard-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -72,6 +158,36 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "display-qemu"
|
name = "display-qemu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -149,6 +265,16 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "driver-rng-virtio"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hw-virtio",
|
||||||
|
"libkos",
|
||||||
|
"port-entropy",
|
||||||
|
"types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "echo"
|
name = "echo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -165,6 +291,16 @@ version = "0.1.0"
|
|||||||
name = "gatekeeper"
|
name = "gatekeeper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gps-server"
|
name = "gps-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -195,6 +331,15 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "input-qemu"
|
name = "input-qemu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -231,6 +376,22 @@ dependencies = [
|
|||||||
name = "keychain-server"
|
name = "keychain-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keystore"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
|
"blake2",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.191"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libkos"
|
name = "libkos"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -271,6 +432,12 @@ version = "0.1.0"
|
|||||||
name = "notification-server"
|
name = "notification-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -292,10 +459,25 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poly1305"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||||
|
dependencies = [
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "port-block-device"
|
name = "port-block-device"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "port-entropy"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "port-framebuffer"
|
name = "port-framebuffer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -395,6 +577,12 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync-server"
|
name = "sync-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -412,10 +600,26 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types"
|
name = "types"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usb-server"
|
name = "usb-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -423,3 +627,15 @@ version = "0.1.0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "user-manager"
|
name = "user-manager"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -14,6 +14,9 @@ members = [
|
|||||||
"crates/domain",
|
"crates/domain",
|
||||||
"crates/elf",
|
"crates/elf",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
|
"crates/keystore",
|
||||||
|
"crates/ports/entropy",
|
||||||
|
"crates/drivers/rng/virtio",
|
||||||
"crates/tools/app-image",
|
"crates/tools/app-image",
|
||||||
"crates/init",
|
"crates/init",
|
||||||
"crates/kernel",
|
"crates/kernel",
|
||||||
@@ -72,6 +75,14 @@ types = { path = "crates/types" }
|
|||||||
domain = { path = "crates/domain" }
|
domain = { path = "crates/domain" }
|
||||||
elf = { path = "crates/elf" }
|
elf = { path = "crates/elf" }
|
||||||
manifest = { path = "crates/manifest" }
|
manifest = { path = "crates/manifest" }
|
||||||
|
keystore = { path = "crates/keystore" }
|
||||||
|
# Audited RustCrypto implementations; never our own.
|
||||||
|
argon2 = { version = "0.5", default-features = false }
|
||||||
|
chacha20poly1305 = { version = "0.10", default-features = false }
|
||||||
|
blake2 = { version = "0.10", default-features = false }
|
||||||
|
zeroize = { version = "1", default-features = false }
|
||||||
|
port-entropy = { path = "crates/ports/entropy" }
|
||||||
|
driver-rng-virtio = { path = "crates/drivers/rng/virtio" }
|
||||||
platform = { path = "crates/platform" }
|
platform = { path = "crates/platform" }
|
||||||
kernel = { path = "crates/kernel" }
|
kernel = { path = "crates/kernel" }
|
||||||
server-framework = { path = "crates/server-framework" }
|
server-framework = { path = "crates/server-framework" }
|
||||||
|
|||||||
10
crates/drivers/rng/virtio/Cargo.toml
Normal file
10
crates/drivers/rng/virtio/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "driver-rng-virtio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
libkos = { workspace = true }
|
||||||
|
hw-virtio = { workspace = true }
|
||||||
|
port-entropy = { workspace = true }
|
||||||
74
crates/drivers/rng/virtio/src/lib.rs
Normal file
74
crates/drivers/rng/virtio/src/lib.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
//! virtio-rng (virtio spec 5.4): the device fills a buffer with entropy.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use hw_virtio::mmio::{MmioAccess, Registers};
|
||||||
|
use hw_virtio::queue::{Descriptor, VirtioQueue};
|
||||||
|
use libkos::device::Dma;
|
||||||
|
use port_entropy::Entropy;
|
||||||
|
use types::address::{ByteOffset, VirtualAddress};
|
||||||
|
use types::bounded::PageCount;
|
||||||
|
|
||||||
|
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x2A000);
|
||||||
|
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x2B000);
|
||||||
|
const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x2D000);
|
||||||
|
/// Asked for at most this much at a time.
|
||||||
|
const CHUNK: usize = 256;
|
||||||
|
|
||||||
|
pub struct VirtioRng {
|
||||||
|
regs: Registers,
|
||||||
|
queue: VirtioQueue,
|
||||||
|
buffer: Dma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtioRng {
|
||||||
|
/// Takes over the virtio-rng device behind `mmio`; `None` if it is not
|
||||||
|
/// one or any step fails.
|
||||||
|
pub fn new(mmio: types::bounded::CapSlot) -> Option<Self> {
|
||||||
|
let regs = Registers::new(libkos::device::map_mmio(mmio, MMIO_VA).ok()?);
|
||||||
|
if regs.read(hw_virtio::mmio::REG_MAGIC) != hw_virtio::mmio::MAGIC
|
||||||
|
|| regs.read(hw_virtio::mmio::REG_DEVICE_ID) != hw_virtio::mmio::DEVICE_ID_RNG
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rings = libkos::device::dma(PageCount::new(2)?, VQUEUE_VA).ok()?;
|
||||||
|
let buffer = libkos::device::dma(PageCount::new(1)?, BUFFER_VA).ok()?;
|
||||||
|
hw_virtio::mmio::init_legacy_device(
|
||||||
|
®s,
|
||||||
|
rings.physical,
|
||||||
|
hw_virtio::queue::QUEUE_SIZE as u32,
|
||||||
|
);
|
||||||
|
Some(Self {
|
||||||
|
regs,
|
||||||
|
queue: VirtioQueue::new(rings.window),
|
||||||
|
buffer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entropy for VirtioRng {
|
||||||
|
fn fill(&mut self, buf: &mut [u8]) -> bool {
|
||||||
|
let mut done = 0;
|
||||||
|
while done < buf.len() {
|
||||||
|
let want = (buf.len() - done).min(CHUNK);
|
||||||
|
let desc = [Descriptor {
|
||||||
|
addr: self.buffer.physical.raw() as u64,
|
||||||
|
len: want as u32,
|
||||||
|
writable: true,
|
||||||
|
}];
|
||||||
|
let got = match self.queue.submit_written(&self.regs, &desc) {
|
||||||
|
Some(n) if n > 0 => (n as usize).min(want),
|
||||||
|
// The device gave nothing (or never answered): no randomness.
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
self.buffer
|
||||||
|
.window
|
||||||
|
.copy_to(ByteOffset::ZERO, &mut buf[done..done + got]);
|
||||||
|
done += got;
|
||||||
|
}
|
||||||
|
// What was handed out is not left lying in the buffer.
|
||||||
|
self.buffer.window.fill_u32(ByteOffset::ZERO, CHUNK / 4, 0);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ pub const STATUS_DRIVER: u32 = 2;
|
|||||||
pub const STATUS_DRIVER_OK: u32 = 4;
|
pub const STATUS_DRIVER_OK: u32 = 4;
|
||||||
|
|
||||||
pub const DEVICE_ID_BLOCK: u32 = 2;
|
pub const DEVICE_ID_BLOCK: u32 = 2;
|
||||||
|
pub const DEVICE_ID_RNG: u32 = 4;
|
||||||
pub const DEVICE_ID_GPU: u32 = 16;
|
pub const DEVICE_ID_GPU: u32 = 16;
|
||||||
pub const DEVICE_ID_INPUT: u32 = 18;
|
pub const DEVICE_ID_INPUT: u32 = 18;
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ impl VirtioQueue {
|
|||||||
|
|
||||||
/// Hands the device a chain of `descs` and waits until it is used.
|
/// Hands the device a chain of `descs` and waits until it is used.
|
||||||
pub fn submit(&self, mmio: &impl MmioAccess, descs: &[Descriptor]) -> bool {
|
pub fn submit(&self, mmio: &impl MmioAccess, descs: &[Descriptor]) -> bool {
|
||||||
|
self.submit_written(mmio, descs).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `submit`; how many bytes the device wrote into the chain's
|
||||||
|
/// writable buffers (it may write fewer than they hold).
|
||||||
|
pub fn submit_written(&self, mmio: &impl MmioAccess, descs: &[Descriptor]) -> Option<u32> {
|
||||||
if descs.is_empty() || descs.len() > QUEUE_SIZE {
|
if descs.is_empty() || descs.len() > QUEUE_SIZE {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
let r = &self.rings;
|
let r = &self.rings;
|
||||||
for (i, d) in descs.iter().enumerate() {
|
for (i, d) in descs.iter().enumerate() {
|
||||||
@@ -56,9 +62,11 @@ impl VirtioQueue {
|
|||||||
core::hint::spin_loop();
|
core::hint::spin_loop();
|
||||||
timeout -= 1;
|
timeout -= 1;
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
// The used element just added: the chain's head id, then its length.
|
||||||
|
let element = USED + 4 + (avail_idx as usize % QUEUE_SIZE) * 8;
|
||||||
|
Some(r.read::<u32>(element + 4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
crates/keystore/Cargo.toml
Normal file
10
crates/keystore/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "keystore"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argon2 = { workspace = true }
|
||||||
|
chacha20poly1305 = { workspace = true }
|
||||||
|
blake2 = { workspace = true }
|
||||||
|
zeroize = { workspace = true }
|
||||||
224
crates/keystore/src/lib.rs
Normal file
224
crates/keystore/src/lib.rs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
//! The keystore's contents (see the vision's Gatekeeper note): two sealed
|
||||||
|
//! key slots, the failed-attempt counter, and the wipe state, in a record
|
||||||
|
//! kept twice so a torn write never loses both. Pure: the gatekeeper does
|
||||||
|
//! the I/O; the crypto is RustCrypto's (argon2id, ChaCha20-Poly1305,
|
||||||
|
//! BLAKE2s).
|
||||||
|
//!
|
||||||
|
//! One slot seals the master key under the real passcode, the other a
|
||||||
|
//! "wipe" marker under the duress passcode (or is plain random bytes when
|
||||||
|
//! there is none). Which is which is random, and both look like noise:
|
||||||
|
//! nothing on the device tells them apart, and every unlock tries both.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
mod record;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub use record::{Copies, KdfParams, RECORD_SIZE, Record, State};
|
||||||
|
|
||||||
|
use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
|
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, Nonce, Tag};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
pub use argon2::Block;
|
||||||
|
|
||||||
|
pub const KEY_SIZE: usize = 32;
|
||||||
|
const SALT_SIZE: usize = 16;
|
||||||
|
const NONCE_SIZE: usize = 12;
|
||||||
|
const TAG_SIZE: usize = 16;
|
||||||
|
/// What a slot seals: its kind, then a key.
|
||||||
|
const SEALED_SIZE: usize = 1 + KEY_SIZE;
|
||||||
|
const SLOT_SIZE: usize = SALT_SIZE + NONCE_SIZE + SEALED_SIZE + TAG_SIZE;
|
||||||
|
const REAL: u8 = 1;
|
||||||
|
const DURESS: u8 = 2;
|
||||||
|
|
||||||
|
/// The master key, wiped from memory when dropped. Compared only in tests:
|
||||||
|
/// a plain `==` on secrets would not be constant-time.
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
|
pub struct Key(pub [u8; KEY_SIZE]);
|
||||||
|
|
||||||
|
impl Drop for Key {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for Key {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
f.write_str("Key(..)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a passcode turned out to be.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
|
pub enum Outcome {
|
||||||
|
Real(Key),
|
||||||
|
Duress,
|
||||||
|
Wrong,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills a buffer with randomness; false if it could not.
|
||||||
|
pub trait Random {
|
||||||
|
fn fill(&mut self, buf: &mut [u8]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// argon2id over the caller's memory (`params.memory_kib` blocks): no heap.
|
||||||
|
fn derive(passcode: &[u8], salt: &[u8], params: KdfParams, memory: &mut [Block]) -> Option<Key> {
|
||||||
|
let p = Params::new(params.memory_kib, params.passes, 1, Some(KEY_SIZE)).ok()?;
|
||||||
|
let memory = memory.get_mut(..params.memory_kib as usize)?;
|
||||||
|
let mut out = Key([0; KEY_SIZE]);
|
||||||
|
Argon2::new(Algorithm::Argon2id, Version::V0x13, p)
|
||||||
|
.hash_password_into_with_memory(passcode, salt, &mut out.0, memory)
|
||||||
|
.ok()?;
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A slot: salt, nonce, the sealed kind and key, the tag.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Slot(pub [u8; SLOT_SIZE]);
|
||||||
|
|
||||||
|
impl Slot {
|
||||||
|
fn random(rng: &mut impl Random) -> Option<Self> {
|
||||||
|
let mut s = [0u8; SLOT_SIZE];
|
||||||
|
rng.fill(&mut s).then_some(Self(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `kind` and `key`, sealed under a key derived from `passcode`.
|
||||||
|
fn seal(
|
||||||
|
kind: u8,
|
||||||
|
key: &Key,
|
||||||
|
passcode: &[u8],
|
||||||
|
params: KdfParams,
|
||||||
|
memory: &mut [Block],
|
||||||
|
rng: &mut impl Random,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let mut s = [0u8; SLOT_SIZE];
|
||||||
|
let (salt, rest) = s.split_at_mut(SALT_SIZE);
|
||||||
|
let (nonce, rest) = rest.split_at_mut(NONCE_SIZE);
|
||||||
|
let (sealed, tag) = rest.split_at_mut(SEALED_SIZE);
|
||||||
|
if !rng.fill(salt) || !rng.fill(nonce) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let kek = derive(passcode, salt, params, memory)?;
|
||||||
|
sealed[0] = kind;
|
||||||
|
sealed[1..].copy_from_slice(&key.0);
|
||||||
|
let t = ChaCha20Poly1305::new((&kek.0).into())
|
||||||
|
.encrypt_in_place_detached(Nonce::from_slice(nonce), b"", sealed)
|
||||||
|
.ok()?;
|
||||||
|
tag.copy_from_slice(&t);
|
||||||
|
Some(Self(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the slot seals, if `passcode` opens it.
|
||||||
|
fn open(&self, passcode: &[u8], params: KdfParams, memory: &mut [Block]) -> Option<(u8, Key)> {
|
||||||
|
let (salt, rest) = self.0.split_at(SALT_SIZE);
|
||||||
|
let (nonce, rest) = rest.split_at(NONCE_SIZE);
|
||||||
|
let (sealed, tag) = rest.split_at(SEALED_SIZE);
|
||||||
|
let kek = derive(passcode, salt, params, memory)?;
|
||||||
|
let mut plain = [0u8; SEALED_SIZE];
|
||||||
|
plain.copy_from_slice(sealed);
|
||||||
|
let opened = ChaCha20Poly1305::new((&kek.0).into())
|
||||||
|
.decrypt_in_place_detached(
|
||||||
|
Nonce::from_slice(nonce),
|
||||||
|
b"",
|
||||||
|
&mut plain,
|
||||||
|
Tag::from_slice(tag),
|
||||||
|
)
|
||||||
|
.is_ok();
|
||||||
|
let mut key = Key([0; KEY_SIZE]);
|
||||||
|
key.0.copy_from_slice(&plain[1..]);
|
||||||
|
let kind = plain[0];
|
||||||
|
plain.zeroize();
|
||||||
|
opened.then_some((kind, key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Record {
|
||||||
|
/// A fresh keystore: a new random master key, sealed under `real`; a
|
||||||
|
/// duress slot under `duress`, or noise. Returns it and the master key.
|
||||||
|
pub fn setup(
|
||||||
|
real: &[u8],
|
||||||
|
duress: Option<&[u8]>,
|
||||||
|
threshold: u8,
|
||||||
|
params: KdfParams,
|
||||||
|
memory: &mut [Block],
|
||||||
|
rng: &mut impl Random,
|
||||||
|
) -> Option<(Self, Key)> {
|
||||||
|
let mut master = Key([0; KEY_SIZE]);
|
||||||
|
if !rng.fill(&mut master.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let real_slot = Slot::seal(REAL, &master, real, params, memory, rng)?;
|
||||||
|
let other = match duress {
|
||||||
|
Some(d) => {
|
||||||
|
// Its key is never used: random, so the slot looks like the other.
|
||||||
|
let mut filler = Key([0; KEY_SIZE]);
|
||||||
|
if !rng.fill(&mut filler.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Slot::seal(DURESS, &filler, d, params, memory, rng)?
|
||||||
|
}
|
||||||
|
None => Slot::random(rng)?,
|
||||||
|
};
|
||||||
|
let mut coin = [0u8; 1];
|
||||||
|
if !rng.fill(&mut coin) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let slots = if coin[0] & 1 == 0 {
|
||||||
|
[real_slot, other]
|
||||||
|
} else {
|
||||||
|
[other, real_slot]
|
||||||
|
};
|
||||||
|
let record = Self {
|
||||||
|
generation: 1,
|
||||||
|
state: State::Active,
|
||||||
|
threshold: threshold.max(1),
|
||||||
|
failures: 0,
|
||||||
|
params,
|
||||||
|
slots,
|
||||||
|
};
|
||||||
|
Some((record, master))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries `passcode` on both slots, always both, so the time taken does
|
||||||
|
/// not tell which (if any) it opened.
|
||||||
|
pub fn unlock(&self, passcode: &[u8], memory: &mut [Block]) -> Outcome {
|
||||||
|
let a = self.slots[0].open(passcode, self.params, memory);
|
||||||
|
let b = self.slots[1].open(passcode, self.params, memory);
|
||||||
|
match a.or(b) {
|
||||||
|
Some((REAL, key)) => Outcome::Real(key),
|
||||||
|
Some((DURESS, _)) => Outcome::Duress,
|
||||||
|
_ => Outcome::Wrong,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same record, one later, with `changes` made.
|
||||||
|
pub fn next(&self, changes: impl FnOnce(&mut Self)) -> Self {
|
||||||
|
let mut r = *self;
|
||||||
|
r.generation = self.generation.wrapping_add(1);
|
||||||
|
changes(&mut r);
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This record with both slots overwritten by noise and marked as
|
||||||
|
/// being wiped, so an interrupted wipe is finished on the next boot.
|
||||||
|
pub fn wiping(&self, rng: &mut impl Random) -> Option<Self> {
|
||||||
|
let slots = [Slot::random(rng)?, Slot::random(rng)?];
|
||||||
|
Some(self.next(|r| {
|
||||||
|
r.state = State::Wiping;
|
||||||
|
r.slots = slots;
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this record must be wiped before anything else: a wipe was
|
||||||
|
/// interrupted, or too many wrong passcodes.
|
||||||
|
pub fn must_wipe(&self) -> bool {
|
||||||
|
self.state == State::Wiping || self.failures >= self.threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
137
crates/keystore/src/record.rs
Normal file
137
crates/keystore/src/record.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
//! The record's bytes, and the two copies it is kept in.
|
||||||
|
|
||||||
|
use blake2::{Blake2s256, Digest};
|
||||||
|
|
||||||
|
use crate::{SLOT_SIZE, Slot};
|
||||||
|
|
||||||
|
/// One record fills one sector.
|
||||||
|
pub const RECORD_SIZE: usize = 512;
|
||||||
|
const MAGIC: &[u8; 8] = b"KOSKEYS1";
|
||||||
|
const CHECKSUM: usize = 32;
|
||||||
|
const BODY: usize = 8 + 8 + 4 + 8 + 2 * SLOT_SIZE;
|
||||||
|
|
||||||
|
/// How slow a passcode guess is: argon2id's memory (KiB) and passes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct KdfParams {
|
||||||
|
pub memory_kib: u32,
|
||||||
|
pub passes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdfParams {
|
||||||
|
/// argon2id with 16 MiB and 3 passes: above OWASP's minimum (12 MiB, 3).
|
||||||
|
pub const DEFAULT: Self = Self {
|
||||||
|
memory_kib: 16 * 1024,
|
||||||
|
passes: 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum State {
|
||||||
|
Active,
|
||||||
|
/// A wipe started and may not have finished.
|
||||||
|
Wiping,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Record {
|
||||||
|
/// Which copy is newer: the higher one.
|
||||||
|
pub generation: u64,
|
||||||
|
pub state: State,
|
||||||
|
/// Wrong passcodes in a row that wipe the keys.
|
||||||
|
pub threshold: u8,
|
||||||
|
/// Wrong passcodes in a row so far; counted before a passcode is
|
||||||
|
/// checked, so pulling the power cannot save a guess.
|
||||||
|
pub failures: u8,
|
||||||
|
pub params: KdfParams,
|
||||||
|
pub slots: [Slot; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for Record {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
f.debug_struct("Record")
|
||||||
|
.field("generation", &self.generation)
|
||||||
|
.field("state", &self.state)
|
||||||
|
.field("failures", &self.failures)
|
||||||
|
.field("threshold", &self.threshold)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checksum(body: &[u8]) -> [u8; CHECKSUM] {
|
||||||
|
Blake2s256::digest(body).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Record {
|
||||||
|
pub fn encode(&self) -> [u8; RECORD_SIZE] {
|
||||||
|
let mut out = [0u8; RECORD_SIZE];
|
||||||
|
out[0..8].copy_from_slice(MAGIC);
|
||||||
|
out[8..16].copy_from_slice(&self.generation.to_le_bytes());
|
||||||
|
out[16] = match self.state {
|
||||||
|
State::Active => 1,
|
||||||
|
State::Wiping => 2,
|
||||||
|
};
|
||||||
|
out[17] = self.threshold;
|
||||||
|
out[18] = self.failures;
|
||||||
|
out[20..24].copy_from_slice(&self.params.memory_kib.to_le_bytes());
|
||||||
|
out[24..28].copy_from_slice(&self.params.passes.to_le_bytes());
|
||||||
|
out[28..28 + SLOT_SIZE].copy_from_slice(&self.slots[0].0);
|
||||||
|
out[28 + SLOT_SIZE..BODY].copy_from_slice(&self.slots[1].0);
|
||||||
|
let sum = checksum(&out[..BODY]);
|
||||||
|
out[BODY..BODY + CHECKSUM].copy_from_slice(&sum);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `None` unless it is a whole, unaltered record (magic and checksum).
|
||||||
|
pub fn decode(bytes: &[u8]) -> Option<Self> {
|
||||||
|
let bytes = bytes.get(..RECORD_SIZE)?;
|
||||||
|
if &bytes[0..8] != MAGIC || bytes[BODY..BODY + CHECKSUM] != checksum(&bytes[..BODY]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap_or([0; 4]));
|
||||||
|
let state = match bytes[16] {
|
||||||
|
1 => State::Active,
|
||||||
|
2 => State::Wiping,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let mut a = [0u8; SLOT_SIZE];
|
||||||
|
let mut b = [0u8; SLOT_SIZE];
|
||||||
|
a.copy_from_slice(&bytes[28..28 + SLOT_SIZE]);
|
||||||
|
b.copy_from_slice(&bytes[28 + SLOT_SIZE..BODY]);
|
||||||
|
Some(Self {
|
||||||
|
generation: u64::from_le_bytes(bytes[8..16].try_into().ok()?),
|
||||||
|
state,
|
||||||
|
threshold: bytes[17],
|
||||||
|
failures: bytes[18],
|
||||||
|
params: crate::KdfParams {
|
||||||
|
memory_kib: word(20),
|
||||||
|
passes: word(24),
|
||||||
|
},
|
||||||
|
slots: [Slot(a), Slot(b)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The record's two copies, as read from the device.
|
||||||
|
pub struct Copies<'a> {
|
||||||
|
pub copies: [&'a [u8]; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Copies<'_> {
|
||||||
|
/// The newest whole record and the copy it is in; `None` for a device
|
||||||
|
/// never set up (or wiped).
|
||||||
|
pub fn newest(&self) -> Option<(Record, usize)> {
|
||||||
|
let [a, b] = self.copies.map(Record::decode);
|
||||||
|
match (a, b) {
|
||||||
|
(Some(a), Some(b)) if b.generation > a.generation => Some((b, 1)),
|
||||||
|
(Some(a), _) => Some((a, 0)),
|
||||||
|
(None, Some(b)) => Some((b, 1)),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the next write goes: over the copy that is not the newest, so
|
||||||
|
/// a torn write leaves the newest whole.
|
||||||
|
pub fn next_copy(&self) -> usize {
|
||||||
|
self.newest().map_or(0, |(_, copy)| 1 - copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
172
crates/keystore/src/tests.rs
Normal file
172
crates/keystore/src/tests.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
extern crate std;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
use std::vec;
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
|
/// Fast enough for tests; the real ones are `KdfParams::DEFAULT`.
|
||||||
|
const FAST: KdfParams = KdfParams {
|
||||||
|
memory_kib: 8,
|
||||||
|
passes: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A deterministic stand-in for the hardware's randomness.
|
||||||
|
struct Counter(u8);
|
||||||
|
|
||||||
|
impl Random for Counter {
|
||||||
|
fn fill(&mut self, buf: &mut [u8]) -> bool {
|
||||||
|
for b in buf {
|
||||||
|
self.0 = self.0.wrapping_mul(31).wrapping_add(7);
|
||||||
|
*b = self.0;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Broken;
|
||||||
|
|
||||||
|
impl Random for Broken {
|
||||||
|
fn fill(&mut self, _buf: &mut [u8]) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory() -> Vec<Block> {
|
||||||
|
vec![Block::default(); FAST.memory_kib as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(duress: Option<&[u8]>, seed: u8) -> (Record, Key) {
|
||||||
|
Record::setup(b"1234", duress, 5, FAST, &mut memory(), &mut Counter(seed)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_real_passcode_opens_the_master_key_and_nothing_else_does() {
|
||||||
|
let (record, master) = setup(Some(b"0000"), 1);
|
||||||
|
let mem = &mut memory();
|
||||||
|
assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master));
|
||||||
|
assert_eq!(record.unlock(b"0000", mem), Outcome::Duress);
|
||||||
|
for wrong in [&b"1235"[..], b"", b"12345", b"123"] {
|
||||||
|
assert_eq!(record.unlock(wrong, mem), Outcome::Wrong, "{wrong:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn without_a_duress_passcode_its_slot_opens_for_nothing() {
|
||||||
|
let (record, master) = setup(None, 2);
|
||||||
|
let mem = &mut memory();
|
||||||
|
assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master));
|
||||||
|
assert_eq!(record.unlock(b"0000", mem), Outcome::Wrong);
|
||||||
|
assert_eq!(
|
||||||
|
record.unlock(b"", mem),
|
||||||
|
Outcome::Wrong,
|
||||||
|
"not even an empty one"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn which_slot_is_real_is_not_fixed() {
|
||||||
|
// Different randomness puts the real slot first in some setups, second
|
||||||
|
// in others: its position gives nothing away.
|
||||||
|
let mem = &mut memory();
|
||||||
|
let mut firsts = [false; 2];
|
||||||
|
for seed in 0..16 {
|
||||||
|
let (record, _) = setup(Some(b"0000"), seed);
|
||||||
|
let only_first = Record {
|
||||||
|
slots: [record.slots[0], Slot(record.slots[1].0.map(|b| !b))],
|
||||||
|
..record
|
||||||
|
};
|
||||||
|
firsts[matches!(only_first.unlock(b"1234", mem), Outcome::Real(_)) as usize] = true;
|
||||||
|
}
|
||||||
|
assert_eq!(firsts, [true, true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn records_round_trip_and_damage_is_detected() {
|
||||||
|
let (record, _) = setup(Some(b"0000"), 3);
|
||||||
|
let bytes = record.encode();
|
||||||
|
assert_eq!(Record::decode(&bytes), Some(record));
|
||||||
|
for at in [0, 9, 18, 40, 200] {
|
||||||
|
let mut torn = bytes;
|
||||||
|
torn[at] ^= 1;
|
||||||
|
assert_eq!(Record::decode(&torn), None, "byte {at}");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
Record::decode(&[0; RECORD_SIZE]),
|
||||||
|
None,
|
||||||
|
"a blank device holds no record"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_newest_whole_copy_wins_and_writes_go_to_the_other() {
|
||||||
|
let (old, _) = setup(None, 4);
|
||||||
|
let new = old.next(|r| r.failures = 1);
|
||||||
|
let (a, b) = (old.encode(), new.encode());
|
||||||
|
let copies = Copies { copies: [&a, &b] };
|
||||||
|
assert_eq!(copies.newest(), Some((new, 1)));
|
||||||
|
assert_eq!(copies.next_copy(), 0, "the older copy is overwritten");
|
||||||
|
// A torn write of the newer copy leaves the older one.
|
||||||
|
let mut torn = b;
|
||||||
|
torn[100] ^= 0xFF;
|
||||||
|
let copies = Copies {
|
||||||
|
copies: [&a, &torn],
|
||||||
|
};
|
||||||
|
assert_eq!(copies.newest(), Some((old, 0)));
|
||||||
|
assert_eq!(copies.next_copy(), 1);
|
||||||
|
// Newest wins wherever it is.
|
||||||
|
let copies = Copies { copies: [&b, &a] };
|
||||||
|
assert_eq!(copies.newest(), Some((new, 0)));
|
||||||
|
let blank = [0u8; RECORD_SIZE];
|
||||||
|
assert_eq!(
|
||||||
|
Copies {
|
||||||
|
copies: [&blank, &blank]
|
||||||
|
}
|
||||||
|
.newest(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wiping_leaves_nothing_any_passcode_opens() {
|
||||||
|
let (record, _) = setup(Some(b"0000"), 5);
|
||||||
|
let wiped = record.wiping(&mut Counter(99)).unwrap();
|
||||||
|
assert_eq!(wiped.state, State::Wiping);
|
||||||
|
assert!(
|
||||||
|
wiped.must_wipe(),
|
||||||
|
"an interrupted wipe is finished next boot"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
wiped.generation > record.generation,
|
||||||
|
"it is the newest copy"
|
||||||
|
);
|
||||||
|
let mem = &mut memory();
|
||||||
|
for p in [&b"1234"[..], b"0000"] {
|
||||||
|
assert_eq!(wiped.unlock(p, mem), Outcome::Wrong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_many_failures_mean_a_wipe() {
|
||||||
|
let (record, _) = setup(None, 6);
|
||||||
|
assert!(!record.must_wipe());
|
||||||
|
let worn = record.next(|r| r.failures = r.threshold);
|
||||||
|
assert!(worn.must_wipe());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_randomness_means_no_keystore() {
|
||||||
|
assert!(Record::setup(b"1234", None, 5, FAST, &mut memory(), &mut Broken).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_default_parameters_are_accepted_by_argon2() {
|
||||||
|
assert!(
|
||||||
|
argon2::Params::new(
|
||||||
|
KdfParams::DEFAULT.memory_kib,
|
||||||
|
KdfParams::DEFAULT.passes,
|
||||||
|
1,
|
||||||
|
Some(KEY_SIZE)
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
}
|
||||||
6
crates/ports/entropy/Cargo.toml
Normal file
6
crates/ports/entropy/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "port-entropy"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
10
crates/ports/entropy/src/lib.rs
Normal file
10
crates/ports/entropy/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! Where randomness comes from: keys, salts, nonces.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub trait Entropy {
|
||||||
|
/// Fills `buf` with random bytes from the hardware; false if it could
|
||||||
|
/// not (then nothing in `buf` may be used).
|
||||||
|
fn fill(&mut self, buf: &mut [u8]) -> bool;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user