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:
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()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user