Remove mentat-backed implementation of login storage

This commit is contained in:
Thom Chiovoloni 2018-09-07 15:07:22 -07:00
Родитель 46dc121469
Коммит b8ee8b90b2
18 изменённых файлов: 1 добавлений и 4475 удалений

Просмотреть файл

@ -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(())
}