Skip to main content

qualia_dik/
lib.rs

1// Copyright (C) 2026 Qualia2FA contributors
2//
3// This library is free software; you can redistribute it and/or modify it under
4// the terms of the GNU Lesser General Public License as published by the Free
5// Software Foundation; either version 3 of the License, or (at your option) any
6// later version.
7//
8// This library is distributed in the hope that it will be useful, but WITHOUT
9// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
11// details.
12//
13// You should have received a copy of the GNU Lesser General Public License
14// along with this library; if not, see <https://www.gnu.org/licenses/>.
15
16//! Device identity kit: the **public-key side** of a device identity, plus the
17//! **private-key side** that drives the platform-native key store.
18//!
19//! # Scope
20//!
21//! | Module | Responsibility | Holds private keys |
22//! |------|--------|----------|
23//! | [`device`] | Public-key representation, fingerprint ([`DeviceId`]), signature verification | No |
24//! | [`keystore`] | Key generation, public-key retrieval, message signing, master key storage | Yes (in platform hardware) |
25//!
26//! Both halves share the single error type [`Error`]: the caller sees the failure of one
27//! device-identity operation, not the failure of two subsystems.
28//!
29//! This crate is the only place in the project where `unsafe` is allowed, and the only
30//! place that calls platform-native APIs (Windows CNG, Android Keystore, iOS Security
31//! framework). Every `unsafe` operation must be written explicitly inside an `unsafe {}`
32//! block; `#![deny(unsafe_op_in_unsafe_fn)]` enforces this.
33//!
34//! Representation and verification live in the same crate as generation and signing so that
35//! a device can verify **another** device's signature even if it holds no hardware key of
36//! its own — a desktop machine without a TPM, for example.
37//!
38//! # A standalone library
39//!
40//! Any project that needs a P-256 identity key pair held in platform hardware can use this
41//! crate without accepting the Qualia2FA vault format, the sync protocol, or anything else.
42//! Downstream crates depend on this one, never the reverse: protocol logic, cryptographic
43//! primitives, and anything else that can be written in pure Rust and is unrelated to device
44//! identity belongs downstream.
45//!
46//! # Both halves of the platform key store
47//!
48//! The platform key store holds two things, and this crate exposes one contract for each:
49//!
50//! | What | Contract | Where the key material lives |
51//! |---|---|---|
52//! | The **device identity** | [`DeviceKeyStore`] / [`DeviceSigner`] | in the platform, and never leaves it |
53//! | The **application's master key** | [`DirectMasterKeyStore`] / [`WrappedMasterKeyStore`] | see below |
54//!
55//! The master key is [`MASTER_KEY_LEN`] bytes of opaque material in [`MasterKey`], and there
56//! are **two ways to hold it**, chosen by the caller through the type it reaches for:
57//!
58//! - **Direct** ([`DirectMasterKeyStore`]): the platform store keeps the material itself and
59//!   hands it back on request. The caller persists nothing.
60//! - **Wrapped** ([`WrappedMasterKeyStore`]): the platform store creates a **non-exportable**
61//!   key-encryption key and wraps the material with it; only the wrapped bytes
62//!   ([`WrappedMasterKey`]) are persisted, and the caller keeps them next to the data they
63//!   open. The key-encryption key and its operations are **not** part of the interface.
64//!
65//! Either way the store reports what actually answered through `backend()`, with the same
66//! rule as everywhere else in this crate: software is reported as
67//! [`keystore::Backend::Software`], never dressed up as hardware. A platform that implements
68//! neither form returns [`Error::UnsupportedPlatform`].
69//!
70//! # Signature algorithm: ECDSA P-256
71//!
72//! The algorithm is a hard constraint rather than a preference, because the private key has
73//! to live in platform hardware:
74//!
75//! - **Android**: KeyMint HAL's `Algorithm.aidl` lists only `RSA=1, EC=3, AES=32,
76//!   TRIPLE_DES=33, HMAC=128` — there is no EdDSA, and `KeyProperties.java` defines no
77//!   Ed25519 constant. `CURVE_25519` exists, but it is an **XDH key-agreement curve, not a
78//!   signature algorithm**.
79//! - **iOS**: Apple documents the Secure Enclave as NIST P-256 only.
80//!
81//! P-256/ECDSA is therefore the only signature algorithm that can be held in hardware on
82//! both mobile platforms; any other choice restricts mobile identity keys to software
83//! storage.
84//!
85//! # Platform backends
86//!
87//! | Platform | Backend | Status |
88//! |---|---|---|
89//! | Windows | CNG `NCrypt*`, TPM 2.0 (`MS_PLATFORM_CRYPTO_PROVIDER`) | Implemented |
90//! | Windows (no TPM) | CNG software KSP (`MS_KEY_STORAGE_PROVIDER`) | Implemented (fallback) |
91//! | macOS | Secure Enclave | Not implemented (placeholder backend) |
92//! | Linux | TPM 2.0 | Not implemented (placeholder backend) |
93//! | Android | Keystore / StrongBox | Implemented |
94//! | iOS | Secure Enclave | Not implemented (placeholder backend) |
95//!
96//! Unimplemented platforms **never degrade silently**: calling
97//! [`keystore::DeviceKeyStore::backend`] yields [`keystore::Backend::Unavailable`], and key
98//! creation returns [`Error::UnsupportedPlatform`].
99//!
100//! "Not implemented" and "no hardware present" are two different things and have two
101//! different values: [`keystore::Backend::Unavailable`] means this platform has no backend
102//! yet, while [`keystore::Backend::Software`] means a backend exists but this machine has no
103//! hardware. Merging them would make "switch platform" and "switch machine"
104//! indistinguishable to callers.
105//!
106//! The two master key forms follow the same rule, with one platform specific detail worth
107//! knowing before choosing between them:
108//!
109//! | Platform | Direct | Wrapped |
110//! |---|---|---|
111//! | Windows | software provider (the TPM provider will not hand material back) | TPM where there is one, software otherwise |
112//! | Android | implemented, reports what the keystore says about the entry | implemented, StrongBox preferred |
113//! | Others | [`Error::UnsupportedPlatform`] | [`Error::UnsupportedPlatform`] |
114//!
115//! # I/O, clock, and environment
116//!
117//! The crate performs no I/O, reads no clock, and reads no environment variables; the only
118//! exceptions are `cfg` and the platform APIs themselves. Hardware handles are owned by the
119//! key store object and released when it is dropped.
120//!
121//! # Example
122//!
123//! Generate → read public key → sign → verify, using the in-memory backend:
124//!
125//! ```
126//! # #[cfg(feature = "mock")]
127//! # fn demo() -> Result<(), qualia_dik::Error> {
128//! use qualia_dik::{DeviceKeyStore, device};
129//! # use qualia_dik::MockKeyStore;
130//!
131//! let store = MockKeyStore::new();
132//! let signer = store.create("my-device")?;
133//!
134//! println!("device id: {}", signer.public_key().device_id());
135//!
136//! let message = b"a message to sign";
137//! let signature = signer.sign(message)?;
138//!
139//! device::verify(signer.public_key(), message, &signature)?;
140//! # Ok(())
141//! # }
142//! ```
143//!
144//! Holding the master key, in either form:
145//!
146//! ```
147//! # #[cfg(feature = "mock")]
148//! # fn demo() -> Result<(), qualia_dik::Error> {
149//! use qualia_dik::{
150//!     DirectMasterKeyStore, MasterKey, MockDirectMasterKeyStore, MockWrappedMasterKeyStore,
151//!     WrappedMasterKeyStore,
152//! };
153//!
154//! let key = MasterKey::from_bytes([0x2A; qualia_dik::MASTER_KEY_LEN]);
155//!
156//! // Direct: the store keeps the material, the caller persists nothing.
157//! let direct = MockDirectMasterKeyStore::new();
158//! direct.store("account-1", &key)?;
159//! let loaded = direct.load("account-1")?;
160//!
161//! // Wrapped: the store keeps a key-encryption key, the caller persists these bytes.
162//! let wrapped_store = MockWrappedMasterKeyStore::new();
163//! let wrapped = wrapped_store.store("account-1", &key)?;
164//! let loaded_again = wrapped_store.load("account-1", &wrapped)?;
165//!
166//! assert_eq!(loaded.as_bytes(), loaded_again.as_bytes());
167//! # Ok(())
168//! # }
169//! ```
170//!
171//! The public key is not a secret and may be propagated freely. Verification needs neither
172//! the key store nor the private key, which is what makes the public-key side usable on its
173//! own.
174//!
175//! `MockKeyStore` exists only under the `mock` feature: it is a path with no hardware
176//! protection and is therefore off by default in production builds. To run this example
177//! locally:
178//!
179//! ```console
180//! cargo test --doc --features mock
181//! ```
182//!
183//! Without that feature `cargo test --doc` still passes, because the whole example is
184//! compiled out by the `cfg`.
185//!
186//! To obtain the real backend of the current platform **without** enabling `mock`, use
187//! [`keystore::PlatformKeyStore`].
188
189#![warn(missing_docs)]
190#![warn(clippy::missing_docs_in_private_items)]
191#![deny(unsafe_op_in_unsafe_fn)]
192
193pub mod device;
194mod error;
195pub mod keystore;
196
197pub use device::{DEVICE_ID_LEN, DeviceId, DevicePublicKey, DeviceSignature};
198pub use error::{Error, Result};
199
200pub use keystore::{
201    Backend, DeviceKeyStore, DeviceSigner, DirectMasterKeyStore, KeyStoreError, MASTER_KEY_LEN,
202    MasterKey, WrappedMasterKey, WrappedMasterKeyStore,
203};
204
205#[cfg(any(test, feature = "mock"))]
206pub use keystore::{MockDirectMasterKeyStore, MockKeyStore, MockWrappedMasterKeyStore};
207
208/// Whether this platform has an implemented hardware key store backend.
209///
210/// Callers (UI in particular) can use this to tell the user that without hardware
211/// protection the device identity key is held in software, which lowers its security.
212/// **This function does not probe whether hardware is actually usable** — that would require
213/// opening a key store; it answers "does this platform have an implementation".
214///
215/// For the runtime situation, use [`DeviceKeyStore::backend`].
216///
217/// Windows and Android return `true` today: those are the platforms
218/// [`keystore::PlatformKeyStore`] has a backend for. On every other platform the function
219/// returns `false`, and every operation of [`keystore::PlatformKeyStore`] returns
220/// [`Error::UnsupportedPlatform`] — there is **no** fallback to a software key.
221#[must_use]
222pub const fn platform_backend_is_implemented() -> bool {
223    cfg!(any(target_os = "windows", target_os = "android"))
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    /// Pins the answer on the platform this test is compiled on.
231    #[test]
232    fn platform_backend_is_implemented_is_pinned_per_platform() {
233        let implemented = platform_backend_is_implemented();
234
235        #[cfg(target_os = "windows")]
236        assert!(implemented, "Windows has a CNG backend");
237        #[cfg(target_os = "android")]
238        assert!(implemented, "Android has a Keystore backend");
239        #[cfg(not(any(target_os = "windows", target_os = "android")))]
240        assert!(!implemented, "this platform has no key store backend");
241    }
242
243    /// Ties the predicate to the backend `PlatformKeyStore` actually resolves to, so that
244    /// pointing `PlatformKeyStore` at a new backend without revisiting the predicate fails
245    /// here instead of answering `false` on a platform that has one.
246    #[test]
247    fn platform_backend_is_implemented_agrees_with_the_selected_backend() {
248        let selected = core::any::type_name::<keystore::PlatformKeyStore>();
249        let wired_up = !selected.contains("platform_stub");
250
251        assert_eq!(
252            platform_backend_is_implemented(),
253            wired_up,
254            "PlatformKeyStore resolves to {selected}"
255        );
256    }
257}