Skip to main content

Crate qualia_dik

Crate qualia_dik 

Source
Expand description

Device identity kit: the public-key side of a device identity, plus the private-key side that drives the platform-native key store.

§Scope

ModuleResponsibilityHolds private keys
devicePublic-key representation, fingerprint (DeviceId), signature verificationNo
keystoreKey generation, public-key retrieval, message signing, master key storageYes (in platform hardware)

Both halves share the single error type Error: the caller sees the failure of one device-identity operation, not the failure of two subsystems.

This crate is the only place in the project where unsafe is allowed, and the only place that calls platform-native APIs (Windows CNG, Android Keystore, iOS Security framework). Every unsafe operation must be written explicitly inside an unsafe {} block; #![deny(unsafe_op_in_unsafe_fn)] enforces this.

Representation and verification live in the same crate as generation and signing so that a device can verify another device’s signature even if it holds no hardware key of its own — a desktop machine without a TPM, for example.

§A standalone library

Any project that needs a P-256 identity key pair held in platform hardware can use this crate without accepting the Qualia2FA vault format, the sync protocol, or anything else. Downstream crates depend on this one, never the reverse: protocol logic, cryptographic primitives, and anything else that can be written in pure Rust and is unrelated to device identity belongs downstream.

§Both halves of the platform key store

The platform key store holds two things, and this crate exposes one contract for each:

WhatContractWhere the key material lives
The device identityDeviceKeyStore / DeviceSignerin the platform, and never leaves it
The application’s master keyDirectMasterKeyStore / WrappedMasterKeyStoresee below

The master key is MASTER_KEY_LEN bytes of opaque material in MasterKey, and there are two ways to hold it, chosen by the caller through the type it reaches for:

  • Direct (DirectMasterKeyStore): the platform store keeps the material itself and hands it back on request. The caller persists nothing.
  • Wrapped (WrappedMasterKeyStore): the platform store creates a non-exportable key-encryption key and wraps the material with it; only the wrapped bytes (WrappedMasterKey) are persisted, and the caller keeps them next to the data they open. The key-encryption key and its operations are not part of the interface.

Either way the store reports what actually answered through backend(), with the same rule as everywhere else in this crate: software is reported as keystore::Backend::Software, never dressed up as hardware. A platform that implements neither form returns Error::UnsupportedPlatform.

§Signature algorithm: ECDSA P-256

The algorithm is a hard constraint rather than a preference, because the private key has to live in platform hardware:

  • Android: KeyMint HAL’s Algorithm.aidl lists only RSA=1, EC=3, AES=32, TRIPLE_DES=33, HMAC=128 — there is no EdDSA, and KeyProperties.java defines no Ed25519 constant. CURVE_25519 exists, but it is an XDH key-agreement curve, not a signature algorithm.
  • iOS: Apple documents the Secure Enclave as NIST P-256 only.

P-256/ECDSA is therefore the only signature algorithm that can be held in hardware on both mobile platforms; any other choice restricts mobile identity keys to software storage.

§Platform backends

PlatformBackendStatus
WindowsCNG NCrypt*, TPM 2.0 (MS_PLATFORM_CRYPTO_PROVIDER)Implemented
Windows (no TPM)CNG software KSP (MS_KEY_STORAGE_PROVIDER)Implemented (fallback)
macOSSecure EnclaveNot implemented (placeholder backend)
LinuxTPM 2.0Not implemented (placeholder backend)
AndroidKeystore / StrongBoxImplemented
iOSSecure EnclaveNot implemented (placeholder backend)

Unimplemented platforms never degrade silently: calling keystore::DeviceKeyStore::backend yields keystore::Backend::Unavailable, and key creation returns Error::UnsupportedPlatform.

“Not implemented” and “no hardware present” are two different things and have two different values: keystore::Backend::Unavailable means this platform has no backend yet, while keystore::Backend::Software means a backend exists but this machine has no hardware. Merging them would make “switch platform” and “switch machine” indistinguishable to callers.

The two master key forms follow the same rule, with one platform specific detail worth knowing before choosing between them:

PlatformDirectWrapped
Windowssoftware provider (the TPM provider will not hand material back)TPM where there is one, software otherwise
Androidimplemented, reports what the keystore says about the entryimplemented, StrongBox preferred
OthersError::UnsupportedPlatformError::UnsupportedPlatform

§I/O, clock, and environment

The crate performs no I/O, reads no clock, and reads no environment variables; the only exceptions are cfg and the platform APIs themselves. Hardware handles are owned by the key store object and released when it is dropped.

§Example

Generate → read public key → sign → verify, using the in-memory backend:

use qualia_dik::{DeviceKeyStore, device};

let store = MockKeyStore::new();
let signer = store.create("my-device")?;

println!("device id: {}", signer.public_key().device_id());

let message = b"a message to sign";
let signature = signer.sign(message)?;

device::verify(signer.public_key(), message, &signature)?;

Holding the master key, in either form:

use qualia_dik::{
    DirectMasterKeyStore, MasterKey, MockDirectMasterKeyStore, MockWrappedMasterKeyStore,
    WrappedMasterKeyStore,
};

let key = MasterKey::from_bytes([0x2A; qualia_dik::MASTER_KEY_LEN]);

// Direct: the store keeps the material, the caller persists nothing.
let direct = MockDirectMasterKeyStore::new();
direct.store("account-1", &key)?;
let loaded = direct.load("account-1")?;

// Wrapped: the store keeps a key-encryption key, the caller persists these bytes.
let wrapped_store = MockWrappedMasterKeyStore::new();
let wrapped = wrapped_store.store("account-1", &key)?;
let loaded_again = wrapped_store.load("account-1", &wrapped)?;

assert_eq!(loaded.as_bytes(), loaded_again.as_bytes());

The public key is not a secret and may be propagated freely. Verification needs neither the key store nor the private key, which is what makes the public-key side usable on its own.

MockKeyStore exists only under the mock feature: it is a path with no hardware protection and is therefore off by default in production builds. To run this example locally:

cargo test --doc --features mock

Without that feature cargo test --doc still passes, because the whole example is compiled out by the cfg.

To obtain the real backend of the current platform without enabling mock, use keystore::PlatformKeyStore.

Re-exports§

pub use device::DEVICE_ID_LEN;
pub use device::DeviceId;
pub use device::DevicePublicKey;
pub use device::DeviceSignature;
pub use keystore::Backend;
pub use keystore::DeviceKeyStore;
pub use keystore::DeviceSigner;
pub use keystore::DirectMasterKeyStore;
pub use keystore::KeyStoreError;
pub use keystore::MASTER_KEY_LEN;
pub use keystore::MasterKey;
pub use keystore::WrappedMasterKey;
pub use keystore::WrappedMasterKeyStore;

Modules§

device
Device identity: the long-term key a device uses to prove to other devices that it is itself.
keystore
Platform key store: create a P-256 key in hardware, read its public key and sign a message with it.

Enums§

Error
Failures that can arise from device identity handling and the platform key store.

Functions§

platform_backend_is_implemented
Whether this platform has an implemented hardware key store backend.

Type Aliases§

Result
Unified result type of this crate.