autofill/sync/credit_card/
mod.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*/
5
6pub mod incoming;
7pub mod outgoing;
8
9use super::engine::{ConfigSyncEngine, EngineConfig, SyncEngineStorageImpl};
10use super::{
11    MergeResult, Metadata, ProcessIncomingRecordImpl, ProcessOutgoingRecordImpl, SyncRecord,
12    UnknownFields,
13};
14use crate::db::models::credit_card::InternalCreditCard;
15use crate::encryption::EncryptorDecryptor;
16use crate::error::*;
17use crate::sync_merge_field_check;
18use incoming::IncomingCreditCardsImpl;
19use outgoing::OutgoingCreditCardsImpl;
20use rusqlite::Transaction;
21use serde::{Deserialize, Serialize};
22use std::sync::Arc;
23use sync_guid::Guid;
24use types::Timestamp;
25
26// The engine.
27pub(crate) fn create_engine(store: Arc<crate::Store>) -> ConfigSyncEngine<InternalCreditCard> {
28    ConfigSyncEngine::new(
29        EngineConfig {
30            namespace: "credit_cards".to_string(),
31            collection: "creditcards".into(),
32        },
33        store,
34        Box::new(CreditCardsEngineStorageImpl {}),
35    )
36}
37
38pub(super) struct CreditCardsEngineStorageImpl {}
39
40impl SyncEngineStorageImpl<InternalCreditCard> for CreditCardsEngineStorageImpl {
41    fn get_incoming_impl(
42        &self,
43        enc_key: &Option<String>,
44    ) -> Result<Box<dyn ProcessIncomingRecordImpl<Record = InternalCreditCard>>> {
45        let enc_key = match enc_key {
46            None => return Err(Error::MissingEncryptionKey),
47            Some(enc_key) => enc_key,
48        };
49        let encdec = EncryptorDecryptor::new(enc_key)?;
50        Ok(Box::new(IncomingCreditCardsImpl { encdec }))
51    }
52
53    fn reset_storage(&self, tx: &Transaction<'_>) -> Result<()> {
54        tx.execute_batch(
55            "DELETE FROM credit_cards_mirror;
56            DELETE FROM credit_cards_tombstones;",
57        )?;
58        Ok(())
59    }
60
61    fn get_outgoing_impl(
62        &self,
63        enc_key: &Option<String>,
64    ) -> Result<Box<dyn ProcessOutgoingRecordImpl<Record = InternalCreditCard>>> {
65        let enc_key = match enc_key {
66            None => return Err(Error::MissingEncryptionKey),
67            Some(enc_key) => enc_key,
68        };
69        let encdec = EncryptorDecryptor::new(enc_key)?;
70        Ok(Box::new(OutgoingCreditCardsImpl { encdec }))
71    }
72}
73
74// These structs are a representation of what's stored on the sync server for non-tombstone records.
75// (The actual server doesn't have `id` in the payload but instead in the envelope)
76#[derive(Default, Debug, Deserialize, Serialize)]
77pub(crate) struct CreditCardPayload {
78    id: Guid,
79
80    // For some historical reason and unlike most other sync records, creditcards
81    // are serialized with this explicit 'entry' object.
82    pub(super) entry: PayloadEntry,
83}
84
85// Note that the sync payload contains the "unencrypted" cc_number - but our
86// internal structs have the cc_number_enc/cc_number_last_4 pair, so we need to
87// take care going to and from.
88// (The scare-quotes around "unencrypted" are to reflect that, obviously, the
89// payload itself *is* encrypted by sync, but the number is plain-text in that
90// payload)
91#[derive(Default, Debug, Deserialize, Serialize)]
92#[serde(default, rename_all = "kebab-case")]
93pub(super) struct PayloadEntry {
94    pub cc_name: String,
95    pub cc_number: String,
96    pub cc_exp_month: i64,
97    pub cc_exp_year: i64,
98    pub cc_type: String,
99    // metadata (which isn't kebab-case for some historical reason...)
100    #[serde(rename = "timeCreated")]
101    pub time_created: Timestamp,
102    #[serde(rename = "timeLastUsed")]
103    pub time_last_used: Timestamp,
104    #[serde(rename = "timeLastModified")]
105    pub time_last_modified: Timestamp,
106    #[serde(rename = "timesUsed")]
107    pub times_used: i64,
108    pub version: u32, // always 3 for credit-cards
109    // Fields that the current schema did not expect, we store them only internally
110    // to round-trip them back to sync without processing them in any way
111    #[serde(flatten)]
112    pub unknown_fields: UnknownFields,
113}
114
115impl InternalCreditCard {
116    fn from_payload(p: CreditCardPayload, encdec: &EncryptorDecryptor) -> Result<Self> {
117        if p.entry.version != 3 {
118            // when new versions are introduced we will start accepting and
119            // converting old ones - but 3 is the lowest we support.
120            return Err(Error::InvalidSyncPayload(format!(
121                "invalid version - {}",
122                p.entry.version
123            )));
124        }
125        // need to encrypt the cleartext in the sync record.
126        let cc_number_enc = encdec.encrypt(&p.entry.cc_number)?;
127        let cc_number_last_4 = get_last_4(&p.entry.cc_number);
128
129        Ok(InternalCreditCard {
130            guid: p.id,
131            cc_name: p.entry.cc_name,
132            cc_number_enc,
133            cc_number_last_4,
134            cc_exp_month: p.entry.cc_exp_month,
135            cc_exp_year: p.entry.cc_exp_year,
136            cc_type: p.entry.cc_type,
137            metadata: Metadata {
138                time_created: p.entry.time_created,
139                time_last_used: p.entry.time_last_used,
140                time_last_modified: p.entry.time_last_modified,
141                times_used: p.entry.times_used,
142                sync_change_counter: 0,
143            },
144        })
145    }
146
147    pub(crate) fn into_payload(self, encdec: &EncryptorDecryptor) -> Result<CreditCardPayload> {
148        let cc_number = encdec.decrypt(&self.cc_number_enc)?;
149        Ok(CreditCardPayload {
150            id: self.guid,
151            entry: PayloadEntry {
152                cc_name: self.cc_name,
153                cc_number,
154                cc_exp_month: self.cc_exp_month,
155                cc_exp_year: self.cc_exp_year,
156                cc_type: self.cc_type,
157                time_created: self.metadata.time_created,
158                time_last_used: self.metadata.time_last_used,
159                time_last_modified: self.metadata.time_last_modified,
160                times_used: self.metadata.times_used,
161                version: 3,
162                unknown_fields: Default::default(),
163            },
164        })
165    }
166}
167
168impl SyncRecord for InternalCreditCard {
169    fn record_name() -> &'static str {
170        "CreditCard"
171    }
172
173    fn id(&self) -> &Guid {
174        &self.guid
175    }
176
177    fn metadata(&self) -> &Metadata {
178        &self.metadata
179    }
180
181    fn metadata_mut(&mut self) -> &mut Metadata {
182        &mut self.metadata
183    }
184
185    /// Performs a three-way merge between an incoming, local, and mirror record.
186    /// If a merge cannot be successfully completed (ie, if we find the same
187    /// field has changed both locally and remotely since the last sync), the
188    /// local record data is returned with a new guid and updated sync metadata.
189    /// Note that mirror being None is an edge-case and typically means first
190    /// sync since a "reset" (eg, disconnecting and reconnecting.
191    #[allow(clippy::cognitive_complexity)] // Looks like clippy considers this after macro-expansion...
192    fn merge(incoming: &Self, local: &Self, mirror: &Option<Self>) -> MergeResult<Self> {
193        let mut merged_record: Self = Default::default();
194        // guids must be identical
195        assert_eq!(incoming.guid, local.guid);
196
197        if let Some(m) = mirror {
198            assert_eq!(incoming.guid, m.guid)
199        };
200
201        merged_record.guid = incoming.guid.clone();
202
203        sync_merge_field_check!(cc_name, incoming, local, mirror, merged_record);
204        // XXX - It looks like this will allow us to merge a locally changed
205        // cc_number_enc and remotely changed cc_number_last_4, which is nonsensical.
206        // Given sync itself is populating this it needs more thought.
207        sync_merge_field_check!(cc_number_enc, incoming, local, mirror, merged_record);
208        sync_merge_field_check!(cc_number_last_4, incoming, local, mirror, merged_record);
209        sync_merge_field_check!(cc_exp_month, incoming, local, mirror, merged_record);
210        sync_merge_field_check!(cc_exp_year, incoming, local, mirror, merged_record);
211        sync_merge_field_check!(cc_type, incoming, local, mirror, merged_record);
212
213        merged_record.metadata = incoming.metadata;
214        merged_record
215            .metadata
216            .merge(&local.metadata, mirror.as_ref().map(|m| m.metadata()));
217
218        MergeResult::Merged {
219            merged: merged_record,
220        }
221    }
222}
223
224/// Returns a with the given local record's data but with a new guid and
225/// fresh sync metadata.
226fn get_forked_record(local_record: InternalCreditCard) -> InternalCreditCard {
227    let mut local_record_data = local_record;
228    local_record_data.guid = Guid::random();
229    local_record_data.metadata.time_created = Timestamp::now();
230    local_record_data.metadata.time_last_used = Timestamp::now();
231    local_record_data.metadata.time_last_modified = Timestamp::now();
232    local_record_data.metadata.times_used = 0;
233    local_record_data.metadata.sync_change_counter = 1;
234
235    local_record_data
236}
237
238// Wow - strings are hard! credit-card sync is the only thing that needs to
239// get the last 4 chars of a string.
240fn get_last_4(v: &str) -> String {
241    v.chars()
242        .rev()
243        .take(4)
244        .collect::<Vec<_>>()
245        .into_iter()
246        .rev()
247        .collect::<String>()
248}
249#[test]
250fn test_last_4() {
251    assert_eq!(get_last_4("testing"), "ting".to_string());
252    assert_eq!(get_last_4("abc"), "abc".to_string());
253    assert_eq!(get_last_4(""), "".to_string());
254}
255
256#[test]
257fn test_to_from_payload() {
258    nss::ensure_initialized();
259    let key = crate::encryption::create_autofill_key().unwrap();
260    let cc_number = "1234567812345678";
261    let cc_number_enc =
262        crate::encryption::encrypt_string(key.clone(), cc_number.to_string()).unwrap();
263    let cc = InternalCreditCard {
264        cc_name: "Shaggy".to_string(),
265        cc_number_enc,
266        cc_number_last_4: "5678".to_string(),
267        cc_exp_month: 12,
268        cc_exp_year: 2021,
269        cc_type: "foo".to_string(),
270        ..Default::default()
271    };
272    let encdec = EncryptorDecryptor::new(&key).unwrap();
273    let payload: CreditCardPayload = cc.clone().into_payload(&encdec).unwrap();
274
275    assert_eq!(payload.id, cc.guid);
276    assert_eq!(payload.entry.cc_name, "Shaggy".to_string());
277    assert_eq!(payload.entry.cc_number, cc_number.to_string());
278    assert_eq!(payload.entry.cc_exp_month, 12);
279    assert_eq!(payload.entry.cc_exp_year, 2021);
280    assert_eq!(payload.entry.cc_type, "foo".to_string());
281
282    // and back.
283    let cc2 = InternalCreditCard::from_payload(payload, &encdec).unwrap();
284    // sadly we can't just check equality because the encrypted value will be
285    // different even if the card number is identical.
286    assert_eq!(cc2.guid, cc.guid);
287    assert_eq!(cc2.cc_name, "Shaggy".to_string());
288    assert_eq!(cc2.cc_number_last_4, cc.cc_number_last_4);
289    assert_eq!(cc2.cc_exp_month, cc.cc_exp_month);
290    assert_eq!(cc2.cc_exp_year, cc.cc_exp_year);
291    assert_eq!(cc2.cc_type, cc.cc_type);
292    // The decrypted number should be the same.
293    assert_eq!(
294        crate::encryption::decrypt_string(key, cc2.cc_number_enc.clone()).unwrap(),
295        cc_number
296    );
297    // But the encrypted value should not.
298    assert_ne!(cc2.cc_number_enc, cc.cc_number_enc);
299}