logins/
error.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use std::ffi::OsString;
6pub type Result<T> = std::result::Result<T, Error>;
7// Functions which are part of the public API should use this Result.
8pub type ApiResult<T> = std::result::Result<T, LoginsApiError>;
9
10pub use error_support::{breadcrumb, handle_error, report_error};
11pub use error_support::{debug, error, info, trace, warn};
12
13use db_crypto::DbCryptoApiError;
14use error_support::{ErrorHandling, GetErrorHandling};
15use jwcrypto::JwCryptoError;
16
17// Errors we return via the public interface.
18#[derive(Debug, thiserror::Error)]
19pub enum LoginsApiError {
20    #[error("NSS not initialized")]
21    NSSUninitialized,
22
23    #[error("NSS error during authentication: {reason}")]
24    NSSAuthenticationError { reason: String },
25
26    #[error("error during authentication: {reason}")]
27    AuthenticationError { reason: String },
28
29    #[error("authentication cancelled")]
30    AuthenticationCanceled,
31
32    #[error("Invalid login: {reason}")]
33    InvalidRecord { reason: String },
34
35    #[error("No record with guid exists (when one was required): {reason:?}")]
36    NoSuchRecord { reason: String },
37
38    #[error("Encryption key is missing.")]
39    MissingKey,
40
41    #[error("Encryption key is not valid.")]
42    InvalidKey,
43
44    #[error("encryption failed: {reason}")]
45    EncryptionFailed { reason: String },
46
47    #[error("decryption failed: {reason}")]
48    DecryptionFailed { reason: String },
49
50    #[error("{reason}")]
51    Interrupted { reason: String },
52
53    #[error("Unexpected Error: {reason}")]
54    UnexpectedLoginsApiError { reason: String },
55}
56
57/// Logins error type
58/// These are "internal" errors used by the implementation. This error type
59/// is never returned to the consumer.
60#[derive(Debug, thiserror::Error)]
61pub enum Error {
62    #[error("Database is closed")]
63    DatabaseClosed,
64
65    #[error("Malformed incoming record")]
66    MalformedIncomingRecord,
67
68    #[error("Invalid login: {0}")]
69    InvalidLogin(#[from] InvalidLogin),
70
71    #[error("The `sync_status` column in DB has an illegal value: {0}")]
72    BadSyncStatus(u8),
73
74    #[error("No record with guid exists (when one was required): {0:?}")]
75    NoSuchRecord(String),
76
77    // Fennec import only works on empty logins tables.
78    #[error("The logins tables are not empty")]
79    NonEmptyTable,
80
81    #[error("encryption failed: {0:?}")]
82    EncryptionFailed(String),
83
84    #[error("decryption failed: {0:?}")]
85    DecryptionFailed(String),
86
87    #[error("Error parsing JSON data: {0}")]
88    JsonError(#[from] serde_json::Error),
89
90    #[error("Error executing SQL: {0}")]
91    SqlError(#[from] rusqlite::Error),
92
93    #[error("Error parsing URL: {0}")]
94    UrlParseError(#[from] url::ParseError),
95
96    #[error("Invalid path: {0:?}")]
97    InvalidPath(OsString),
98
99    #[error("CryptoError({0})")]
100    CryptoError(#[from] DbCryptoApiError),
101
102    #[error("JwCryptoError({0})")]
103    JwCryptoError(#[from] JwCryptoError),
104
105    #[error("{0}")]
106    Interrupted(#[from] interrupt_support::Interrupted),
107
108    #[error("IOError: {0}")]
109    IOError(#[from] std::io::Error),
110
111    #[error("Migration Error: {0}")]
112    MigrationError(String),
113
114    #[error("IncompatibleVersion: {0}")]
115    IncompatibleVersion(i64),
116}
117
118/// Error::InvalidLogin subtypes
119#[derive(Debug, thiserror::Error)]
120pub enum InvalidLogin {
121    // EmptyOrigin error occurs when the login's origin field is empty.
122    #[error("Origin is empty")]
123    EmptyOrigin,
124    #[error("Password is empty")]
125    EmptyPassword,
126    #[error("Login already exists")]
127    DuplicateLogin,
128    #[error("Both `formActionOrigin` and `httpRealm` are present")]
129    BothTargets,
130    #[error("Neither `formActionOrigin` or `httpRealm` are present")]
131    NoTarget,
132    // Login has an illegal origin field, split off from IllegalFieldValue since this is a known
133    // issue with the Desktop logins and we don't want to report it to Sentry (see #5233).
134    #[error("Login has illegal origin: {reason}")]
135    IllegalOrigin { reason: String },
136    #[error("Login has illegal field: {field_info}")]
137    IllegalFieldValue { field_info: String },
138}
139
140// Define how our internal errors are handled and converted to external errors
141// See `support/error/README.md` for how this works, especially the warning about PII.
142impl GetErrorHandling for Error {
143    type ExternalError = LoginsApiError;
144
145    fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
146        match self {
147            Self::InvalidLogin(why) => ErrorHandling::convert(LoginsApiError::InvalidRecord {
148                reason: why.to_string(),
149            }),
150            Self::MalformedIncomingRecord => {
151                ErrorHandling::convert(LoginsApiError::InvalidRecord {
152                    reason: "invalid incoming record".to_string(),
153                })
154            }
155            // Our internal "no such record" error is converted to our public "no such record" error, with no logging and no error reporting.
156            Self::NoSuchRecord(guid) => ErrorHandling::convert(LoginsApiError::NoSuchRecord {
157                reason: guid.to_string(),
158            }),
159            // NonEmptyTable error is just a sanity check to ensure we aren't asked to migrate into an
160            // existing DB - consumers should never actually do this, and will never expect to handle this as a specific
161            // error - so it gets reported to the error reporter and converted to an "internal" error.
162            Self::NonEmptyTable => {
163                ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
164                    reason: "must be an empty DB to migrate".to_string(),
165                })
166                .report_error("logins-migration")
167            }
168            Self::Interrupted(_) => ErrorHandling::convert(LoginsApiError::Interrupted {
169                reason: self.to_string(),
170            }),
171            Error::SqlError(rusqlite::Error::SqliteFailure(err, _)) => match err.code {
172                rusqlite::ErrorCode::DatabaseCorrupt => {
173                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
174                        reason: self.to_string(),
175                    })
176                    .report_error("logins-db-corrupt")
177                }
178                rusqlite::ErrorCode::DiskFull => {
179                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
180                        reason: self.to_string(),
181                    })
182                    .report_error("logins-db-disk-full")
183                }
184                _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
185                    reason: self.to_string(),
186                })
187                .report_error("logins-unexpected"),
188            },
189            // Unexpected errors that we report to Sentry.  We should watch the reports for these
190            // and do one or more of these things if we see them:
191            //   - Fix the underlying issue
192            //   - Add breadcrumbs or other context to help uncover the issue
193            //   - Decide that these are expected errors and move them to the above case
194            _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
195                reason: self.to_string(),
196            })
197            .report_error("logins-unexpected"),
198        }
199    }
200}
201
202// The bridged sync engine (`sync::bridge`) deals in `anyhow::Result`, as that's
203// what the `sync15` `SyncEngine`/`BridgedEngineWrapper` use. This lets UniFFI map
204// those errors onto our public error type when the bridge methods are exposed
205// via the UDL.
206impl From<anyhow::Error> for LoginsApiError {
207    fn from(value: anyhow::Error) -> Self {
208        LoginsApiError::UnexpectedLoginsApiError {
209            reason: value.to_string(),
210        }
211    }
212}
213
214impl From<uniffi::UnexpectedUniFFICallbackError> for LoginsApiError {
215    fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
216        LoginsApiError::UnexpectedLoginsApiError {
217            reason: error.to_string(),
218        }
219    }
220}