#[non_exhaustive]pub enum Error {
Show 126 variants
Device(Error),
Base32InvalidCharacter {
index: usize,
},
Base32InvalidLength {
length: usize,
},
Base32InvalidPadding,
SecretEmpty,
SecretTooShort {
required: usize,
actual: usize,
},
UnsupportedAlgorithm {
name: String,
},
UnsupportedDigits {
value: String,
},
DigitsZero,
DigitsTooMany {
count: u8,
},
InvalidPeriod,
PinRequired {
otp_type: &'static str,
},
YandexSecretLength {
len: usize,
},
YandexSecretChecksum,
RandomnessUnavailable,
UriUnsupportedScheme,
UriMissingType,
UriUnsupportedType {
name: String,
},
UriParameterNotSupported {
parameter: &'static str,
uri_type: &'static str,
},
YandexAlgorithmUnsupported {
algorithm: &'static str,
},
SteamAlgorithmUnsupported {
algorithm: &'static str,
},
SteamDigitsUnsupported {
digits: u8,
},
UriInvalidPin {
reason: &'static str,
},
UriMissingSecret,
UriMissingCounter,
UriUnexpectedCounter,
UriDuplicateParameter {
name: String,
},
UriInvalidParameter {
name: String,
reason: String,
},
UriInvalidPercentEncoding,
UriIssuerMismatch {
in_label: String,
in_parameter: String,
},
UriAccountContainsColon {
account: String,
},
VaultTruncated,
VaultBadMagic,
VaultUnsupportedVersion {
version: u8,
},
VaultReservedByteNotZero {
value: u8,
},
VaultUnknownRecordKind {
value: u8,
},
VaultRecordTooLong {
limit: usize,
actual: usize,
},
VaultTooManyRecords,
VaultTrailingBytes {
count: usize,
},
VaultDuplicateEntryId,
VaultBadKeyCheck,
VaultEntryNotFound,
VaultDuplicateGroupId,
VaultGroupNotFound,
VaultGroupUnknownEntry,
VaultTopGroupImmutable,
VaultGroupTooManyChildren {
limit: usize,
actual: usize,
},
VaultGroupDuplicateChild,
VaultGroupCycle,
VaultGroupDepthExceeded {
limit: usize,
actual: usize,
},
ImportFileTooLarge {
limit: usize,
actual: usize,
},
ImportTooManyEntries {
limit: usize,
actual: usize,
},
ImportTooDeeplyNested {
limit: usize,
},
ImportMalformed {
reason: &'static str,
},
ImportUnsupportedVersion {
version: u64,
supported: u64,
},
ImportPasswordRequired,
ImportEncryptionUnsupported {
reason: &'static str,
},
ImportWrongPassword,
ImportCorrupted,
ImportInvalidKdfParameters {
reason: &'static str,
},
ExportPasswordRequired,
ExportPasswordEmpty,
ExportQrTooLong {
position: usize,
},
ExportTooLarge,
ExportParameterRefused {
parameter: &'static str,
},
PortableBadMagic,
PortableUnsupportedVersion {
version: u8,
},
PortableTruncated,
PortableLengthMismatch,
PortableTooLarge {
limit: usize,
actual: usize,
},
PortableVaultFileWithoutKey,
PortablePasswordRequired,
VaultDuplicateField {
tag: u8,
},
VaultCorrupt,
VaultInvalidUtf8,
VaultDecryptionFailed,
MasterKeyWrongLength {
expected: usize,
actual: usize,
},
FieldTooLong {
field: &'static str,
limit: usize,
actual: usize,
},
VaultIo {
reason: String,
},
DevicePublicKeyInvalid {
reason: &'static str,
},
DeviceSignatureInvalid {
reason: &'static str,
},
PairingPayloadMalformed {
reason: &'static str,
},
PairingUnsupportedVersion {
version: u8,
},
PairingAuthenticationFailed,
RecoveryFileMalformed {
reason: &'static str,
},
RecoveryParametersInvalid {
reason: &'static str,
},
RecoveryPassphraseEmpty,
RecoveryAuthenticationFailed,
RecoveryEncryptionFailed,
RecoveryKdfFailed,
LockFileMalformed {
reason: &'static str,
},
LockParametersInvalid {
reason: &'static str,
},
LockPassphraseEmpty,
LockPasswordRejected,
LockEncryptionFailed,
LockKdfFailed,
UnsupportedFormatVersion {
version: u8,
},
AuthMessageMalformed {
reason: &'static str,
},
AuthProofFailed,
DeviceKeyMissing,
DeviceNotRegistered,
DeviceIdentityMismatch,
SecureStoreWriteFailed {
reason: &'static str,
},
TimestampZero,
SyncBadMagic,
SyncUnsupportedVersion {
version: u8,
},
SyncEmptyPayload,
SyncTruncated,
SyncRecordCountMismatch {
declared: usize,
actual: usize,
},
SyncTooManyRecords {
count: usize,
limit: usize,
},
SyncCompressionFailed,
SyncDecompressionFailed,
SyncPayloadTooLarge {
limit: usize,
},
SyncUnknownRecordKind {
value: u8,
},
SyncMalformed {
reason: &'static str,
},
SyncAuthenticationFailed,
SyncIconRequiresByteTransport,
SyncAlreadyApplied,
SyncIdLogFull {
limit: usize,
},
TransportFailed {
reason: &'static str,
},
SyncSenderMismatch,
SyncUnknownPayloadKind {
value: u8,
},
TransportUnsupported {
operation: &'static str,
},
TransportAlreadyUsed,
TransportMessageTooLarge {
needed: usize,
limit: usize,
},
SyncNoSessionFound,
}Expand description
The unified error type of this library.
The enum is #[non_exhaustive], so a match outside this crate must have a wildcard arm,
and adding a variant is not a breaking change for downstream code. Variants are added as
the library grows.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Device(Error)
A failure reported by the device identity crate.
§Why this wraps the foreign error instead of copying its variants
Device identity (public key representation, fingerprints, signature verification and
the platform key store) lives in qualia_dik, an
independently reusable crate. This crate needs the same failure classification, not
a look-alike one of its own.
Copying the variants would let the two enums drift: the other crate gains a
HardwareUnavailable, this one forgets it, and “no TPM” reaches Core callers through a
silent _ => wildcard arm. Wrapping makes the compiler point out every new variant that
needs handling when the dependency is upgraded.
The source() chain also shows that the failure came from the device identity crate,
instead of flattening it into a local reason string.
Base32InvalidCharacter
The Base32 input contains a character outside the alphabet.
Only the position is reported, never the character at that position, so that input content (which may come from a key) does not reach the logs.
Fields
Base32InvalidLength
The Base32 input length (after padding is stripped) is not a valid remainder modulo 8.
The only valid Base32 length remainders are 0, 2, 4, 5 and 7.
Base32InvalidPadding
Corrupt Base32 encoding: the unused bits of the last character are not zero.
This is not a padding-format problem — padding itself may be absent or present (both are accepted). It means the encoded data itself is corrupt, for example significant bits written into padding bits that must be zero.
SecretEmpty
The OTP secret is empty.
Kept separate from Error::SecretTooShort: an empty seed is a structural problem
(the URI has no secret at all, or Base32 decodes to nothing), while a short seed is a
strength problem. The former must be rejected, the latter is a UI warning — merging
them leaves the caller unable to tell the two apart.
SecretTooShort
The OTP secret is shorter than the minimum required by the selected algorithm.
Note: this library currently rejects only empty secrets and never returns this error
for a short one. HMAC accepts keys of any length and short seeds exist in the real world;
a short seed is a warning to be surfaced by the UI, see
otp::recommended_min_secret_len.
Fields
UnsupportedAlgorithm
The algorithm parameter of an otpauth:// URI is not recognized.
Rejected rather than falling back to SHA-1: a silent fallback gives the user a code that is consistently wrong, with nothing in the error to trace it back.
Fields
UnsupportedDigits
The digits parameter of an otpauth:// URI is outside the supported range.
Reported when the value is not a plain decimal digit count in the first place
("", "six", "06", a number too large for a byte). A well-formed number that is
merely out of range is reported by Error::DigitsZero or Error::DigitsTooMany,
which carry the reason.
DigitsZero
A code was requested with zero digits.
A zero-width code is not a code: there would be nothing to display and nothing to
compare, and 10^0 = 1 makes every code identical.
DigitsTooMany
A code was requested with ten or more digits.
The limit comes from the truncation, not from the integer width: RFC 4226 §5.3
extracts a 31-bit value from the HMAC, so at ten digits the reachable code space is
only 2^31 / 10^10 (about 21%) of the range — codes above 2^31 can never appear,
and the ones that can are not uniformly distributed. Widening the integer type does
not help; supporting ten digits would need a different truncation, which no existing
implementation would agree with.
InvalidPeriod
The TOTP period is 0, which would cause a division by zero.
PinRequired
A code was requested from an algorithm that mixes a user-entered PIN into the computation, but no PIN is set.
mOTP and Yandex OTP both fold a PIN into the code. Without one the algorithm has no defined output: the reference implementation raises an error rather than substituting an empty string. This is deliberately an error and not a default — a code computed with an empty PIN looks perfectly plausible and no server will ever accept it, which is the hardest kind of failure for a user to diagnose.
An empty PIN is a different thing and is accepted: it means the user’s PIN is the empty string, which the algorithm handles like any other value.
YandexSecretLength
A Yandex OTP secret is neither 16 nor 26 bytes long.
The scheme has exactly two secret forms: 16 bytes (what a QR code carries) and 26 bytes (16 bytes plus a trailing checksum). Any other length is not a Yandex secret, so it is rejected instead of being truncated to something plausible.
YandexSecretChecksum
The trailing checksum of a 26-byte Yandex OTP secret does not match its contents.
The checksum is a polynomial remainder over the secret (see yaotp::normalize_secret),
and it is a data-integrity check, not a cryptographic one: it exists so that a
mistyped or truncated secret is reported as damaged at import time instead of turning
into codes that no server will ever accept. It carries no information about the secret
itself.
Cryptographic random bytes cannot be obtained from the operating system.
This is an unrecoverable failure: the library deliberately uses no user-space PRNG as a fallback, because a deterministic seed makes independently generated keys collide. Failing the operation and letting the user retry is preferable to silently switching to a weak random source.
Triggered when the getrandom backend is unavailable on the target platform (a
restricted sandbox, a missing syscall, or a kernel entropy source that failed to
initialize).
It carries no getrandom::Error detail: that type’s internal representation differs
across versions and platforms and offers the caller nothing actionable — retrying or
telling the user is all that can be done.
UriUnsupportedScheme
The URI scheme is not one this library understands.
UriMissingType
The URI has no type component (no / after the scheme).
UriUnsupportedType
The URI type is not one of the supported schemes.
UriParameterNotSupported
A parameter that has no meaning for this type is present.
Rejected rather than ignored, for the reason Error::UriUnexpectedCounter gives: a
parameter that is silently dropped is a configuration the user believes in and that the
code does not implement.
Fields
YandexAlgorithmUnsupported
A yaotp URI asked for an algorithm other than SHA-256.
Yandex OTP truncates an eight-byte window of the digest, so a shorter digest (SHA-1 has 20 bytes and the window starts as late as byte 15) would read past its end. The scheme is defined for SHA-256 only; honouring the parameter is impossible and ignoring it would compute codes that never match.
SteamAlgorithmUnsupported
A steam URI asked for an algorithm other than SHA-1.
SteamDigitsUnsupported
A steam URI asked for a code length other than five.
UriInvalidPin
A pin parameter is not usable.
The pin travels Base32-encoded (so that any character a user may choose survives the query string), and the decoded bytes must be UTF-8.
UriMissingSecret
The URI has no secret parameter.
UriMissingCounter
The HOTP URI has no counter parameter.
UriUnexpectedCounter
A TOTP URI carries a counter parameter.
Rejected rather than ignored: such a URI signals a faulty generator, and ignoring it hands the user an entry that looks normal but means something else.
UriDuplicateParameter
The URI contains a known parameter more than once.
Implementations disagree on whether the first or the last occurrence wins: a classic parsing ambiguity, and an attack surface.
UriInvalidParameter
A URI parameter value is invalid.
Fields
UriInvalidPercentEncoding
The percent encoding in the URI is malformed.
UriIssuerMismatch
The issuer in the label and the issuer parameter disagree.
Rejected: two different issuers mean the URI was assembled or tampered with, and silently picking one would bind an entry’s codes to the wrong service.
Fields
UriAccountContainsColon
The account name in the URI label contains :.
The label uses : to separate issuer from account, so a : inside the account name
cannot be told apart from the separator and creates round-trip ambiguity: after a
serialize-then-parse cycle, the ownership of issuer and account changes silently.
Percent encoding does not help — %3A and a literal : are indistinguishable once
decoded.
Both parsing and serialization therefore reject it, enforcing the invariant.
VaultTruncated
The vault file is shorter than its declared structure, or the structure is cut off here.
The most common structural problem: the file was truncated, or a length field claims more bytes than actually remain.
VaultBadMagic
The magic number at the start of the vault file does not match.
It means this is not a vault file (the wrong file was picked, or the file was replaced with something else).
VaultUnsupportedVersion
The vault file format version is not supported by this build.
VaultReservedByteNotZero
The reserved byte in the vault file header is not 0.
Reserved bytes exist precisely so that they may become non-zero later. The current version must reject a non-zero value: it means the file was written by a newer version that may have given the byte a meaning this version does not know, and reading on would parse it under wrong assumptions.
VaultUnknownRecordKind
The record kind byte in the vault file is not recognized.
Not treated as a live entry: guessing that an unknown kind “is probably an entry” would let a tombstone written by a future version come back to life as an entry here.
VaultRecordTooLong
A single record exceeds the length limit.
The limit exists to reject a malicious file claiming to hold 4 GiB and to avoid allocating huge buffers for untrusted input. This is therefore a structural problem, not the business problem of “an entry that is too large”.
VaultTooManyRecords
The vault holds more records than the limit allows.
VaultTrailingBytes
The vault file has extra bytes at the end.
Rejected rather than ignored: extra bytes mean content was appended and this version cannot explain what it is. Ignoring them would make the fact that the file was modified disappear.
VaultDuplicateEntryId
The same entry_id appears twice in the vault file.
Rejected rather than overwritten: a duplicate ID means the file was crafted or corrupted. Silently keeping one of them makes the user lose a 2FA entry, showing up as “one code suddenly stopped working” — extremely hard to trace.
VaultBadKeyCheck
The master key check value in the file has the wrong structure (length mismatch).
Strictly separated from Error::VaultDecryptionFailed: this one is a structural
problem, that one means a wrong key or a modified file. Merging the two reports a
crafted file as “your key is wrong”, sending the user to compare keys when the problem is
somewhere else entirely.
The check value is a fixed-length plaintext field, so a length mismatch means the file was truncated, appended to, or never written in the format at all — all three mean “this file is untrustworthy”, not “wrong key”.
VaultEntryNotFound
No entry with this identifier exists.
Different from “the vault is empty”: the caller named one specific entry and it is not there. The layer above must tell two UI actions apart — “the list is empty, invite the user to add an entry” versus “the entry you were working on is gone, refresh the list”. Treating the latter as the former shows an empty list while the user was just looking at the entry.
A common cause is synchronization having deleted the entry on the other device while this one still holds its identifier.
VaultDuplicateGroupId
A vault file holds two group records with the same identifier.
Refused rather than treated as an overwrite, for the same reason as
Error::VaultDuplicateEntryId: one of the two orderings would be dropped silently, and
an ordering that disappears takes the user’s arrangement with it.
VaultGroupNotFound
No group with that identifier exists.
VaultGroupUnknownEntry
An ordering names an entry the vault does not hold.
Rejected when setting an ordering: the caller passes identifiers, and one that names nothing means the caller is out of step with the vault. When reading a file, a reference to a missing entry is skipped instead - there the caller is a peer device and the entries may simply not have arrived yet.
VaultTopGroupImmutable
The top group cannot be renamed, removed or reordered.
It is the collection of the entries that belong to no named group: it is pinned at the top of the interface and has no name to edit, so every operation that would change what it is is refused rather than silently ignored. Its contents (the ordering) remain editable, which is the whole point of it being a group.
VaultGroupTooManyChildren
One group holds more child references than the format can carry.
The list travels in a single length-prefixed node, so it is bounded by
Error::FieldTooLong’s limit divided by the size of one child reference.
VaultGroupDuplicateChild
One child holds two positions in the same ordering.
A list is a sequence: the same entry or group cannot come both before and after another one. Two different groups holding the same child is a different question, and the vault answers it by moving the child instead of refusing.
VaultGroupCycle
Moving the children would put a group inside itself.
The tree has no cycles by definition: “the parent of A is B” and “the parent of B is A” cannot both hold. Offering such a list is refused before anything is written, rather than left for a traversal to discover - a cycle makes every walk over the tree either loop forever or need a visited set, and the one place to prevent it is where the list is set.
VaultGroupDepthExceeded
A group would be nested more deeply than the tree allows.
The top group is depth zero. Past the limit the interface has no room to indent a group, so the nesting is refused when a list is written and repaired when a file is read.
Fields
ImportFileTooLarge
The export is larger than this library reads.
Compared before anything is allocated or parsed: the size comes from the file, and a small header claiming gigabytes must not turn into an allocation.
ImportTooManyEntries
The export holds more entries than this library reads.
ImportTooDeeplyNested
The export nests more deeply than this library reads.
Checked by scanning the bytes before they reach the JSON parser: that parser recurses, so a deeply nested document would exhaust the stack, and a stack overflow is not a catchable error - it takes the process down, which is exactly what “no input may cause a panic” forbids.
ImportMalformed
The export is structurally wrong: not the expected format, a missing field, a value of the wrong kind.
ImportUnsupportedVersion
The export declares a version this library does not read.
ImportPasswordRequired
The export is encrypted and this call carried no password.
ImportEncryptionUnsupported
The export is encrypted in a way this reader cannot open.
Distinct from Error::ImportPasswordRequired, which means “give me a password”: here a
password was given and there is no slot it could open - the export is locked to the
platform key store or to a biometric prompt of the device that wrote it.
ImportWrongPassword
The password does not open the encrypted export.
One error for every failing slot: whether the password is wrong or a slot was tampered with is not something the reader can tell apart, and a message that guessed would be wrong half the time.
ImportCorrupted
The export decrypted but its contents do not open.
Distinct from Error::ImportWrongPassword on purpose, because it means something else
to the user: the password was accepted (a slot authenticated), so the database is
damaged or was modified after it was written.
ImportInvalidKdfParameters
The key derivation parameters of an encrypted export are outside the accepted range.
Refused before the derivation runs: those numbers come from the file, they decide how much memory and time the derivation costs, and the reference implementation does not bound them.
ExportPasswordRequired
An encrypted export was asked for without a password.
The caller decides this before calling crate::export::export with
crate::export::ExportFormat::needs_password; reaching this error means the decision
was made wrongly, and the alternative - writing an unencrypted file because no password
arrived - would be a silent downgrade of something the user asked to protect.
ExportPasswordEmpty
The password given for an encrypted export is empty.
Refused rather than accepted: an empty password is one anybody can guess by trying nothing at all, and the file holds every secret in the vault.
ExportQrTooLong
An entry’s URI does not fit a QR code, so the HTML export cannot draw it.
Reported with the entry’s position and nothing else: an error message ends up in a log, and the entry’s name and issuer are the user’s own account data.
ExportTooLarge
The vault is larger than the container of an encrypted export.
AES-256-GCM caps a message at 2^36 - 32 bytes, and the whole database goes into one message. A vault would have to hold tens of gigabytes of entries to reach it.
ExportParameterRefused
A fixed parameter of an export format was refused by the code that implements it.
Unreachable in practice: the parameters are constants of the format, and a test pins that
the implementations accept them. It is an error rather than a panic because this library
does not panic, and rather than an unwrap because an unwrap in a library is a crash
waiting for the one build where the constant was edited.
PortableBadMagic
A portable vault file (.q2fa) does not begin with a magic this library knows.
The three shapes are the plain vault container, the passphrase-protected one, and the old key-only recovery file. A caller that expected a backup and got something else is told so before any password is asked for.
PortableUnsupportedVersion
A portable vault file declares a container version this library does not read.
PortableTruncated
A portable vault file ends before the length its header declares.
Checked before the payload is allocated: the length comes from the file and is attacker-controlled.
PortableLengthMismatch
A portable vault file is longer or shorter than its lengths add up to.
Refused rather than ignored: trailing bytes mean the file was appended to or assembled from parts, and silently reading the first part would hide that.
PortableTooLarge
A portable vault payload is larger than this library accepts.
PortableVaultFileWithoutKey
The file is a bare vault file, which carries ciphertext but no master key.
It looks like a backup and is not one: importing it would replace the local vault with ciphertext the local key probably cannot open. The caller is told so it can say what to do (export a vault backup instead), rather than leaving the user with a vault that cannot be read.
PortablePasswordRequired
A passphrase-protected portable vault was opened without a password.
VaultDuplicateField
The plaintext structure of a vault entry contains a duplicated field node.
Implementations disagree on whether the first or the last occurrence wins: a classic parsing ambiguity, and an attack surface — an attacker can append an overriding node after a legitimate one.
VaultCorrupt
The plaintext structure of a vault entry is corrupt (missing required field, wrong field length, and so on).
Note that this error can only occur after authentication has succeeded — the AEAD tag covers the whole plaintext, so successful decryption means the plaintext is the one that was written. Reaching it means the writing side produced an invalid structure (a bug in this library), or the key comes from an implementation of another version. A parser called directly with arbitrary plaintext can trigger it as well, since such a call bypasses the AEAD.
VaultInvalidUtf8
A text field of a vault entry is not valid UTF-8.
Rejected rather than lossily replaced: replacement turns corrupt data into an entry that looks normal but has different content, and the user never learns that the account name was changed.
VaultDecryptionFailed
Vault decryption or authentication failed.
It does not distinguish a wrong master key, tampered ciphertext and an AAD mismatch, for two reasons:
- Useful to an attacker: telling them apart is an oracle that reveals whether a guessed key is correct, or which part was modified.
- Useless to the user: all three call for exactly the same action — this vault cannot be opened.
Whether the wrong master key was picked is something the layer above decides from where the key came from; the cryptographic layer need not leak anything for that.
MasterKeyWrongLength
The master key has the wrong length.
FieldTooLong
A variable-length field of a vault entry exceeds the limit.
The limit cannot be enforced before decryption from the ciphertext length (that length
says nothing about the field length), so it must be checked while parsing after
decryption. See crate::vault::Entry::MAX_FIELD_LEN.
Fields
VaultIo
Reading or writing a vault file failed.
It carries only the operating system error description, never file content — reason
comes from std::io::Error, which does not include the bytes that were written.
DevicePublicKeyInvalid
A device identity public key is invalid.
Public keys come from external input (QR codes, the network) and must be validated before use. Invalid-curve attacks are real: handing over a point that is not on the curve but lies in a small subgroup can leak private-key information through the peer’s computation. This error is therefore part of the security boundary, not a format check.
reason is a fixed internal description and contains no raw input bytes — they may
come from an attacker, and putting them into an error message turns logs into an injection
point.
DeviceSignatureInvalid
A device identity signature is malformed or fails verification.
A malformed signature and a failed verification deliberately share this variant. The caller’s response is identical (reject this authentication), and telling them apart would only give an attacker a small oracle.
PairingPayloadMalformed
The pairing payload (QR code content) is structurally invalid.
Separate from Error::PairingUnsupportedVersion: a wrong length or a missing required
field is damage this version can judge, whereas an unrecognized version is something
this version cannot read. For the user the advice differs — “regenerate the QR code”
versus “upgrade the app” — so the two are not merged.
reason is a fixed internal description and contains no input bytes: QR code content
may come from an attacker.
PairingUnsupportedVersion
The pairing payload protocol version is not supported.
It must be rejected outright rather than interpreted as best as possible: reading a future version’s fields under this version’s layout would turn a “this operation is forbidden” written by the future version into “allowed”.
PairingAuthenticationFailed
Pairing message authentication failed.
Tampered ciphertext, a wrong tag, a message from another session and a joiner using the wrong public key deliberately share this variant. The caller’s response is identical (this pairing failed, scan the code again), and telling them apart would give an attacker an oracle to probe with.
RecoveryFileMalformed
The recovery file structure is invalid.
Separate from Error::RecoveryAuthenticationFailed because the two mean completely
different things to the user: this variant says “wrong file / broken file”, while that
one says “wrong passphrase”. Merging them makes the user retry the passphrase in a
direction where retrying cannot help.
reason is a fixed internal description and contains no input bytes.
RecoveryParametersInvalid
The derivation parameters of the recovery file are invalid.
These parameters come from the file itself and are therefore untrusted input. Without
this validation, a file edited to say m_cost = u32::MAX would turn an import into a
memory-exhaustion attack.
RecoveryPassphraseEmpty
The passphrase is empty.
Rejected outright rather than accepted as a weak passphrase: accepting it would hand the user, behind a screen that looks successful, a backup that can be opened in about a second.
RecoveryAuthenticationFailed
Authentication of the recovery file failed: wrong passphrase, or a tampered file.
The two causes deliberately share this variant (same reasoning as
Error::PairingAuthenticationFailed).
RecoveryEncryptionFailed
The encryption step of the recovery file failed.
It can only come from an AEAD implementation limit (plaintext or AAD too long) and does not happen with the parameters in use; it is listed separately so that this case is not misreported as a wrong passphrase.
RecoveryKdfFailed
The Argon2id computation failed.
The most common real cause is out of memory: the default parameters allocate 64 MiB at once.
LockFileMalformed
The password-wrapped master key file is structurally invalid.
Separate from Error::LockPasswordRejected: this variant means the bytes are not a
lock wrap at all (wrong length or magic), while that one means the file looked right but
the password did not open it. Merging them would make the user retry the password when
retrying cannot help.
reason is a fixed internal description and contains no input bytes.
LockParametersInvalid
The derivation parameters of a password-wrapped master key are invalid.
These parameters come from the file and are therefore untrusted input. They are checked before Argon2id runs, so a crafted file cannot make the application allocate several terabytes or derive forever.
LockPassphraseEmpty
The unlock password is empty.
An empty password is rejected outright rather than accepted as a weak one: accepting it would present an unlock screen that anybody can pass in a second.
LockPasswordRejected
The password did not open the wrapped master key.
A wrong password and a tampered file deliberately share this variant: telling them apart would give an attacker an oracle for whether a guess was right, and the user’s response is the same in both cases.
LockEncryptionFailed
The encryption step of the password wrap failed.
It can only come from an AEAD implementation limit and does not happen with the lengths in use; it is listed separately so that this case is not misreported as a wrong password.
LockKdfFailed
The Argon2id computation behind a password wrap failed.
The most common real cause is out of memory: the default parameters allocate 64 MiB at once.
UnsupportedFormatVersion
The version of a persisted format is not supported.
Used for data that is kept for a long time, such as recovery files: when a file written by a future version reaches an older one, it must be rejected outright instead of being interpreted under the current layout. The latter would turn “the new version improved security” into “the older version accepted it under a weaker interpretation”.
AuthMessageMalformed
A challenge-response message is structurally invalid.
A wrong length, or a carried public key that is not a valid curve point. This is a
different thing from Error::AuthProofFailed: this variant says the message is not
even well-formed, that one says the message is well-formed but the peer cannot compute the
correct tag.
reason is a fixed internal description and contains no input bytes.
AuthProofFailed
The proof or confirmation tag of the challenge-response does not match.
It means “the peer failed to prove that it holds the master key”. A tampered tag, an inconsistent session identifier, or a peer that has no master key at all — the three causes deliberately share this variant, because the caller’s response is identical (declare this authentication failed and disconnect) and telling them apart would give a probeable signal.
DeviceKeyMissing
This device has no device identity yet, and this step needs one.
Different from “creating the identity failed”: this is a caller asking for a signature before obtaining an identity — a call-order error, not a platform capability problem.
DeviceNotRegistered
This device has never registered or logged in.
Deliberately separate from “a master key exists but cannot be read”: the former is the normal path (send the user to register or log in), the latter means data may already be damaged. Sharing one error would make the UI treat the latter as “not logged in yet” and send the user to register again, permanently losing the existing data.
DeviceIdentityMismatch
The declared device identity is not the local one.
SecureStoreWriteFailed
Writing the master key to platform storage failed.
reason is a fixed string and must not contain key material: it is logged, and this is
one of the very few paths that touch the master key.
TimestampZero
The timestamp is 0.
0 usually means the caller forgot to pass a time (this library never reads a clock; time
always comes from the caller). Letting such a value into the vault pollutes the
time-ordered merge logic, and the result is consistently wrong and hard to trace.
SyncBadMagic
The magic number of the sync payload does not match.
Separate from “the payload is corrupt”: a bad magic usually means this is not a sync payload at all (the user picked the wrong file, or another protocol’s data was fed in), which points the investigation in a completely different direction than “a payload produced by this application is broken”.
SyncUnsupportedVersion
The sync payload format version is not supported.
SyncEmptyPayload
The payload is empty.
Reported whether it is discovered in the compression layer or in the outer envelope. “There is no payload at all” and “the payload is broken” point in completely different directions when investigating: the former is usually a caller passing an empty buffer, the latter a peer or transport problem.
SyncTruncated
The payload is truncated.
SyncRecordCountMismatch
The record count declared in the payload does not match its actual content.
Separate from Error::SyncTruncated: truncation means “not enough bytes”, this one means
“enough bytes, but they do not add up” — the payload was reassembled, a stronger signal
than truncation.
Fields
SyncTooManyRecords
The payload contains more records than the limit allows.
SyncCompressionFailed
Compression failed.
Deflate is defined for every input, so this does not occur in practice. It exists so that
the return value of miniz_oxide is not unwrapped.
SyncDecompressionFailed
The decompressed content exceeds MAX_DECOMPRESSED_LEN, or the input is not a valid
deflate stream at all.
Corruption, truncation and a length mismatch deliberately share one variant: their handling is identical (reject this sync, ask the peer to resend), and separating them has no actionable value for the user.
SyncPayloadTooLarge
The payload exceeds the length limit after decompression.
§Why it is separate from Error::SyncDecompressionFailed
Because it is the only cause in that group that does not represent an attack. A user who really has seventy thousand entries will hit the limit, and telling them “the payload is corrupt” would send them to check the network, the devices and the pairing — none of which is the problem. Telling them “too large, use the LAN instead” is actionable.
The price is that an attacker can probe the limit’s position with one bit, and the limit is in the open-source code anyway. A related fact: decompression happens after the outer AEAD, so input reaching this point has already been authenticated.
SyncUnknownRecordKind
The payload contains an unknown record kind.
As with the equivalent vault-file error, an unknown kind is rejected rather than skipped: skipping makes semantics added by a future version silently disappear on older versions, and “the sync finished but a few entries are missing” is one of the hardest failures to trace.
SyncMalformed
The payload contains an unexplained field or structure.
This variant is the catch-all: every structural problem that can be named has its own variant, so reaching this one means “the payload failed a self-consistency check, but which one cannot be said”.
Fields
SyncAuthenticationFailed
Authentication of the payload’s outer layer failed.
Separate from Error::VaultDecryptionFailed: that one means “the vault file cannot be
opened”, this one means “this sync payload is untrustworthy”. The client handles them
differently — the former asks the user to check the master key, the latter only needs “this
sync failed, please retry”.
It does not distinguish a wrong master key, a tampered payload and a mismatched sender identifier: telling them apart helps an attacker and has no actionable value for the user.
SyncIconRequiresByteTransport
The vault holds entries with icons and the channel cannot carry them.
An icon is a picture of up to 64 KiB, and a channel with a declared message limit (a QR code, for instance) cannot hold a payload that carries one. The refusal happens before anything is sealed, so the user is told “this channel cannot move icons” instead of seeing a capacity error that says nothing about icons, and the icon is never silently dropped: a sync that quietly loses what the user sees is worse than one that is refused.
The fix is not to shrink the icon here: the byte-stream channel (a LAN connection) has no message limit and carries the vault as it is.
SyncAlreadyApplied
This sync payload has already been processed.
Replaying an old payload is harmless in itself (merging is idempotent), but doing a full decryption and merge for nothing is real cost, and it misleads the “last synced at” shown in the UI. Payload identifiers that have been seen are therefore remembered, and duplicates rejected.
This is a separate error rather than a silent success: the caller needs to know “the sync did happen, this one is a duplicate”, not “a sync just succeeded”.
SyncIdLogFull
The log of used payload identifiers is full.
Failing explicitly beats silently dropping the oldest entry: a silent drop makes a very old payload acceptable again, which is exactly what replay protection must prevent. On seeing this error the caller should tell the user that the sync history is too long; what to do about it is a higher-level policy decision.
TransportFailed
The transport channel itself failed (disconnect, timeout, platform refusal).
§Why the underlying error is not passed through
The underlying failures are things like std::io::Error or platform permission errors —
abstractions that do not belong to this crate. This crate has
#![forbid(unsafe_code)] and depends on no I/O, so writing those types into the error enum
would freeze platform details into the protocol layer. Only an opaque reason is kept — it
is meant for logs, not for program decisions.
SyncSenderMismatch
The device identifier reported by the peer disagrees with the sender claimed by the transport channel.
§Why this check must exist
The sender inside the payload is self-reported, and its presence in the AAD only
guarantees that no third party changed it. On a broadcast channel (LAN multicast) any
device can replay a payload signed by somebody else verbatim, so the payload says B
while C actually sent it. This check takes “who sent this” from the transport channel and
compares it with the value the payload claims.
Note that it stops impersonation, not replay — replay is stopped by
Error::SyncAlreadyApplied.
SyncUnknownPayloadKind
The kind claimed by the payload is not one this version knows.
Separate from Error::SyncUnknownRecordKind: that one is about the kind of one record
inside the payload (entry or tombstone), this one about the kind of the whole payload
(state exchange or request). See crate::sync::PayloadKind.
TransportUnsupported
The transport channel does not support this operation.
For example, a channel that can carry only a single message (a QR code) is asked to open a
long-lived connection. It is not used for “this channel cannot fit the payload” — that
is Error::TransportMessageTooLarge, and the two are handled quite differently (the
former can only switch channels; the latter can switch modes or send the user to the LAN).
TransportAlreadyUsed
The one-shot channel has already been used.
A QR code is the typical one-shot channel: one code carries exactly one exchange. Treating it as reusable would turn “scan the same code twice” into two syncs, which is a side path around replay protection.
TransportMessageTooLarge
A single message exceeds what the channel can carry.
The usual trigger is a QR code: it is a fixed-capacity channel, so when the message does not fit the only options are switching mode or switching channel. The caller should use this to steer the user towards the LAN, not truncate the payload — a truncated payload fails AEAD, so what the user sees is “corrupted data”.
SyncNoSessionFound
No joinable sync session was found.
Separate from “found one, but the handshake failed”: the former is the normal “nobody nearby”, where the caller should let the user retry or check the network; only the latter is an anomaly.
Trait Implementations§
Source§impl Error for Error
impl Error for Error
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()