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