Remove mentat-backed implementation of login storage
This commit is contained in:
Родитель
46dc121469
Коммит
b8ee8b90b2
|
@ -2,11 +2,8 @@
|
|||
members = [
|
||||
"fxa-client",
|
||||
"fxa-client/ffi",
|
||||
"logins",
|
||||
"sandvich/desktop",
|
||||
"sync15-adapter",
|
||||
"sync15/passwords",
|
||||
"sync15/passwords/ffi",
|
||||
"sync15-adapter"
|
||||
]
|
||||
|
||||
# For RSA keys cloning. Remove once openssl 0.10.8+ is released.
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
[package]
|
||||
name = "logins"
|
||||
version = "0.0.1"
|
||||
|
||||
[lib]
|
||||
name = "logins"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
lazy_static = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
serde = "^1.0.63"
|
||||
serde_derive = "^1.0.63"
|
||||
serde_json = "1.0"
|
||||
|
||||
[dependencies.mentat]
|
||||
git = "https://github.com/mozilla/mentat"
|
||||
tag = "v0.8.1"
|
||||
features = ["sqlcipher"]
|
||||
default_features = false
|
|
@ -1,646 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
//! An interface to *credentials* (username/password pairs, optionally titled) and *logins* (usages
|
||||
//! at points in time), stored in a Mentat store.
|
||||
//!
|
||||
//! [`Credential`] is the main type exposed. Credentials are identified by opaque IDs.
|
||||
//!
|
||||
//! Store credentials in Mentat with [`add_credential`]. Retrieve credentials present in the Mentat
|
||||
//! store with [`get_credential`] and [`get_all_credentials`].
|
||||
//!
|
||||
//! Record local usages of a credential with [`touch_by_id`]. Retrieve local metadata about a
|
||||
//! credential with [`times_used`], [`time_last_used`], and [`time_last_modified`].
|
||||
//!
|
||||
//! Remove credentials from the Mentat store with [`delete_by_id`] and [`delete_by_ids`].
|
||||
|
||||
use mentat::{
|
||||
Binding,
|
||||
DateTime,
|
||||
Entid,
|
||||
QueryInputs,
|
||||
QueryResults,
|
||||
Queryable,
|
||||
StructuredMap,
|
||||
TxReport,
|
||||
TypedValue,
|
||||
Utc,
|
||||
};
|
||||
|
||||
use mentat::entity_builder::{
|
||||
BuildTerms,
|
||||
TermBuilder,
|
||||
};
|
||||
|
||||
use mentat::conn::{
|
||||
InProgress,
|
||||
};
|
||||
|
||||
use errors::{
|
||||
Error,
|
||||
Result,
|
||||
};
|
||||
use types::{
|
||||
Credential,
|
||||
CredentialId,
|
||||
};
|
||||
use vocab::{
|
||||
CREDENTIAL_ID,
|
||||
CREDENTIAL_USERNAME,
|
||||
CREDENTIAL_PASSWORD,
|
||||
CREDENTIAL_CREATED_AT,
|
||||
CREDENTIAL_TITLE,
|
||||
LOGIN_AT,
|
||||
// TODO: connect logins to specific LOGIN_DEVICE.
|
||||
LOGIN_CREDENTIAL,
|
||||
// TODO: connect logins to LOGIN_FORM.
|
||||
};
|
||||
|
||||
impl Credential {
|
||||
/// Produce a `Credential` from a structured map (as returned by a pull expression).
|
||||
pub(crate) fn from_structured_map(map: &StructuredMap) -> Option<Self> {
|
||||
let id = map[&*CREDENTIAL_ID].as_string().map(|x| (**x).clone()).map(CredentialId).unwrap(); // XXX
|
||||
let username = map.get(&*CREDENTIAL_USERNAME).and_then(|username| username.as_string()).map(|x| (**x).clone()); // XXX
|
||||
let password = map[&*CREDENTIAL_PASSWORD].as_string().map(|x| (**x).clone()).unwrap(); // XXX
|
||||
let created_at = map[&*CREDENTIAL_CREATED_AT].as_instant().map(|x| (*x).clone()).unwrap(); // XXX
|
||||
let title = map.get(&*CREDENTIAL_TITLE).and_then(|username| username.as_string()).map(|x| (**x).clone()); // XXX
|
||||
// TODO: device.
|
||||
|
||||
Some(Credential {
|
||||
id,
|
||||
created_at,
|
||||
username,
|
||||
password,
|
||||
title,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert the given `credential` against the given `builder`.
|
||||
///
|
||||
/// N.b., this uses the (globally) named tempid "c", so it can't be used twice against the same
|
||||
/// builder!
|
||||
pub(crate) fn build_credential(builder: &mut TermBuilder, credential: Credential) -> Result<()> {
|
||||
let c = builder.named_tempid("c");
|
||||
|
||||
builder.add(c.clone(),
|
||||
CREDENTIAL_ID.clone(),
|
||||
TypedValue::typed_string(credential.id))?;
|
||||
if let Some(username) = credential.username {
|
||||
builder.add(c.clone(),
|
||||
CREDENTIAL_USERNAME.clone(),
|
||||
TypedValue::String(username.into()))?;
|
||||
}
|
||||
builder.add(c.clone(),
|
||||
CREDENTIAL_PASSWORD.clone(),
|
||||
TypedValue::String(credential.password.into()))?;
|
||||
// TODO: set created to the transaction timestamp. This might require implementing
|
||||
// (transaction-instant), which requires some thought because it is a "delayed binding".
|
||||
builder.add(c.clone(),
|
||||
CREDENTIAL_CREATED_AT.clone(),
|
||||
TypedValue::Instant(credential.created_at))?;
|
||||
if let Some(title) = credential.title {
|
||||
builder.add(c.clone(),
|
||||
CREDENTIAL_TITLE.clone(),
|
||||
TypedValue::String(title.into()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transact the given `credential` against the given `InProgress` write.
|
||||
///
|
||||
/// If a credential with the given ID exists, it will be modified in place.
|
||||
pub fn add_credential(in_progress: &mut InProgress, credential: Credential) -> Result<TxReport> {
|
||||
let mut builder = TermBuilder::new();
|
||||
build_credential(&mut builder, credential.clone())?;
|
||||
in_progress.transact_builder(builder).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Fetch the credential with given `id`.
|
||||
pub fn get_credential<Q>(queryable: &Q, id: CredentialId) -> Result<Option<Credential>> where Q: Queryable {
|
||||
let q = r#"[:find
|
||||
(pull ?c [:credential/id :credential/username :credential/password :credential/createdAt :credential/title]) .
|
||||
:in
|
||||
?id
|
||||
:where
|
||||
[?c :credential/id ?id]
|
||||
]"#;
|
||||
|
||||
let inputs = QueryInputs::with_value_sequence(vec![
|
||||
(var!(?id), TypedValue::typed_string(&id)),
|
||||
]);
|
||||
|
||||
let scalar = queryable.q_once(q, inputs)?.into_scalar()?;
|
||||
let credential = match scalar {
|
||||
Some(Binding::Map(cm)) => Ok(Credential::from_structured_map(cm.as_ref())),
|
||||
Some(other) => {
|
||||
error!("Unexpected query result: {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
},
|
||||
None => Ok(None),
|
||||
};
|
||||
|
||||
credential
|
||||
}
|
||||
|
||||
/// Fetch all known credentials.
|
||||
///
|
||||
/// No ordering is implied.
|
||||
pub fn get_all_credentials<Q>(queryable: &Q) -> Result<Vec<Credential>>
|
||||
where Q: Queryable {
|
||||
let q = r#"[
|
||||
:find
|
||||
[?id ...]
|
||||
:where
|
||||
[_ :credential/id ?id]
|
||||
:order
|
||||
(asc ?id) ; We order for testing convenience.
|
||||
]"#;
|
||||
|
||||
let ids: Result<Vec<_>> = queryable.q_once(q, None)?
|
||||
.into_coll()?
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
match id {
|
||||
Binding::Scalar(TypedValue::String(id)) => Ok(CredentialId((*id).clone())),
|
||||
other => {
|
||||
error!("Unexpected query result: {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let ids = ids?;
|
||||
|
||||
// TODO: do this more efficiently.
|
||||
let mut cs = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids {
|
||||
get_credential(queryable, id)?.map(|c| cs.push(c));
|
||||
}
|
||||
|
||||
Ok(cs)
|
||||
}
|
||||
|
||||
/// Record a local usage of the credential with given `id`, optionally `at` the given timestamp.
|
||||
pub fn touch_by_id(in_progress: &mut InProgress, id: CredentialId, at: Option<DateTime<Utc>>) -> Result<TxReport> {
|
||||
// TODO: Also record device.
|
||||
|
||||
let mut builder = TermBuilder::new();
|
||||
let l = builder.named_tempid("l");
|
||||
|
||||
// New login.
|
||||
builder.add(l.clone(),
|
||||
LOGIN_AT.clone(),
|
||||
// TODO: implement and use (tx-instant).
|
||||
TypedValue::Instant(at.unwrap_or_else(|| ::mentat::now())))?;
|
||||
builder.add(l.clone(),
|
||||
LOGIN_CREDENTIAL.clone(),
|
||||
TermBuilder::lookup_ref(CREDENTIAL_ID.clone(), TypedValue::typed_string(id)))?;
|
||||
|
||||
in_progress.transact_builder(builder).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Delete the credential with the given `id`, if one exists.
|
||||
pub fn delete_by_id(in_progress: &mut InProgress, id: CredentialId) -> Result<bool> {
|
||||
Ok(delete_by_ids(in_progress, ::std::iter::once(id))? == 1)
|
||||
}
|
||||
|
||||
/// Delete credentials with the given `ids`, if any exist.
|
||||
pub fn delete_by_ids<I>(in_progress: &mut InProgress, ids: I) -> Result<usize>
|
||||
where I: IntoIterator<Item=CredentialId> {
|
||||
// TODO: implement and use some version of `:db/retractEntity`, rather than onerously deleting
|
||||
// credential data and usage data.
|
||||
//
|
||||
// N.b., I'm not deleting the dangling link from `:sync.password/credential` here. That's a
|
||||
// choice; not deleting that link allows the Sync password to discover that its underlying
|
||||
// credential has been removed (although, deleting that link reveals the information as well).
|
||||
// Using `:db/retractEntity` in some form impacts this decision.
|
||||
let q = r#"[
|
||||
:find
|
||||
?e ?a ?v
|
||||
:in
|
||||
?id
|
||||
:where
|
||||
(or-join [?e ?a ?v ?id]
|
||||
(and
|
||||
[?e :credential/id ?id]
|
||||
[?e ?a ?v])
|
||||
(and
|
||||
[?c :credential/id ?id]
|
||||
[?e :login/credential ?c]
|
||||
[?e ?a ?v]))
|
||||
]"#;
|
||||
|
||||
let mut builder = TermBuilder::new();
|
||||
let mut deleted = 0;
|
||||
for id in ids {
|
||||
let inputs = QueryInputs::with_value_sequence(vec![(var!(?id), TypedValue::typed_string(id))]);
|
||||
let results = in_progress.q_once(q, inputs)?.results;
|
||||
|
||||
match results {
|
||||
QueryResults::Rel(vals) => {
|
||||
if vals.row_count() > 0 {
|
||||
deleted += 1;
|
||||
}
|
||||
for vs in vals {
|
||||
match (vs.len(), vs.get(0), vs.get(1), vs.get(2)) {
|
||||
(3, Some(&Binding::Scalar(TypedValue::Ref(e))), Some(&Binding::Scalar(TypedValue::Ref(a))), Some(&Binding::Scalar(ref v))) => {
|
||||
builder.retract(e, a, v.clone())?; // TODO: don't clone.
|
||||
}
|
||||
other => {
|
||||
error!("Unexpected query result: {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
other => {
|
||||
error!("Unexpected query result: {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
in_progress.transact_builder(builder).map_err(|e| e.into()).and(Ok(deleted))
|
||||
}
|
||||
|
||||
/// Find a credential matching the given `username` and `password`, if one exists.
|
||||
///
|
||||
/// It is possible that multiple credentials match, in which case one is chosen at random. (This is
|
||||
/// an impedance mismatch between the model of logins we're driving towards and the requirements of
|
||||
/// Sync 1.5 passwords to do content-aware merging.)
|
||||
pub fn find_credential_by_content<Q>(queryable: &Q, username: String, password: String) -> Result<Option<Credential>>
|
||||
where Q: Queryable
|
||||
{
|
||||
let q = r#"[:find ?id .
|
||||
:in
|
||||
?username ?password
|
||||
:where
|
||||
[?c :credential/id ?id]
|
||||
[?c :credential/username ?username]
|
||||
[?c :credential/password ?password]]"#;
|
||||
|
||||
let inputs = QueryInputs::with_value_sequence(vec![(var!(?username), TypedValue::String(username.clone().into())),
|
||||
(var!(?password), TypedValue::String(password.clone().into()))]);
|
||||
let id = match queryable.q_once(q, inputs)?.into_scalar()? {
|
||||
Some(x) => {
|
||||
match x.into_string() {
|
||||
Some(x) => CredentialId((*x).clone()),
|
||||
None => {
|
||||
error!("Unexpected query result! find_credential_by_content returned None");
|
||||
bail!(Error::BadQueryResultType);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
get_credential(queryable, id)
|
||||
}
|
||||
|
||||
/// Return the number of times the credential with given `id` has been used locally, or `None` if
|
||||
/// such a credential doesn't exist, optionally limiting to usages strictly after the given
|
||||
/// `after_tx`.
|
||||
// TODO: u64.
|
||||
// TODO: filter by devices.
|
||||
pub fn times_used<Q>(queryable: &Q, id: CredentialId, after_tx: Option<Entid>) -> Result<Option<i64>>
|
||||
where Q: Queryable
|
||||
{
|
||||
// TODO: Don't run this first query to determine if a credential (ID) exists. This is only here
|
||||
// because it's surprisingly awkward to return `None` rather than `0` for a non-existent
|
||||
// credential ID.
|
||||
if get_credential(queryable, id.clone())?.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let q = r#"[:find
|
||||
(count ?l) .
|
||||
:in
|
||||
?id ?after_tx
|
||||
:where
|
||||
[?c :credential/id ?id]
|
||||
[?l :login/credential ?c]
|
||||
[?l :login/at _ ?login-tx]
|
||||
[(tx-after ?login-tx ?after_tx)]]"#;
|
||||
|
||||
// TODO: drop the comparison when `after_tx` is `None`.
|
||||
let values =
|
||||
QueryInputs::with_value_sequence(vec![(var!(?id), TypedValue::typed_string(&id)),
|
||||
(var!(?after_tx), TypedValue::Ref(after_tx.unwrap_or(0)))]);
|
||||
|
||||
let local_times_used = match queryable.q_once(q, values)?.into_scalar()? {
|
||||
Some(Binding::Scalar(TypedValue::Long(times_used))) => Some(times_used), // TODO: work out overflow for u64.
|
||||
None => None,
|
||||
Some(other) => {
|
||||
error!("Unexpected result from times_used query! {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
},
|
||||
};
|
||||
|
||||
Ok(local_times_used)
|
||||
}
|
||||
|
||||
/// Return the last time the credential with given `id` was used locally, or `None` if such a
|
||||
/// credential doesn't exist, optionally limiting to usages strictly after the given `after_tx`.
|
||||
// TODO: filter by devices.
|
||||
pub fn time_last_used<Q>(queryable: &Q, id: CredentialId, after_tx: Option<Entid>) -> Result<Option<DateTime<Utc>>>
|
||||
where Q: Queryable
|
||||
{
|
||||
let q = r#"[:find
|
||||
(max ?at) .
|
||||
:in
|
||||
?id ?after_tx
|
||||
:where
|
||||
[?c :credential/id ?id]
|
||||
[?l :login/credential ?c]
|
||||
[?l :login/at ?at ?login-tx]
|
||||
[(tx-after ?login-tx ?after_tx)]
|
||||
]"#;
|
||||
|
||||
// TODO: drop the comparison when `after_tx` is `None`.
|
||||
let values =
|
||||
QueryInputs::with_value_sequence(vec![(var!(?id), TypedValue::typed_string(id)),
|
||||
(var!(?after_tx), TypedValue::Ref(after_tx.unwrap_or(0)))]);
|
||||
|
||||
let local_time_last_used = match queryable.q_once(q, values)?.into_scalar()? {
|
||||
Some(Binding::Scalar(TypedValue::Instant(time_last_used))) => Some(time_last_used),
|
||||
None => None,
|
||||
Some(other) => {
|
||||
error!("Unexpected query result! {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(local_time_last_used)
|
||||
}
|
||||
|
||||
/// Return the last time the credential with given `id` was modified locally, or `None` if such a
|
||||
/// credential doesn't exist.
|
||||
pub fn time_last_modified<Q>(queryable: &Q, id: CredentialId) -> Result<Option<DateTime<Utc>>>
|
||||
where Q: Queryable
|
||||
{
|
||||
// TODO: handle optional usernames.
|
||||
let q = r#"[:find
|
||||
[?username-txInstant ?password-txInstant]
|
||||
:in
|
||||
?id
|
||||
:where
|
||||
[?credential :credential/id ?id]
|
||||
[?credential :credential/username ?username ?username-tx]
|
||||
[?username-tx :db/txInstant ?username-txInstant]
|
||||
[?credential :credential/password ?password ?password-tx]
|
||||
[?password-tx :db/txInstant ?password-txInstant]]"#;
|
||||
let inputs = QueryInputs::with_value_sequence(vec![(var!(?id), TypedValue::typed_string(id))]);
|
||||
|
||||
match queryable.q_once(q, inputs)?.into_tuple()? {
|
||||
Some((Binding::Scalar(TypedValue::Instant(username_tx_instant)),
|
||||
Binding::Scalar(TypedValue::Instant(password_tx_instant)))) => {
|
||||
let last_modified = ::std::cmp::max(username_tx_instant, password_tx_instant);
|
||||
Ok(Some(last_modified))
|
||||
},
|
||||
None => Ok(None),
|
||||
Some(other) => {
|
||||
error!("Unexpected query result: {:?}", other);
|
||||
bail!(Error::BadQueryResultType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mentat::{
|
||||
FromMicros,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
use tests::{
|
||||
testing_store,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref CREDENTIAL1: Credential = {
|
||||
Credential {
|
||||
id: CredentialId("1".into()),
|
||||
username: Some("user1@mockymid.com".into()),
|
||||
password: "password1".into(),
|
||||
created_at: DateTime::<Utc>::from_micros(1523908112453),
|
||||
title: None,
|
||||
}
|
||||
};
|
||||
|
||||
static ref CREDENTIAL2: Credential = {
|
||||
Credential {
|
||||
id: CredentialId("2".into()),
|
||||
username: Some("user2@mockymid.com".into()),
|
||||
password: "password2".into(),
|
||||
created_at: DateTime::<Utc>::from_micros(1523909000000),
|
||||
title: Some("marché".into()), // Observe accented character.
|
||||
}
|
||||
};
|
||||
|
||||
static ref CREDENTIAL_WITHOUT_USERNAME: Credential = {
|
||||
Credential {
|
||||
id: CredentialId("3".into()),
|
||||
username: None,
|
||||
password: "password3".into(),
|
||||
created_at: DateTime::<Utc>::from_micros(1523909111111),
|
||||
title: Some("credential without username".into()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// First, let's add a single credential.
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
|
||||
let c = get_credential(&in_progress, CREDENTIAL1.id.clone()).expect("to get_credential 1");
|
||||
assert_eq!(Some(CREDENTIAL1.clone()), c);
|
||||
|
||||
let cs = get_all_credentials(&in_progress).expect("to get_all_credentials 1");
|
||||
assert_eq!(vec![CREDENTIAL1.clone()], cs);
|
||||
|
||||
// Now a second one.
|
||||
add_credential(&mut in_progress, CREDENTIAL2.clone()).expect("to add_credential 2");
|
||||
|
||||
let c = get_credential(&in_progress, CREDENTIAL1.id.clone()).expect("to get_credential 1");
|
||||
assert_eq!(Some(CREDENTIAL1.clone()), c);
|
||||
|
||||
let c = get_credential(&in_progress, CREDENTIAL2.id.clone()).expect("to get_credential 2");
|
||||
assert_eq!(Some(CREDENTIAL2.clone()), c);
|
||||
|
||||
let cs = get_all_credentials(&in_progress).expect("to get_all_credentials 2");
|
||||
assert_eq!(vec![CREDENTIAL1.clone(), CREDENTIAL2.clone()], cs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_without_username() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// Let's verify that we can serialize and deserialize a credential without a username.
|
||||
add_credential(&mut in_progress, CREDENTIAL_WITHOUT_USERNAME.clone()).unwrap();
|
||||
|
||||
let c = get_credential(&in_progress, CREDENTIAL_WITHOUT_USERNAME.id.clone()).unwrap();
|
||||
assert_eq!(Some(CREDENTIAL_WITHOUT_USERNAME.clone()), c);
|
||||
|
||||
let cs = get_all_credentials(&in_progress).unwrap();
|
||||
assert_eq!(vec![CREDENTIAL_WITHOUT_USERNAME.clone()], cs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_by_id() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// First, let's add a few credentials.
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
add_credential(&mut in_progress, CREDENTIAL2.clone()).expect("to add_credential 2");
|
||||
|
||||
let deleted = delete_by_id(&mut in_progress, CREDENTIAL1.id.clone()).expect("to delete by id");
|
||||
assert!(deleted);
|
||||
|
||||
// The record's gone.
|
||||
let c = get_credential(&in_progress,
|
||||
CREDENTIAL1.id.clone()).expect("to get_credential");
|
||||
assert_eq!(c, None);
|
||||
|
||||
// If we try to delete again, that's okay.
|
||||
let deleted = delete_by_id(&mut in_progress, CREDENTIAL1.id.clone()).expect("to delete by id when it's already deleted");
|
||||
assert!(!deleted);
|
||||
|
||||
let c = get_credential(&in_progress,
|
||||
CREDENTIAL1.id.clone()).expect("to get_credential");
|
||||
assert_eq!(c, None);
|
||||
|
||||
// The other password wasn't deleted.
|
||||
let c = get_credential(&in_progress,
|
||||
CREDENTIAL2.id.clone()).expect("to get_credential");
|
||||
assert_eq!(c, Some(CREDENTIAL2.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_by_ids() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// First, let's add a few credentials.
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
add_credential(&mut in_progress, CREDENTIAL2.clone()).expect("to add_credential 2");
|
||||
|
||||
let iters = ::std::iter::once(CREDENTIAL1.id.clone()).chain(::std::iter::once(CREDENTIAL2.id.clone()));
|
||||
let count = delete_by_ids(&mut in_progress, iters.clone()).expect("to delete_by_ids");
|
||||
|
||||
assert_eq!(count, 2);
|
||||
|
||||
// The records are gone.
|
||||
let c = get_credential(&in_progress,
|
||||
CREDENTIAL1.id.clone()).expect("to get_credential");
|
||||
assert_eq!(c, None);
|
||||
|
||||
let c = get_credential(&in_progress,
|
||||
CREDENTIAL2.id.clone()).expect("to get_credential");
|
||||
assert_eq!(c, None);
|
||||
|
||||
// If we try to delete again, that's okay.
|
||||
let count = delete_by_ids(&mut in_progress, iters.clone()).expect("to delete_by_ids");
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_credential_by_content() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
|
||||
let c = find_credential_by_content(&in_progress,
|
||||
CREDENTIAL1.username.clone().unwrap(),
|
||||
CREDENTIAL1.password.clone()).expect("to find_credential_by_content");
|
||||
assert_eq!(c, Some(CREDENTIAL1.clone()));
|
||||
|
||||
let c = find_credential_by_content(&in_progress,
|
||||
"incorrect username".to_string(),
|
||||
CREDENTIAL1.password.clone()).expect("to find_credential_by_content");
|
||||
assert_eq!(c, None);
|
||||
|
||||
let c = find_credential_by_content(&in_progress,
|
||||
CREDENTIAL1.username.clone().unwrap(),
|
||||
"incorrect password".to_string()).expect("to find_credential_by_content");
|
||||
assert_eq!(c, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_times_used() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// First, let's add a few credentials.
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
add_credential(&mut in_progress, CREDENTIAL2.clone()).expect("to add_credential 2");
|
||||
|
||||
let report1 = touch_by_id(&mut in_progress, CREDENTIAL1.id.clone(), None).expect("touch_by_id");
|
||||
let now1 = ::mentat::now();
|
||||
|
||||
let report2 = touch_by_id(&mut in_progress, CREDENTIAL2.id.clone(), None).expect("touch_by_id");
|
||||
let now2 = ::mentat::now();
|
||||
|
||||
touch_by_id(&mut in_progress, CREDENTIAL1.id.clone(), Some(now1)).expect("touch_by_id");
|
||||
let report3 = touch_by_id(&mut in_progress, CREDENTIAL2.id.clone(), Some(now2)).expect("touch_by_id");
|
||||
|
||||
assert_eq!(None, times_used(&in_progress, "unknown credential".into(), None).expect("times_used"));
|
||||
|
||||
assert_eq!(Some(2), times_used(&in_progress, CREDENTIAL1.id.clone(), None).expect("times_used"));
|
||||
assert_eq!(Some(1), times_used(&in_progress, CREDENTIAL1.id.clone(), Some(report1.tx_id)).expect("times_used"));
|
||||
assert_eq!(Some(1), times_used(&in_progress, CREDENTIAL1.id.clone(), Some(report2.tx_id)).expect("times_used"));
|
||||
assert_eq!(Some(0), times_used(&in_progress, CREDENTIAL1.id.clone(), Some(report3.tx_id)).expect("times_used"));
|
||||
|
||||
assert_eq!(Some(2), times_used(&in_progress, CREDENTIAL2.id.clone(), None).expect("times_used"));
|
||||
assert_eq!(Some(2), times_used(&in_progress, CREDENTIAL2.id.clone(), Some(report1.tx_id)).expect("times_used"));
|
||||
assert_eq!(Some(1), times_used(&in_progress, CREDENTIAL2.id.clone(), Some(report2.tx_id)).expect("times_used"));
|
||||
assert_eq!(Some(0), times_used(&in_progress, CREDENTIAL2.id.clone(), Some(report3.tx_id)).expect("times_used"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_time_used() {
|
||||
let mut store = testing_store();
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
// First, let's add a few credentials.
|
||||
add_credential(&mut in_progress, CREDENTIAL1.clone()).expect("to add_credential 1");
|
||||
add_credential(&mut in_progress, CREDENTIAL2.clone()).expect("to add_credential 2");
|
||||
|
||||
// Just so there is a visit for credential 2, in case there is an error across credentials.
|
||||
touch_by_id(&mut in_progress, CREDENTIAL2.id.clone(), None).expect("touch_by_id");
|
||||
|
||||
touch_by_id(&mut in_progress, CREDENTIAL1.id.clone(), None).expect("touch_by_id");
|
||||
let now1 = ::mentat::now();
|
||||
touch_by_id(&mut in_progress, CREDENTIAL1.id.clone(), Some(now1)).expect("touch_by_id");
|
||||
|
||||
assert_eq!(None, time_last_used(&in_progress, "unknown credential".into(), None).expect("time_last_used"));
|
||||
|
||||
assert_eq!(Some(now1), time_last_used(&in_progress, CREDENTIAL1.id.clone(), None).expect("time_last_used"));
|
||||
|
||||
// This is a little unusual. We're going to record consecutive usages with timestamps going
|
||||
// backwards in time.
|
||||
let now2 = ::mentat::now();
|
||||
let report = touch_by_id(&mut in_progress, CREDENTIAL2.id.clone(), Some(now2)).expect("touch_by_id");
|
||||
touch_by_id(&mut in_progress, CREDENTIAL2.id.clone(), Some(now1)).expect("touch_by_id");
|
||||
|
||||
assert_eq!(Some(now2), time_last_used(&in_progress, CREDENTIAL2.id.clone(), None).expect("time_last_used"));
|
||||
assert_eq!(Some(now1), time_last_used(&in_progress, CREDENTIAL2.id.clone(), Some(report.tx_id)).expect("time_last_used"));
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std; // To refer to std::result::Result.
|
||||
|
||||
use mentat::{
|
||||
MentatError,
|
||||
};
|
||||
use failure::Fail;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($e:expr) => (
|
||||
return Err($e.into());
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "bad query result type")]
|
||||
BadQueryResultType,
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
MentatError(#[cause] MentatError),
|
||||
}
|
||||
|
||||
// Because Mentat doesn't expose its entire API from the top-level `mentat` crate, we sometimes
|
||||
// witness error types that are logically subsumed by `MentatError`. We wrap those here, since
|
||||
// _our_ consumers should not care about the specific Mentat error type.
|
||||
impl<E: Into<MentatError> + std::fmt::Debug> From<E> for Error {
|
||||
fn from(error: E) -> Error {
|
||||
error!("MentatError -> LoginsError {:?}", error);
|
||||
let mentat_err: MentatError = error.into();
|
||||
if let Some(bt) = mentat_err.backtrace() {
|
||||
debug!("Backtrace: {:?}", bt);
|
||||
}
|
||||
Error::MentatError(mentat_err)
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
///! This module implements special `serde` support for `ServerPassword` instances.
|
||||
///!
|
||||
///! Unfortunately, there doesn't seem to be a good way to directly deserialize `ServerPassword`
|
||||
///! from JSON because of `target`. In theory `#[serde(flatten)]` on that property would do it, but
|
||||
///! Firefox for Desktop writes records like `{"httpRealm": null, "formSubmitURL": "..."}`, e.g.,
|
||||
///! where both fields are present, but one is `null`. This breaks `serde`. We therefore use a
|
||||
///! custom serializer and deserializer through the `SerializablePassword` type.
|
||||
|
||||
use serde::{
|
||||
self,
|
||||
Deserializer,
|
||||
Serializer,
|
||||
};
|
||||
|
||||
use mentat::{
|
||||
DateTime,
|
||||
FromMillis,
|
||||
ToMillis,
|
||||
Utc,
|
||||
};
|
||||
|
||||
use types::{
|
||||
FormTarget,
|
||||
ServerPassword,
|
||||
SyncGuid,
|
||||
};
|
||||
|
||||
fn zero_timestamp() -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_millis(0)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SerializablePassword {
|
||||
pub id: String,
|
||||
pub hostname: String,
|
||||
|
||||
#[serde(rename = "formSubmitURL")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub form_submit_url: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub http_realm: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
pub password: String,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub username_field: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password_field: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub time_created: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub time_password_changed: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub time_last_used: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub times_used: usize,
|
||||
}
|
||||
|
||||
impl From<ServerPassword> for SerializablePassword {
|
||||
fn from(sp: ServerPassword) -> SerializablePassword {
|
||||
let (form_submit_url, http_realm) = match sp.target {
|
||||
FormTarget::FormSubmitURL(url) => (Some(url), None),
|
||||
FormTarget::HttpRealm(realm) => (None, Some(realm)),
|
||||
};
|
||||
SerializablePassword {
|
||||
id: sp.uuid.0,
|
||||
username_field: sp.username_field,
|
||||
password_field: sp.password_field,
|
||||
|
||||
form_submit_url,
|
||||
http_realm,
|
||||
|
||||
hostname: sp.hostname,
|
||||
username: sp.username,
|
||||
password: sp.password,
|
||||
|
||||
times_used: sp.times_used,
|
||||
time_password_changed: sp.time_password_changed.to_millis(),
|
||||
time_last_used: sp.time_last_used.to_millis(),
|
||||
time_created: sp.time_created.to_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::Serialize for ServerPassword {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
SerializablePassword::from(self.clone()).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for ServerPassword {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<ServerPassword, D::Error> {
|
||||
let s = SerializablePassword::deserialize(deserializer)?;
|
||||
let target = match (s.form_submit_url, s.http_realm) {
|
||||
(Some(_), Some(_)) =>
|
||||
return Err(serde::de::Error::custom("ServerPassword has both formSubmitURL and httpRealm")),
|
||||
(None, None) =>
|
||||
return Err(serde::de::Error::custom("ServerPassword is missing both formSubmitURL and httpRealm")),
|
||||
(Some(url), None) =>
|
||||
FormTarget::FormSubmitURL(url),
|
||||
(None, Some(realm)) =>
|
||||
FormTarget::HttpRealm(realm),
|
||||
};
|
||||
|
||||
Ok(ServerPassword {
|
||||
uuid: SyncGuid(s.id),
|
||||
modified: zero_timestamp(),
|
||||
hostname: s.hostname,
|
||||
username: s.username,
|
||||
password: s.password,
|
||||
target,
|
||||
username_field: s.username_field,
|
||||
password_field: s.password_field,
|
||||
times_used: s.times_used,
|
||||
time_created: FromMillis::from_millis(s.time_created),
|
||||
time_last_used: FromMillis::from_millis(s.time_last_used),
|
||||
time_password_changed: FromMillis::from_millis(s.time_password_changed),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
//! This crate is an interface for working with Sync 1.5 passwords and arbitrary logins.
|
||||
//!
|
||||
//! We use "passwords" or "password records" to talk about Sync 1.5's object format stored in the
|
||||
//! "passwords" collection. We use "logins" to talk about local credentials, which will grow to be
|
||||
//! more general than Sync 1.5's limited object format.
|
||||
//!
|
||||
//! For Sync 1.5 passwords, we reference the somewhat out-dated but still useful [client
|
||||
//! documentation](https://mozilla-services.readthedocs.io/en/latest/sync/objectformats.html#passwords).
|
||||
//!
|
||||
//! # Data model
|
||||
//!
|
||||
//! There are three fundamental parts to the model of logins implemented:
|
||||
//! 1. *credentials* are username/password pairs
|
||||
//! 1. *forms* are contexts where credentials can be used
|
||||
//! 1. *logins* are usages: this *credential* was used to login to this *form*
|
||||
//!
|
||||
//! In this model, a user might have a single username/password pair for their Google Account;
|
||||
//! enter it into multiple forms (say, login forms on "mail.google.com" and "calendar.google.com",
|
||||
//! and a password reset form on "accounts.google.com"); and have used the login forms weekly but
|
||||
//! the password reset form only once.
|
||||
//!
|
||||
//! This model can grow to accommodate new types of credentials and new contexts for usage. A new
|
||||
//! credential might be a hardware key (like Yubikey) that is identified by a device serial number;
|
||||
//! or it might be a cookie from a web browser login. And a password manager might be on a mobile
|
||||
//! device and not embedded in a Web browser: it might provide credentials to specific Apps as a
|
||||
//! platform-specific password filling API. In this case, the context is not a *form*.
|
||||
//!
|
||||
//! To support Sync 1.5, we add a fourth fundamental part to the model: a Sync password notion that
|
||||
//! glues together a credential, a form, and some materialized logins usage data. The
|
||||
//! [`ServerPassword`] type captures these notions.
|
||||
//!
|
||||
//! # Limitations of the Sync 1.5 object model
|
||||
//!
|
||||
//! There are many limitations of the Sync 1.5 object model, but the two most significant for this
|
||||
//! implementation are:
|
||||
//!
|
||||
//! 1. A consumer that is *not a Web browser* can't smoothly create Sync 1.5 password records!
|
||||
//! Consider the password manager on a mobile device not embedded in a Web browser: there is no way
|
||||
//! for it to associate login usage with a particular web site, let alone a particular form. That
|
||||
//! is, the only usage context that Sync 1.5 password records accommodates looks exactly like
|
||||
//! Firefox's usage context. (Any consumer can fabricate required entries in the `ServerPassword`
|
||||
//! type, or require the user to provide them -- but the product experience will suffer.)
|
||||
//!
|
||||
//! 1. It can't represent the use of the same username/password pair across more than one site,
|
||||
//! leading to the creation of add-ons like
|
||||
//! [mass-password-reset](https://addons.mozilla.org/en-US/firefox/addon/mass-password-reset/). There
|
||||
//! is a many-to-many relationship between credentials and forms. Firefox Desktop and Firefox Sync
|
||||
//! both duplicate credentials when they're saved after use in multiple places. But conversely,
|
||||
//! note that there are situations in which the same username and password mean different things:
|
||||
//! the most common is password reuse.
|
||||
|
||||
#![recursion_limit="128"]
|
||||
|
||||
#![crate_name = "logins"]
|
||||
|
||||
extern crate chrono;
|
||||
extern crate failure;
|
||||
#[macro_use] extern crate failure_derive;
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
#[macro_use] extern crate mentat;
|
||||
|
||||
pub mod credentials;
|
||||
pub mod errors;
|
||||
pub use errors::{
|
||||
Error,
|
||||
Result,
|
||||
};
|
||||
mod json;
|
||||
pub mod passwords;
|
||||
pub mod types;
|
||||
pub use types::{
|
||||
Credential,
|
||||
CredentialId,
|
||||
FormTarget,
|
||||
ServerPassword,
|
||||
SyncGuid,
|
||||
};
|
||||
mod vocab;
|
||||
pub use vocab::{
|
||||
CREDENTIAL_VOCAB,
|
||||
FORM_VOCAB,
|
||||
LOGIN_VOCAB,
|
||||
ensure_vocabulary,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use mentat::{
|
||||
Store,
|
||||
};
|
||||
|
||||
pub(crate) fn testing_store() -> Store {
|
||||
let mut store = Store::open("").expect("opened");
|
||||
|
||||
// Scoped borrow of `store`.
|
||||
{
|
||||
let mut in_progress = store.begin_transaction().expect("begun successfully");
|
||||
|
||||
ensure_vocabulary(&mut in_progress).expect("to ensure_vocabulary");
|
||||
|
||||
in_progress.commit().expect("commit succeeded");
|
||||
}
|
||||
|
||||
store
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,122 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
///! This module defines some core types that support Sync 1.5 passwords and arbitrary logins.
|
||||
|
||||
use std::convert::{
|
||||
AsRef,
|
||||
};
|
||||
|
||||
use mentat::{
|
||||
DateTime,
|
||||
Utc,
|
||||
Uuid,
|
||||
};
|
||||
|
||||
/// Firefox Sync password records must have at least a formSubmitURL or httpRealm, but not both.
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)] // , Serialize, Deserialize)]
|
||||
pub enum FormTarget {
|
||||
// #[serde(rename = "httpRealm")]
|
||||
HttpRealm(String),
|
||||
|
||||
// #[serde(rename = "formSubmitURL")]
|
||||
FormSubmitURL(String),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SyncGuid(pub String);
|
||||
|
||||
impl AsRef<str> for SyncGuid {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for SyncGuid where T: Into<String> {
|
||||
fn from(x: T) -> SyncGuid {
|
||||
SyncGuid(x.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Sync 1.5 password record.
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub struct ServerPassword {
|
||||
/// The UUID of this record, returned by the remote server as part of this record's envelope.
|
||||
///
|
||||
/// For historical reasons, Sync 1.5 passwords use a UUID rather than a (9 character) GUID like
|
||||
/// other collections.
|
||||
pub uuid: SyncGuid,
|
||||
|
||||
/// The time last modified, returned by the remote server as part of this record's envelope.
|
||||
pub modified: DateTime<Utc>,
|
||||
|
||||
/// Material fields. A password without a username corresponds to an XXX.
|
||||
pub hostname: String,
|
||||
pub username: Option<String>,
|
||||
pub password: String,
|
||||
|
||||
pub target: FormTarget,
|
||||
|
||||
/// Metadata. Unfortunately, not all clients pass-through (let alone collect and propagate!)
|
||||
/// metadata correctly.
|
||||
pub times_used: usize,
|
||||
|
||||
pub time_created: DateTime<Utc>,
|
||||
pub time_last_used: DateTime<Utc>,
|
||||
pub time_password_changed: DateTime<Utc>,
|
||||
|
||||
/// Mostly deprecated: these fields were once used to help with form fill.
|
||||
pub username_field: Option<String>,
|
||||
pub password_field: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub struct CredentialId(pub String);
|
||||
|
||||
impl AsRef<str> for CredentialId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialId {
|
||||
pub fn random() -> Self {
|
||||
CredentialId(Uuid::new_v4().hyphenated().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for CredentialId where T: Into<String> {
|
||||
fn from(x: T) -> CredentialId {
|
||||
CredentialId(x.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A username/password pair, optionally decorated with a user-specified title.
|
||||
///
|
||||
/// A credential is uniquely identified by its `id`.
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub struct Credential {
|
||||
/// A stable opaque identifier uniquely naming this credential.
|
||||
pub id: CredentialId,
|
||||
|
||||
// The username associated to this credential.
|
||||
pub username: Option<String>,
|
||||
|
||||
// The password associated to this credential.
|
||||
pub password: String,
|
||||
|
||||
// When the credential was created. This is best-effort: it's the timestamp observed by the
|
||||
// device on which the credential was created, which is incomparable with timestamps observed by
|
||||
// other devices in the constellation (including any servers).
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
/// An optional user-specified title of this credential, like `My LDAP`.
|
||||
pub title: Option<String>,
|
||||
}
|
|
@ -1,376 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
use mentat::{
|
||||
InProgress,
|
||||
Keyword,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat::vocabulary;
|
||||
use mentat::vocabulary::{
|
||||
VersionedStore,
|
||||
};
|
||||
|
||||
use errors::{
|
||||
Result,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CREDENTIAL_ID: Keyword = {
|
||||
kw!(:credential/id)
|
||||
};
|
||||
|
||||
pub static ref CREDENTIAL_USERNAME: Keyword = {
|
||||
kw!(:credential/username)
|
||||
};
|
||||
|
||||
pub static ref CREDENTIAL_PASSWORD: Keyword = {
|
||||
kw!(:credential/password)
|
||||
};
|
||||
|
||||
pub static ref CREDENTIAL_CREATED_AT: Keyword = {
|
||||
kw!(:credential/createdAt)
|
||||
};
|
||||
|
||||
pub static ref CREDENTIAL_TITLE: Keyword = {
|
||||
kw!(:credential/title)
|
||||
};
|
||||
|
||||
/// The vocabulary describing *credentials*, i.e., username/password pairs; `:credential/*`.
|
||||
///
|
||||
/// ```edn
|
||||
/// [:credential/username :db.type/string :db.cardinality/one]
|
||||
/// [:credential/password :db.type/string :db.cardinality/one]
|
||||
/// [:credential/created :db.type/instant :db.cardinality/one]
|
||||
/// ; An application might allow users to name their credentials; e.g., "My LDAP".
|
||||
/// [:credential/title :db.type/string :db.cardinality/one]
|
||||
/// ```
|
||||
pub static ref CREDENTIAL_VOCAB: vocabulary::Definition = {
|
||||
vocabulary::Definition {
|
||||
name: kw!(:org.mozilla/credential),
|
||||
version: 1,
|
||||
attributes: vec![
|
||||
(CREDENTIAL_ID.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.unique(vocabulary::attribute::Unique::Identity)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(CREDENTIAL_USERNAME.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(CREDENTIAL_PASSWORD.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(CREDENTIAL_CREATED_AT.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(CREDENTIAL_TITLE.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
],
|
||||
pre: vocabulary::Definition::no_op,
|
||||
post: vocabulary::Definition::no_op,
|
||||
}
|
||||
};
|
||||
|
||||
pub static ref LOGIN_AT: Keyword = {
|
||||
kw!(:login/at)
|
||||
};
|
||||
|
||||
pub static ref LOGIN_DEVICE: Keyword = {
|
||||
kw!(:login/device)
|
||||
};
|
||||
|
||||
pub static ref LOGIN_CREDENTIAL: Keyword = {
|
||||
kw!(:login/credential)
|
||||
};
|
||||
|
||||
pub static ref LOGIN_FORM: Keyword = {
|
||||
kw!(:login/form)
|
||||
};
|
||||
|
||||
/// The vocabulary describing *logins* (usages); `:logins/*`.
|
||||
///
|
||||
/// This is metadata capturing user behavior.
|
||||
///
|
||||
/// ```edn
|
||||
// [:login/at :db.type/instant :db.cardinality/one]
|
||||
// [:login/device :db.type/ref :db.cardinality/one]
|
||||
// [:login/credential :db.type/ref :db.cardinality/one]
|
||||
// [:login/form :db.type/ref :db.cardinality/one]
|
||||
/// ```
|
||||
pub static ref LOGIN_VOCAB: vocabulary::Definition = {
|
||||
vocabulary::Definition {
|
||||
name: kw!(:org.mozilla/login),
|
||||
version: 1,
|
||||
attributes: vec![
|
||||
(LOGIN_AT.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(LOGIN_DEVICE.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(LOGIN_CREDENTIAL.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(LOGIN_FORM.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.build()),
|
||||
],
|
||||
pre: vocabulary::Definition::no_op,
|
||||
post: vocabulary::Definition::no_op,
|
||||
}
|
||||
};
|
||||
|
||||
pub static ref FORM_HOSTNAME: Keyword = {
|
||||
kw!(:form/hostname)
|
||||
};
|
||||
|
||||
pub static ref FORM_SUBMIT_URL: Keyword = {
|
||||
kw!(:form/submitUrl)
|
||||
};
|
||||
|
||||
pub static ref FORM_USERNAME_FIELD: Keyword = {
|
||||
kw!(:form/usernameField)
|
||||
};
|
||||
|
||||
pub static ref FORM_PASSWORD_FIELD: Keyword = {
|
||||
kw!(:form/passwordField)
|
||||
};
|
||||
|
||||
pub static ref FORM_HTTP_REALM: Keyword = {
|
||||
kw!(:form/httpRealm)
|
||||
};
|
||||
|
||||
// This is arguably backwards. In the future, we'd like forms to be independent of Sync 1.5
|
||||
// password records, in the way that we're making credentials independent of password records.
|
||||
// For now, however, we don't want to add an identifier and identify forms by content, so we're
|
||||
// linking a form to a unique Sync password. Having the link go in this direction lets us
|
||||
// upsert the form.
|
||||
pub static ref FORM_SYNC_PASSWORD: Keyword = {
|
||||
kw!(:form/syncPassword)
|
||||
};
|
||||
|
||||
/// The vocabulary describing *forms* (usage contexts in a Web browser); `:forms/*`.
|
||||
///
|
||||
/// A form is either an HTTP login box _or_ a Web form.
|
||||
///
|
||||
/// ```edn
|
||||
/// [:http/httpRealm :db.type/string :db.cardinality/one]
|
||||
/// ; It's possible that hostname or submitUrl are unique-identity attributes.
|
||||
/// [:form/hostname :db.type/string :db.cardinality/one]
|
||||
/// [:form/submitUrl :db.type/string :db.cardinality/one]
|
||||
/// [:form/usernameField :db.type/string :db.cardinality/one]
|
||||
/// [:form/passwordField :db.type/string :db.cardinality/one]
|
||||
pub static ref FORM_VOCAB: vocabulary::Definition = {
|
||||
vocabulary::Definition {
|
||||
name: kw!(:org.mozilla/form),
|
||||
version: 1,
|
||||
attributes: vec![
|
||||
(FORM_SYNC_PASSWORD.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.unique(vocabulary::attribute::Unique::Identity)
|
||||
.build()),
|
||||
(FORM_HOSTNAME.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(FORM_SUBMIT_URL.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(FORM_USERNAME_FIELD.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(FORM_PASSWORD_FIELD.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(FORM_HTTP_REALM.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.build()),
|
||||
],
|
||||
pre: vocabulary::Definition::no_op,
|
||||
post: vocabulary::Definition::no_op,
|
||||
}
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_UUID: Keyword = {
|
||||
kw!(:sync.password/uuid)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_CREDENTIAL: Keyword = {
|
||||
kw!(:sync.password/credential)
|
||||
};
|
||||
|
||||
// Use materialTx for material change comparisons, metadataTx for metadata change
|
||||
// comparisons. Downloading updates materialTx only. We only use materialTx to
|
||||
// determine whether or not to upload. Uploaded records are built using metadataTx,
|
||||
// however. Successful upload sets both materialTx and metadataTx.
|
||||
pub(crate) static ref SYNC_PASSWORD_MATERIAL_TX: Keyword = {
|
||||
kw!(:sync.password/materialTx)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_METADATA_TX: Keyword = {
|
||||
kw!(:sync.password/metadataTx)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_SERVER_MODIFIED: Keyword = {
|
||||
kw!(:sync.password/serverModified)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_TIMES_USED: Keyword = {
|
||||
kw!(:sync.password/timesUsed)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_TIME_CREATED: Keyword = {
|
||||
kw!(:sync.password/timeCreated)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_TIME_LAST_USED: Keyword = {
|
||||
kw!(:sync.password/timeLastUsed)
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORD_TIME_PASSWORD_CHANGED: Keyword = {
|
||||
kw!(:sync.password/timePasswordChanged)
|
||||
};
|
||||
|
||||
/// The vocabulary describing *Sync 1.5 passwords*; `:sync.password/*`.
|
||||
///
|
||||
/// A Sync 1.5 password joins a credential (via `:sync.password/credential), a form (via the inverse relationship `:form/syncPassword`), and usages together.
|
||||
///
|
||||
/// Consumers should not use this vocabulary directly; it is here only to support Sync 1.5.
|
||||
pub(crate) static ref SYNC_PASSWORD_VOCAB: vocabulary::Definition = {
|
||||
vocabulary::Definition {
|
||||
name: kw!(:org.mozilla/sync.password),
|
||||
version: 1,
|
||||
attributes: vec![
|
||||
(SYNC_PASSWORD_CREDENTIAL.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.unique(vocabulary::attribute::Unique::Identity)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_UUID.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::String)
|
||||
.multival(false)
|
||||
.unique(vocabulary::attribute::Unique::Identity)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_MATERIAL_TX.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_METADATA_TX.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Ref)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_SERVER_MODIFIED.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_TIMES_USED.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Long)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_TIME_CREATED.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_TIME_LAST_USED.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
(SYNC_PASSWORD_TIME_PASSWORD_CHANGED.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Instant)
|
||||
.multival(false)
|
||||
.build()),
|
||||
],
|
||||
pre: vocabulary::Definition::no_op,
|
||||
post: vocabulary::Definition::no_op,
|
||||
}
|
||||
};
|
||||
|
||||
pub(crate) static ref SYNC_PASSWORDS_LAST_SERVER_TIMESTAMP: Keyword = {
|
||||
kw!(:sync.passwords/lastServerTimestamp)
|
||||
};
|
||||
|
||||
/// The vocabulary describing the last time the Sync 1.5 "passwords" collection was synced.
|
||||
///
|
||||
/// Consumers should not use this vocabulary directly; it is here only to support Sync 1.5.
|
||||
pub(crate) static ref SYNC_PASSWORDS_VOCAB: vocabulary::Definition = {
|
||||
vocabulary::Definition {
|
||||
name: kw!(:org.mozilla/sync.passwords),
|
||||
version: 1,
|
||||
attributes: vec![
|
||||
(SYNC_PASSWORDS_LAST_SERVER_TIMESTAMP.clone(),
|
||||
vocabulary::AttributeBuilder::helpful()
|
||||
.value_type(ValueType::Double)
|
||||
.multival(false)
|
||||
.build()),
|
||||
],
|
||||
pre: vocabulary::Definition::no_op,
|
||||
post: vocabulary::Definition::no_op,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Ensure that the Mentat vocabularies describing *credentials*, *logins*, *forms*, and *Sync 1.5
|
||||
/// passwords* is present in the store.
|
||||
///
|
||||
/// This will install or upgrade the vocabularies as necessary, and should be called by every
|
||||
/// consumer early in its lifecycle.
|
||||
pub fn ensure_vocabulary(in_progress: &mut InProgress) -> Result<()> {
|
||||
debug!("Ensuring logins vocabulary is installed.");
|
||||
|
||||
in_progress.verify_core_schema()?;
|
||||
|
||||
in_progress.ensure_vocabulary(&CREDENTIAL_VOCAB)?;
|
||||
in_progress.ensure_vocabulary(&LOGIN_VOCAB)?;
|
||||
in_progress.ensure_vocabulary(&FORM_VOCAB)?;
|
||||
in_progress.ensure_vocabulary(&SYNC_PASSWORD_VOCAB)?;
|
||||
in_progress.ensure_vocabulary(&SYNC_PASSWORDS_VOCAB)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
[package]
|
||||
name = "sync15_passwords"
|
||||
version = "0.1.0"
|
||||
|
||||
[lib]
|
||||
name = "sync15_passwords"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
log = "0.4"
|
||||
|
||||
serde = "^1.0.63"
|
||||
serde_derive = "^1.0.63"
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.5"
|
||||
prettytable-rs = "0.6"
|
||||
url = "1.6.0"
|
||||
|
||||
[dependencies.sync15-adapter]
|
||||
path = "../../sync15-adapter"
|
||||
|
||||
[dependencies.mentat]
|
||||
git = "https://github.com/mozilla/mentat"
|
||||
tag = "v0.8.1"
|
||||
features = ["sqlcipher"]
|
||||
default_features = false
|
||||
|
||||
[dependencies.logins]
|
||||
path = "../../logins"
|
||||
# features = ["sqlcipher"]
|
||||
# default_features = false
|
|
@ -1 +0,0 @@
|
|||
../tests/sync_pass_mentat.rs
|
|
@ -1,32 +0,0 @@
|
|||
[package]
|
||||
name = "loginsapi_ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Mark Hammond <mhammond@skippinet.com.au>"]
|
||||
|
||||
[lib]
|
||||
name = "loginsapi_ffi"
|
||||
crate-type = ["lib", "staticlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
failure = "0.1.1"
|
||||
log = "0.4"
|
||||
url = "1.6.0"
|
||||
reqwest = "0.8.2"
|
||||
|
||||
[dependencies.ffi-toolkit]
|
||||
#path="../../../../ffi-toolkit"
|
||||
git = "https://github.com/mozilla/ffi-toolkit.git"
|
||||
branch = "master"
|
||||
|
||||
[dependencies.sync15-adapter]
|
||||
path = "../../../sync15-adapter"
|
||||
|
||||
[dependencies.sync15_passwords]
|
||||
path = ".."
|
||||
|
||||
[dependencies.mentat]
|
||||
git = "https://github.com/mozilla/mentat"
|
||||
tag = "v0.8.1"
|
||||
features = ["sqlcipher"]
|
||||
default_features = false
|
|
@ -1,192 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
use ffi_toolkit::string::{
|
||||
string_to_c_char
|
||||
};
|
||||
use std::ptr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use sync15_passwords::{
|
||||
Result,
|
||||
Sync15PasswordsError,
|
||||
Sync15PasswordsErrorKind,
|
||||
};
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use sync::{
|
||||
ErrorKind as Sync15ErrorKind
|
||||
};
|
||||
|
||||
pub unsafe fn with_translated_result<F, T>(error: *mut ExternError, callback: F) -> *mut T
|
||||
where F: FnOnce() -> Result<T> {
|
||||
translate_result(callback(), error)
|
||||
}
|
||||
|
||||
pub unsafe fn with_translated_void_result<F>(error: *mut ExternError, callback: F)
|
||||
where F: FnOnce() -> Result<()> {
|
||||
translate_void_result(callback(), error);
|
||||
}
|
||||
|
||||
pub unsafe fn with_translated_value_result<F, T>(error: *mut ExternError, callback: F) -> T
|
||||
where F: FnOnce() -> Result<T>, T: Default {
|
||||
try_translate_result(callback(), error).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub unsafe fn with_translated_string_result<F>(error: *mut ExternError, callback: F) -> *mut c_char
|
||||
where F: FnOnce() -> Result<String> {
|
||||
if let Some(s) = try_translate_result(callback(), error) {
|
||||
string_to_c_char(s)
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn with_translated_opt_string_result<F>(error: *mut ExternError, callback: F) -> *mut c_char
|
||||
where F: FnOnce() -> Result<Option<String>> {
|
||||
if let Some(Some(s)) = try_translate_result(callback(), error) {
|
||||
string_to_c_char(s)
|
||||
} else {
|
||||
// This is either an error case, or callback returned None.
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// C-compatible Error code. Negative codes are not expected to be handled by
|
||||
/// the application, a code of zero indicates that no error occurred, and a
|
||||
/// positive error code indicates an error that will likely need to be handled
|
||||
/// by the application
|
||||
#[repr(i32)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ExternErrorCode {
|
||||
// TODO: When/if we make this API panic-safe, add an `UnexpectedPanic = -2`
|
||||
|
||||
/// An unexpected error occurred which likely cannot be meaningfully handled
|
||||
/// by the application.
|
||||
OtherError = -1,
|
||||
|
||||
/// No error occcurred.
|
||||
NoError = 0,
|
||||
|
||||
/// Indicates the FxA credentials are invalid, and should be refreshed.
|
||||
AuthInvalidError = 1,
|
||||
|
||||
// TODO: lockbox indicated that they would want to know when we fail to open
|
||||
// the DB due to invalid key.
|
||||
}
|
||||
|
||||
// XXX rest of this is COPYPASTE from mentat/ffi/util.rs, this likely belongs in ffi-toolkit
|
||||
// (something similar is there, but it is more error prone and not usable in a general way)
|
||||
// XXX Actually, once errors are more stable we should do something more like fxa (and put that in
|
||||
// ffi-toolkit). Yesterday I thought this was impossible but IDK I was tired? It's possible.
|
||||
|
||||
/// Represents an error that occurred on the mentat side. Many mentat FFI functions take a
|
||||
/// `*mut ExternError` as the last argument. This is an out parameter that indicates an
|
||||
/// error that occurred during that function's execution (if any).
|
||||
///
|
||||
/// For functions that use this pattern, if the ExternError's message property is null, then no
|
||||
/// error occurred. If the message is non-null then it contains a string description of the
|
||||
/// error that occurred.
|
||||
///
|
||||
/// Important: This message is allocated on the heap and it is the consumer's responsibility to
|
||||
/// free it using `destroy_mentat_string`!
|
||||
///
|
||||
/// While this pattern is not ergonomic in Rust, it offers two main benefits:
|
||||
///
|
||||
/// 1. It avoids defining a large number of `Result`-shaped types in the FFI consumer, as would
|
||||
/// be required with something like an `struct ExternResult<T> { ok: *mut T, err:... }`
|
||||
/// 2. It offers additional type safety over `struct ExternResult { ok: *mut c_void, err:... }`,
|
||||
/// which helps avoid memory safety errors.
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub struct ExternError {
|
||||
|
||||
/// A string message, primarially intended for debugging.
|
||||
pub message: *mut c_char,
|
||||
|
||||
/// Error code.
|
||||
/// - A code of 0 indicates no error
|
||||
/// - A negative error code indicates an error which is not expected to be
|
||||
/// handled by the application.
|
||||
pub code: ExternErrorCode,
|
||||
|
||||
// TODO: We probably want an extra (json?) property for misc. metadata.
|
||||
}
|
||||
|
||||
impl Default for ExternError {
|
||||
fn default() -> ExternError {
|
||||
ExternError {
|
||||
message: ptr::null_mut(),
|
||||
code: ExternErrorCode::NoError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_code(err: &Sync15PasswordsError) -> ExternErrorCode {
|
||||
match err.kind() {
|
||||
Sync15PasswordsErrorKind::Sync15AdapterError(e) => {
|
||||
match e.kind() {
|
||||
Sync15ErrorKind::TokenserverHttpError(StatusCode::Unauthorized) => {
|
||||
ExternErrorCode::AuthInvalidError
|
||||
}
|
||||
_ => ExternErrorCode::OtherError,
|
||||
}
|
||||
}
|
||||
_ => ExternErrorCode::OtherError,
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate Result<T, E>, into something C can understand, when T is not `#[repr(C)]`
|
||||
///
|
||||
/// - If `result` is `Ok(v)`, moves `v` to the heap and returns a pointer to it, and sets
|
||||
/// `error` to a state indicating that no error occurred (`message` is null).
|
||||
/// - If `result` is `Err(e)`, returns a null pointer and stores a string representing the error
|
||||
/// message (which was allocated on the heap and should eventually be freed) into
|
||||
/// `error.message`
|
||||
pub unsafe fn translate_result<T>(result: Result<T>, error: *mut ExternError) -> *mut T {
|
||||
// TODO: can't unwind across FFI...
|
||||
assert!(!error.is_null(), "Error output parameter is not optional");
|
||||
let error = &mut *error;
|
||||
error.message = ptr::null_mut();
|
||||
match result {
|
||||
Ok(val) => Box::into_raw(Box::new(val)),
|
||||
Err(e) => {
|
||||
error!("Rust Error: {:?}", e);
|
||||
error.message = string_to_c_char(e.to_string());
|
||||
error.code = get_code(&e);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn try_translate_result<T>(result: Result<T>, error: *mut ExternError) -> Option<T> {
|
||||
// TODO: can't unwind across FFI...
|
||||
assert!(!error.is_null(), "Error output parameter is not optional");
|
||||
let error = &mut *error;
|
||||
error.message = ptr::null_mut();
|
||||
match result {
|
||||
Ok(val) => Some(val),
|
||||
Err(e) => {
|
||||
error!("Rust Error: {:?}", e);
|
||||
error.message = string_to_c_char(e.to_string());
|
||||
error.code = get_code(&e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identical to `translate_result`, but with additional type checking for the case that we have
|
||||
/// a `Result<(), E>` (which we're about to drop on the floor).
|
||||
pub unsafe fn translate_void_result(result: Result<()>, error: *mut ExternError) {
|
||||
// TODO: update this comment.
|
||||
// Note that Box<T> guarantees that if T is zero sized, it's not heap allocated. So not
|
||||
// only do we never need to free the return value of this, it would be a problem if someone did.
|
||||
translate_result(result, error);
|
||||
}
|
|
@ -1,289 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
// We take the "low road" here when returning the structs - we expose the
|
||||
// items (and arrays of items) as strings, which are JSON. The rust side of
|
||||
// the world gets serialization and deserialization for free and it makes
|
||||
// memory management that little bit simpler.
|
||||
|
||||
extern crate failure;
|
||||
extern crate serde_json;
|
||||
extern crate url;
|
||||
extern crate reqwest;
|
||||
|
||||
#[macro_use] extern crate ffi_toolkit;
|
||||
extern crate mentat;
|
||||
extern crate sync15_passwords;
|
||||
extern crate sync15_adapter as sync;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
mod error;
|
||||
|
||||
use error::{
|
||||
ExternError,
|
||||
with_translated_result,
|
||||
with_translated_value_result,
|
||||
with_translated_void_result,
|
||||
with_translated_string_result,
|
||||
with_translated_opt_string_result,
|
||||
};
|
||||
|
||||
use std::os::raw::{
|
||||
c_char,
|
||||
};
|
||||
use std::sync::{Once, ONCE_INIT};
|
||||
|
||||
use ffi_toolkit::string::{
|
||||
c_char_to_string,
|
||||
};
|
||||
|
||||
pub use ffi_toolkit::memory::{
|
||||
destroy_c_char,
|
||||
};
|
||||
|
||||
use sync::{
|
||||
Sync15StorageClient,
|
||||
Sync15StorageClientInit,
|
||||
GlobalState,
|
||||
};
|
||||
use sync15_passwords::{
|
||||
passwords,
|
||||
PasswordEngine,
|
||||
ServerPassword,
|
||||
};
|
||||
|
||||
pub struct SyncInfo {
|
||||
state: GlobalState,
|
||||
client: Sync15StorageClient,
|
||||
// Used so that we know whether or not we need to re-initialize `client`
|
||||
last_client_init: Sync15StorageClientInit,
|
||||
}
|
||||
|
||||
pub struct PasswordState {
|
||||
engine: PasswordEngine,
|
||||
sync: Option<SyncInfo>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
extern { pub fn __android_log_write(level: ::std::os::raw::c_int, tag: *const c_char, text: *const c_char) -> ::std::os::raw::c_int; }
|
||||
|
||||
struct DevLogger;
|
||||
impl log::Log for DevLogger {
|
||||
fn enabled(&self, _: &log::Metadata) -> bool { true }
|
||||
fn log(&self, record: &log::Record) {
|
||||
let message = format!("{}:{} -- {}", record.level(), record.target(), record.args());
|
||||
println!("{}", message);
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
unsafe {
|
||||
let message = ::std::ffi::CString::new(message).unwrap();
|
||||
let level_int = match record.level() {
|
||||
log::Level::Trace => 2,
|
||||
log::Level::Debug => 3,
|
||||
log::Level::Info => 4,
|
||||
log::Level::Warn => 5,
|
||||
log::Level::Error => 6,
|
||||
};
|
||||
let message = message.as_ptr();
|
||||
let tag = b"RustInternal\0";
|
||||
__android_log_write(level_int, tag.as_ptr() as *const c_char, message);
|
||||
}
|
||||
}
|
||||
// TODO ios (use NSLog(__CFStringMakeConstantString(b"%s\0"), ...), maybe windows? (OutputDebugStringA)
|
||||
}
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
static INIT_LOGGER: Once = ONCE_INIT;
|
||||
static DEV_LOGGER: &'static log::Log = &DevLogger;
|
||||
|
||||
fn init_logger() {
|
||||
log::set_logger(DEV_LOGGER).unwrap();
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
info!("Hooked up rust logger!");
|
||||
}
|
||||
|
||||
define_destructor!(sync15_passwords_state_destroy, PasswordState);
|
||||
|
||||
// This is probably too many string arguments...
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_state_new(
|
||||
mentat_db_path: *const c_char,
|
||||
encryption_key: *const c_char,
|
||||
error: *mut ExternError
|
||||
) -> *mut PasswordState {
|
||||
INIT_LOGGER.call_once(init_logger);
|
||||
with_translated_result(error, || {
|
||||
|
||||
let store = mentat::Store::open_with_key(c_char_to_string(mentat_db_path),
|
||||
c_char_to_string(encryption_key))?;
|
||||
|
||||
let engine = PasswordEngine::new(store)?;
|
||||
Ok(PasswordState {
|
||||
engine,
|
||||
sync: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// indirection to help `?` figure out the target error type
|
||||
fn parse_url(url: &str) -> sync::Result<url::Url> {
|
||||
Ok(url::Url::parse(url)?)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_sync(
|
||||
state: *mut PasswordState,
|
||||
key_id: *const c_char,
|
||||
access_token: *const c_char,
|
||||
sync_key: *const c_char,
|
||||
tokenserver_url: *const c_char,
|
||||
error: *mut ExternError
|
||||
) {
|
||||
with_translated_void_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
|
||||
let root_sync_key = sync::KeyBundle::from_ksync_base64(
|
||||
c_char_to_string(sync_key).into())?;
|
||||
|
||||
let requested_init = Sync15StorageClientInit {
|
||||
key_id: c_char_to_string(key_id).into(),
|
||||
access_token: c_char_to_string(access_token).into(),
|
||||
tokenserver_url: parse_url(c_char_to_string(tokenserver_url))?,
|
||||
};
|
||||
|
||||
// TODO: If `to_ready` (or anything else with a ?) fails below, this
|
||||
// `take()` means we end up with `state.sync.is_none()`, which means the
|
||||
// next sync will redownload meta/global, crypto/keys, etc. without
|
||||
// needing to. (AFAICT fixing this requires a change in sync15-adapter,
|
||||
// since to_ready takes GlobalState as a move, and it's not clear if
|
||||
// that change even is a good idea).
|
||||
let mut sync_info = state.sync.take().map(Ok)
|
||||
.unwrap_or_else(|| -> sync::Result<SyncInfo> {
|
||||
let state = GlobalState::default();
|
||||
let client = Sync15StorageClient::new(requested_init.clone())?;
|
||||
Ok(SyncInfo {
|
||||
state,
|
||||
client,
|
||||
last_client_init: requested_init.clone(),
|
||||
})
|
||||
})?;
|
||||
|
||||
// If the options passed for initialization of the storage client aren't
|
||||
// the same as the ones we used last time, reinitialize it. (Note that
|
||||
// we could avoid the comparison in the case where we had `None` in
|
||||
// `state.sync` before, but this probably doesn't matter).
|
||||
if requested_init != sync_info.last_client_init {
|
||||
sync_info.client = Sync15StorageClient::new(requested_init.clone())?;
|
||||
sync_info.last_client_init = requested_init;
|
||||
}
|
||||
|
||||
{ // Scope borrow of `sync_info.client`
|
||||
let mut state_machine =
|
||||
sync::SetupStateMachine::for_readonly_sync(&sync_info.client, &root_sync_key);
|
||||
|
||||
let next_sync_state = state_machine.to_ready(sync_info.state)?;
|
||||
sync_info.state = next_sync_state;
|
||||
}
|
||||
|
||||
// We don't use a ? on the next line so that even if `state.engine.sync`
|
||||
// fails, we don't forget the sync_state.
|
||||
let result = state.engine.sync(&sync_info.client, &sync_info.state);
|
||||
state.sync = Some(sync_info);
|
||||
result
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_touch(state: *mut PasswordState, id: *const c_char, error: *mut ExternError) {
|
||||
with_translated_void_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
state.engine.touch_credential(c_char_to_string(id).into())?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_delete(state: *mut PasswordState, id: *const c_char, error: *mut ExternError) -> bool {
|
||||
with_translated_value_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
let deleted = state.engine.delete_credential(c_char_to_string(id).into())?;
|
||||
Ok(deleted)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_wipe(state: *mut PasswordState, error: *mut ExternError) {
|
||||
with_translated_void_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
state.engine.wipe()?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_reset(state: *mut PasswordState, error: *mut ExternError) {
|
||||
with_translated_void_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
state.engine.reset()?;
|
||||
// XXX We probably need to clear out some things from `state.service`!
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_get_all(state: *mut PasswordState, error: *mut ExternError) -> *mut c_char {
|
||||
with_translated_string_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
// Type declaration is just to make sure we have the right type (and for documentation)
|
||||
let passwords: Vec<ServerPassword> = {
|
||||
let mut in_progress_read = state.engine.store.begin_read()?;
|
||||
passwords::get_all_sync_passwords(&mut in_progress_read)?
|
||||
};
|
||||
let result = serde_json::to_string(&passwords)?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sync15_passwords_get_by_id(state: *mut PasswordState, id: *const c_char, error: *mut ExternError) -> *mut c_char {
|
||||
with_translated_opt_string_result(error, || {
|
||||
assert_pointer_not_null!(state);
|
||||
let state = &mut *state;
|
||||
// Type declaration is just to make sure we have the right type (and for documentation)
|
||||
let maybe_pass: Option<ServerPassword> = {
|
||||
let mut in_progress_read = state.engine.store.begin_read()?;
|
||||
passwords::get_sync_password(&mut in_progress_read, c_char_to_string(id).into())?
|
||||
};
|
||||
let pass = if let Some(p) = maybe_pass { p } else {
|
||||
return Ok(None)
|
||||
};
|
||||
Ok(Some(serde_json::to_string(&pass)?))
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn wtf_destroy_c_char(s: *mut c_char) {
|
||||
// the "pub use" above should should be enough to expose this?
|
||||
// It appears that is enough to expose it in a windows DLL, but for
|
||||
// some reason it's not expored for Android.
|
||||
// *sob* - and now that I've defined this, suddenly this *and*
|
||||
// destroy_c_char are exposed (and removing this again removes the
|
||||
// destroy_c_char)
|
||||
// Oh well, a yak for another day.
|
||||
destroy_c_char(s);
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
use sync15_adapter as sync;
|
||||
use self::sync::{
|
||||
ServerTimestamp,
|
||||
OutgoingChangeset,
|
||||
Payload,
|
||||
};
|
||||
|
||||
use mentat::{
|
||||
self,
|
||||
DateTime,
|
||||
FromMillis,
|
||||
Utc,
|
||||
};
|
||||
|
||||
use logins::{
|
||||
credentials,
|
||||
passwords,
|
||||
Credential,
|
||||
CredentialId,
|
||||
SyncGuid,
|
||||
ServerPassword,
|
||||
ensure_vocabulary,
|
||||
};
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use errors::{
|
||||
Sync15PasswordsError,
|
||||
Result,
|
||||
};
|
||||
|
||||
// TODO: These probably don't all need to be public!
|
||||
pub struct PasswordEngine {
|
||||
pub last_server_timestamp: ServerTimestamp,
|
||||
pub current_tx_id: Option<mentat::Entid>,
|
||||
pub store: mentat::store::Store,
|
||||
}
|
||||
|
||||
impl PasswordEngine {
|
||||
|
||||
pub fn new(mut store: mentat::store::Store) -> Result<PasswordEngine> {
|
||||
let last_server_timestamp: ServerTimestamp = { // Scope borrow of `store`.
|
||||
let mut in_progress = store.begin_transaction()?;
|
||||
|
||||
ensure_vocabulary(&mut in_progress)?;
|
||||
|
||||
let timestamp = passwords::get_last_server_timestamp(&in_progress)?;
|
||||
|
||||
in_progress.commit()?;
|
||||
|
||||
ServerTimestamp(timestamp.unwrap_or_default())
|
||||
};
|
||||
|
||||
Ok(PasswordEngine {
|
||||
current_tx_id: None,
|
||||
last_server_timestamp,
|
||||
store,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn touch_credential(&mut self, id: String) -> Result<()> {
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
credentials::touch_by_id(&mut in_progress, CredentialId(id), None)?;
|
||||
in_progress.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_credential(&mut self, id: String) -> Result<bool> {
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
let deleted = credentials::delete_by_id(&mut in_progress, CredentialId(id))?;
|
||||
in_progress.commit()?;
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub fn update_credential(&mut self, id: &str, updater: impl FnMut(&mut Credential)) -> Result<bool> {
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
|
||||
let mut credential = credentials::get_credential(&in_progress, CredentialId(id.into()))?;
|
||||
if credential.as_mut().map(updater).is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
credentials::add_credential(&mut in_progress, credential.unwrap())?;
|
||||
in_progress.commit()?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn sync(
|
||||
&mut self,
|
||||
client: &sync::Sync15StorageClient,
|
||||
state: &sync::GlobalState,
|
||||
) -> Result<()> {
|
||||
let ts = self.last_server_timestamp;
|
||||
sync::synchronize(client, state, self, "passwords".into(), ts, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
{ // Scope borrow of self.
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
passwords::reset_client(&mut in_progress)?;
|
||||
in_progress.commit()?;
|
||||
}
|
||||
|
||||
self.last_server_timestamp = 0.0.into();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wipe(&mut self) -> Result<()> {
|
||||
self.last_server_timestamp = 0.0.into();
|
||||
|
||||
// let mut in_progress = store.begin_transaction().map_err(|_| "failed to begin_transaction")?;
|
||||
// // reset_client(&mut in_progress).map_err(|_| "failed to reset_client")?;
|
||||
// in_progress.commit().map_err(|_| "failed to commit")?;
|
||||
|
||||
// self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_unsynced_changes(&mut self) -> Result<(Vec<Payload>, ServerTimestamp)> {
|
||||
let mut result = vec![];
|
||||
|
||||
let in_progress_read = self.store.begin_read()?;
|
||||
|
||||
let deleted = passwords::get_deleted_sync_password_uuids_to_upload(&in_progress_read)?;
|
||||
debug!("{} deleted records to upload: {:?}", deleted.len(), deleted);
|
||||
|
||||
for r in deleted {
|
||||
result.push(Payload::new_tombstone(r.0))
|
||||
}
|
||||
|
||||
let modified = passwords::get_modified_sync_passwords_to_upload(&in_progress_read)?;
|
||||
debug!("{} modified records to upload: {:?}", modified.len(), modified.iter().map(|r| &r.uuid.0).collect::<Vec<_>>());
|
||||
|
||||
for r in modified {
|
||||
result.push(Payload::from_record(r)?);
|
||||
}
|
||||
|
||||
Ok((result, self.last_server_timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
impl sync::Store for PasswordEngine {
|
||||
type Error = Sync15PasswordsError;
|
||||
|
||||
fn apply_incoming(
|
||||
&mut self,
|
||||
inbound: sync::IncomingChangeset
|
||||
) -> Result<OutgoingChangeset> {
|
||||
debug!("Remote collection has {} changes timestamped at {}",
|
||||
inbound.changes.len(), inbound.timestamp);
|
||||
|
||||
{ // Scope borrow of inbound.changes.
|
||||
let (to_delete, to_apply): (Vec<_>, Vec<_>) = inbound.changes.iter().partition(|(payload, _)| payload.is_tombstone());
|
||||
debug!("{} records to delete: {:?}", to_delete.len(), to_delete);
|
||||
debug!("{} records to apply: {:?}", to_apply.len(), to_apply);
|
||||
}
|
||||
|
||||
self.current_tx_id = { // Scope borrow of self.
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
|
||||
for (payload, server_timestamp) in inbound.changes {
|
||||
if payload.is_tombstone() {
|
||||
passwords::delete_by_sync_uuid(&mut in_progress, payload.id().into())?;
|
||||
} else {
|
||||
debug!("Applying: {:?}", payload);
|
||||
|
||||
let mut server_password: ServerPassword = payload.clone().into_record()?;
|
||||
server_password.modified = DateTime::<Utc>::from_millis(server_timestamp.as_millis() as i64);
|
||||
|
||||
passwords::apply_password(&mut in_progress, server_password)?;
|
||||
}
|
||||
}
|
||||
|
||||
let current_tx_id = in_progress.last_tx_id();
|
||||
in_progress.commit()?;
|
||||
|
||||
Some(current_tx_id)
|
||||
};
|
||||
|
||||
let (outbound_changes, last_server_timestamp) = self.get_unsynced_changes()?;
|
||||
|
||||
let outbound = OutgoingChangeset {
|
||||
changes: outbound_changes,
|
||||
timestamp: last_server_timestamp,
|
||||
collection: "passwords".into()
|
||||
};
|
||||
|
||||
debug!("After applying incoming changes, local collection has {} outgoing changes timestamped at {}",
|
||||
outbound.changes.len(), outbound.timestamp);
|
||||
|
||||
Ok(outbound)
|
||||
}
|
||||
|
||||
fn sync_finished(&mut self, new_last_server_timestamp: ServerTimestamp, records_synced: &[String]) -> Result<()> {
|
||||
debug!("Synced {} outbound changes at remote timestamp {}", records_synced.len(), new_last_server_timestamp);
|
||||
for id in records_synced {
|
||||
trace!(" {:?}", id);
|
||||
}
|
||||
|
||||
let current_tx_id = self.current_tx_id.unwrap(); // XXX
|
||||
|
||||
{ // Scope borrow of self.
|
||||
let mut in_progress = self.store.begin_transaction()?;
|
||||
|
||||
let deleted = passwords::get_deleted_sync_password_uuids_to_upload(&in_progress)?;
|
||||
let deleted: BTreeSet<String> = deleted.into_iter().map(|x| x.0).collect();
|
||||
|
||||
let (deleted, uploaded): (Vec<_>, Vec<_>) =
|
||||
records_synced.iter().cloned().partition(|id| deleted.contains(id));
|
||||
|
||||
passwords::mark_synced_by_sync_uuids(&mut in_progress, uploaded.into_iter().map(SyncGuid).collect(), current_tx_id)?;
|
||||
passwords::delete_by_sync_uuids(&mut in_progress, deleted.into_iter().map(SyncGuid).collect())?;
|
||||
|
||||
passwords::set_last_server_timestamp(&mut in_progress, new_last_server_timestamp.0)?;
|
||||
|
||||
in_progress.commit()?;
|
||||
};
|
||||
|
||||
self.last_server_timestamp = new_last_server_timestamp;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std; // To refer to std::result::Result.
|
||||
|
||||
use serde_json;
|
||||
|
||||
use mentat;
|
||||
use logins;
|
||||
use sync15_adapter;
|
||||
use failure::{Context, Backtrace, Fail};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Sync15PasswordsError>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($e:expr) => (
|
||||
return Err($e.into());
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sync15PasswordsError(Box<Context<Sync15PasswordsErrorKind>>);
|
||||
|
||||
impl Fail for Sync15PasswordsError {
|
||||
#[inline]
|
||||
fn cause(&self) -> Option<&Fail> {
|
||||
self.0.cause()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.0.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Sync15PasswordsError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&*self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sync15PasswordsError {
|
||||
#[inline]
|
||||
pub fn kind(&self) -> &Sync15PasswordsErrorKind {
|
||||
&*self.0.get_context()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sync15PasswordsErrorKind> for Sync15PasswordsError {
|
||||
#[inline]
|
||||
fn from(kind: Sync15PasswordsErrorKind) -> Sync15PasswordsError {
|
||||
Sync15PasswordsError(Box::new(Context::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<Sync15PasswordsErrorKind>> for Sync15PasswordsError {
|
||||
#[inline]
|
||||
fn from(inner: Context<Sync15PasswordsErrorKind>) -> Sync15PasswordsError {
|
||||
Sync15PasswordsError(Box::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Sync15PasswordsErrorKind {
|
||||
#[fail(display = "{}", _0)]
|
||||
MentatError(#[cause] mentat::MentatError),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
LoginsError(#[cause] logins::Error),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
Sync15AdapterError(#[cause] sync15_adapter::Error),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
SerdeJSONError(#[cause] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<mentat::MentatError> for Sync15PasswordsErrorKind {
|
||||
fn from(error: mentat::MentatError) -> Sync15PasswordsErrorKind {
|
||||
Sync15PasswordsErrorKind::MentatError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<logins::Error> for Sync15PasswordsErrorKind {
|
||||
fn from(error: logins::Error) -> Sync15PasswordsErrorKind {
|
||||
Sync15PasswordsErrorKind::LoginsError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sync15_adapter::Error> for Sync15PasswordsErrorKind {
|
||||
fn from(error: sync15_adapter::Error) -> Sync15PasswordsErrorKind {
|
||||
Sync15PasswordsErrorKind::Sync15AdapterError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Sync15PasswordsErrorKind {
|
||||
fn from(error: serde_json::Error) -> Sync15PasswordsErrorKind {
|
||||
Sync15PasswordsErrorKind::SerdeJSONError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mentat::MentatError> for Sync15PasswordsError {
|
||||
fn from(error: mentat::MentatError) -> Sync15PasswordsError {
|
||||
Sync15PasswordsErrorKind::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<logins::Error> for Sync15PasswordsError {
|
||||
fn from(error: logins::Error) -> Sync15PasswordsError {
|
||||
Sync15PasswordsErrorKind::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sync15_adapter::Error> for Sync15PasswordsError {
|
||||
fn from(error: sync15_adapter::Error) -> Sync15PasswordsError {
|
||||
Sync15PasswordsErrorKind::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Sync15PasswordsError {
|
||||
fn from(error: serde_json::Error) -> Sync15PasswordsError {
|
||||
Sync15PasswordsErrorKind::from(error).into()
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
#![crate_name = "sync15_passwords"]
|
||||
|
||||
extern crate failure;
|
||||
#[macro_use] extern crate failure_derive;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate serde_json;
|
||||
|
||||
extern crate mentat;
|
||||
|
||||
extern crate logins;
|
||||
extern crate sync15_adapter;
|
||||
|
||||
pub mod engine;
|
||||
pub use engine::{
|
||||
PasswordEngine,
|
||||
};
|
||||
pub mod errors;
|
||||
pub use errors::{
|
||||
Sync15PasswordsError,
|
||||
Sync15PasswordsErrorKind,
|
||||
Result,
|
||||
};
|
||||
|
||||
pub use logins::{
|
||||
ServerPassword,
|
||||
credentials,
|
||||
passwords,
|
||||
};
|
|
@ -1,414 +0,0 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
extern crate env_logger;
|
||||
extern crate failure;
|
||||
#[macro_use] extern crate prettytable;
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate url;
|
||||
|
||||
extern crate logins;
|
||||
extern crate mentat;
|
||||
extern crate sync15_adapter as sync;
|
||||
extern crate sync15_passwords;
|
||||
|
||||
use sync15_passwords::PasswordEngine;
|
||||
|
||||
use mentat::{
|
||||
DateTime,
|
||||
FromMillis,
|
||||
Utc,
|
||||
};
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
use failure::Error;
|
||||
use std::fs;
|
||||
use std::process;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OAuthCredentials {
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
keys: HashMap<String, ScopedKeyData>,
|
||||
expires_in: u64,
|
||||
auth_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedKeyData {
|
||||
k: String,
|
||||
kid: String,
|
||||
scope: String,
|
||||
}
|
||||
|
||||
use logins::{
|
||||
Credential,
|
||||
FormTarget,
|
||||
ServerPassword,
|
||||
};
|
||||
use logins::passwords;
|
||||
|
||||
fn do_auth(recur: bool) -> Result<OAuthCredentials, Error> {
|
||||
match fs::File::open("./credentials.json") {
|
||||
Err(_) => {
|
||||
if recur {
|
||||
panic!("Failed to open credentials 2nd time");
|
||||
}
|
||||
println!("No credentials found, invoking boxlocker.py...");
|
||||
process::Command::new("python")
|
||||
.arg("../boxlocker/boxlocker.py").output()
|
||||
.expect("Failed to run boxlocker.py");
|
||||
return do_auth(true);
|
||||
},
|
||||
Ok(mut file) => {
|
||||
let mut s = String::new();
|
||||
file.read_to_string(&mut s)?;
|
||||
let creds: OAuthCredentials = serde_json::from_str(&s)?;
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
if creds.expires_in + creds.auth_at < time {
|
||||
println!("Warning, credentials may be stale.");
|
||||
}
|
||||
Ok(creds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_string<S: AsRef<str>>(prompt: S) -> Option<String> {
|
||||
print!("{}: ", prompt.as_ref());
|
||||
let _ = io::stdout().flush(); // Don't care if flush fails really.
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).expect("Failed to read line...");
|
||||
if let Some('\n') = s.chars().next_back() { s.pop(); }
|
||||
if let Some('\r') = s.chars().next_back() { s.pop(); }
|
||||
if s.len() == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize<S: AsRef<str>>(prompt: S) -> Option<usize> {
|
||||
if let Some(s) = prompt_string(prompt) {
|
||||
match s.parse::<usize>() {
|
||||
Ok(n) => Some(n),
|
||||
Err(_) => {
|
||||
println!("Couldn't parse!");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn duration_ms(dur: Duration) -> u64 {
|
||||
dur.as_secs() * 1000 + ((dur.subsec_nanos() / 1_000_000) as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unix_time_ms() -> u64 {
|
||||
duration_ms(SystemTime::now().duration_since(UNIX_EPOCH).unwrap())
|
||||
}
|
||||
|
||||
fn read_login() -> ServerPassword {
|
||||
let username = prompt_string("username"); // .unwrap_or(String::new());
|
||||
let password = prompt_string("password").unwrap_or(String::new());
|
||||
let form_submit_url = prompt_string("form_submit_url");
|
||||
let hostname = prompt_string("hostname");
|
||||
let http_realm = prompt_string("http_realm");
|
||||
let username_field = prompt_string("username_field"); // .unwrap_or(String::new());
|
||||
let password_field = prompt_string("password_field"); // .unwrap_or(String::new());
|
||||
let ms_i64 = unix_time_ms() as i64;
|
||||
ServerPassword {
|
||||
uuid: sync::util::random_guid().unwrap().into(),
|
||||
username,
|
||||
password,
|
||||
username_field,
|
||||
password_field,
|
||||
target: match form_submit_url {
|
||||
Some(form_submit_url) => FormTarget::FormSubmitURL(form_submit_url),
|
||||
None => FormTarget::HttpRealm(http_realm.unwrap_or(String::new())), // XXX this makes little sense.
|
||||
},
|
||||
hostname: hostname.unwrap_or(String::new()), // XXX.
|
||||
time_created: DateTime::<Utc>::from_millis(ms_i64),
|
||||
time_password_changed: DateTime::<Utc>::from_millis(ms_i64),
|
||||
times_used: 0,
|
||||
time_last_used: DateTime::<Utc>::from_millis(ms_i64),
|
||||
|
||||
modified: DateTime::<Utc>::from_millis(ms_i64), // XXX what should we do here?
|
||||
}
|
||||
}
|
||||
|
||||
fn update_string(field_name: &str, field: &mut String, extra: &str) -> bool {
|
||||
let opt_s = prompt_string(format!("new {} [now {}{}]", field_name, field, extra));
|
||||
if let Some(s) = opt_s {
|
||||
*field = s;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn string_opt(o: &Option<String>) -> Option<&str> {
|
||||
o.as_ref().map(|s| s.as_ref())
|
||||
}
|
||||
|
||||
fn string_opt_or<'a>(o: &'a Option<String>, or: &'a str) -> &'a str {
|
||||
string_opt(o).unwrap_or(or)
|
||||
}
|
||||
|
||||
// fn update_login(record: &mut ServerPassword) {
|
||||
// update_string("username", &mut record.username, ", leave blank to keep");
|
||||
// let changed_password = update_string("password", &mut record.password, ", leave blank to keep");
|
||||
|
||||
// if changed_password {
|
||||
// record.time_password_changed = unix_time_ms() as i64;
|
||||
// }
|
||||
|
||||
// update_string("username_field", &mut record.username_field, ", leave blank to keep");
|
||||
// update_string("password_field", &mut record.password_field, ", leave blank to keep");
|
||||
|
||||
// if prompt_bool(&format!("edit hostname? (now {}) [yN]", string_opt_or(&record.hostname, "(none)"))).unwrap_or(false) {
|
||||
// record.hostname = prompt_string("hostname");
|
||||
// }
|
||||
|
||||
// if prompt_bool(&format!("edit form_submit_url? (now {}) [yN]", string_opt_or(&record.form_submit_url, "(none)"))).unwrap_or(false) {
|
||||
// record.form_submit_url = prompt_string("form_submit_url");
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update_credential(record: &mut Credential) {
|
||||
let mut username = record.username.clone().unwrap_or("".into());
|
||||
if update_string("username", &mut username, ", leave blank to keep") {
|
||||
record.username = Some(username);
|
||||
}
|
||||
update_string("password", &mut record.password, ", leave blank to keep");
|
||||
update_string("title", &mut record.password, ", leave blank to keep");
|
||||
}
|
||||
|
||||
fn prompt_bool(msg: &str) -> Option<bool> {
|
||||
let result = prompt_string(msg);
|
||||
result.and_then(|r| match r.chars().next().unwrap() {
|
||||
'y' | 'Y' | 't' | 'T' => Some(true),
|
||||
'n' | 'N' | 'f' | 'F' => Some(false),
|
||||
_ => None
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn prompt_chars(msg: &str) -> Option<char> {
|
||||
prompt_string(msg).and_then(|r| r.chars().next())
|
||||
}
|
||||
|
||||
fn as_table<'a, I>(records: I) -> (prettytable::Table, Vec<String>) where I: IntoIterator<Item=&'a ServerPassword> {
|
||||
let mut table = prettytable::Table::new();
|
||||
table.add_row(row![
|
||||
"(idx)", "id",
|
||||
"username", "password",
|
||||
"usernameField", "passwordField",
|
||||
"hostname",
|
||||
"formSubmitURL"
|
||||
// Skipping metadata so this isn't insanely long
|
||||
]);
|
||||
|
||||
let v: Vec<_> = records.into_iter().enumerate().map(|(index, rec)| {
|
||||
let target = match &rec.target {
|
||||
&FormTarget::FormSubmitURL(ref form_submit_url) => form_submit_url,
|
||||
&FormTarget::HttpRealm(ref http_realm) => http_realm,
|
||||
};
|
||||
|
||||
table.add_row(row![
|
||||
index,
|
||||
rec.uuid.as_ref(),
|
||||
string_opt_or(&rec.username, "<username>"),
|
||||
&rec.password,
|
||||
string_opt_or(&rec.username_field, "<username_field>"),
|
||||
string_opt_or(&rec.password_field, "<password_field>"),
|
||||
&rec.hostname,
|
||||
target
|
||||
]);
|
||||
|
||||
rec.uuid.0.clone()
|
||||
}).collect();
|
||||
|
||||
(table, v)
|
||||
}
|
||||
|
||||
fn show_all(e: &mut PasswordEngine) -> Result<Vec<String>, Error> {
|
||||
let records = {
|
||||
let mut in_progress_read = e.store.begin_read()?;
|
||||
// .map_err(|_| "failed to begin_read")?;
|
||||
|
||||
passwords::get_all_sync_passwords(&mut in_progress_read)?
|
||||
// .map_err(|_| "failed to get_all_sync_passwords")?
|
||||
};
|
||||
|
||||
let (table, map) = as_table(&records);
|
||||
table.printstd();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn prompt_record_id(e: &mut PasswordEngine, action: &str) -> Result<Option<String>, Error> {
|
||||
let index_to_id = show_all(e)?;
|
||||
let input = match prompt_usize(&format!("Enter (idx) of record to {}", action)) {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
println!("Bad input");
|
||||
return Ok(None);
|
||||
},
|
||||
};
|
||||
|
||||
if input >= index_to_id.len() {
|
||||
println!("No such index");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(index_to_id[input].clone().into()))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let oauth_data = do_auth(false)?;
|
||||
|
||||
let scope = &oauth_data.keys["https://identity.mozilla.com/apps/oldsync"];
|
||||
|
||||
let client = sync::Sync15StorageClient::new(sync::Sync15StorageClientInit {
|
||||
key_id: scope.kid.clone(),
|
||||
access_token: oauth_data.access_token.clone(),
|
||||
tokenserver_url: url::Url::parse("https://oauth-sync.dev.lcip.org/syncserver/token/1.0/sync/1.5")?,
|
||||
})?;
|
||||
let mut sync_state = sync::GlobalState::default();
|
||||
|
||||
let root_sync_key = sync::KeyBundle::from_ksync_base64(&scope.k)?;
|
||||
|
||||
let mut state_machine =
|
||||
sync::SetupStateMachine::for_readonly_sync(&client, &root_sync_key);
|
||||
sync_state = state_machine.to_ready(sync_state)?;
|
||||
|
||||
let mut engine = PasswordEngine::new(mentat::Store::open("logins.mentatdb")?)?;
|
||||
println!("Performing startup sync; engine has last server timestamp {}.", engine.last_server_timestamp);
|
||||
|
||||
if let Err(e) = engine.sync(&client, &sync_state) {
|
||||
println!("Initial sync failed: {}", e);
|
||||
if !prompt_bool("Would you like to continue [yN]").unwrap_or(false) {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
show_all(&mut engine)?;
|
||||
|
||||
loop {
|
||||
// match prompt_chars("[A]dd, [D]elete, [U]pdate, [S]ync, [V]iew, [R]eset, [W]ipe or [Q]uit").unwrap_or('?') {
|
||||
match prompt_chars("[T]ouch credential, [D]elete credential, [U]pdate credential, [S]ync, [V]iew, [R]eset, [W]ipe, or [Q]uit").unwrap_or('?') {
|
||||
'T' | 't' => {
|
||||
println!("Touching (recording usage of) credential");
|
||||
if let Some(id) = prompt_record_id(&mut engine, "touch (record usage of)")? {
|
||||
// Here we're using that the credential uuid and the Sync 1.5 uuid are the same;
|
||||
// that's not a stable assumption.
|
||||
if let Err(e) = engine.touch_credential(id) {
|
||||
println!("Failed to touch credential! {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 'A' | 'a' => {
|
||||
// println!("Adding new record");
|
||||
// let record = read_login();
|
||||
// if let Err(e) = engine.create(record) {
|
||||
// println!("Failed to create record! {}", e);
|
||||
// }
|
||||
// }
|
||||
'D' | 'd' => {
|
||||
println!("Deleting credential");
|
||||
if let Some(id) = prompt_record_id(&mut engine, "delete")? {
|
||||
// Here we're using that the credential uuid and the Sync 1.5 uuid are the same;
|
||||
// that's not a stable assumption.
|
||||
if let Err(e) = engine.delete_credential(id) {
|
||||
println!("Failed to delete credential! {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
'U' | 'u' => {
|
||||
println!("Updating credential fields");
|
||||
if let Some(id) = prompt_record_id(&mut engine, "update")? {
|
||||
// Here we're using that the credential uuid and the Sync 1.5 uuid are the same;
|
||||
// that's not a stable assumption.
|
||||
if let Err(e) = engine.update_credential(&id, update_credential) {
|
||||
println!("Failed to update credential! {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
'R' | 'r' => {
|
||||
println!("Resetting client's last server timestamp (was {}).", engine.last_server_timestamp);
|
||||
if let Err(e) = engine.reset() {
|
||||
println!("Failed to reset! {}", e);
|
||||
}
|
||||
}
|
||||
'W' | 'w' => {
|
||||
println!("Wiping all data from client!");
|
||||
if let Err(e) = engine.wipe() {
|
||||
println!("Failed to wipe! {}", e);
|
||||
}
|
||||
}
|
||||
'S' | 's' => {
|
||||
println!("Syncing engine with last server timestamp {}!", engine.last_server_timestamp);
|
||||
if let Err(e) = engine.sync(&client, &sync_state) {
|
||||
println!("Sync failed! {}", e);
|
||||
} else {
|
||||
println!("Sync was successful!");
|
||||
}
|
||||
}
|
||||
'V' | 'v' => {
|
||||
// println!("Engine has {} records, a last sync timestamp of {}, and {} queued changes",
|
||||
// engine.records.len(), engine.last_sync, engine.changes.len());
|
||||
println!("Engine has a last server timestamp of {}", engine.last_server_timestamp);
|
||||
|
||||
{ // Scope borrow of engine.
|
||||
let in_progress_read = engine.store.begin_read()?;
|
||||
// .map_err(|_| "failed to begin_read")?;
|
||||
|
||||
let deleted = passwords::get_deleted_sync_password_uuids_to_upload(&in_progress_read)?;
|
||||
// .map_err(|_| "failed to get_deleted_sync_password_uuids_to_upload")?;
|
||||
println!("{} deleted records to upload: {:?}", deleted.len(), deleted);
|
||||
|
||||
let modified = passwords::get_modified_sync_passwords_to_upload(&in_progress_read)?;
|
||||
// .map_err(|_| "failed to get_modified_sync_passwords_to_upload")?;
|
||||
println!("{} modified records to upload:", modified.len());
|
||||
|
||||
if !modified.is_empty() {
|
||||
let (table, _map) = as_table(&modified);
|
||||
table.printstd();
|
||||
}
|
||||
}
|
||||
|
||||
println!("Local collection:");
|
||||
show_all(&mut engine)?;
|
||||
}
|
||||
'Q' | 'q' => {
|
||||
break;
|
||||
}
|
||||
'?' => {
|
||||
continue;
|
||||
}
|
||||
c => {
|
||||
println!("Unknown action '{}', exiting.", c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Exiting (bye!)");
|
||||
Ok(())
|
||||
}
|
Загрузка…
Ссылка в новой задаче