Skip to main content

coven_crypto/custody/
error.rs

1//! Custody failures, retaining causes without printing secret-bearing buffers.
2
3use zeroize::Zeroize;
4
5/// A failure unlocking, keeping or forgetting keys or host secrets (E11).
6#[derive(Debug, thiserror::Error)]
7pub enum KeyError {
8    /// The device-only installation id is malformed.
9    #[error("invalid device identity entry")]
10    DeviceIdEncoding,
11    /// The handle that owns this custody has closed.
12    #[error("store is closed")]
13    StoreClosed,
14    /// A filesystem operation failed; the cause says whether bytes changed.
15    #[error("custody file failed: {0}")]
16    File(coven_foundation::files::FileError),
17    /// A cryptographic service was unavailable or stored bytes failed validation.
18    #[error("custody cryptography failed: {0}")]
19    Crypto(#[from] crate::CryptoError),
20    /// Decrypted or keychain-provided material was malformed.
21    #[error("invalid custody material: {0}")]
22    Material(#[from] crate::MaterialError),
23    /// A wrong passphrase or altered file failed authentication.
24    #[error("wrong passphrase or damaged custody file")]
25    PassphraseAuthentication,
26    /// The passphrase file's header was malformed or unsupported.
27    #[error("invalid passphrase file header")]
28    PassphraseHeader,
29    /// Recorded Argon2id parameters exceed the accepted resource bounds.
30    #[error("Argon2id parameters are outside the accepted bounds")]
31    PassphraseParameters,
32    /// The device lacks resources needed to unlock or keep keys.
33    #[error("key custody resources unavailable: {0}")]
34    Unavailable(#[source] Box<dyn std::error::Error + Send + Sync>),
35    /// The OS credential store refused an operation.
36    #[error("keychain failed: {0}")]
37    Keychain(#[source] KeychainError),
38    /// Custody cannot be accessed right now, but the same call succeeds once
39    /// the session unlocks: native NoStorageAccess, or on Apple
40    /// errSecInteractionNotAllowed (OSStatus -25308), which a locked device,
41    /// a sleeping display or a session that can't show UI answers (§12.4).
42    /// The native cause says why access was denied.
43    #[error("keychain is unavailable in this session: {0}")]
44    KeychainTemporarilyUnavailable(#[source] KeychainError),
45    /// Apple's errSecMissingEntitlement (OSStatus -34018): the app has no
46    /// keychain-access-groups entitlement backed by a provisioning profile.
47    /// Every keychain call fails the same way until the app is signed with
48    /// one; retrying changes nothing.
49    #[error(
50        "the app lacks its keychain-access-groups entitlement \
51         (errSecMissingEntitlement, OSStatus -34018): {0}"
52    )]
53    MissingKeychainEntitlement(#[source] KeychainError),
54    /// The service name is empty or contains a NUL.
55    #[error("keyring service requires a nonempty name without NUL")]
56    InvalidServiceName,
57    /// The person's entries need iCloud Keychain or Block Store: Apple
58    /// platforms, or an Android phone with Google Play services.
59    #[error("saved restore codes need iCloud Keychain or Block Store")]
60    Unsupported,
61    /// Block Store already holds its 16 entries for the app, so a new entry
62    /// can't be saved (§13.1). Forgetting a saved code makes room.
63    #[error("Block Store already holds its 16 entries for this app")]
64    SavedEntriesFull,
65    /// A saved code doesn't fit in Block Store's 1,024 bytes for an entry,
66    /// even with the store's name shortened (§13.1).
67    #[error("the saved restore code doesn't fit in a Block Store entry")]
68    SavedEntryTooLarge,
69    /// The person left the store, so their saved entries stay deleted (§14.1).
70    #[error("the person left the store; their saved entries stay deleted")]
71    Left,
72    /// A saved restore-code account's id part is not a store id as coven
73    /// writes it.
74    #[error("invalid store id in saved restore-code entry")]
75    RestoreCodeStoreId,
76    /// The call's runtime is shutting down and dropped it partway, so what
77    /// it changed in custody is unknown (E15).
78    #[error(transparent)]
79    Shutdown(#[from] coven_foundation::task::Shutdown),
80    /// The call's custody work panicked: a bug, which the caller reports
81    /// (E15). What it changed in custody is unknown.
82    #[error(transparent)]
83    Bug(#[from] coven_foundation::task::Bug),
84}
85
86impl coven_foundation::task::CarriesBug for KeyError {
87    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
88        match self {
89            Self::Bug(bug) => Some(bug),
90            Self::File(error) => error.bug(),
91            _ => None,
92        }
93    }
94}
95
96impl From<coven_foundation::files::FileError> for KeyError {
97    fn from(error: coven_foundation::files::FileError) -> Self {
98        match error {
99            coven_foundation::files::FileError::Bug(bug) => Self::Bug(bug),
100            error => Self::File(error),
101        }
102    }
103}
104
105impl From<coven_foundation::task::TaskFailed> for KeyError {
106    fn from(failed: coven_foundation::task::TaskFailed) -> Self {
107        failed.into_error()
108    }
109}
110
111/// A native keychain error whose diagnostic output never includes secret bytes.
112/// Its original typed cause remains available through `std::error::Error`.
113pub struct KeychainError(keyring_core::Error);
114
115/// errSecInteractionNotAllowed: the device or session is locked.
116#[cfg(any(target_os = "macos", target_os = "ios"))]
117const ERR_SEC_INTERACTION_NOT_ALLOWED: i32 = -25308;
118/// errSecMissingEntitlement: no keychain-access-groups entitlement.
119#[cfg(any(target_os = "macos", target_os = "ios"))]
120const ERR_SEC_MISSING_ENTITLEMENT: i32 = -34018;
121
122impl From<KeychainError> for KeyError {
123    fn from(error: KeychainError) -> Self {
124        #[cfg(any(target_os = "macos", target_os = "ios"))]
125        match error.apple_status() {
126            Some(ERR_SEC_INTERACTION_NOT_ALLOWED) => {
127                return Self::KeychainTemporarilyUnavailable(error)
128            }
129            Some(ERR_SEC_MISSING_ENTITLEMENT) => return Self::MissingKeychainEntitlement(error),
130            _ => {}
131        }
132        match &error.0 {
133            keyring_core::Error::NoStorageAccess(_) => Self::KeychainTemporarilyUnavailable(error),
134            _ => Self::Keychain(error),
135        }
136    }
137}
138
139impl KeychainError {
140    /// The OSStatus behind an Apple keychain failure. apple-native-keyring-store
141    /// gives its own variant only to NoStorageAccess (-25291, -25292) and
142    /// NoEntry (-25300); every other status, the locked keychain's -25308
143    /// included, arrives as a PlatformFailure boxing security-framework's
144    /// error, whose code is the status.
145    #[cfg(any(target_os = "macos", target_os = "ios"))]
146    fn apple_status(&self) -> Option<i32> {
147        let keyring_core::Error::PlatformFailure(cause) = &self.0 else {
148            return None;
149        };
150        cause.downcast_ref::<security_framework::base::Error>().map(|status| status.code())
151    }
152}
153
154impl From<keyring_core::Error> for KeychainError {
155    fn from(mut error: keyring_core::Error) -> Self {
156        // Native error variants may carry the secret they failed to decode.
157        // Preserve the error kind and cause, but erase those bytes immediately.
158        match &mut error {
159            keyring_core::Error::BadEncoding(bytes)
160            | keyring_core::Error::BadDataFormat(bytes, _) => bytes.zeroize(),
161            _ => {}
162        }
163        Self(error)
164    }
165}
166
167impl std::fmt::Debug for KeychainError {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        // A status code carries no secret, and names the refusal.
170        #[cfg(any(target_os = "macos", target_os = "ios"))]
171        if let Some(status) = self.apple_status() {
172            return write!(f, "PlatformFailure(OSStatus {status})");
173        }
174        let kind = match &self.0 {
175            keyring_core::Error::PlatformFailure(_) => "PlatformFailure",
176            keyring_core::Error::NoStorageAccess(_) => "NoStorageAccess",
177            keyring_core::Error::NoEntry => "NoEntry",
178            keyring_core::Error::BadEncoding(_) => "BadEncoding",
179            keyring_core::Error::BadDataFormat(_, _) => "BadDataFormat",
180            keyring_core::Error::BadStoreFormat(_) => "BadStoreFormat",
181            keyring_core::Error::TooLong(_, _) => "TooLong",
182            keyring_core::Error::Invalid(_, _) => "Invalid",
183            keyring_core::Error::Ambiguous(_) => "Ambiguous",
184            keyring_core::Error::NoDefaultStore => "NoDefaultStore",
185            keyring_core::Error::NotSupportedByStore(_) => "NotSupportedByStore",
186            _ => "NativeError",
187        };
188        f.write_str(kind)
189    }
190}
191
192impl std::fmt::Display for KeychainError {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        std::fmt::Debug::fmt(self, f)
195    }
196}
197
198impl std::error::Error for KeychainError {
199    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
200        Some(&self.0)
201    }
202}
203
204#[cfg(test)]
205#[path = "error_tests.rs"]
206mod tests;