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    #[error("IncompatibleVersion: {0}")]
118    IncompatibleVersion(i64),
119}
120
121/// Error::InvalidLogin subtypes
122#[derive(Debug, thiserror::Error)]
123pub enum InvalidLogin {
124    // EmptyOrigin error occurs when the login's origin field is empty.
125    #[error("Origin is empty")]
126    EmptyOrigin,
127    #[error("Password is empty")]
128    EmptyPassword,
129    #[error("Login already exists")]
130    DuplicateLogin,
131    #[error("Both `formActionOrigin` and `httpRealm` are present")]
132    BothTargets,
133    #[error("Neither `formActionOrigin` or `httpRealm` are present")]
134    NoTarget,
135    // Login has an illegal origin field, split off from IllegalFieldValue since this is a known
136    // issue with the Desktop logins and we don't want to report it to Sentry (see #5233).
137    #[error("Login has illegal origin: {reason}")]
138    IllegalOrigin { reason: String },
139    #[error("Login has illegal field: {field_info}")]
140    IllegalFieldValue { field_info: String },
141}
142
143// Define how our internal errors are handled and converted to external errors
144// See `support/error/README.md` for how this works, especially the warning about PII.
145impl GetErrorHandling for Error {
146    type ExternalError = LoginsApiError;
147
148    fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
149        match self {
150            Self::InvalidLogin(why) => ErrorHandling::convert(LoginsApiError::InvalidRecord {
151                reason: why.to_string(),
152            }),
153            Self::MalformedIncomingRecord => {
154                ErrorHandling::convert(LoginsApiError::InvalidRecord {
155                    reason: "invalid incoming record".to_string(),
156                })
157            }
158            // Our internal "no such record" error is converted to our public "no such record" error, with no logging and no error reporting.
159            Self::NoSuchRecord(guid) => ErrorHandling::convert(LoginsApiError::NoSuchRecord {
160                reason: guid.to_string(),
161            }),
162            // NonEmptyTable error is just a sanity check to ensure we aren't asked to migrate into an
163            // existing DB - consumers should never actually do this, and will never expect to handle this as a specific
164            // error - so it gets reported to the error reporter and converted to an "internal" error.
165            Self::NonEmptyTable => {
166                ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
167                    reason: "must be an empty DB to migrate".to_string(),
168                })
169                .report_error("logins-migration")
170            }
171            Self::Interrupted(_) => ErrorHandling::convert(LoginsApiError::Interrupted {
172                reason: self.to_string(),
173            }),
174            Self::SyncAdapterError(e) => match e {
175                Sync15Error::TokenserverHttpError(401) | Sync15Error::BadKeyLength(..) => {
176                    ErrorHandling::convert(LoginsApiError::SyncAuthInvalid {
177                        reason: e.to_string(),
178                    })
179                    .log_warning()
180                }
181                Sync15Error::RequestError(_) => {
182                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
183                        reason: e.to_string(),
184                    })
185                    .log_warning()
186                }
187                _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
188                    reason: self.to_string(),
189                })
190                .report_error("logins-sync"),
191            },
192            Error::SqlError(rusqlite::Error::SqliteFailure(err, _)) => match err.code {
193                rusqlite::ErrorCode::DatabaseCorrupt => {
194                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
195                        reason: self.to_string(),
196                    })
197                    .report_error("logins-db-corrupt")
198                }
199                rusqlite::ErrorCode::DiskFull => {
200                    ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
201                        reason: self.to_string(),
202                    })
203                    .report_error("logins-db-disk-full")
204                }
205                _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
206                    reason: self.to_string(),
207                })
208                .report_error("logins-unexpected"),
209            },
210            // Unexpected errors that we report to Sentry.  We should watch the reports for these
211            // and do one or more of these things if we see them:
212            //   - Fix the underlying issue
213            //   - Add breadcrumbs or other context to help uncover the issue
214            //   - Decide that these are expected errors and move them to the above case
215            _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError {
216                reason: self.to_string(),
217            })
218            .report_error("logins-unexpected"),
219        }
220    }
221}
222
223impl From<uniffi::UnexpectedUniFFICallbackError> for LoginsApiError {
224    fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
225        LoginsApiError::UnexpectedLoginsApiError {
226            reason: error.to_string(),
227        }
228    }
229}