pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid device public key: {reason}")]
DevicePublicKeyInvalid {
reason: &'static str,
},
#[error("invalid device signature or verification failed: {reason}")]
DeviceSignatureInvalid {
reason: &'static str,
},
#[error("platform {platform} has no hardware key store backend yet")]
UnsupportedPlatform {
platform: &'static str,
},
#[error("no usable hardware key store on this machine: {reason}")]
HardwareUnavailable {
reason: String,
},
#[error("platform key store call failed ({operation}, native code 0x{code:08X})")]
Hardware {
operation: &'static str,
code: i32,
},
#[error("key {label} does not exist")]
KeyNotFound {
label: String,
},
#[error("platform key store call failed ({operation}): {detail}")]
Jni {
operation: &'static str,
detail: String,
},
#[error("key {label} already exists")]
KeyAlreadyExists {
label: String,
},
#[error("invalid digest length: expected {expected} bytes, got {actual} bytes")]
InvalidDigestLength {
expected: usize,
actual: usize,
},
#[error("invalid key label: {reason}")]
InvalidKeyLabel {
reason: &'static str,
},
#[error("hardware returned an invalid public key: {reason}")]
InvalidPublicKey {
reason: &'static str,
},
#[error(
"hardware returned an invalid signature length: expected {expected} bytes, got {actual} bytes"
)]
InvalidSignatureLength {
expected: usize,
actual: usize,
},
#[error("the wrapped master key cannot be unwrapped ({reason}, native code 0x{code:08X})")]
InvalidWrappedKey {
reason: &'static str,
code: i32,
},
#[error("the platform key store returned an invalid master key: {reason}")]
InvalidMasterKey {
reason: &'static str,
},
#[error("the platform key store will not return the stored material: {reason}")]
KeyNotExportable {
reason: &'static str,
},
#[error("the OS random source is unavailable")]
RandomnessUnavailable,
}
impl Error {
#[must_use]
pub const fn is_missing_hardware(&self) -> bool {
matches!(
self,
Self::UnsupportedPlatform { .. } | Self::HardwareUnavailable { .. }
)
}
#[must_use]
pub const fn is_environmental(&self) -> bool {
matches!(
self,
Self::UnsupportedPlatform { .. }
| Self::HardwareUnavailable { .. }
| Self::Hardware { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_hardware_errors_are_classified_as_missing_hardware() {
let missing = [
Error::UnsupportedPlatform { platform: "linux" },
Error::HardwareUnavailable {
reason: "no tpm".into(),
},
];
for error in &missing {
assert!(
error.is_missing_hardware(),
"{error:?} must be classified as missing hardware"
);
}
let present = [
Error::KeyNotFound {
label: "device".into(),
},
Error::Hardware {
operation: "NCryptSignHash",
code: 0x8009_000F_u32 as i32,
},
Error::InvalidDigestLength {
expected: 32,
actual: 20,
},
];
for error in &present {
assert!(
!error.is_missing_hardware(),
"{error:?} must not be classified as missing hardware"
);
}
}
#[test]
fn hardware_error_shows_hex_code() {
let text = Error::Hardware {
operation: "NCryptOpenStorageProvider",
code: 0x8009_000F_u32 as i32,
}
.to_string();
assert!(text.contains("8009000F"), "actual: {text}");
assert!(text.contains("NCryptOpenStorageProvider"), "actual: {text}");
}
#[test]
fn negative_native_codes_print_as_twos_complement() {
let text = Error::Hardware {
operation: "NCryptOpenKey",
code: -1,
}
.to_string();
assert!(text.contains("FFFFFFFF"), "actual: {text}");
}
#[test]
fn environmental_failures_are_recognised() {
assert!(
Error::UnsupportedPlatform { platform: "ios" }.is_environmental(),
"an unsupported platform is an environmental problem"
);
assert!(
Error::Hardware {
operation: "op",
code: 0,
}
.is_environmental(),
"a platform call failure is an environmental problem"
);
assert!(
!Error::DeviceSignatureInvalid { reason: "mismatch" }.is_environmental(),
"a verification failure is **not** an environmental problem; it is a security event"
);
assert!(
!Error::DevicePublicKeyInvalid {
reason: "off-curve point"
}
.is_environmental(),
"an invalid public key is not an environmental problem"
);
}
#[test]
fn the_two_invalid_public_key_cases_are_distinct() {
let from_peer = Error::DevicePublicKeyInvalid {
reason: "the point is not on the P-256 curve",
};
let from_hardware = Error::InvalidPublicKey {
reason: "the length is not 65 bytes",
};
assert!(
!from_peer.is_environmental(),
"an invalid public key from a peer is a normal rejection path, not an environmental problem"
);
assert!(
!from_hardware.is_environmental(),
"an invalid public key from hardware is an internal inconsistency, not an environmental problem"
);
assert_ne!(from_peer.to_string(), from_hardware.to_string());
}
#[test]
fn implements_std_error() {
fn assert_is_std_error<E: std::error::Error>() {}
assert_is_std_error::<Error>();
}
#[test]
fn master_key_failures_are_not_environmental() {
let cases = [
Error::InvalidWrappedKey {
reason: "the authentication tag does not match",
code: 0x8009_0027_u32 as i32,
},
Error::InvalidMasterKey {
reason: "the material is not 32 bytes long",
},
Error::KeyNotExportable {
reason: "the key store refuses to export the private key",
},
];
for error in &cases {
assert!(
!error.is_environmental(),
"{error:?} must not be classified as environmental"
);
assert!(
!error.is_missing_hardware(),
"{error:?} must not be classified as missing hardware"
);
}
}
#[test]
fn invalid_wrapped_key_shows_the_native_code() {
let text = Error::InvalidWrappedKey {
reason: "the platform rejected the bytes",
code: 0x8009_0027_u32 as i32,
}
.to_string();
assert!(text.contains("80090027"), "actual: {text}");
}
#[test]
fn display_includes_diagnostic_numbers() {
let cases: [(Error, &str); 3] = [
(
Error::Hardware {
operation: "NCryptOpenKey",
code: 0x8009_0016_u32 as i32,
},
"0x80090016",
),
(Error::UnsupportedPlatform { platform: "linux" }, "linux"),
(
Error::KeyNotFound {
label: "device".into(),
},
"device",
),
];
for (error, needle) in cases {
let text = error.to_string();
assert!(
text.contains(needle),
"error text {text:?} does not contain {needle:?}"
);
}
}
}