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 error_support::{ErrorHandling, GetErrorHandling};
14use jwcrypto::JwCryptoError;
15use sync15::Error as Sync15Error;
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("SyncAuthInvalid error {reason}")]
54    SyncAuthInvalid { reason: String },
55
56    #[error("Unexpected Error: {reason}")]
57    UnexpectedLoginsApiError { reason: String },
58}
59
60/// Logins error type
61/// These are "internal" errors used by the implementation. This error type
62/// is never returned to the consumer.
63#[derive(Debug, thiserror::Error)]
64pub enum Error {
65    #[error("Database is closed")]
66    DatabaseClosed,
67
68    #[error("Malformed incoming record")]
69    MalformedIncomingRecord,
70
71    #[error("Invalid login: {0}")]
72    InvalidLogin(#[from] InvalidLogin),
73
74    #[error("The `sync_status` column in DB has an illegal value: {0}")]
75    BadSyncStatus(u8),
76
77    #[error("No record with guid exists (when one was required): {0:?}")]
78    NoSuchRecord(String),
79
80    // Fennec import only works on empty logins tables.
81    #[error("The logins tables are not empty")]
82    NonEmptyTable,
83
84    #[error("encryption failed: {0:?}")]
85    EncryptionFailed(String),
86
87    #[error("decryption failed: {0:?}")]
88    DecryptionFailed(String),
89
90    #[error("Error synchronizing: {0}")]
91    SyncAdapterError(#[from] sync15::Error),
92
93    #[error("Error parsing JSON data: {0}")]
94    JsonError(#[from] serde_json::Error),
95
96    #[error("Error executing SQL: {0}")]
97    SqlError(#[from] rusqlite::Error),
98
99    #[error("Error parsing URL: {0}")]
100    UrlParseError(#[from] url::ParseError),
101
102    #[error("Invalid path: {0:?}")]
103    InvalidPath(OsString),
104
105    #[error("CryptoError({0})")]
106    CryptoError(#[from] JwCryptoError),
107
108    #[error("{0}")]
109    Interrupted(#[from] interrupt_support::Interrupted),
110
111    #[error("IOError: {0}")]
112    IOError(#[from] std::io::Error),
113
114    #[error("Migration Error: {0}")]
115    MigrationError(String),
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")]
135    IllegalOrigin,
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            Self::SyncAdapterError(e) => match e {
172                Sync15Error::TokenserverHttpError(401) | Sync15Error::BadKeyLength(..) => {
173                    ErrorHandling::convert(LoginsApiError::SyncAuthInvalid {
174                        reason: e.to_string(),
175                    })
176                    .log_warning()
177                }
178                Sync15Error::RequestError(_) => {
179                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
180                        reason: e.to_string(),
181                    })
182                    .log_warning()
183                }
184                _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
185                    reason: self.to_string(),
186                })
187                .report_error("logins-sync"),
188            },
189            Error::SqlError(rusqlite::Error::SqliteFailure(err, _)) => match err.code {
190                rusqlite::ErrorCode::DatabaseCorrupt => {
191                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
192                        reason: self.to_string(),
193                    })
194                    .report_error("logins-db-corrupt")
195                }
196                rusqlite::ErrorCode::DiskFull => {
197                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
198                        reason: self.to_string(),
199                    })
200                    .report_error("logins-db-disk-full")
201                }
202                _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
203                    reason: self.to_string(),
204                })
205                .report_error("logins-unexpected"),
206            },
207            // Unexpected errors that we report to Sentry.  We should watch the reports for these
208            // and do one or more of these things if we see them:
209            //   - Fix the underlying issue
210            //   - Add breadcrumbs or other context to help uncover the issue
211            //   - Decide that these are expected errors and move them to the above case
212            _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
213                reason: self.to_string(),
214            })
215            .report_error("logins-unexpected"),
216        }
217    }
218}
219
220impl From<uniffi::UnexpectedUniFFICallbackError> for LoginsApiError {
221    fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
222        LoginsApiError::UnexpectedLoginsApiError {
223            reason: error.to_string(),
224        }
225    }
226}