use crate::device::{DevicePublicKey, DeviceSignature};
use crate::{Error, Result};
#[cfg(any(test, feature = "mock", target_os = "windows", target_os = "android"))]
use crate::device::{PUBLIC_KEY_LEN, SIGNATURE_LEN};
pub const DIGEST_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Backend {
Tpm,
SecureEnclave,
AndroidKeystore,
Software,
Unavailable,
}
impl Backend {
#[must_use]
pub const fn is_hardware_backed(self) -> bool {
matches!(
self,
Self::Tpm | Self::SecureEnclave | Self::AndroidKeystore
)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Tpm => "tpm",
Self::SecureEnclave => "secure-enclave",
Self::AndroidKeystore => "android-keystore",
Self::Software => "software",
Self::Unavailable => "unavailable",
}
}
}
pub trait DeviceSigner: core::fmt::Debug {
fn backend(&self) -> Backend;
fn public_key(&self) -> &DevicePublicKey;
fn sign(&self, message: &[u8]) -> Result<DeviceSignature> {
let digest = sha256(message);
self.sign_digest(&digest)
}
fn sign_digest(&self, digest: &[u8]) -> Result<DeviceSignature>;
}
pub trait DeviceKeyStore: core::fmt::Debug {
fn backend(&self) -> Backend;
fn open(&self, label: &str) -> Result<Box<dyn DeviceSigner>>;
fn create(&self, label: &str) -> Result<Box<dyn DeviceSigner>>;
fn delete(&self, label: &str) -> Result<()>;
fn open_or_create(&self, label: &str) -> Result<Box<dyn DeviceSigner>> {
match self.open(label) {
Ok(signer) => Ok(signer),
Err(Error::KeyNotFound { .. }) => match self.create(label) {
Ok(signer) => Ok(signer),
Err(Error::KeyAlreadyExists { .. }) => self.open(label),
Err(other) => Err(other),
},
Err(other) => Err(other),
}
}
}
pub const MASTER_KEY_LEN: usize = 32;
#[derive(Clone)]
pub struct MasterKey {
bytes: [u8; MASTER_KEY_LEN],
}
impl MasterKey {
#[must_use]
pub const fn from_bytes(bytes: [u8; MASTER_KEY_LEN]) -> Self {
Self { bytes }
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; MASTER_KEY_LEN] {
&self.bytes
}
}
impl core::fmt::Debug for MasterKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MasterKey")
.field("len", &MASTER_KEY_LEN)
.finish_non_exhaustive()
}
}
impl Drop for MasterKey {
fn drop(&mut self) {
use p256::elliptic_curve::zeroize::Zeroize;
self.bytes.zeroize();
}
}
#[derive(Clone)]
pub struct WrappedMasterKey {
bytes: Vec<u8>,
}
impl WrappedMasterKey {
#[must_use]
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self { bytes }
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
impl core::fmt::Debug for WrappedMasterKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("WrappedMasterKey")
.field("len", &self.bytes.len())
.finish_non_exhaustive()
}
}
pub trait DirectMasterKeyStore: core::fmt::Debug {
fn backend(&self) -> Backend;
fn store(&self, label: &str, key: &MasterKey) -> Result<()>;
fn load(&self, label: &str) -> Result<MasterKey>;
fn delete(&self, label: &str) -> Result<()>;
}
pub trait WrappedMasterKeyStore: core::fmt::Debug {
fn backend(&self) -> Backend;
fn store(&self, label: &str, key: &MasterKey) -> Result<WrappedMasterKey>;
fn load(&self, label: &str, wrapped: &WrappedMasterKey) -> Result<MasterKey>;
fn delete(&self, label: &str) -> Result<()>;
}
#[cfg(any(test, feature = "mock", target_os = "windows", target_os = "android"))]
#[must_use]
pub(crate) fn secrets_equal(a: &[u8], b: &[u8]) -> bool {
use p256::elliptic_curve::subtle::ConstantTimeEq;
a.ct_eq(b).into()
}
#[must_use]
pub fn sha256(message: &[u8]) -> [u8; DIGEST_LEN] {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(message);
let mut out = [0_u8; DIGEST_LEN];
out.copy_from_slice(&digest);
out
}
#[cfg(any(test, feature = "mock", target_os = "windows", target_os = "android"))]
pub(crate) fn signature_from_raw(raw: &[u8]) -> Result<DeviceSignature> {
if raw.len() != SIGNATURE_LEN {
return Err(Error::InvalidSignatureLength {
expected: SIGNATURE_LEN,
actual: raw.len(),
});
}
DeviceSignature::from_bytes(raw)
}
#[cfg(any(test, feature = "mock", target_os = "windows", target_os = "android"))]
pub(crate) fn public_key_from_sec1(bytes: &[u8]) -> Result<DevicePublicKey> {
if bytes.len() != PUBLIC_KEY_LEN {
return Err(Error::InvalidPublicKey {
reason: "the length is not that of a 65-byte uncompressed SEC1 point",
});
}
DevicePublicKey::from_sec1_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hardware_backed_classification() {
assert!(Backend::Tpm.is_hardware_backed());
assert!(Backend::SecureEnclave.is_hardware_backed());
assert!(Backend::AndroidKeystore.is_hardware_backed());
assert!(!Backend::Software.is_hardware_backed());
assert!(!Backend::Unavailable.is_hardware_backed());
}
#[test]
fn backend_names_are_stable_and_unique() {
let all = [
Backend::Tpm,
Backend::SecureEnclave,
Backend::AndroidKeystore,
Backend::Software,
Backend::Unavailable,
];
let mut names: Vec<&str> = all.iter().map(|b| b.as_str()).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(names.len(), before, "the backend names contain duplicates");
for name in names {
assert!(!name.is_empty());
assert!(
name.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
"{name} is not a lower-case form suitable for persistence"
);
}
}
#[test]
fn sha256_matches_known_vector() {
let digest = sha256(b"");
let expected = [
0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F,
0xB9, 0x24, 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B,
0x78, 0x52, 0xB8, 0x55,
];
assert_eq!(digest, expected);
}
#[test]
fn signature_from_raw_rejects_wrong_length() {
assert!(signature_from_raw(&[]).is_err());
assert!(signature_from_raw(&[0_u8; SIGNATURE_LEN - 1]).is_err());
assert!(signature_from_raw(&[0_u8; SIGNATURE_LEN + 1]).is_err());
assert!(signature_from_raw(&[0_u8; SIGNATURE_LEN]).is_ok());
}
#[test]
fn public_key_from_sec1_rejects_wrong_length() {
assert!(public_key_from_sec1(&[]).is_err());
assert!(public_key_from_sec1(&[0x04; PUBLIC_KEY_LEN - 1]).is_err());
assert!(public_key_from_sec1(&[0x04; PUBLIC_KEY_LEN + 1]).is_err());
}
#[test]
fn master_key_round_trips_its_bytes() {
let bytes = [0xA5_u8; MASTER_KEY_LEN];
let key = MasterKey::from_bytes(bytes);
assert_eq!(key.as_bytes(), &bytes);
}
#[test]
fn master_key_debug_does_not_show_the_material() {
let bytes = [0x5A_u8; MASTER_KEY_LEN];
let text = format!("{:?}", MasterKey::from_bytes(bytes));
assert!(
!text.contains("90") && !text.contains("5A") && !text.contains("5a"),
"the debug output of a master key shows its material: {text}"
);
assert!(
text.contains("MasterKey"),
"the debug output does not name the type: {text}"
);
}
#[test]
fn wrapped_master_key_debug_shows_only_the_length() {
let wrapped = WrappedMasterKey::from_bytes(vec![0x33_u8; 20]);
let text = format!("{wrapped:?}");
assert!(text.contains("20"), "the length is missing: {text}");
assert!(
!text.contains("51") && !text.contains("33"),
"the debug output shows the bytes: {text}"
);
assert_eq!(wrapped.as_bytes().len(), 20);
assert_eq!(wrapped.into_bytes().len(), 20);
}
#[test]
fn secrets_equal_matches_equality() {
assert!(secrets_equal(b"abc", b"abc"));
assert!(secrets_equal(b"", b""));
assert!(!secrets_equal(b"abc", b"abd"));
assert!(!secrets_equal(b"abc", b"ab"));
assert!(!secrets_equal(b"ab", b"abc"));
}
}