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*/
56pub mod incoming;
7pub mod outgoing;
89use 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;
2526// 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}
3738pub(super) struct CreditCardsEngineStorageImpl {}
3940impl SyncEngineStorageImpl<InternalCreditCard> for CreditCardsEngineStorageImpl {
41fn get_incoming_impl(
42&self,
43 enc_key: &Option<String>,
44 ) -> Result<Box<dyn ProcessIncomingRecordImpl<Record = InternalCreditCard>>> {
45let enc_key = match enc_key {
46None => return Err(Error::MissingEncryptionKey),
47Some(enc_key) => enc_key,
48 };
49let encdec = EncryptorDecryptor::new(enc_key)?;
50Ok(Box::new(IncomingCreditCardsImpl { encdec }))
51 }
5253fn reset_storage(&self, tx: &Transaction<'_>) -> Result<()> {
54 tx.execute_batch(
55"DELETE FROM credit_cards_mirror;
56 DELETE FROM credit_cards_tombstones;",
57 )?;
58Ok(())
59 }
6061fn get_outgoing_impl(
62&self,
63 enc_key: &Option<String>,
64 ) -> Result<Box<dyn ProcessOutgoingRecordImpl<Record = InternalCreditCard>>> {
65let enc_key = match enc_key {
66None => return Err(Error::MissingEncryptionKey),
67Some(enc_key) => enc_key,
68 };
69let encdec = EncryptorDecryptor::new(enc_key)?;
70Ok(Box::new(OutgoingCreditCardsImpl { encdec }))
71 }
72}
7374// 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,
7980// For some historical reason and unlike most other sync records, creditcards
81 // are serialized with this explicit 'entry' object.
82pub(super) entry: PayloadEntry,
83}
8485// 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 {
94pub cc_name: String,
95pub cc_number: String,
96pub cc_exp_month: i64,
97pub cc_exp_year: i64,
98pub cc_type: String,
99// metadata (which isn't kebab-case for some historical reason...)
100#[serde(rename = "timeCreated")]
101pub time_created: Timestamp,
102#[serde(rename = "timeLastUsed")]
103pub time_last_used: Timestamp,
104#[serde(rename = "timeLastModified")]
105pub time_last_modified: Timestamp,
106#[serde(rename = "timesUsed")]
107pub times_used: i64,
108pub 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)]
112pub unknown_fields: UnknownFields,
113}
114115impl InternalCreditCard {
116fn from_payload(p: CreditCardPayload, encdec: &EncryptorDecryptor) -> Result<Self> {
117if 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.
120return Err(Error::InvalidSyncPayload(format!(
121"invalid version - {}",
122 p.entry.version
123 )));
124 }
125// need to encrypt the cleartext in the sync record.
126let cc_number_enc = encdec.encrypt(&p.entry.cc_number)?;
127let cc_number_last_4 = get_last_4(&p.entry.cc_number);
128129Ok(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 }
146147pub(crate) fn into_payload(self, encdec: &EncryptorDecryptor) -> Result<CreditCardPayload> {
148let cc_number = encdec.decrypt(&self.cc_number_enc)?;
149Ok(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}
167168impl SyncRecord for InternalCreditCard {
169fn record_name() -> &'static str {
170"CreditCard"
171}
172173fn id(&self) -> &Guid {
174&self.guid
175 }
176177fn metadata(&self) -> &Metadata {
178&self.metadata
179 }
180181fn metadata_mut(&mut self) -> &mut Metadata {
182&mut self.metadata
183 }
184185/// 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...
192fn merge(incoming: &Self, local: &Self, mirror: &Option<Self>) -> MergeResult<Self> {
193let mut merged_record: Self = Default::default();
194// guids must be identical
195assert_eq!(incoming.guid, local.guid);
196197if let Some(m) = mirror {
198assert_eq!(incoming.guid, m.guid)
199 };
200201 merged_record.guid = incoming.guid.clone();
202203sync_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.
207sync_merge_field_check!(cc_number_enc, incoming, local, mirror, merged_record);
208sync_merge_field_check!(cc_number_last_4, incoming, local, mirror, merged_record);
209sync_merge_field_check!(cc_exp_month, incoming, local, mirror, merged_record);
210sync_merge_field_check!(cc_exp_year, incoming, local, mirror, merged_record);
211sync_merge_field_check!(cc_type, incoming, local, mirror, merged_record);
212213 merged_record.metadata = incoming.metadata;
214 merged_record
215 .metadata
216 .merge(&local.metadata, mirror.as_ref().map(|m| m.metadata()));
217218 MergeResult::Merged {
219 merged: merged_record,
220 }
221 }
222}
223224/// 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 {
227let 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;
234235 local_record_data
236}
237238// 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() {
251assert_eq!(get_last_4("testing"), "ting".to_string());
252assert_eq!(get_last_4("abc"), "abc".to_string());
253assert_eq!(get_last_4(""), "".to_string());
254}
255256#[test]
257fn test_to_from_payload() {
258 nss::ensure_initialized();
259let key = crate::encryption::create_autofill_key().unwrap();
260let cc_number = "1234567812345678";
261let cc_number_enc =
262crate::encryption::encrypt_string(key.clone(), cc_number.to_string()).unwrap();
263let 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 };
272let encdec = EncryptorDecryptor::new(&key).unwrap();
273let payload: CreditCardPayload = cc.clone().into_payload(&encdec).unwrap();
274275assert_eq!(payload.id, cc.guid);
276assert_eq!(payload.entry.cc_name, "Shaggy".to_string());
277assert_eq!(payload.entry.cc_number, cc_number.to_string());
278assert_eq!(payload.entry.cc_exp_month, 12);
279assert_eq!(payload.entry.cc_exp_year, 2021);
280assert_eq!(payload.entry.cc_type, "foo".to_string());
281282// and back.
283let 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.
286assert_eq!(cc2.guid, cc.guid);
287assert_eq!(cc2.cc_name, "Shaggy".to_string());
288assert_eq!(cc2.cc_number_last_4, cc.cc_number_last_4);
289assert_eq!(cc2.cc_exp_month, cc.cc_exp_month);
290assert_eq!(cc2.cc_exp_year, cc.cc_exp_year);
291assert_eq!(cc2.cc_type, cc.cc_type);
292// The decrypted number should be the same.
293assert_eq!(
294crate::encryption::decrypt_string(key, cc2.cc_number_enc.clone()).unwrap(),
295 cc_number
296 );
297// But the encrypted value should not.
298assert_ne!(cc2.cc_number_enc, cc.cc_number_enc);
299}