Bug 1675190 - Vendor new application-services into mozilla-central. r=extension-reviewers,janerik,eoger,dmose,rpl

Differential Revision: https://phabricator.services.mozilla.com/D95829
This commit is contained in:
Mark Hammond 2020-11-05 03:50:21 +00:00
parent 95a53c1f64
commit bcb7a913bb
142 changed files with 4631 additions and 1615 deletions

View File

@ -20,7 +20,7 @@ rev = "f7c35a30ff25521bebe64c19d3f306569ecb5385"
[source."https://github.com/mozilla/application-services"]
git = "https://github.com/mozilla/application-services"
replace-with = "vendored-sources"
rev = "641353a8648602ce17d23c89b88e2a22d108fb03"
rev = "1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
[source."https://github.com/mozilla-spidermonkey/jsparagus"]
git = "https://github.com/mozilla-spidermonkey/jsparagus"

97
Cargo.lock generated
View File

@ -1228,16 +1228,15 @@ dependencies = [
[[package]]
name = "ece"
version = "1.1.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e831571186ec514efc697af518eee5e4aaecc13edeb4a918227f7bcba2c20b1"
checksum = "53d97f19730c1eb3332d0657d0f3ca72795d77c61d8eb26bdd7f15edc0c61eb2"
dependencies = [
"base64 0.12.0",
"byteorder",
"failure",
"failure_derive",
"once_cell",
"serde",
"thiserror",
]
[[package]]
@ -1318,9 +1317,9 @@ checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
[[package]]
name = "error-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"failure",
"thiserror",
]
[[package]]
@ -1389,9 +1388,9 @@ dependencies = [
[[package]]
name = "ffi-support"
version = "0.4.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087be066eb6e85d7150f0c5400018a32802f99d688b2d3868c526f7bbfe17960"
checksum = "f85d4d1be103c0b2d86968f0b0690dc09ac0ba205b90adb0389b552869e5000e"
dependencies = [
"lazy_static",
"log",
@ -1681,23 +1680,26 @@ dependencies = [
[[package]]
name = "fxa-client"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"anyhow",
"base64 0.12.0",
"byteorder",
"error-support",
"failure",
"ffi-support",
"hex",
"jwcrypto",
"lazy_static",
"log",
"prost",
"prost-derive",
"rand_rccrypto",
"rc_crypto",
"serde",
"serde_derive",
"serde_json",
"sync-guid",
"sync15",
"thiserror",
"url",
"viaduct",
]
@ -2191,15 +2193,15 @@ dependencies = [
[[package]]
name = "hawk"
version = "3.1.1"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57528ce5133f688e1bc4daadc3e50bf9093d40e8a1f64c6e506ccbae005e57e6"
checksum = "7539c8d8699bae53238aacd3f93cfb0bcaef77b85dc963902b9367c5d7a84c48"
dependencies = [
"anyhow",
"base64 0.12.0",
"failure",
"log",
"once_cell",
"rand",
"thiserror",
"url",
]
@ -2402,7 +2404,7 @@ dependencies = [
[[package]]
name = "interrupt-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
[[package]]
name = "intl-memoizer"
@ -2584,6 +2586,19 @@ dependencies = [
"smoosh",
]
[[package]]
name = "jwcrypto"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"base64 0.12.0",
"rc_crypto",
"serde",
"serde_derive",
"serde_json",
"thiserror",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -3400,27 +3415,27 @@ dependencies = [
[[package]]
name = "nss"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"base64 0.12.0",
"error-support",
"failure",
"failure_derive",
"nss_sys",
"serde",
"serde_derive",
"thiserror",
]
[[package]]
name = "nss_build_common"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
[[package]]
name = "nss_sys"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"libsqlite3-sys",
"nss_build_common",
]
@ -4046,6 +4061,16 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rand_rccrypto"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"rand",
"rand_core",
"rc_crypto",
]
[[package]]
name = "range-alloc"
version = "0.1.1"
@ -4099,16 +4124,14 @@ dependencies = [
[[package]]
name = "rc_crypto"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"base64 0.12.0",
"ece",
"error-support",
"failure",
"failure_derive",
"hawk",
"libsqlite3-sys",
"nss",
"thiserror",
]
[[package]]
@ -4711,7 +4734,7 @@ dependencies = [
[[package]]
name = "sql-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"ffi-support",
"interrupt-support",
@ -4909,7 +4932,7 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"base64 0.12.0",
"rand",
@ -4920,12 +4943,12 @@ dependencies = [
[[package]]
name = "sync15"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"anyhow",
"base16",
"base64 0.12.0",
"error-support",
"failure",
"ffi-support",
"interrupt-support",
"lazy_static",
@ -4936,6 +4959,7 @@ dependencies = [
"serde_json",
"sync-guid",
"sync15-traits",
"thiserror",
"url",
"viaduct",
]
@ -4943,11 +4967,10 @@ dependencies = [
[[package]]
name = "sync15-traits"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"failure",
"anyhow",
"ffi-support",
"interrupt-support",
"log",
"serde",
"serde_json",
@ -5526,10 +5549,8 @@ checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
[[package]]
name = "viaduct"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"failure",
"failure_derive",
"ffi-support",
"log",
"once_cell",
@ -5537,6 +5558,7 @@ dependencies = [
"prost-derive",
"serde",
"serde_json",
"thiserror",
"url",
]
@ -5648,10 +5670,10 @@ dependencies = [
[[package]]
name = "webext-storage"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=641353a8648602ce17d23c89b88e2a22d108fb03#641353a8648602ce17d23c89b88e2a22d108fb03"
source = "git+https://github.com/mozilla/application-services?rev=1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f#1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f"
dependencies = [
"error-support",
"failure",
"ffi-support",
"interrupt-support",
"lazy_static",
"log",
@ -5663,6 +5685,7 @@ dependencies = [
"sql-support",
"sync-guid",
"sync15-traits",
"thiserror",
"url",
]

View File

@ -74,10 +74,11 @@ class RustFxAccount {
* `completeOAuthFlow(...)` to complete the flow.
*
* @param {[string]} scopes
* @param {string} entryPoint - a string for metrics.
* @returns {Promise<string>} a URL string that the caller should navigate to.
*/
async beginOAuthFlow(scopes) {
return promisify(this.bridge.beginOAuthFlow, scopes);
async beginOAuthFlow(scopes, entryPoint = "desktop") {
return promisify(this.bridge.beginOAuthFlow, scopes, entryPoint);
}
/**
* Complete an OAuth flow initiated by `beginOAuthFlow(...)`.

View File

@ -21,4 +21,4 @@ nsstring = { path = "../../../../xpcom/rust/nsstring" }
xpcom = { path = "../../../../xpcom/rust/xpcom" }
storage_variant = { path = "../../../../storage/variant" }
thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
fxa-client = { git = "https://github.com/mozilla/application-services", rev = "641353a8648602ce17d23c89b88e2a22d108fb03", features = ["gecko"] }
fxa-client = { git = "https://github.com/mozilla/application-services", rev = "1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f", features = ["gecko"] }

View File

@ -115,11 +115,12 @@ impl Bridge {
xpcom_method!(
begin_oauth_flow => BeginOAuthFlow(
scopes: *const ThinVec<nsCString>,
entry_point: *const nsACString,
callback: *const mozIFirefoxAccountsBridgeCallback
)
);
punt!(begin_oauth_flow, scopes: &ThinVec<nsCString>);
punt!(begin_oauth_flow, scopes: &ThinVec<nsCString>, entry_point: &nsACString);
xpcom_method!(
complete_oauth_flow => CompleteOAuthFlow(

View File

@ -14,7 +14,7 @@ use xpcom::{interfaces::nsIVariant, RefPtr};
/// result to its callback.
pub enum Punt {
ToJson,
BeginOAuthFlow(Vec<String>),
BeginOAuthFlow(Vec<String>, String),
CompleteOAuthFlow(String, String),
Disconnect,
GetAccessToken(String, Option<u64>),

View File

@ -9,7 +9,9 @@ use crate::punt::{
use atomic_refcell::AtomicRefCell;
use fxa_client::{
device::{
Capability as FxaDeviceCapability, PushSubscription as FxaPushSubscription,
Capability as FxaDeviceCapability,
CommandFetchReason,
PushSubscription as FxaPushSubscription,
Type as FxaDeviceType,
},
FirefoxAccount,
@ -65,6 +67,7 @@ impl PuntTask {
pub fn for_begin_oauth_flow(
fxa: &Arc<Mutex<FirefoxAccount>>,
scopes: &[nsCString],
entry_point: &nsACString,
callback: &mozIFirefoxAccountsBridgeCallback,
) -> error::Result<PuntTask> {
let scopes = scopes.iter().try_fold(
@ -74,7 +77,8 @@ impl PuntTask {
Ok(acc)
},
)?;
Self::new(fxa, Punt::BeginOAuthFlow(scopes), callback)
let entry_point = str::from_utf8(&*entry_point)?.into();
Self::new(fxa, Punt::BeginOAuthFlow(scopes, entry_point), callback)
}
/// Creates a task that calls complete_oauth_flow.
@ -385,9 +389,9 @@ impl PuntTask {
let mut fxa = fxa.lock()?;
Ok(match punt {
Punt::ToJson => fxa.to_json().map(PuntResult::String),
Punt::BeginOAuthFlow(scopes) => {
Punt::BeginOAuthFlow(scopes, entry_point) => {
let scopes: Vec<&str> = scopes.iter().map(AsRef::as_ref).collect();
fxa.begin_oauth_flow(&scopes).map(PuntResult::String)
fxa.begin_oauth_flow(&scopes, &entry_point, None).map(PuntResult::String)
}
Punt::CompleteOAuthFlow(code, state) => fxa
.complete_oauth_flow(&code, &state)
@ -447,7 +451,7 @@ impl PuntTask {
Punt::HandlePushMessage(payload) => fxa
.handle_push_message(&payload)
.map(PuntResult::json_stringify),
Punt::PollDeviceCommands => fxa.poll_device_commands().map(PuntResult::json_stringify),
Punt::PollDeviceCommands => fxa.poll_device_commands(CommandFetchReason::Poll).map(PuntResult::json_stringify),
Punt::SendSingleTab(target_id, title, url) => fxa
.send_tab(&target_id, &title, &url)
.map(|_| PuntResult::Null),

View File

@ -25,7 +25,7 @@ interface mozIFirefoxAccountsBridge : nsISupports {
void initFromJSON(in AUTF8String json);
void stateJSON(in mozIFirefoxAccountsBridgeCallback callback);
void beginOAuthFlow(in Array<AUTF8String> scopes, in mozIFirefoxAccountsBridgeCallback callback);
void beginOAuthFlow(in Array<AUTF8String> scopes, in AUTF8String entryPoint, in mozIFirefoxAccountsBridgeCallback callback);
void completeOAuthFlow(in AUTF8String code, in AUTF8String state, in mozIFirefoxAccountsBridgeCallback callback);
void disconnect(in mozIFirefoxAccountsBridgeCallback callback);

View File

@ -8,14 +8,14 @@ edition = "2018"
[dependencies]
atomic_refcell = "0.1"
cstr = "0.1"
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "641353a8648602ce17d23c89b88e2a22d108fb03" }
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f" }
log = "0.4"
moz_task = { path = "../../../xpcom/rust/moz_task" }
nserror = { path = "../../../xpcom/rust/nserror" }
nsstring = { path = "../../../xpcom/rust/nsstring" }
serde_json = "1"
storage_variant = { path = "../../../storage/variant" }
sync15-traits = { git = "https://github.com/mozilla/application-services", rev = "641353a8648602ce17d23c89b88e2a22d108fb03" }
sync15-traits = { git = "https://github.com/mozilla/application-services", rev = "1c4dd52e61324eb124ba41cfbb56dcbc6b930d9f" }
xpcom = { path = "../../../xpcom/rust/xpcom" }
[dependencies.thin-vec]

View File

@ -1 +1 @@
{"files":{"CODE_OF_CONDUCT.md":"902d5357af363426631d907e641e220b3ec89039164743f8442b3f120479b7cf","Cargo.toml":"18b3b72b404bdbb875662867c6b0379a76fd119ed14c9d4c0c6ddd465535f324","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"95b8f2f9a8adefedc23db615756366eb5555fc2e256152e3f534fddcfebe18ea","src/aes128gcm.rs":"4e512b0239964e710cb430a5a25a98a0e6804da1513cccbd7acad41ff248de5a","src/aesgcm.rs":"94a098826c33c886bfe0dbe5360f98bff54e6acc93e994c3187d8e01cf3739a7","src/common.rs":"adb2c1a31b208e8dc48885a12d8e2e6bcdf6e45579316b7188a02f19cd774f75","src/crypto/holder.rs":"5e105be636de0cedfb3d84d81377caeafd3f5bab7c5c181096f8893dfe72f37a","src/crypto/mod.rs":"27194e01b55c81d84b84a96985092538bfbbd2fbf0210420a98e2d7a6a0af594","src/crypto/openssl.rs":"31fdf546b93f21219b059bb1901edc4e2f43712eb37974dfa2e8569646104008","src/error.rs":"8b77bc1c3afb2181e3810831a9d16871b0aa5f59e1b3366118dacda6ea1472c2","src/lib.rs":"083e424abac4c7f978a7f53b4e0e8acc6f10c3d9208726085a7b7c9a44c47787"},"package":"2e831571186ec514efc697af518eee5e4aaecc13edeb4a918227f7bcba2c20b1"}
{"files":{"CODE_OF_CONDUCT.md":"902d5357af363426631d907e641e220b3ec89039164743f8442b3f120479b7cf","Cargo.toml":"a79a698dd321168a78077b9611101873d7e4221876eccb5b08f4e283d074584b","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"95b8f2f9a8adefedc23db615756366eb5555fc2e256152e3f534fddcfebe18ea","src/aes128gcm.rs":"5239bcd2ce3d4768f68c10e8fa2f9c49bd459282768939160bd21c0cdc46ca91","src/aesgcm.rs":"41bab3915ced4ec00c8da4763faf0436c3ad3f6fada4bf5fd41f639904988c5c","src/common.rs":"ac8425072c22e32f2b6f7a34d9795fb766e989ce737cdd5565c34560acb976f9","src/crypto/holder.rs":"38424503c9e0c36c304bc38dd4db268672f8a9f2fa70525d785eecae54aa47a9","src/crypto/mod.rs":"27194e01b55c81d84b84a96985092538bfbbd2fbf0210420a98e2d7a6a0af594","src/crypto/openssl.rs":"5661f3a9fe2ce2a4d9c5cefe8a8aa9a99e1216d4129ff6b2f2e017102e02f60a","src/error.rs":"53a75d97335b9ec63fc325436f59d5baf66c8265eb847c4f5e40716dbb50438f","src/lib.rs":"6c45f743c71b6410d2c5808be786da98bb813384d565801f99c309258042a99c"},"package":"53d97f19730c1eb3332d0657d0f3ca72795d77c61d8eb26bdd7f15edc0c61eb2"}

View File

@ -13,7 +13,7 @@
[package]
edition = "2018"
name = "ece"
version = "1.1.2"
version = "1.2.1"
authors = ["Edouard Oger <eoger@fastmail.com>", "JR Conlin <jrconlin@gmail.com>"]
description = "Encrypted Content-Encoding for HTTP Rust implementation."
keywords = ["http-ece", "web-push"]
@ -25,22 +25,16 @@ version = "0.12"
[dependencies.byteorder]
version = "1.3"
[dependencies.failure]
version = "0.1"
[dependencies.failure_derive]
version = "0.1"
[dependencies.hkdf]
version = "0.7"
version = "0.9"
optional = true
[dependencies.lazy_static]
version = "1.2"
version = "1.4"
optional = true
[dependencies.once_cell]
version = "1.0"
version = "1.4"
[dependencies.openssl]
version = "0.10"
@ -52,10 +46,13 @@ features = ["derive"]
optional = true
[dependencies.sha2]
version = "0.8"
version = "0.9"
optional = true
[dependencies.thiserror]
version = "1.0"
[dev-dependencies.hex]
version = "0.3"
version = "0.4"
[features]
backend-openssl = ["openssl", "lazy_static", "hkdf", "sha2"]

View File

@ -95,29 +95,29 @@ impl Aes128GcmEceWebPush {
payload: &[u8],
) -> Result<Vec<u8>> {
if payload.len() < ECE_AES128GCM_HEADER_LENGTH {
return Err(ErrorKind::HeaderTooShort.into());
return Err(Error::HeaderTooShort);
}
let key_id_len = payload[ECE_SALT_LENGTH + 4] as usize;
if payload.len() < ECE_AES128GCM_HEADER_LENGTH + key_id_len {
return Err(ErrorKind::HeaderTooShort.into());
return Err(Error::HeaderTooShort);
}
let rs = BigEndian::read_u32(&payload[ECE_SALT_LENGTH..]);
if rs < ECE_AES128GCM_MIN_RS {
return Err(ErrorKind::InvalidRecordSize.into());
return Err(Error::InvalidRecordSize);
}
let salt = &payload[0..ECE_SALT_LENGTH];
if key_id_len != ECE_WEBPUSH_PUBLIC_KEY_LENGTH {
return Err(ErrorKind::InvalidKeyLength.into());
return Err(Error::InvalidKeyLength);
}
let key_id_pos = ECE_AES128GCM_HEADER_LENGTH;
let key_id = &payload[key_id_pos..key_id_pos + key_id_len];
let ciphertext_start = ECE_AES128GCM_HEADER_LENGTH + key_id_len;
if payload.len() == ciphertext_start {
return Err(ErrorKind::ZeroCiphertext.into());
return Err(Error::ZeroCiphertext);
}
let ciphertext = &payload[ciphertext_start..];
let cryptographer = crypto::holder::get_cryptographer();
@ -153,11 +153,11 @@ impl EceWebPush for Aes128GcmEceWebPush {
fn unpad(block: &[u8], last_record: bool) -> Result<&[u8]> {
let pos = match block.iter().rposition(|&b| b != 0) {
Some(pos) => pos,
None => return Err(ErrorKind::ZeroCiphertext.into()),
None => return Err(Error::ZeroCiphertext),
};
let expected_delim = if last_record { 2 } else { 1 };
if block[pos] != expected_delim {
return Err(ErrorKind::DecryptPadding.into());
return Err(Error::DecryptPadding);
}
Ok(&block[..pos])
}

View File

@ -182,7 +182,14 @@ impl EceWebPush for AesGcmEceWebPush {
}
fn unpad(block: &[u8], _: bool) -> Result<&[u8]> {
Ok(&block[2..])
let padding_size = (((block[0] as u16) << 8) | block[1] as u16) as usize;
if padding_size >= block.len() - 2 {
return Err(Error::DecryptPadding);
}
if block[2..(2 + padding_size)].iter().any(|b| *b != 0u8) {
return Err(Error::DecryptPadding);
}
Ok(&block[(2 + padding_size)..])
}
/// Derives the "aesgcm" decryption keyn and nonce given the receiver private
@ -221,7 +228,7 @@ fn encode_keys(raw_key1: &[u8], raw_key2: &[u8]) -> Result<Vec<u8>> {
let mut combined = vec![0u8; ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH];
if raw_key1.len() > ECE_WEBPUSH_RAW_KEY_LENGTH || raw_key2.len() > ECE_WEBPUSH_RAW_KEY_LENGTH {
return Err(ErrorKind::InvalidKeyLength.into());
return Err(Error::InvalidKeyLength);
}
// length prefix each key
combined[0] = 0;

View File

@ -67,13 +67,13 @@ pub trait EceWebPush {
plaintext: &[u8],
) -> Result<Vec<u8>> {
if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH {
return Err(ErrorKind::InvalidAuthSecret.into());
return Err(Error::InvalidAuthSecret);
}
if salt.len() != ECE_SALT_LENGTH {
return Err(ErrorKind::InvalidSalt.into());
return Err(Error::InvalidSalt);
}
if plaintext.is_empty() {
return Err(ErrorKind::ZeroPlaintext.into());
return Err(Error::ZeroPlaintext);
}
let (key, nonce) = Self::derive_key_and_nonce(
EceMode::ENCRYPT,
@ -136,7 +136,7 @@ pub trait EceWebPush {
// We have padding left, but not enough plaintext to form a full record.
// Writing trailing padding-only records will still leak size information,
// so we force the caller to pick a smaller padding length.
return Err(ErrorKind::EncryptPadding.into());
return Err(Error::EncryptPadding);
}
let iv = generate_iv(&nonce, counter);
@ -163,17 +163,17 @@ pub trait EceWebPush {
ciphertext: &[u8],
) -> Result<Vec<u8>> {
if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH {
return Err(ErrorKind::InvalidAuthSecret.into());
return Err(Error::InvalidAuthSecret);
}
if salt.len() != ECE_SALT_LENGTH {
return Err(ErrorKind::InvalidSalt.into());
return Err(Error::InvalidSalt);
}
if ciphertext.is_empty() {
return Err(ErrorKind::ZeroCiphertext.into());
return Err(Error::ZeroCiphertext);
}
if Self::needs_trailer(rs, ciphertext.len()) {
// If we're missing a trailing block, the ciphertext is truncated.
return Err(ErrorKind::DecryptTruncated.into());
return Err(Error::DecryptTruncated);
}
let (key, nonce) = Self::derive_key_and_nonce(
EceMode::DECRYPT,
@ -188,7 +188,7 @@ pub trait EceWebPush {
.enumerate()
.map(|(count, record)| {
if record.len() <= ECE_TAG_LENGTH {
return Err(ErrorKind::BlockTooShort.into());
return Err(Error::BlockTooShort);
}
let iv = generate_iv(&nonce, count);
assert!(record.len() > ECE_TAG_LENGTH);
@ -196,7 +196,7 @@ pub trait EceWebPush {
let plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, record)?;
let last_record = count == records_count - 1;
if plaintext.len() < Self::pad_size() {
return Err(ErrorKind::BlockTooShort.into());
return Err(Error::BlockTooShort);
}
Ok(Self::unpad(&plaintext, last_record)?.to_vec())
})

View File

@ -3,13 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use super::Cryptographer;
use failure::Fail;
use once_cell::sync::OnceCell;
static CRYPTOGRAPHER: OnceCell<&'static dyn Cryptographer> = OnceCell::new();
#[derive(Debug, Fail)]
#[fail(display = "Cryptographer already initialized")]
#[derive(Debug, thiserror::Error)]
#[error("Cryptographer already initialized")]
pub struct SetCryptographerError(());
/// Sets the global object that will be used for cryptographic operations.

View File

@ -159,7 +159,7 @@ impl Cryptographer for OpensslCryptographer {
}
fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>> {
let hk = Hkdf::<Sha256>::extract(Some(&salt[..]), &secret);
let (_, hk) = Hkdf::<Sha256>::extract(Some(&salt[..]), &secret);
let mut okm = vec![0u8; len];
hk.expand(&info, &mut okm).unwrap();
Ok(okm)

View File

@ -2,127 +2,50 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use failure::{Backtrace, Context, Fail};
use std::{boxed::Box, fmt, result};
pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub struct Error(Box<Context<ErrorKind>>);
impl Fail for Error {
#[inline]
fn cause(&self) -> Option<&dyn Fail> {
self.0.cause()
}
#[inline]
fn backtrace(&self) -> Option<&Backtrace> {
self.0.backtrace()
}
}
impl fmt::Display for Error {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&*self.0, f)
}
}
impl Error {
#[inline]
pub fn kind(&self) -> &ErrorKind {
&*self.0.get_context()
}
}
impl From<ErrorKind> for Error {
#[inline]
fn from(kind: ErrorKind) -> Error {
Error(Box::new(Context::new(kind)))
}
}
impl From<Context<ErrorKind>> for Error {
#[inline]
fn from(inner: Context<ErrorKind>) -> Error {
Error(Box::new(inner))
}
}
#[derive(Debug, Fail)]
pub enum ErrorKind {
#[fail(display = "Invalid auth secret")]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid auth secret")]
InvalidAuthSecret,
#[fail(display = "Invalid salt")]
#[error("Invalid salt")]
InvalidSalt,
#[fail(display = "Invalid key length")]
#[error("Invalid key length")]
InvalidKeyLength,
#[fail(display = "Invalid record size")]
#[error("Invalid record size")]
InvalidRecordSize,
#[fail(display = "Invalid header size (too short)")]
#[error("Invalid header size (too short)")]
HeaderTooShort,
#[fail(display = "Truncated ciphertext")]
#[error("Truncated ciphertext")]
DecryptTruncated,
#[fail(display = "Zero-length ciphertext")]
#[error("Zero-length ciphertext")]
ZeroCiphertext,
#[fail(display = "Zero-length plaintext")]
#[error("Zero-length plaintext")]
ZeroPlaintext,
#[fail(display = "Block too short")]
#[error("Block too short")]
BlockTooShort,
#[fail(display = "Invalid decryption padding")]
#[error("Invalid decryption padding")]
DecryptPadding,
#[fail(display = "Invalid encryption padding")]
#[error("Invalid encryption padding")]
EncryptPadding,
#[fail(display = "Could not decode base64 entry")]
DecodeError,
#[error("Could not decode base64 entry")]
DecodeError(#[from] base64::DecodeError),
#[fail(display = "Crypto backend error")]
#[error("Crypto backend error")]
CryptoError,
#[cfg(feature = "backend-openssl")]
#[fail(display = "OpenSSL error: {}", _0)]
OpenSSLError(#[fail(cause)] openssl::error::ErrorStack),
}
impl From<base64::DecodeError> for Error {
#[inline]
fn from(_: base64::DecodeError) -> Error {
ErrorKind::DecodeError.into()
}
}
#[cfg(feature = "backend-openssl")]
macro_rules! impl_from_error {
($(($variant:ident, $type:ty)),+) => ($(
impl From<$type> for ErrorKind {
#[inline]
fn from(e: $type) -> ErrorKind {
ErrorKind::$variant(e)
}
}
impl From<$type> for Error {
#[inline]
fn from(e: $type) -> Error {
ErrorKind::from(e).into()
}
}
)*);
}
#[cfg(feature = "backend-openssl")]
impl_from_error! {
(OpenSSLError, ::openssl::error::ErrorStack)
#[error("OpenSSL error: {0}")]
OpenSSLError(#[from] openssl::error::ErrorStack),
}

View File

@ -264,8 +264,8 @@ mod aes128gcm_tests {
"45b74d2b69be9b074de3b35aa87e7c15611d",
)
.unwrap_err();
match err.kind() {
ErrorKind::HeaderTooShort => {}
match err {
Error::HeaderTooShort => {}
_ => unreachable!(),
};
}
@ -279,8 +279,8 @@ mod aes128gcm_tests {
"de5b696b87f1a15cb6adebdd79d6f99e000000120100b6bc1826c37c9f73dd6b4859c2b505181952",
)
.unwrap_err();
match err.kind() {
ErrorKind::InvalidKeyLength => {}
match err {
Error::InvalidKeyLength => {}
_ => unreachable!(),
};
}
@ -293,8 +293,8 @@ mod aes128gcm_tests {
"355a38cd6d9bef15990e2d3308dbd600",
"8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd"
).unwrap_err();
match err.kind() {
ErrorKind::OpenSSLError(_) => {}
match err {
Error::OpenSSLError(_) => {}
_ => unreachable!(),
};
}
@ -307,8 +307,8 @@ mod aes128gcm_tests {
"40c241fde4269ee1e6d725592d982718",
"dbe215507d1ad3d2eaeabeae6e874d8f0000001241047bc4343f34a8348cdc4e462ffc7c40aa6a8c61a739c4c41d45125505f70e9fc5f9efa86852dd488dcf8e8ea2cafb75e07abd5ee7c9d5c038bafef079571b0bda294411ce98c76dd031c0e580577a4980a375e45ed30429be0e2ee9da7e6df8696d01b8ec"
).unwrap_err();
match err.kind() {
ErrorKind::DecryptPadding => {}
match err {
Error::DecryptPadding => {}
_ => unreachable!(),
};
}
@ -369,6 +369,43 @@ mod aesgcm_tests {
assert!(result == plaintext)
}
#[test]
fn test_decode_padding() {
// generated the content using pywebpush, which verified against the client.
let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag";
let priv_key_raw = "yerDmA9uNFoaUnSt2TkWWLwPseG1qtzS2zdjUl8Z7tc";
let pub_key_raw = "BLBlTYure2QVhJCiDt4gRL0JNmUBMxtNB5B6Z1hDg5h-Epw6mVFV4whoYGBlWNY-ENR1FObkGFyMf7-6ZMHMAxw";
// Incoming Crypto-Key: dh=
let dh = "BCX7KJ_1Em-LjeB56E2KDoMjKDhTaDhjv8c6dwbvZQZ_Gsfp3AT54x2zYUPcBwd1GVyGsk55ProJ98cFrVxrPz4";
// Incoming Encryption-Key: salt=
let salt = "x2I2OZpSCoe-Cc5UW36Nng";
// Incoming Body (this is normally raw bytes. It's encoded here for presentation)
let ciphertext = base64::decode_config("Ua3-WW5kTbt11dBTiXBP6_hLBYhBNOtDFfue5QHMTd2DicL0wutDnt5z9pjRJ76w562egPq5qro95YLnsX0NWGmDQbsQ0Azds6jcBGsxHPt0p5GELAtR4AJj2OsB_LV7dTuGHN2SqsyXLARjTFN2wsF3xWhmuw",
base64::URL_SAFE_NO_PAD).unwrap();
let plaintext = "Tabs are the real indent";
let block = AesGcmEncryptedBlock::new(
&base64::decode_config(dh, base64::URL_SAFE_NO_PAD).unwrap(),
&base64::decode_config(salt, base64::URL_SAFE_NO_PAD).unwrap(),
4096,
ciphertext,
)
.unwrap();
let result = try_decrypt(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap();
println!(
"Result: b64={}",
base64::encode_config(&result, base64::URL_SAFE_NO_PAD)
);
println!(
"Plaintext: b64={}",
base64::encode_config(&plaintext, base64::URL_SAFE_NO_PAD)
);
assert!(result == plaintext)
}
#[test]
fn test_e2e() {
let (local_key, remote_key) = generate_keys().unwrap();

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"21b095cc85324ada8cf714deb719e3e892f2f5222538e063db56fde0f81bd17c","src/lib.rs":"4581b12eb58f9fb5275c7af74fbc4521b82ef224b6ba81f0e785c372ba95f8c6"},"package":null}
{"files":{"Cargo.toml":"8419c1fdf71e30634e2ca9dbbabf0f83fdb7dde6a945060c31a16acdbec6894f","src/lib.rs":"70fa9bba695574d97a6e1add79c400744cbd0d60639cbcdc0121c89fa20f9c81"},"package":null}

View File

@ -6,5 +6,8 @@ edition = "2018"
license = "MPL-2.0"
[dependencies]
failure = "0.1"
thiserror = "1.0"
[dependencies.backtrace]
optional = true
version = "0.3"

View File

@ -2,59 +2,109 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#[cfg(feature = "backtrace")]
/// Re-export of the `backtrace` crate for use in macros and
/// to ensure the needed version is kept in sync in dependents.
pub use backtrace;
#[cfg(not(feature = "backtrace"))]
/// A compatibility shim for `backtrace`.
pub mod backtrace {
use std::fmt;
pub struct Backtrace;
impl fmt::Debug for Backtrace {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Not available")
}
}
}
/// Define a wrapper around the the provided ErrorKind type.
/// See also `define_error` which is more likely to be what you want.
#[macro_export]
macro_rules! define_error_wrapper {
($Kind:ty) => {
/// Re-exported, so that using crate::error::* gives you the .context()
/// method, which we don't use much but should *really* use more.
pub use failure::ResultExt;
pub type Result<T, E = Error> = std::result::Result<T, E>;
struct ErrorData {
kind: $Kind,
backtrace: Option<std::sync::Mutex<$crate::backtrace::Backtrace>>,
}
#[derive(Debug)]
pub struct Error(Box<failure::Context<$Kind>>);
impl failure::Fail for Error {
fn cause(&self) -> Option<&dyn failure::Fail> {
self.0.cause()
impl ErrorData {
#[cold]
fn new(kind: $Kind) -> Self {
ErrorData {
kind,
#[cfg(feature = "backtrace")]
backtrace: Some(std::sync::Mutex::new(
$crate::backtrace::Backtrace::new_unresolved(),
)),
#[cfg(not(feature = "backtrace"))]
backtrace: None,
}
}
fn backtrace(&self) -> Option<&failure::Backtrace> {
self.0.backtrace()
#[cfg(feature = "backtrace")]
#[cold]
fn get_backtrace(&self) -> Option<&std::sync::Mutex<$crate::backtrace::Backtrace>> {
self.backtrace.as_ref().map(|mutex| {
mutex.lock().unwrap().resolve();
mutex
})
}
fn name(&self) -> Option<&str> {
self.0.name()
#[cfg(not(feature = "backtrace"))]
#[cold]
fn get_backtrace(&self) -> Option<&std::sync::Mutex<$crate::backtrace::Backtrace>> {
None
}
}
impl std::fmt::Debug for ErrorData {
#[cfg(feature = "backtrace")]
#[cold]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut bt = self.backtrace.unwrap().lock().unwrap();
bt.resolve();
write!(f, "{:?}\n\n{}", bt, self.kind)
}
#[cfg(not(feature = "backtrace"))]
#[cold]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.kind)
}
}
#[derive(Debug, thiserror::Error)]
pub struct Error(Box<ErrorData>);
impl Error {
#[cold]
pub fn kind(&self) -> &$Kind {
&self.0.kind
}
#[cold]
pub fn backtrace(&self) -> Option<&std::sync::Mutex<$crate::backtrace::Backtrace>> {
self.0.get_backtrace()
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&*self.0, f)
}
}
impl Error {
pub fn kind(&self) -> &$Kind {
&*self.0.get_context()
}
}
impl From<failure::Context<$Kind>> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(ctx: failure::Context<$Kind>) -> Error {
Error(Box::new(ctx))
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.kind(), f)
}
}
impl From<$Kind> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(kind: $Kind) -> Self {
Error(Box::new(failure::Context::new(kind)))
fn from(ctx: $Kind) -> Error {
Error(Box::new(ErrorData::new(ctx)))
}
}
};
@ -66,14 +116,6 @@ macro_rules! define_error_wrapper {
#[macro_export]
macro_rules! define_error_conversions {
($Kind:ident { $(($variant:ident, $type:ty)),* $(,)? }) => ($(
impl From<$type> for $Kind {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(e: $type) -> $Kind {
$Kind::$variant(e)
}
}
impl From<$type> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"7a7e6f298886b5427ccc30719c24816a12c5ec5344ec7d8e610e4d9fdc7d65d4","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"63e747d86bdeb67638f26b4b75107f129c5f12de432ae83ccdb1ccbe28debf30","README.md":"67780fbfcaf2cd01e3b7f5c7d1ef8b9e385b7cd4435358954aec24a85755ced2","src/error.rs":"e4c87fe305f7fbb830348c0f5b181385c96aa9561dfa1536ef1ce09065e6ce83","src/ffistr.rs":"12a4f351c248e150da18b6ea3797eca65f63e8fa24c62828a2510b9c3a4b8ca5","src/handle_map.rs":"d5f22ad76260f8c1c9cc019c4fec579281b75ae670742b1c4f2470ae56cce87c","src/into_ffi.rs":"bde79bf6b2bbc3108654bf14ac4216c4c5f2a91962de6f814d0bac1bde243c1c","src/lib.rs":"3aa48de38137e2c8784c65b7d981af01b668e1543fc1cf35d52535828bace13f","src/macros.rs":"479153198e1676fdca0be53cbd437a05cd807a2520bacfdb8849a742188f1359","src/string.rs":"966d2b41fae4e7a6083eb142a57e669e4bafd833f01c8b24fc67dff4fb4a5595"},"package":"087be066eb6e85d7150f0c5400018a32802f99d688b2d3868c526f7bbfe17960"}
{"files":{"Cargo.toml":"59a3a656f31bd225f1864abff65b678bdc5c863e14e426f980d4d8b02f1ca747","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"63e747d86bdeb67638f26b4b75107f129c5f12de432ae83ccdb1ccbe28debf30","README.md":"ac7041bc517c1fc051fa9773527955c6debc12473f80b9f24bea7119c3fb35be","src/error.rs":"e4c87fe305f7fbb830348c0f5b181385c96aa9561dfa1536ef1ce09065e6ce83","src/ffistr.rs":"12a4f351c248e150da18b6ea3797eca65f63e8fa24c62828a2510b9c3a4b8ca5","src/handle_map.rs":"d6ac073e1559aad561e32a8c2b7b18257fd7f70c1cf4004a8f90ed6202a185e8","src/into_ffi.rs":"bde79bf6b2bbc3108654bf14ac4216c4c5f2a91962de6f814d0bac1bde243c1c","src/lib.rs":"fcdb5231573677f8d1fa7b37b36319a688dc459d8a56c5dd9144eace25b22c94","src/macros.rs":"479153198e1676fdca0be53cbd437a05cd807a2520bacfdb8849a742188f1359","src/string.rs":"966d2b41fae4e7a6083eb142a57e669e4bafd833f01c8b24fc67dff4fb4a5595"},"package":"f85d4d1be103c0b2d86968f0b0690dc09ac0ba205b90adb0389b552869e5000e"}

View File

@ -13,7 +13,7 @@
[package]
edition = "2018"
name = "ffi-support"
version = "0.4.0"
version = "0.4.2"
authors = ["Thom Chiovoloni <tchiovoloni@mozilla.com>"]
description = "A crate to help expose Rust functions over the FFI."
readme = "README.md"
@ -22,22 +22,14 @@ categories = ["development-tools::ffi"]
license = "Apache-2.0 / MIT"
repository = "https://github.com/mozilla/application-services"
[dependencies.backtrace]
version = "0.3.38"
version = "0.3.48"
optional = true
[dependencies.lazy_static]
version = "1.4.0"
version = "1.4"
[dependencies.log]
version = "0.4"
[dev-dependencies.env_logger]
version = "0.7.0"
[dev-dependencies.rand]
version = "0.7.2"
[dev-dependencies.rayon]
version = "1.3.0"
[features]
default = []

View File

@ -21,7 +21,7 @@ Add the following to your Cargo.toml
ffi-support = "0.1.1"
```
For further examples, the examples in the docs is the best starting point, followed by the usage code in the [mozilla/application-services](https://github.com/mozilla/application-services) repo (for example [here](https://github.com/mozilla/application-services/blob/master/components/places/ffi/src/lib.rs) or [here](https://github.com/mozilla/application-services/blob/master/components/places/src/ffi.rs)).
For further examples, the examples in the docs is the best starting point, followed by the usage code in the [mozilla/application-services](https://github.com/mozilla/application-services) repo (for example [here](https://github.com/mozilla/application-services/blob/main/components/places/ffi/src/lib.rs) or [here](https://github.com/mozilla/application-services/blob/main/components/places/src/ffi.rs)).
## License

View File

@ -1065,10 +1065,9 @@ lazy_static::lazy_static! {
#[cfg(test)]
mod test {
use super::*;
use std::sync::Arc;
#[derive(PartialEq, Debug)]
struct Foobar(usize);
pub(super) struct Foobar(usize);
#[test]
fn test_invalid_handle() {
@ -1166,154 +1165,90 @@ mod test {
}
}
fn with_error<F: FnOnce(&mut ExternError) -> T, T>(callback: F) -> T {
let mut e = ExternError::success();
let result = callback(&mut e);
if let Some(m) = unsafe { e.get_and_consume_message() } {
panic!("unexpected error: {}", m);
/// Tests that check our behavior when panicing.
///
/// Naturally these require panic=unwind, which means we can't run them when
/// generating coverage (well, `-Zprofile`-based coverage can't -- although
/// ptrace-based coverage like tarpaulin can), and so we turn them off.
///
/// (For clarity, `cfg(coverage)` is not a standard thing. We add it in
/// `automation/emit_coverage_info.sh`, and you can force it by adding
/// "--cfg coverage" to your RUSTFLAGS manually if you need to do so).
#[cfg(not(coverage))]
mod panic_tests {
use super::*;
struct PanicOnDrop(());
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("intentional panic (drop)");
}
}
result
}
struct DropChecking {
counter: Arc<AtomicUsize>,
id: usize,
}
impl Drop for DropChecking {
fn drop(&mut self) {
let val = self.counter.fetch_add(1, Ordering::SeqCst);
log::debug!("Dropped {} :: {}", self.id, val);
}
}
#[test]
fn test_concurrent_drop() {
use rand::prelude::*;
use rayon::prelude::*;
let _ = env_logger::try_init();
let drop_counter = Arc::new(AtomicUsize::new(0));
let id = Arc::new(AtomicUsize::new(1));
let map = ConcurrentHandleMap::new();
let count = 1000;
let mut handles = (0..count)
.into_par_iter()
.map(|_| {
let id = id.fetch_add(1, Ordering::SeqCst);
let handle = with_error(|e| {
map.insert_with_output(e, || {
log::debug!("Created {}", id);
DropChecking {
counter: drop_counter.clone(),
id,
}
})
});
(id, handle)
})
.collect::<Vec<_>>();
handles.shuffle(&mut thread_rng());
assert_eq!(drop_counter.load(Ordering::SeqCst), 0);
handles.par_iter().for_each(|(id, h)| {
with_error(|e| {
map.call_with_output(e, *h, |val| {
assert_eq!(val.id, *id);
})
});
});
assert_eq!(drop_counter.load(Ordering::SeqCst), 0);
handles.par_iter().for_each(|(id, h)| {
with_error(|e| {
map.call_with_output(e, *h, |val| {
assert_eq!(val.id, *id);
})
});
});
handles.par_iter().for_each(|(id, h)| {
let item = map
.remove_u64(*h)
.expect("remove to succeed")
.expect("item to exist");
assert_eq!(item.id, *id);
let h = map.insert(item).into_u64();
map.delete_u64(h).expect("delete to succeed");
});
assert_eq!(drop_counter.load(Ordering::SeqCst), count);
}
struct PanicOnDrop(());
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("intentional panic (drop)");
}
}
#[test]
fn test_panicking_drop() {
let map = ConcurrentHandleMap::new();
let h = map.insert(PanicOnDrop(())).into_u64();
let mut e = ExternError::success();
crate::call_with_result(&mut e, || map.delete_u64(h));
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 0);
}
#[test]
fn test_panicking_call_with() {
let map = ConcurrentHandleMap::new();
let h = map.insert(Foobar(0)).into_u64();
let mut e = ExternError::success();
map.call_with_output(&mut e, h, |_thing| {
panic!("intentional panic (call_with_output)");
});
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
{
#[test]
fn test_panicking_drop() {
let map = ConcurrentHandleMap::new();
let h = map.insert(PanicOnDrop(())).into_u64();
let mut e = ExternError::success();
crate::call_with_result(&mut e, || map.delete_u64(h));
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 1);
let mut seen = false;
for e in &inner.entries {
if let EntryState::Active(v) = &e.state {
assert!(!seen);
assert!(v.is_poisoned());
seen = true;
assert_eq!(inner.len(), 0);
}
#[test]
fn test_panicking_call_with() {
let map = ConcurrentHandleMap::new();
let h = map.insert(Foobar(0)).into_u64();
let mut e = ExternError::success();
map.call_with_output(&mut e, h, |_thing| {
panic!("intentional panic (call_with_output)");
});
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
{
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 1);
let mut seen = false;
for e in &inner.entries {
if let EntryState::Active(v) = &e.state {
assert!(!seen);
assert!(v.is_poisoned());
seen = true;
}
}
}
assert!(map.delete_u64(h).is_ok());
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 0);
}
assert!(map.delete_u64(h).is_ok());
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 0);
}
#[test]
fn test_panicking_insert_with() {
let map = ConcurrentHandleMap::new();
let mut e = ExternError::success();
let res = map.insert_with_output(&mut e, || {
panic!("intentional panic (insert_with_output)");
});
#[test]
fn test_panicking_insert_with() {
let map = ConcurrentHandleMap::new();
let mut e = ExternError::success();
let res = map.insert_with_output(&mut e, || {
panic!("intentional panic (insert_with_output)");
});
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
assert_eq!(e.get_code(), crate::ErrorCode::PANIC);
let _ = unsafe { e.get_and_consume_message() };
assert_eq!(res, 0);
assert_eq!(res, 0);
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 0);
assert!(!map.map.is_poisoned());
let inner = map.map.read().unwrap();
inner.assert_valid();
assert_eq!(inner.len(), 0);
}
}
}

View File

@ -348,13 +348,34 @@ fn init_panic_handling_once() {}
/// `i64` is used for the length instead of `u64` and `usize` because JNA has interop
/// issues with both these types.
///
/// ByteBuffer does not implement Drop. This is intentional. Memory passed into it will
/// be leaked if it is not explicitly destroyed by calling [`ByteBuffer::destroy`]. This
/// is because in the future, we may allow it's use for passing data into Rust code.
/// ByteBuffer assuming ownership of the data would make this a problem.
/// ### `Drop` is not implemented
///
/// Note that alling `destroy` manually is not typically needed or recommended,
/// and instead you should use [`define_bytebuffer_destructor!`].
/// ByteBuffer does not implement Drop. This is intentional. Memory passed into it will
/// be leaked if it is not explicitly destroyed by calling [`ByteBuffer::destroy`], or
/// [`ByteBuffer::destroy_into_vec`]. This is for two reasons:
///
/// 1. In the future, we may allow it to be used for data that is not managed by
/// the Rust allocator\*, and `ByteBuffer` assuming it's okay to automatically
/// deallocate this data with the Rust allocator.
///
/// 2. Automatically running destructors in unsafe code is a
/// [frequent footgun](https://without.boats/blog/two-memory-bugs-from-ringbahn/)
/// (among many similar issues across many crates).
///
/// Note that calling `destroy` manually is often not needed, as usually you should
/// be passing these to the function defined by [`define_bytebuffer_destructor!`] from
/// the other side of the FFI.
///
/// Because this type is essentially *only* useful in unsafe or FFI code (and because
/// the most common usage pattern does not require manually managing the memory), it
/// does not implement `Drop`.
///
/// \* Note: in the case of multiple Rust shared libraries loaded at the same time,
/// there may be multiple instances of "the Rust allocator" (one per shared library),
/// in which case we're referring to whichever instance is active for the code using
/// the `ByteBuffer`. Note that this doesn't occur on all platforms or build
/// configurations, but treating allocators in different shared libraries as fully
/// independent is always safe.
///
/// ## Layout/fields
///
@ -367,21 +388,28 @@ fn init_panic_handling_once() {}
///
/// ```c,no_run
/// struct ByteBuffer {
/// // Note: This should never be negative, but values above
/// // INT64_MAX / i64::MAX are not allowed.
/// int64_t len;
/// uint8_t *data; // note: nullable
/// // Note: nullable!
/// uint8_t *data;
/// };
/// ```
///
/// In rust, there are two fields, in this order: `len: i64`, and `data: *mut u8`.
///
/// For clarity, the fact that the data pointer is nullable means that `Option<ByteBuffer>` is not
/// the same size as ByteBuffer, and additionally is not FFI-safe (the latter point is not
/// currently guaranteed anyway as of the time of writing this comment).
///
/// ### Description of fields
///
/// `data` is a pointer to an array of `len` bytes. Not that data can be a null pointer and therefore
/// `data` is a pointer to an array of `len` bytes. Note that data can be a null pointer and therefore
/// should be checked.
///
/// The bytes array is allocated on the heap and must be freed on it as well. Critically, if there
/// are multiple rust packages using being used in the same application, it *must be freed on the
/// same heap that allocated it*, or you will corrupt both heaps.
/// are multiple rust shared libraries using being used in the same application, it *must be freed
/// on the same heap that allocated it*, or you will corrupt both heaps.
///
/// Typically, this object is managed on the other side of the FFI (on the "FFI consumer"), which
/// means you must expose a function to release the resources of `data` which can be done easily
@ -410,6 +438,9 @@ impl ByteBuffer {
/// This will panic if the buffer length (`usize`) cannot fit into a `i64`.
#[inline]
pub fn new_with_size(size: usize) -> Self {
// Note: `Vec` requires this internally on 64 bit platforms (and has a
// stricter requirement on 32 bit ones), so this is just to be explicit.
assert!(size < i64::MAX as usize);
let mut buf = vec![];
buf.reserve_exact(size);
buf.resize(size, 0);
@ -434,16 +465,77 @@ impl ByteBuffer {
Self { data, len }
}
/// Convert this `ByteBuffer` into a Vec<u8>. This is the only way
/// to access the data from inside the buffer.
/// View the data inside this `ByteBuffer` as a `&[u8]`.
// TODO: Is it worth implementing `Deref`? Patches welcome if you need this.
#[inline]
pub fn as_slice(&self) -> &[u8] {
if self.data.is_null() {
&[]
} else {
unsafe { std::slice::from_raw_parts(self.data, self.len()) }
}
}
#[inline]
fn len(&self) -> usize {
use std::convert::TryInto;
self.len
.try_into()
.expect("ByteBuffer length negative or overflowed")
}
/// View the data inside this `ByteBuffer` as a `&mut [u8]`.
// TODO: Is it worth implementing `DerefMut`? Patches welcome if you need this.
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
if self.data.is_null() {
&mut []
} else {
unsafe { std::slice::from_raw_parts_mut(self.data, self.len()) }
}
}
/// Deprecated alias for [`ByteBuffer::destroy_into_vec`].
#[inline]
#[deprecated = "Name is confusing, please use `destroy_into_vec` instead"]
pub fn into_vec(self) -> Vec<u8> {
self.destroy_into_vec()
}
/// Convert this `ByteBuffer` into a Vec<u8>, taking ownership of the
/// underlying memory, which will be freed using the rust allocator once the
/// `Vec<u8>`'s lifetime is done.
///
/// If this is undesirable, you can do `bb.as_slice().to_vec()` to get a
/// `Vec<u8>` containing a copy of this `ByteBuffer`'s underlying data.
///
/// ## Caveats
///
/// This is safe so long as the buffer is empty, or the data was allocated
/// by Rust code, e.g. this is a ByteBuffer created by
/// `ByteBuffer::from_vec` or `Default::default`.
///
/// If the ByteBuffer were allocated by something other than the
/// current/local Rust `global_allocator`, then calling `destroy` is
/// fundamentally broken.
///
/// For example, if it were allocated externally by some other language's
/// runtime, or if it were allocated by the global allocator of some other
/// Rust shared object in the same application, the behavior is undefined
/// (and likely to cause problems).
///
/// Note that this currently can only happen if the `ByteBuffer` is passed
/// to you via an `extern "C"` function that you expose, as opposed to being
/// created locally.
#[inline]
pub fn destroy_into_vec(self) -> Vec<u8> {
if self.data.is_null() {
vec![]
} else {
// This is correct because we convert to a Box<[u8]> first, which is
// a design constraint of RawVec.
unsafe { Vec::from_raw_parts(self.data, self.len as usize, self.len as usize) }
let len = self.len();
// Safety: This is correct because we convert to a Box<[u8]> first,
// which is a design constraint of RawVec.
unsafe { Vec::from_raw_parts(self.data, len, len) }
}
}
@ -458,12 +550,22 @@ impl ByteBuffer {
/// by Rust code, e.g. this is a ByteBuffer created by
/// `ByteBuffer::from_vec` or `Default::default`.
///
/// If the ByteBuffer were passed into Rust (which you shouldn't do, since
/// theres no way to see the data in Rust currently), then calling `destroy`
/// is fundamentally broken.
/// If the ByteBuffer were allocated by something other than the
/// current/local Rust `global_allocator`, then calling `destroy` is
/// fundamentally broken.
///
/// For example, if it were allocated externally by some other language's
/// runtime, or if it were allocated by the global allocator of some other
/// Rust shared object in the same application, the behavior is undefined
/// (and likely to cause problems).
///
/// Note that this currently can only happen if the `ByteBuffer` is passed
/// to you via an `extern "C"` function that you expose, as opposed to being
/// created locally.
#[inline]
pub fn destroy(self) {
drop(self.into_vec())
// Note: the drop is just for clarity, of course.
drop(self.destroy_into_vec())
}
}
@ -476,3 +578,46 @@ impl Default for ByteBuffer {
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_bb_access() {
let mut bb = ByteBuffer::from(vec![1u8, 2, 3]);
assert_eq!(bb.as_slice(), &[1u8, 2, 3]);
assert_eq!(bb.as_mut_slice(), &mut [1u8, 2, 3]);
bb.as_mut_slice()[2] = 4;
// Use into_vec to cover both into_vec and destroy_into_vec.
#[allow(deprecated)]
{
assert_eq!(bb.into_vec(), &[1u8, 2, 4]);
}
}
#[test]
fn test_bb_empty() {
let mut bb = ByteBuffer::default();
assert_eq!(bb.as_slice(), &[]);
assert_eq!(bb.as_mut_slice(), &[]);
assert_eq!(bb.destroy_into_vec(), &[]);
}
#[test]
fn test_bb_new() {
let bb = ByteBuffer::new_with_size(5);
assert_eq!(bb.as_slice(), &[0u8, 0, 0, 0, 0]);
bb.destroy();
let bb = ByteBuffer::new_with_size(0);
assert_eq!(bb.as_slice(), &[]);
assert!(!bb.data.is_null());
bb.destroy();
let bb = ByteBuffer::from_vec(vec![]);
assert_eq!(bb.as_slice(), &[]);
assert!(!bb.data.is_null());
bb.destroy();
}
}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"d9be79dd941f9a16b7e1b861820a718ce1eacc3085f62c1738157e1c8a47f0e0","examples/devices_api.rs":"87dfa7e92b33f4a3b0334aacf379a082149db85800947e927dbbf409dbb568ca","examples/migration.rs":"2577e4eb3a14ac8a4e81b6ab79b6658871225b08deb2fb573211fd02daebbdfa","examples/oauth_flow.rs":"99b0cb7b2a12051d2e0893ca9202124e0b229e888ef541518ac1c64ce89d6aff","src/commands/mod.rs":"0197979f5851da2300a481b6f7d0bd1ac7e00b08383339564b2372519965ca58","src/commands/send_tab.rs":"07e9ddc6b172bb3cf1af61f610fb4d39f73e9d63e304a05903f60ee76e3fa75e","src/config.rs":"b7771f5eb581b85a664202c8a63af158f36b672825c0dc164a2d986bfc2bf6fd","src/device.rs":"58c6e8b98280653fe110311d37c3bdd17838bb5dc00709e3697046f0e6a9adf2","src/error.rs":"845280fdd99b13eb79d370f3585ccf4df21cfa4c97697bd9f173fa960b735886","src/ffi.rs":"ffd787adf013d57f776a7a5aac0b895b7bbbd83fbd0ebf5e6f139b404ea40a46","src/fxa_msg_types.proto":"e09fa503f531e83e28d8a813f20338df0cf16f96b7059975d55421a0657656a6","src/http_client.rs":"a0b8f955682ca02af601fbbc60d6476fa07e15916b0bc54b5cf1b0fab4b49dfe","src/lib.rs":"7b151b9a84bcd6a9bca7ecb1689e8909259ee058666ce20bcf8182b9b080c3b0","src/migrator.rs":"8711aad7272f24c4d4d6e57142bafea883331022a35951f06151b7c9855e2c7a","src/mozilla.appservices.fxaclient.protobuf.rs":"e28aaa9ebf0874c9b9e9a482aaf0ee5205b5bff031d4f998d740db198831e9bb","src/oauth.rs":"ae70655b4b38df8313d633be4be52af232981ad3b98334545abc9b447b4a3cea","src/oauth/attached_clients.rs":"e0fd277d4294fd7982100e171cce790ea4acefb22ab1c7ab7b36f8593038cc43","src/profile.rs":"7197b630ebb992fb517a4fd9f5c4c03a24d5f66c4d888ccd5489eda074a5e556","src/push.rs":"dc1e846d4a5b8e7b0615583b4649cf3b0643d608651f6b7a0432f1aebdbb5197","src/scoped_keys.rs":"509bab207ac38e3662c0df9cd58b874f48c91b0728441bdefd684b56b536c514","src/scopes.rs":"2cb0799b0fb8b338124137456fa3cf8d6f56c0ea5709daef4e305c06dd0045c5","src/send_tab.rs":"8fc9ce5d56aa796cd72f7a920e695dfc23146b329f682812a12cb8c336b177ed","src/state_persistence.rs":"7a4a50726b6da30609dc6ca279419806e07c1843c9d7e5cdc1ce0db5ab9a5a2b","src/util.rs":"ecfd2d55dd0b329aac07979a3849485f932af6dd2990f02805ad4668f2c97d67"},"package":null}
{"files":{"Cargo.toml":"d39e902bf4333f2dfd6e152ed3c55f46ab369c485deb9d101b930caa9688664c","src/auth.rs":"55d554055fdcfb4200a6a0fad64e92d522f629aaff4db82aec230f7c1affd2f5","src/commands/mod.rs":"0197979f5851da2300a481b6f7d0bd1ac7e00b08383339564b2372519965ca58","src/commands/send_tab.rs":"527602e281c85287d641c3c712c78627478d33bb66e1533b9b8758d389a727cb","src/config.rs":"b59ef2d751925a35df7cfd2f0454dc11e9ecfff227293785728ac082dd603065","src/device.rs":"d171e343410fbccae16cc5ec6fa10cb9c40544544dbf2906ac46663cb1fcc2b7","src/error.rs":"d2601b4fc7f0492757ab843e0b26a166f2315c9739151cd82773dc19c7393261","src/ffi.rs":"87be94bd84d408c526df50c1af1d76761fc65bf4424169e4c76387be5795ac59","src/fxa_msg_types.proto":"b31777f821bb37e67cae9982cfa43ad2b331dc8ee67b14177085da3290d76d4c","src/http_client.rs":"45cf71da6f350e7474ae0a1dbbb9d7ea55fc829ea2654ed6437b8cacb23a77f7","src/lib.rs":"ec6c7d16fe26a8d7ae8aba292cba30cccca5afad84c607624e11916f10b977e3","src/migrator.rs":"220c142fbd87fbb3f0ee3d8d8c77d625c09bc2b9f4d0d44b3140787eebf30d2f","src/mozilla.appservices.fxaclient.protobuf.rs":"d8a4446a024ffd6dddffe4b85679c23e491c2d859ab34859a420b94940678d8b","src/oauth.rs":"1eda658c5d70458fe64b9eae2080fdcd00ea0e63fbab78238b7cd333a1f8039e","src/oauth/attached_clients.rs":"e0fd277d4294fd7982100e171cce790ea4acefb22ab1c7ab7b36f8593038cc43","src/profile.rs":"b92741613a5f7a7f381171b550320edd89d93824f072f5089fcf6c6318adf88c","src/push.rs":"c7e714d733463bcccd2627d8eca8041e389f737b2a1162667a7217c739832c18","src/scoped_keys.rs":"65bb1c8fa1c24bc3c342c7f5d45843915a4fcace7a509fc6fbd7809fb7c85024","src/scopes.rs":"000360f2193812b20e146cb5bf2782ae7a3c50883f28d018baa572c127d09391","src/send_tab.rs":"a54add670f507dedb626c7eb197a59197da984e44ba1000b683df083b875d7e5","src/state_persistence.rs":"cba27bf9e91727e8a55cd983aece5bf5a77f14e7cd37b72d4b09dcee9e0a871f","src/telemetry.rs":"207ac2940dd7ff8b7a61689fc69a9f933eb13b4abf87dab4472230a2beeb62e2","src/util.rs":"7eb861c2ee72714cd437dc0720b97ca4ca7b8a5590391f8ede00005721a24f81"},"package":null}

View File

@ -8,13 +8,12 @@ exclude = ["/android", "/ios"]
[dependencies]
base64 = "0.12"
byteorder = "1.3"
failure = "0.1"
hex = "0.4"
lazy_static = "1.4"
log = "0.4"
prost = "0.6"
prost-derive = "0.6"
rand_rccrypto = { path = "../support/rand_rccrypto" }
serde = { version = "1", features = ["rc"] }
serde_derive = "1"
serde_json = "1"
@ -22,16 +21,19 @@ sync15 = { path = "../sync15" }
url = "2.1"
ffi-support = "0.4"
viaduct = { path = "../viaduct" }
jwcrypto = { path = "../support/jwcrypto" }
rc_crypto = { path = "../support/rc_crypto", features = ["ece", "hawk"] }
error-support = { path = "../support/error" }
thiserror = "1.0"
anyhow = "1.0"
sync-guid = { path = "../support/guid", features = ["random"] }
[dev-dependencies]
viaduct-reqwest = { path = "../support/viaduct-reqwest" }
cli-support = { path = "../support/cli" }
dialoguer = "0.6"
webbrowser = "0.5"
mockiato = "0.9"
mockito = "0.27"
[features]
default = []
gecko = [ "rc_crypto/gecko" ]
integration_test = []

View File

@ -1,156 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use cli_support::prompt::prompt_string;
use dialoguer::Select;
use fxa_client::{device, Config, FirefoxAccount, IncomingDeviceCommand};
use std::{
collections::HashMap,
fs,
io::{Read, Write},
sync::{Arc, Mutex},
thread, time,
};
use url::Url;
static CREDENTIALS_PATH: &str = "credentials.json";
static CONTENT_SERVER: &str = "https://accounts.firefox.com";
static CLIENT_ID: &str = "a2270f727f45f648";
static REDIRECT_URI: &str = "https://accounts.firefox.com/oauth/success/a2270f727f45f648";
static SCOPES: &[&str] = &["profile", "https://identity.mozilla.com/apps/oldsync"];
static DEFAULT_DEVICE_NAME: &str = "Bobo device";
fn load_fxa_creds() -> Result<FirefoxAccount, failure::Error> {
let mut file = fs::File::open(CREDENTIALS_PATH)?;
let mut s = String::new();
file.read_to_string(&mut s)?;
Ok(FirefoxAccount::from_json(&s)?)
}
fn load_or_create_fxa_creds(cfg: Config) -> Result<FirefoxAccount, failure::Error> {
let acct = load_fxa_creds().or_else(|_e| create_fxa_creds(cfg))?;
persist_fxa_state(&acct);
Ok(acct)
}
fn persist_fxa_state(acct: &FirefoxAccount) {
let json = acct.to_json().unwrap();
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true)
.open(CREDENTIALS_PATH)
.unwrap();
write!(file, "{}", json).unwrap();
file.flush().unwrap();
}
fn create_fxa_creds(cfg: Config) -> Result<FirefoxAccount, failure::Error> {
let mut acct = FirefoxAccount::with_config(cfg);
let oauth_uri = acct.begin_oauth_flow(&SCOPES)?;
if webbrowser::open(&oauth_uri.as_ref()).is_err() {
println!("Please visit this URL, sign in, and then copy-paste the final URL below.");
println!("\n {}\n", oauth_uri);
} else {
println!("Please paste the final URL below:\n");
}
let redirect_uri: String = prompt_string("Final URL").unwrap();
let redirect_uri = Url::parse(&redirect_uri).unwrap();
let query_params: HashMap<_, _> = redirect_uri.query_pairs().into_owned().collect();
let code = &query_params["code"];
let state = &query_params["state"];
acct.complete_oauth_flow(&code, &state).unwrap();
persist_fxa_state(&acct);
Ok(acct)
}
fn main() -> Result<(), failure::Error> {
viaduct_reqwest::use_reqwest_backend();
let cfg = Config::new(CONTENT_SERVER, CLIENT_ID, REDIRECT_URI);
let mut acct = load_or_create_fxa_creds(cfg)?;
// Make sure the device and the send-tab command are registered.
acct.initialize_device(
DEFAULT_DEVICE_NAME,
device::Type::Desktop,
&[device::Capability::SendTab],
)
.unwrap();
persist_fxa_state(&acct);
let acct: Arc<Mutex<FirefoxAccount>> = Arc::new(Mutex::new(acct));
{
let acct = acct.clone();
thread::spawn(move || {
loop {
let evts = acct
.lock()
.unwrap()
.poll_device_commands()
.unwrap_or_else(|_| vec![]); // Ignore 404 errors for now.
persist_fxa_state(&acct.lock().unwrap());
for e in evts {
match e {
IncomingDeviceCommand::TabReceived { sender, payload } => {
let tab = &payload.entries[0];
match sender {
Some(ref d) => {
println!("Tab received from {}: {}", d.display_name, tab.url)
}
None => println!("Tab received: {}", tab.url),
};
webbrowser::open(&tab.url).unwrap();
}
}
}
thread::sleep(time::Duration::from_secs(1));
}
});
}
// Menu:
loop {
println!("Main menu:");
let mut main_menu = Select::new();
main_menu.items(&["Set Display Name", "Send a Tab", "Quit"]);
main_menu.default(0);
let main_menu_selection = main_menu.interact().unwrap();
match main_menu_selection {
0 => {
let new_name: String = prompt_string("New display name").unwrap();
// Set device display name
acct.lock().unwrap().set_device_name(&new_name).unwrap();
println!("Display name set to: {}", new_name);
}
1 => {
let devices = acct.lock().unwrap().get_devices(false).unwrap();
let devices_names: Vec<String> =
devices.iter().map(|i| i.display_name.clone()).collect();
let mut targets_menu = Select::new();
targets_menu.default(0);
let devices_names_refs: Vec<&str> =
devices_names.iter().map(AsRef::as_ref).collect();
targets_menu.items(&devices_names_refs);
println!("Choose a send-tab target:");
let selection = targets_menu.interact().unwrap();
let target = &devices[selection];
// Payload
let title: String = prompt_string("Title").unwrap();
let url: String = prompt_string("URL").unwrap();
acct.lock()
.unwrap()
.send_tab(&target.id, &title, &url)
.unwrap();
println!("Tab sent!");
}
2 => ::std::process::exit(0),
_ => panic!("Invalid choice!"),
}
}
}

View File

@ -1,37 +0,0 @@
use cli_support::prompt::prompt_string;
use fxa_client::{Config, FirefoxAccount};
use std::{thread, time};
static CLIENT_ID: &str = "3c49430b43dfba77";
static CONTENT_SERVER: &str = "https://accounts.firefox.com";
static REDIRECT_URI: &str = "https://accounts.firefox.com/oauth/success/3c49430b43dfba77";
fn main() {
viaduct_reqwest::use_reqwest_backend();
let config = Config::new(CONTENT_SERVER, CLIENT_ID, REDIRECT_URI);
let mut fxa = FirefoxAccount::with_config(config);
println!("Enter Session token (hex-string):");
let session_token: String = prompt_string("session token").unwrap();
println!("Enter kSync (hex-string):");
let k_sync: String = prompt_string("k_sync").unwrap();
println!("Enter kXCS (hex-string):");
let k_xcs: String = prompt_string("k_xcs").unwrap();
let migration_result =
match fxa.migrate_from_session_token(&session_token, &k_sync, &k_xcs, true) {
Ok(migration_result) => migration_result,
Err(err) => {
println!("Error: {}", err);
// example for offline behaviour
loop {
thread::sleep(time::Duration::from_millis(5000));
let retry = fxa.try_migration();
match retry {
Ok(result) => break result,
Err(_) => println!("Retrying... Are you connected to the internet?"),
}
}
}
};
println!("WOW! You've been migrated in {:?}.", migration_result);
println!("JSON: {}", fxa.to_json().unwrap());
}

View File

@ -1,26 +0,0 @@
use cli_support::prompt::prompt_string;
use fxa_client::{Config, FirefoxAccount};
use std::collections::HashMap;
use url::Url;
const CONTENT_SERVER: &str = "http://127.0.0.1:3030";
const CLIENT_ID: &str = "7f368c6886429f19";
const REDIRECT_URI: &str = "https://mozilla.github.io/notes/fxa/android-redirect.html";
const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
fn main() {
viaduct_reqwest::use_reqwest_backend();
let config = Config::new(CONTENT_SERVER, CLIENT_ID, REDIRECT_URI);
let mut fxa = FirefoxAccount::with_config(config);
let url = fxa.begin_oauth_flow(&SCOPES).unwrap();
println!("Open the following URL:");
println!("{}", url);
let redirect_uri: String = prompt_string("Obtained redirect URI").unwrap();
let redirect_uri = Url::parse(&redirect_uri).unwrap();
let query_params: HashMap<_, _> = redirect_uri.query_pairs().into_owned().collect();
let code = &query_params["code"];
let state = &query_params["state"];
fxa.complete_oauth_flow(&code, &state).unwrap();
let oauth_info = fxa.get_access_token(SCOPES[0], None);
println!("access_token: {:?}", oauth_info);
}

255
third_party/rust/fxa-client/src/auth.rs vendored Normal file
View File

@ -0,0 +1,255 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub use crate::oauth::{AuthorizationPKCEParams, AuthorizationParameters};
use crate::{error::*, http_client, scoped_keys::ScopedKey, util::Xorable, Config};
pub use http_client::{
derive_auth_key_from_session_token, send_authorization_request, send_verification,
AuthorizationRequestParameters,
};
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
use rc_crypto::{digest, hkdf, hmac, pbkdf2};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
pub fn get_sync_keys(
config: &Config,
key_fetch_token: &str,
email: &str,
pw: &str,
) -> Result<(Vec<u8>, Vec<u8>)> {
let acct_keys = get_account_keys(config, key_fetch_token)?;
let wrap_kb = &acct_keys[32..];
let sync_key = derive_sync_key(email, pw, wrap_kb)?;
let xcs_key = derive_xcs_key(email, pw, wrap_kb)?;
Ok((sync_key, xcs_key))
}
pub fn create_keys_jwe(
client_id: &str,
scope: &str,
jwk: &str,
auth_key: &[u8],
config: &Config,
acct_keys: (&[u8], &[u8]),
) -> anyhow::Result<String> {
let scoped: HashMap<String, ScopedKey> =
get_scoped_keys(scope, client_id, auth_key, config, acct_keys)?;
let scoped = serde_json::to_string(&scoped)?;
let scoped = scoped.as_bytes();
let jwk = serde_json::from_str(jwk)?;
let res = jwcrypto::encrypt_to_jwe(
scoped,
EncryptionParameters::ECDH_ES {
enc: EncryptionAlgorithm::A256GCM,
peer_jwk: &jwk,
},
)?;
Ok(res)
}
#[derive(Serialize, Deserialize)]
struct Epk {
crv: String,
kty: String,
x: String,
y: String,
}
fn kwe(name: &str, email: &str) -> Vec<u8> {
format!("identity.mozilla.com/picl/v1/{}:{}", name, email)
.as_bytes()
.to_vec()
}
fn kw(name: &str) -> Vec<u8> {
format!("identity.mozilla.com/picl/v1/{}", name)
.as_bytes()
.to_vec()
}
pub fn get_scoped_keys(
scope: &str,
client_id: &str,
auth_key: &[u8],
config: &Config,
acct_keys: (&[u8], &[u8]),
) -> anyhow::Result<HashMap<String, ScopedKey>> {
let key_data = http_client::get_scoped_key_data_response(scope, client_id, auth_key, config)?;
let mut scoped_keys: HashMap<String, ScopedKey> = HashMap::new();
key_data
.as_object()
.ok_or_else(|| anyhow::Error::msg("Key data not an object"))?
.keys()
.try_for_each(|key| -> anyhow::Result<()> {
let val = key_data
.as_object()
.ok_or_else(|| anyhow::Error::msg("Key data not an object"))?
.get(key)
.ok_or_else(|| anyhow::Error::msg("Key does not exist"))?;
scoped_keys.insert(key.clone(), get_key_for_scope(&key, val, acct_keys)?);
Ok(())
})?;
Ok(scoped_keys)
}
fn get_key_for_scope(
key: &str,
val: &serde_json::Value,
acct_keys: (&[u8], &[u8]),
) -> anyhow::Result<ScopedKey> {
let (sync_key, xcs_key) = acct_keys;
let sync_key = base64::encode_config(sync_key, base64::URL_SAFE_NO_PAD);
let xcs_key = base64::encode_config(xcs_key, base64::URL_SAFE_NO_PAD);
let kid = format!(
"{}-{}",
val.as_object()
.ok_or_else(|| anyhow::Error::msg("Json is not an object"))?
.get("keyRotationTimestamp")
.ok_or_else(|| anyhow::Error::msg("Key rotation timestamp doesn't exist"))?
.as_u64()
.ok_or_else(|| anyhow::Error::msg("Key rotation timestamp is not a number"))?,
xcs_key
);
Ok(ScopedKey {
scope: key.to_string(),
kid,
k: sync_key,
kty: "oct".to_string(),
})
}
fn derive_xcs_key(email: &str, pwd: &str, wrap_kb: &[u8]) -> Result<Vec<u8>> {
let unwrap_kb = derive_unwrap_kb(email, pwd)?;
let kb = xored(wrap_kb, &unwrap_kb)?;
Ok(sha256(&kb)?[0..16].into())
}
fn sha256(kb: &[u8]) -> Result<Vec<u8>> {
let ret = digest::digest(&digest::SHA256, kb)?;
let ret: &[u8] = ret.as_ref();
Ok(ret.to_vec())
}
fn derive_hkdf_sha256_key(ikm: &[u8], salt: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>> {
let salt = hmac::SigningKey::new(&digest::SHA256, salt);
let mut out = vec![0u8; len];
hkdf::extract_and_expand(&salt, ikm, info, &mut out)?;
Ok(out)
}
fn quick_strech_pwd(email: &str, pwd: &str) -> Result<Vec<u8>> {
let salt = kwe("quickStretch", email);
let mut out = [0u8; 32];
pbkdf2::derive(
pwd.as_bytes(),
&salt,
1000,
pbkdf2::HashAlgorithm::SHA256,
&mut out,
)?;
Ok(out.to_vec())
}
pub fn auth_pwd(email: &str, pwd: &str) -> Result<String> {
let streched = quick_strech_pwd(email, pwd)?;
let salt = b"";
let context = kw("authPW");
let derived = derive_hkdf_sha256_key(&streched, salt, &context, 32)?;
Ok(hex::encode(derived))
}
#[derive(Serialize, Deserialize)]
struct Credentials {
key: Vec<u8>,
id: Vec<u8>,
extra: Vec<u8>,
out: Vec<u8>,
}
fn derive_hawk_credentials(token_hex: &str, context: &str, size: usize) -> Result<Credentials> {
let token = hex::decode(token_hex)?;
let out = derive_hkdf_sha256_key(&token, &[0u8; 0], &kw(context), size)?;
let key = out[32..64].to_vec();
let extra = out[64..].to_vec();
Ok(Credentials {
key,
id: out[0..32].to_vec(),
extra,
out: out.to_vec(),
})
}
fn xored(a: &[u8], b: &[u8]) -> Result<Vec<u8>> {
a.xored_with(b)
}
fn derive_unwrap_kb(email: &str, pwd: &str) -> Result<Vec<u8>> {
let streched_pw = quick_strech_pwd(email, pwd)?;
let out = derive_hkdf_sha256_key(&streched_pw, &[0u8; 0], &kw("unwrapBkey"), 32)?;
Ok(out)
}
fn derive_sync_key(email: &str, pwd: &str, wrap_kb: &[u8]) -> Result<Vec<u8>> {
let unwrap_kb = derive_unwrap_kb(email, pwd)?;
let kb = xored(wrap_kb, &unwrap_kb)?;
derive_hkdf_sha256_key(
&kb,
&[0u8; 0],
"identity.mozilla.com/picl/v1/oldsync".as_bytes(),
64,
)
}
fn get_account_keys(config: &Config, key_fetch_token: &str) -> Result<Vec<u8>> {
let creds = derive_hawk_credentials(key_fetch_token, "keyFetchToken", 96)?;
let key_request_key = &creds.extra[0..32];
let more_creds = derive_hkdf_sha256_key(key_request_key, &[0u8; 0], &kw("account/keys"), 96)?;
let _resp_hmac_key = &more_creds[0..32];
let resp_xor_key = &more_creds[32..96];
let bundle = http_client::get_keys_bundle(&config, &creds.out)?;
// Missing MAC matching since this is only for tests
xored(resp_xor_key, &bundle[0..64])
}
#[cfg(test)]
mod tests {
// Test vectors used from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#test-vectors
use super::*;
const EMAIL: &str = "andré@example.org";
const PASSWORD: &str = "pässwörd";
#[test]
fn test_derive_quick_stretch() {
let qs = quick_strech_pwd(EMAIL, PASSWORD).unwrap();
let expected = "e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d";
assert_eq!(expected, hex::encode(qs));
}
#[test]
fn test_auth_pw() {
let auth_pw = auth_pwd(EMAIL, PASSWORD).unwrap();
let expected = "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375";
assert_eq!(auth_pw, expected);
}
#[test]
fn test_derive_unwrap_kb() {
let unwrap_kb = derive_unwrap_kb(EMAIL, PASSWORD).unwrap();
let expected = "de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28";
assert_eq!(hex::encode(unwrap_kb), expected);
}
#[test]
fn test_kb() {
let wrap_kb =
hex::decode("7effe354abecbcb234a8dfc2d7644b4ad339b525589738f2d27341bb8622ecd8")
.unwrap();
let unwrap_kb =
hex::decode("de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28")
.unwrap();
let kb = xored(&wrap_kb, &unwrap_kb).unwrap();
let expected = "a095c51c1c6e384e8d5777d97e3c487a4fc2128a00ab395a73d57fedf41631f0";
assert_eq!(expected, hex::encode(kb));
}
}

View File

@ -13,7 +13,7 @@
/// uses the obtained public key to encrypt the `SendTabPayload` it created that
/// contains the tab to send and finally forms the `EncryptedSendTabPayload` that is
/// then sent to the target device.
use crate::{device::Device, error::*, scoped_keys::ScopedKey, scopes};
use crate::{device::Device, error::*, scoped_keys::ScopedKey, scopes, telemetry};
use rc_crypto::ece::{self, Aes128GcmEceWebPush, EcKeyComponents, WebPushParams};
use rc_crypto::ece_crypto::{RcCryptoLocalKeyPair, RcCryptoRemotePublicKey};
use serde_derive::*;
@ -40,16 +40,26 @@ impl EncryptedSendTabPayload {
#[derive(Debug, Serialize, Deserialize)]
pub struct SendTabPayload {
pub entries: Vec<TabHistoryEntry>,
#[serde(rename = "flowID", default)]
pub flow_id: String,
#[serde(rename = "streamID", default)]
pub stream_id: String,
}
impl SendTabPayload {
pub fn single_tab(title: &str, url: &str) -> Self {
SendTabPayload {
entries: vec![TabHistoryEntry {
title: title.to_string(),
url: url.to_string(),
}],
}
pub fn single_tab(title: &str, url: &str) -> (Self, telemetry::SentCommand) {
let sent_telemetry: telemetry::SentCommand = Default::default();
(
SendTabPayload {
entries: vec![TabHistoryEntry {
title: title.to_string(),
url: url.to_string(),
}],
flow_id: sent_telemetry.flow_id.clone(),
stream_id: sent_telemetry.stream_id.clone(),
},
sent_telemetry,
)
}
fn encrypt(&self, keys: PublicSendTabKeys) -> Result<EncryptedSendTabPayload> {
rc_crypto::ensure_initialized();
@ -218,3 +228,29 @@ fn extract_oldsync_key_components(oldsync_key: &ScopedKey) -> Result<(Vec<u8>, V
let ksync = oldsync_key.key_bytes()?;
Ok((ksync, kxcs))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimal_parse_payload() {
let minimal = r#"{ "entries": []}"#;
let payload: SendTabPayload = serde_json::from_str(minimal).expect("should work");
assert_eq!(payload.flow_id, "".to_string());
}
#[test]
fn test_payload() {
let (payload, telem) = SendTabPayload::single_tab("title", "http://example.com");
let json = serde_json::to_string(&payload).expect("should work");
assert_eq!(telem.flow_id.len(), 12);
assert_eq!(telem.stream_id.len(), 12);
assert_ne!(telem.flow_id, telem.stream_id);
let p2: SendTabPayload = serde_json::from_str(&json).expect("should work");
// no 'PartialEq' derived so check each field individually...
assert_eq!(payload.entries[0].url, "http://example.com".to_string());
assert_eq!(payload.flow_id, p2.flow_id);
assert_eq!(payload.stream_id, p2.stream_id);
}
}

View File

@ -2,29 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::error::*;
use serde_derive::*;
use crate::{error::*, http_client};
use serde_derive::{Deserialize, Serialize};
use std::{cell::RefCell, sync::Arc};
use url::Url;
use viaduct::Request;
#[derive(Deserialize)]
struct ClientConfigurationResponse {
auth_server_base_url: String,
oauth_server_base_url: String,
profile_server_base_url: String,
sync_tokenserver_base_url: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct OpenIdConfigurationResponse {
authorization_endpoint: String,
introspection_endpoint: String,
issuer: String,
jwks_uri: String,
token_endpoint: String,
userinfo_endpoint: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
@ -37,10 +18,10 @@ pub struct Config {
remote_config: RefCell<Option<Arc<RemoteConfig>>>,
}
#[derive(Clone, Debug)]
/// `RemoteConfig` struct stores configuration values from the FxA
/// `/.well-known/fxa-client-configuration` and the
/// `/.well-known/openid-configuration` endpoints.
#[derive(Debug)]
pub struct RemoteConfig {
auth_url: String,
oauth_url: String,
@ -96,7 +77,12 @@ impl Config {
&'a mut self,
token_server_url_override: &str,
) -> &'a mut Self {
self.token_server_url_override = Some(token_server_url_override.to_owned());
// In self-hosting setups it is common to specify the `/1.0/sync/1.5` suffix on the
// tokenserver URL. Accept and strip this form as a convenience for users.
match token_server_url_override.strip_suffix("/1.0/sync/1.5") {
Some(stripped) => self.token_server_url_override = Some(stripped.to_owned()),
None => self.token_server_url_override = Some(token_server_url_override.to_owned()),
}
self
}
@ -145,32 +131,23 @@ impl Config {
return Ok(remote_config);
}
let config_url =
Url::parse(&self.content_url)?.join(".well-known/fxa-client-configuration")?;
let resp: ClientConfigurationResponse =
Request::get(config_url).send()?.require_success()?.json()?;
let openid_config_url =
Url::parse(&self.content_url)?.join(".well-known/openid-configuration")?;
let openid_resp: OpenIdConfigurationResponse = Request::get(openid_config_url)
.send()?
.require_success()?
.json()?;
let client_config = http_client::fxa_client_configuration(self.client_config_url()?)?;
let openid_config = http_client::openid_configuration(self.openid_config_url()?)?;
let remote_config = self.set_remote_config(RemoteConfig {
auth_url: format!("{}/", resp.auth_server_base_url),
oauth_url: format!("{}/", resp.oauth_server_base_url),
profile_url: format!("{}/", resp.profile_server_base_url),
token_server_endpoint_url: format!("{}/", resp.sync_tokenserver_base_url),
authorization_endpoint: openid_resp.authorization_endpoint,
issuer: openid_resp.issuer,
jwks_uri: openid_resp.jwks_uri,
auth_url: format!("{}/", client_config.auth_server_base_url),
oauth_url: format!("{}/", client_config.oauth_server_base_url),
profile_url: format!("{}/", client_config.profile_server_base_url),
token_server_endpoint_url: format!("{}/", client_config.sync_tokenserver_base_url),
authorization_endpoint: openid_config.authorization_endpoint,
issuer: openid_config.issuer,
jwks_uri: openid_config.jwks_uri,
// TODO: bring back openid token endpoint once https://github.com/mozilla/fxa/issues/453 has been resolved
// and the openid response has been switched to the new endpoint.
// token_endpoint: openid_resp.token_endpoint,
token_endpoint: format!("{}/v1/oauth/token", resp.auth_server_base_url),
userinfo_endpoint: openid_resp.userinfo_endpoint,
introspection_endpoint: openid_resp.introspection_endpoint,
// and the openid reponse has been switched to the new endpoint.
// token_endpoint: openid_config.token_endpoint,
token_endpoint: format!("{}/v1/oauth/token", client_config.auth_server_base_url),
userinfo_endpoint: openid_config.userinfo_endpoint,
introspection_endpoint: openid_config.introspection_endpoint,
});
Ok(remote_config)
}
@ -190,6 +167,14 @@ impl Config {
self.content_url()?.join(path).map_err(Into::into)
}
pub fn client_config_url(&self) -> Result<Url> {
Ok(self.content_url_path(".well-known/fxa-client-configuration")?)
}
pub fn openid_config_url(&self) -> Result<Url> {
Ok(self.content_url_path(".well-known/openid-configuration")?)
}
pub fn connect_another_device_url(&self) -> Result<Url> {
self.content_url_path("connect_another_device")
.map_err(Into::into)
@ -367,4 +352,47 @@ mod tests {
"https://foo.bar/"
);
}
#[test]
fn test_tokenserver_url_override_strips_sync_service_prefix() {
let remote_config = RemoteConfig {
auth_url: "https://stable.dev.lcip.org/auth/".to_string(),
oauth_url: "https://oauth-stable.dev.lcip.org/".to_string(),
profile_url: "https://stable.dev.lcip.org/profile/".to_string(),
token_server_endpoint_url: "https://stable.dev.lcip.org/syncserver/token/".to_string(),
authorization_endpoint: "https://oauth-stable.dev.lcip.org/v1/authorization"
.to_string(),
issuer: "https://dev.lcip.org/".to_string(),
jwks_uri: "https://oauth-stable.dev.lcip.org/v1/jwks".to_string(),
token_endpoint: "https://stable.dev.lcip.org/auth/v1/oauth/token".to_string(),
introspection_endpoint: "https://oauth-stable.dev.lcip.org/v1/introspect".to_string(),
userinfo_endpoint: "https://stable.dev.lcip.org/profile/v1/profile".to_string(),
};
let mut config = Config {
content_url: "https://stable.dev.lcip.org/".to_string(),
remote_config: RefCell::new(Some(Arc::new(remote_config))),
client_id: "263ceaa5546dce83".to_string(),
redirect_uri: "https://127.0.0.1:8080".to_string(),
token_server_url_override: None,
};
config.override_token_server_url("https://foo.bar/prefix/1.0/sync/1.5");
assert_eq!(
config.token_server_endpoint_url().unwrap().to_string(),
"https://foo.bar/prefix"
);
config.override_token_server_url("https://foo.bar/prefix-1.0/sync/1.5");
assert_eq!(
config.token_server_endpoint_url().unwrap().to_string(),
"https://foo.bar/prefix-1.0/sync/1.5"
);
config.override_token_server_url("https://foo.bar/1.0/sync/1.5/foobar");
assert_eq!(
config.token_server_endpoint_url().unwrap().to_string(),
"https://foo.bar/1.0/sync/1.5/foobar"
);
}
}

View File

@ -9,10 +9,9 @@ use crate::{
commands,
error::*,
http_client::{
CommandData, DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand,
UpdateDeviceResponse,
DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand, UpdateDeviceResponse,
},
util, CachedResponse, FirefoxAccount, IncomingDeviceCommand,
telemetry, util, CachedResponse, FirefoxAccount, IncomingDeviceCommand,
};
use serde_derive::*;
use std::collections::{HashMap, HashSet};
@ -20,6 +19,15 @@ use std::collections::{HashMap, HashSet};
// An devices response is considered fresh for `DEVICES_FRESHNESS_THRESHOLD` ms.
const DEVICES_FRESHNESS_THRESHOLD: u64 = 60_000; // 1 minute
/// The reason we are fetching commands.
#[derive(Clone, Copy)]
pub enum CommandFetchReason {
/// We are polling in-case we've missed some.
Poll,
/// We got a push notification with the index of the message.
Push(u64),
}
impl FirefoxAccount {
/// Fetches the list of devices from the current account including
/// the current one.
@ -162,19 +170,33 @@ impl FirefoxAccount {
/// Poll and parse any pending available command for our device.
/// This should be called semi-regularly as the main method of
/// commands delivery (push) can sometimes be unreliable on mobile devices.
/// Typically called even when a push notification is received, so that
/// any prior messages for which a push didn't arrive are still handled.
///
/// **💾 This method alters the persisted account state.**
pub fn poll_device_commands(&mut self) -> Result<Vec<IncomingDeviceCommand>> {
pub fn poll_device_commands(
&mut self,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let last_command_index = self.state.last_handled_command.unwrap_or(0);
// We increment last_command_index by 1 because the server response includes the current index.
self.fetch_and_parse_commands(last_command_index + 1, None)
self.fetch_and_parse_commands(last_command_index + 1, None, reason)
}
/// Retrieve and parse a specific command designated by its index.
///
/// **💾 This method alters the persisted account state.**
pub fn fetch_device_command(&mut self, index: u64) -> Result<IncomingDeviceCommand> {
let mut device_commands = self.fetch_and_parse_commands(index, Some(1))?;
///
/// Note that this should not be used if possible, as it does not correctly
/// handle missed messages. It's currently used only on iOS due to platform
/// restrictions (but we should still try and work out how to correctly
/// handle missed messages within those restrictions)
/// (What's wrong: if we get a push for tab-1 and a push for tab-3, and
/// between them I've never explicitly polled, I'll miss tab-2, even if I
/// try polling now)
pub fn ios_fetch_device_command(&mut self, index: u64) -> Result<IncomingDeviceCommand> {
let mut device_commands =
self.fetch_and_parse_commands(index, Some(1), CommandFetchReason::Push(index))?;
let device_command = device_commands
.pop()
.ok_or_else(|| ErrorKind::IllegalState("Index fetch came out empty."))?;
@ -188,6 +210,7 @@ impl FirefoxAccount {
&mut self,
index: u64,
limit: Option<u64>,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let refresh_token = self.get_refresh_token()?;
let pending_commands =
@ -197,7 +220,7 @@ impl FirefoxAccount {
return Ok(Vec::new());
}
log::info!("Handling {} messages", pending_commands.messages.len());
let device_commands = self.parse_commands_messages(pending_commands.messages)?;
let device_commands = self.parse_commands_messages(pending_commands.messages, reason)?;
self.state.last_handled_command = Some(pending_commands.index);
Ok(device_commands)
}
@ -205,11 +228,12 @@ impl FirefoxAccount {
fn parse_commands_messages(
&mut self,
messages: Vec<PendingCommand>,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let devices = self.get_devices(false)?;
let parsed_commands = messages
.into_iter()
.filter_map(|msg| match self.parse_command(msg.data, &devices) {
.filter_map(|msg| match self.parse_command(msg, &devices, reason) {
Ok(device_command) => Some(device_command),
Err(e) => {
log::error!("Error while processing command: {}", e);
@ -222,15 +246,24 @@ impl FirefoxAccount {
fn parse_command(
&mut self,
command_data: CommandData,
command: PendingCommand,
devices: &[Device],
reason: CommandFetchReason,
) -> Result<IncomingDeviceCommand> {
let telem_reason = match reason {
CommandFetchReason::Poll => telemetry::ReceivedReason::Poll,
CommandFetchReason::Push(index) if command.index < index => {
telemetry::ReceivedReason::PushMissed
}
_ => telemetry::ReceivedReason::Push,
};
let command_data = command.data;
let sender = command_data
.sender
.and_then(|s| devices.iter().find(|i| i.id == s).cloned());
match command_data.command.as_str() {
commands::send_tab::COMMAND_NAME => {
self.handle_send_tab_command(sender, command_data.payload)
self.handle_send_tab_command(sender, command_data.payload, telem_reason)
}
_ => Err(ErrorKind::UnknownCommand(command_data.command).into()),
}

View File

@ -2,115 +2,83 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use failure::Fail;
use rc_crypto::hawk;
use std::string;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
#[fail(display = "Unknown OAuth State")]
#[error("Server asked the client to back off, please wait {0} seconds to try again")]
BackoffError(u64),
#[error("Unknown OAuth State")]
UnknownOAuthState,
#[fail(display = "The client requested keys alongside the token but they were not included")]
TokenWithoutKeys,
#[fail(display = "Login state needs to be Married for the current operation")]
NotMarried,
#[fail(display = "Multiple OAuth scopes requested")]
#[error("Multiple OAuth scopes requested")]
MultipleScopesRequested,
#[fail(display = "No cached token for scope {}", _0)]
#[error("No cached token for scope {0}")]
NoCachedToken(String),
#[fail(display = "No cached scoped keys for scope {}", _0)]
#[error("No cached scoped keys for scope {0}")]
NoScopedKey(String),
#[fail(display = "No stored refresh token")]
#[error("No stored refresh token")]
NoRefreshToken,
#[fail(display = "No stored session token")]
#[error("No stored session token")]
NoSessionToken,
#[fail(display = "No stored migration data")]
#[error("No stored migration data")]
NoMigrationData,
#[fail(display = "No stored current device id")]
#[error("No stored current device id")]
NoCurrentDeviceId,
#[fail(display = "Could not find a refresh token in the server response")]
RefreshTokenNotPresent,
#[fail(display = "Action requires a prior device registration")]
DeviceUnregistered,
#[fail(display = "Device target is unknown (Device ID: {})", _0)]
#[error("Device target is unknown (Device ID: {0})")]
UnknownTargetDevice(String),
#[fail(display = "Unrecoverable server error {}", _0)]
#[error("Unrecoverable server error {0}")]
UnrecoverableServerError(&'static str),
#[fail(display = "Invalid OAuth scope value {}", _0)]
InvalidOAuthScopeValue(String),
#[fail(display = "Illegal state: {}", _0)]
#[error("Illegal state: {0}")]
IllegalState(&'static str),
#[fail(display = "Unknown command: {}", _0)]
#[error("Unknown command: {0}")]
UnknownCommand(String),
#[fail(display = "Send Tab diagnosis error: {}", _0)]
#[error("Send Tab diagnosis error: {0}")]
SendTabDiagnosisError(&'static str),
#[fail(display = "Empty names")]
EmptyOAuthScopeNames,
#[fail(display = "Key {} had wrong length, got {}, expected {}", _0, _1, _2)]
BadKeyLength(&'static str, usize, usize),
#[fail(
display = "Cannot xor arrays with different lengths: {} and {}",
_0, _1
)]
#[error("Cannot xor arrays with different lengths: {0} and {1}")]
XorLengthMismatch(usize, usize),
#[fail(display = "Audience URL without a host")]
AudienceURLWithoutHost,
#[fail(display = "Origin mismatch")]
#[error("Origin mismatch")]
OriginMismatch,
#[fail(display = "JWT signature validation failed")]
JWTSignatureValidationFailed,
#[fail(display = "ECDH key generation failed")]
KeyGenerationFailed,
#[fail(display = "Public key computation failed")]
PublicKeyComputationFailed,
#[fail(display = "Remote key and local key mismatch")]
#[error("Remote key and local key mismatch")]
MismatchedKeys,
#[fail(display = "Key import failed")]
KeyImportFailed,
#[error("Could not find a suitable anon_id key")]
NoAnonIdKey,
#[fail(display = "AEAD open failure")]
AEADOpenFailure,
#[error("Client: {0} is not allowed to request scope: {1}")]
ScopeNotAllowed(String, String),
#[fail(display = "Random number generation failure")]
RngFailure,
#[fail(display = "HMAC mismatch")]
HmacMismatch,
#[fail(display = "Unsupported command: {}", _0)]
#[error("Unsupported command: {0}")]
UnsupportedCommand(&'static str),
#[fail(
display = "Remote server error: '{}' '{}' '{}' '{}' '{}'",
code, errno, error, message, info
)]
#[error("Missing URL parameter: {0}")]
MissingUrlParameter(&'static str),
#[error("Null pointer passed to FFI")]
NullPointer,
#[error("Invalid buffer length: {0}")]
InvalidBufferLength(i32),
#[error("Too many calls to auth introspection endpoint")]
AuthCircuitBreakerError,
#[error("Remote server error: '{code}' '{errno}' '{error}' '{message}' '{info}'")]
RemoteError {
code: u64,
errno: u64,
@ -120,41 +88,44 @@ pub enum ErrorKind {
},
// Basically reimplement error_chain's foreign_links. (Ugh, this sucks).
#[fail(display = "Crypto/NSS error: {}", _0)]
CryptoError(#[fail(cause)] rc_crypto::Error),
#[error("Crypto/NSS error: {0}")]
CryptoError(#[from] rc_crypto::Error),
#[fail(display = "http-ece encryption error: {}", _0)]
EceError(#[fail(cause)] rc_crypto::ece::Error),
#[error("http-ece encryption error: {0}")]
EceError(#[from] rc_crypto::ece::Error),
#[fail(display = "Hex decode error: {}", _0)]
HexDecodeError(#[fail(cause)] hex::FromHexError),
#[error("Hex decode error: {0}")]
HexDecodeError(#[from] hex::FromHexError),
#[fail(display = "Base64 decode error: {}", _0)]
Base64Decode(#[fail(cause)] base64::DecodeError),
#[error("Base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[fail(display = "JSON error: {}", _0)]
JsonError(#[fail(cause)] serde_json::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[fail(display = "UTF8 decode error: {}", _0)]
UTF8DecodeError(#[fail(cause)] string::FromUtf8Error),
#[error("JWCrypto error: {0}")]
JwCryptoError(#[from] jwcrypto::JwCryptoError),
#[fail(display = "Network error: {}", _0)]
RequestError(#[fail(cause)] viaduct::Error),
#[error("UTF8 decode error: {0}")]
UTF8DecodeError(#[from] string::FromUtf8Error),
#[fail(display = "Malformed URL error: {}", _0)]
MalformedUrl(#[fail(cause)] url::ParseError),
#[error("Network error: {0}")]
RequestError(#[from] viaduct::Error),
#[fail(display = "Unexpected HTTP status: {}", _0)]
UnexpectedStatus(#[fail(cause)] viaduct::UnexpectedStatus),
#[error("Malformed URL error: {0}")]
MalformedUrl(#[from] url::ParseError),
#[fail(display = "Sync15 error: {}", _0)]
SyncError(#[fail(cause)] sync15::Error),
#[error("Unexpected HTTP status: {0}")]
UnexpectedStatus(#[from] viaduct::UnexpectedStatus),
#[fail(display = "HAWK error: {}", _0)]
HawkError(#[fail(cause)] hawk::Error),
#[error("Sync15 error: {0}")]
SyncError(#[from] sync15::Error),
#[fail(display = "Protobuf decode error: {}", _0)]
ProtobufDecodeError(#[fail(cause)] prost::DecodeError),
#[error("HAWK error: {0}")]
HawkError(#[from] hawk::Error),
#[error("Protobuf decode error: {0}")]
ProtobufDecodeError(#[from] prost::DecodeError),
}
error_support::define_error! {
@ -164,6 +135,7 @@ error_support::define_error! {
(HexDecodeError, hex::FromHexError),
(Base64Decode, base64::DecodeError),
(JsonError, serde_json::Error),
(JwCryptoError, jwcrypto::JwCryptoError),
(UTF8DecodeError, std::string::FromUtf8Error),
(RequestError, viaduct::Error),
(UnexpectedStatus, viaduct::UnexpectedStatus),

View File

@ -12,11 +12,12 @@
//!
//! None of this is that bad in practice, but much of it is not ideal.
pub use crate::oauth::{AuthorizationPKCEParams, AuthorizationParameters, MetricsParams};
use crate::{
commands,
device::{Capability as DeviceCapability, Device, PushSubscription, Type as DeviceType},
msg_types, send_tab, AccessTokenInfo, AccountEvent, Error, ErrorKind, IncomingDeviceCommand,
IntrospectInfo, Profile, ScopedKey,
IntrospectInfo, Profile, Result, ScopedKey,
};
use ffi_support::{
implement_into_ffi_by_delegation, implement_into_ffi_by_protobuf, ErrorCode, ExternError,
@ -28,7 +29,7 @@ pub mod error_codes {
/// Catch-all error code used for anything that's not a panic or covered by AUTHENTICATION.
pub const OTHER: i32 = 1;
/// Used for `ErrorKind::NotMarried`, `ErrorKind::NoCachedTokens`, `ErrorKind::NoScopedKey`
/// Used by `ErrorKind::NoCachedTokens`, `ErrorKind::NoScopedKey`
/// and `ErrorKind::RemoteError`'s where `code == 401`.
pub const AUTHENTICATION: i32 = 2;
@ -36,10 +37,22 @@ pub mod error_codes {
pub const NETWORK: i32 = 3;
}
/// # Safety
/// data is a raw pointer to the protobuf data
/// get_buffer will return an error if the length is invalid,
/// or if the pointer is a null pointer
pub unsafe fn from_protobuf_ptr<T, F: prost::Message + Default + Into<T>>(
data: *const u8,
len: i32,
) -> Result<T> {
let buffer = get_buffer(data, len)?;
let item: Result<F, _> = prost::Message::decode(buffer);
item.map(|inner| inner.into()).map_err(|e| e.into())
}
fn get_code(err: &Error) -> ErrorCode {
match err.kind() {
ErrorKind::RemoteError { code: 401, .. }
| ErrorKind::NotMarried
| ErrorKind::NoRefreshToken
| ErrorKind::NoScopedKey(_)
| ErrorKind::NoCachedToken(_) => {
@ -248,12 +261,12 @@ impl From<msg_types::device::Capability> for DeviceCapability {
impl DeviceCapability {
/// # Safety
/// Deref pointer thus unsafe
pub unsafe fn from_protobuf_array_ptr(ptr: *const u8, len: i32) -> Vec<Self> {
let buffer = get_buffer(ptr, len);
pub unsafe fn from_protobuf_array_ptr(ptr: *const u8, len: i32) -> Result<Vec<Self>> {
let buffer = get_buffer(ptr, len)?;
let capabilities: Result<msg_types::Capabilities, _> = prost::Message::decode(buffer);
capabilities
Ok(capabilities
.map(|cc| cc.to_capabilities_vec())
.unwrap_or_else(|_| vec![])
.unwrap_or_else(|_| vec![]))
}
}
@ -266,13 +279,52 @@ impl msg_types::Capabilities {
}
}
unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> &'a [u8] {
assert!(len >= 0, "Bad buffer len: {}", len);
if len == 0 {
&[]
} else {
assert!(!data.is_null(), "Unexpected null data pointer");
std::slice::from_raw_parts(data, len as usize)
unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> Result<&'a [u8]> {
match len {
len if len < 0 => Err(ErrorKind::InvalidBufferLength(len).into()),
0 => Ok(&[]),
_ => {
if data.is_null() {
return Err(ErrorKind::NullPointer.into());
}
Ok(std::slice::from_raw_parts(data, len as usize))
}
}
}
impl From<msg_types::AuthorizationParams> for AuthorizationParameters {
fn from(proto_params: msg_types::AuthorizationParams) -> Self {
Self {
client_id: proto_params.client_id,
scope: proto_params
.scope
.split_whitespace()
.map(|s| s.to_string())
.collect(),
state: proto_params.state,
access_type: proto_params.access_type,
pkce_params: proto_params
.pkce_params
.map(|pkce_params| pkce_params.into()),
keys_jwk: proto_params.keys_jwk,
}
}
}
impl From<msg_types::MetricsParams> for MetricsParams {
fn from(proto_metrics_params: msg_types::MetricsParams) -> Self {
Self {
parameters: proto_metrics_params.parameters,
}
}
}
impl From<msg_types::AuthorizationPkceParams> for AuthorizationPKCEParams {
fn from(proto_key_params: msg_types::AuthorizationPkceParams) -> Self {
Self {
code_challenge: proto_key_params.code_challenge,
code_challenge_method: proto_key_params.code_challenge_method,
}
}
}

View File

@ -122,3 +122,21 @@ message AccountEvent {
message AccountEvents {
repeated AccountEvent events = 1;
}
message AuthorizationPKCEParams {
required string code_challenge = 1;
required string code_challenge_method = 2;
}
message AuthorizationParams {
required string client_id = 1;
required string scope = 2;
required string state = 3;
required string access_type = 4;
optional AuthorizationPKCEParams pkce_params = 5;
optional string keys_jwk = 6;
}
message MetricsParams {
map<string, string> parameters = 1;
}

View File

@ -3,19 +3,28 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::{config::Config, error::*};
use rc_crypto::hawk::{Credentials, Key, PayloadHasher, RequestBuilder, SHA256};
use rc_crypto::{digest, hkdf, hmac};
use serde_derive::*;
use jwcrypto::Jwk;
use rc_crypto::{
digest,
hawk::{Credentials, Key, PayloadHasher, RequestBuilder, SHA256},
hkdf, hmac,
};
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::{
collections::HashMap,
sync::Mutex,
time::{Duration, Instant},
};
use url::Url;
use viaduct::{header_names, status_codes, Method, Request, Response};
const HAWK_HKDF_SALT: [u8; 32] = [0b0; 32];
const HAWK_KEY_LENGTH: usize = 32;
const RETRY_AFTER_DEFAULT_SECONDS: u64 = 10;
#[cfg_attr(test, mockiato::mockable)]
pub trait FxAClient {
pub(crate) trait FxAClient {
fn refresh_token_with_code(
&self,
config: &Config,
@ -49,11 +58,8 @@ pub trait FxAClient {
fn authorization_code_using_session_token(
&self,
config: &Config,
client_id: &str,
session_token: &str,
scope: &str,
state: &str,
access_type: &str,
auth_params: AuthorizationRequestParameters,
) -> Result<OAuthAuthResponse>;
fn duplicate_session(
&self,
@ -68,6 +74,12 @@ pub trait FxAClient {
profile_access_token: &str,
etag: Option<String>,
) -> Result<Option<ResponseAndETag<ProfileResponse>>>;
fn set_ecosystem_anon_id(
&self,
config: &Config,
access_token: &str,
ecosystem_anon_id: &str,
) -> Result<()>;
fn pending_commands(
&self,
config: &Config,
@ -100,12 +112,35 @@ pub trait FxAClient {
&self,
config: &Config,
session_token: &str,
client_id: &str,
scope: &str,
) -> Result<HashMap<String, ScopedKeyDataResponse>>;
fn fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse>;
fn openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse>;
}
pub struct Client;
enum HttpClientState {
Ok,
Backoff {
backoff_end_duration: Duration,
time_since_backoff: Instant,
},
}
pub struct Client {
state: Mutex<HashMap<String, HttpClientState>>,
}
impl FxAClient for Client {
fn fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse> {
// Why go through two-levels of indirection? It looks kinda dumb.
// Well, `config:Config` also needs to fetch the config, but does not have access
// to an instance of `http_client`, so it calls the helper function directly.
fxa_client_configuration(config.client_config_url()?)
}
fn openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse> {
openid_configuration(config.openid_config_url()?)
}
fn profile(
&self,
config: &Config,
@ -118,7 +153,7 @@ impl FxAClient for Client {
if let Some(etag) = etag {
request = request.header(header_names::IF_NONE_MATCH, format!("\"{}\"", etag))?;
}
let resp = Self::make_request(request)?;
let resp = self.make_request(request)?;
if resp.status == status_codes::NOT_MODIFIED {
return Ok(None);
}
@ -132,6 +167,25 @@ impl FxAClient for Client {
}))
}
fn set_ecosystem_anon_id(
&self,
config: &Config,
access_token: &str,
ecosystem_anon_id: &str,
) -> Result<()> {
let url = config.profile_url_path("v1/ecosystem_anon_id")?;
let body = json!({
"ecosystemAnonId": ecosystem_anon_id,
});
let request = Request::post(url)
.header(header_names::AUTHORIZATION, bearer_token(access_token))?
// If-none-match prevents us from overwriting an already set value.
.header(header_names::IF_NONE_MATCH, "*")?
.body(body.to_string());
self.make_request(request)?;
Ok(())
}
// For the one-off generation of a `refresh_token` and associated meta from transient credentials.
fn refresh_token_with_code(
&self,
@ -165,7 +219,7 @@ impl FxAClient for Client {
let request = HawkRequestBuilder::new(Method::Post, url, &key)
.body(body)
.build()?;
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
// For the regular generation of an `access_token` from long-lived credentials.
@ -202,32 +256,23 @@ impl FxAClient for Client {
let request = HawkRequestBuilder::new(Method::Post, url, &key)
.body(parameters)
.build()?;
Self::make_request(request)?.json().map_err(Into::into)
self.make_request(request)?.json().map_err(Into::into)
}
fn authorization_code_using_session_token(
&self,
config: &Config,
client_id: &str,
session_token: &str,
scope: &str,
state: &str,
access_type: &str,
auth_params: AuthorizationRequestParameters,
) -> Result<OAuthAuthResponse> {
let parameters = json!({
"client_id": client_id,
"scope": scope,
"response_type": "code",
"state": state,
"access_type": access_type,
});
let parameters = serde_json::to_value(&auth_params)?;
let key = derive_auth_key_from_session_token(session_token)?;
let url = config.auth_url_path("v1/oauth/authorization")?;
let request = HawkRequestBuilder::new(Method::Post, url, &key)
.body(parameters)
.build()?;
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn oauth_introspect_refresh_token(
@ -240,7 +285,7 @@ impl FxAClient for Client {
"token": refresh_token,
});
let url = config.introspection_endpoint()?;
Ok(Self::make_request(Request::post(url).json(&body))?.json()?)
Ok(self.make_request(Request::post(url).json(&body))?.json()?)
}
fn duplicate_session(
@ -257,7 +302,7 @@ impl FxAClient for Client {
.body(duplicate_body)
.build()?;
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn destroy_access_token(&self, config: &Config, access_token: &str) -> Result<()> {
@ -288,7 +333,7 @@ impl FxAClient for Client {
if let Some(limit) = limit {
request = request.query(&[("limit", &limit.to_string())])
}
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn invoke_command(
@ -309,7 +354,7 @@ impl FxAClient for Client {
.header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
.header(header_names::CONTENT_TYPE, "application/json")?
.body(body.to_string());
Self::make_request(request)?;
self.make_request(request)?;
Ok(())
}
@ -317,7 +362,7 @@ impl FxAClient for Client {
let url = config.auth_url_path("v1/account/devices")?;
let request =
Request::get(url).header(header_names::AUTHORIZATION, bearer_token(refresh_token))?;
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn update_device(
@ -331,7 +376,7 @@ impl FxAClient for Client {
.header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
.header(header_names::CONTENT_TYPE, "application/json")?
.body(serde_json::to_string(&update)?);
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn destroy_device(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()> {
@ -344,7 +389,7 @@ impl FxAClient for Client {
.header(header_names::CONTENT_TYPE, "application/json")?
.body(body.to_string());
Self::make_request(request)?;
self.make_request(request)?;
Ok(())
}
@ -356,17 +401,18 @@ impl FxAClient for Client {
let url = config.auth_url_path("v1/account/attached_clients")?;
let key = derive_auth_key_from_session_token(session_token)?;
let request = HawkRequestBuilder::new(Method::Get, url, &key).build()?;
Ok(Self::make_request(request)?.json()?)
Ok(self.make_request(request)?.json()?)
}
fn scoped_key_data(
&self,
config: &Config,
session_token: &str,
client_id: &str,
scope: &str,
) -> Result<HashMap<String, ScopedKeyDataResponse>> {
let body = json!({
"client_id": config.client_id,
"client_id": client_id,
"scope": scope,
});
let url = config.auth_url_path("v1/account/scoped-key-data")?;
@ -374,18 +420,38 @@ impl FxAClient for Client {
let request = HawkRequestBuilder::new(Method::Post, url, &key)
.body(body)
.build()?;
Self::make_request(request)?.json().map_err(|e| e.into())
self.make_request(request)?.json().map_err(|e| e.into())
}
}
macro_rules! fetch {
($url:expr) => {
viaduct::Request::get($url)
.send()?
.require_success()?
.json()?
};
}
#[inline]
pub(crate) fn fxa_client_configuration(url: Url) -> Result<ClientConfigurationResponse> {
Ok(fetch!(url))
}
#[inline]
pub(crate) fn openid_configuration(url: Url) -> Result<OpenIdConfigurationResponse> {
Ok(fetch!(url))
}
impl Client {
pub fn new() -> Self {
Self {}
Self {
state: Mutex::new(HashMap::new()),
}
}
fn destroy_token_helper(&self, config: &Config, body: &serde_json::Value) -> Result<()> {
let url = config.oauth_url_path("v1/destroy")?;
Self::make_request(Request::post(url).json(body))?;
self.make_request(Request::post(url).json(body))?;
Ok(())
}
@ -395,25 +461,64 @@ impl Client {
body: serde_json::Value,
) -> Result<OAuthTokenResponse> {
let url = config.token_endpoint()?;
Ok(Self::make_request(Request::post(url).json(&body))?.json()?)
Ok(self.make_request(Request::post(url).json(&body))?.json()?)
}
fn make_request(request: Request) -> Result<Response> {
fn handle_too_many_requests(&self, resp: Response) -> Result<Response> {
let path = resp.url.path().to_string();
if let Some(retry_after) = resp.headers.get_as::<u64, _>(header_names::RETRY_AFTER) {
let retry_after = retry_after.unwrap_or(RETRY_AFTER_DEFAULT_SECONDS);
let time_out_state = HttpClientState::Backoff {
backoff_end_duration: Duration::from_secs(retry_after),
time_since_backoff: Instant::now(),
};
self.state.lock().unwrap().insert(path, time_out_state);
return Err(ErrorKind::BackoffError(retry_after).into());
}
Self::default_handle_response_error(resp)
}
fn default_handle_response_error(resp: Response) -> Result<Response> {
let json: std::result::Result<serde_json::Value, _> = resp.json();
match json {
Ok(json) => Err(ErrorKind::RemoteError {
code: json["code"].as_u64().unwrap_or(0),
errno: json["errno"].as_u64().unwrap_or(0),
error: json["error"].as_str().unwrap_or("").to_string(),
message: json["message"].as_str().unwrap_or("").to_string(),
info: json["info"].as_str().unwrap_or("").to_string(),
}
.into()),
Err(_) => Err(resp.require_success().unwrap_err().into()),
}
}
fn make_request(&self, request: Request) -> Result<Response> {
let url = request.url.path().to_string();
if let HttpClientState::Backoff {
backoff_end_duration,
time_since_backoff,
} = self
.state
.lock()
.unwrap()
.get(&url)
.unwrap_or(&HttpClientState::Ok)
{
let elapsed_time = time_since_backoff.elapsed();
if elapsed_time < *backoff_end_duration {
let remaining = *backoff_end_duration - elapsed_time;
return Err(ErrorKind::BackoffError(remaining.as_secs()).into());
}
}
self.state.lock().unwrap().insert(url, HttpClientState::Ok);
let resp = request.send()?;
if resp.is_success() || resp.status == status_codes::NOT_MODIFIED {
Ok(resp)
} else {
let json: std::result::Result<serde_json::Value, _> = resp.json();
match json {
Ok(json) => Err(ErrorKind::RemoteError {
code: json["code"].as_u64().unwrap_or(0),
errno: json["errno"].as_u64().unwrap_or(0),
error: json["error"].as_str().unwrap_or("").to_string(),
message: json["message"].as_str().unwrap_or("").to_string(),
info: json["info"].as_str().unwrap_or("").to_string(),
}
.into()),
Err(_) => Err(resp.require_success().unwrap_err().into()),
match resp.status {
status_codes::TOO_MANY_REQUESTS => self.handle_too_many_requests(resp),
_ => Self::default_handle_response_error(resp),
}
}
}
@ -438,6 +543,84 @@ pub fn derive_auth_key_from_session_token(session_token: &str) -> Result<Vec<u8>
Ok(out)
}
#[derive(Serialize, Deserialize)]
pub struct AuthorizationRequestParameters {
pub client_id: String,
pub scope: String,
pub state: String,
pub access_type: String,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
pub keys_jwe: Option<String>,
}
// Keeping those functions out of the FxAClient trate becouse functions in the
// FxAClient trate with a `test only` feature upsets the mockiato proc macro
// And it's okay since they are only used in tests. (if they were not test only
// Mockiato would not complain)
#[cfg(feature = "integration_test")]
pub fn send_authorization_request(
config: &Config,
auth_params: AuthorizationRequestParameters,
auth_key: &[u8],
) -> anyhow::Result<String> {
let auth_endpoint = config.auth_url_path("v1/oauth/authorization")?;
let req = HawkRequestBuilder::new(Method::Post, auth_endpoint, auth_key)
.body(serde_json::to_value(&auth_params)?)
.build()?;
let client = Client::new();
let resp: serde_json::Value = client.make_request(req)?.json()?;
Ok(resp
.get("redirect")
.ok_or_else(|| anyhow::Error::msg("No redirect uri"))?
.as_str()
.ok_or_else(|| anyhow::Error::msg("redirect URI is not a string"))?
.to_string())
}
#[cfg(feature = "integration_test")]
pub fn get_scoped_key_data_response(
scope: &str,
client_id: &str,
auth_key: &[u8],
config: &Config,
) -> Result<serde_json::Value> {
let scoped_endpoint = config.auth_url_path("v1/account/scoped-key-data")?;
let body = json!({
"client_id": client_id,
"scope": scope,
});
let req = HawkRequestBuilder::new(Method::Post, scoped_endpoint, auth_key)
.body(body)
.build()?;
let client = Client::new();
let resp = client.make_request(req)?.json()?;
Ok(resp)
}
#[cfg(feature = "integration_test")]
pub fn get_keys_bundle(config: &Config, hkdf_sha256_key: &[u8]) -> Result<Vec<u8>> {
let keys_url = config.auth_url_path("v1/account/keys").unwrap();
let req = HawkRequestBuilder::new(Method::Get, keys_url, hkdf_sha256_key).build()?;
let client = Client::new();
let resp: serde_json::Value = client.make_request(req)?.json()?;
let bundle = hex::decode(
&resp["bundle"]
.as_str()
.ok_or_else(|| ErrorKind::UnrecoverableServerError("bundle not present"))?,
)?;
Ok(bundle)
}
#[cfg(feature = "integration_test")]
pub fn send_verification(config: &Config, body: serde_json::Value) -> Result<Response> {
let verify_endpoint = config
.auth_url_path("v1/recovery_email/verify_code")
.unwrap();
let resp = Request::post(verify_endpoint).json(&body).send()?;
Ok(resp)
}
struct HawkRequestBuilder<'a> {
url: Url,
method: Method,
@ -496,6 +679,27 @@ impl<'a> HawkRequestBuilder<'a> {
}
}
#[derive(Deserialize)]
pub(crate) struct ClientConfigurationResponse {
pub(crate) auth_server_base_url: String,
pub(crate) oauth_server_base_url: String,
pub(crate) profile_server_base_url: String,
pub(crate) sync_tokenserver_base_url: String,
// XXX: Remove Option once all prod servers have this field.
pub(crate) ecosystem_anon_id_keys: Option<Vec<Jwk>>,
}
#[derive(Deserialize)]
pub(crate) struct OpenIdConfigurationResponse {
pub(crate) authorization_endpoint: String,
pub(crate) introspection_endpoint: String,
pub(crate) issuer: String,
pub(crate) jwks_uri: String,
#[allow(dead_code)]
pub(crate) token_endpoint: String,
pub(crate) userinfo_endpoint: String,
}
#[derive(Clone)]
pub struct ResponseAndETag<T> {
pub response: T,
@ -743,19 +947,14 @@ pub struct IntrospectResponse {
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileResponse {
pub uid: String,
pub email: String,
pub locale: String,
#[serde(rename = "displayName")]
pub display_name: Option<String>,
pub avatar: String,
#[serde(rename = "avatarDefault")]
pub avatar_default: bool,
#[serde(rename = "amrValues")]
pub amr_values: Vec<String>,
#[serde(rename = "twoFactorAuthentication")]
pub two_factor_authentication: bool,
pub ecosystem_anon_id: Option<String>,
}
#[derive(Deserialize)]
@ -780,7 +979,7 @@ pub struct DuplicateTokenResponse {
#[cfg(test)]
mod tests {
use super::*;
use mockito::mock;
#[test]
#[allow(non_snake_case)]
fn check_OAauthTokenRequest_serialization() {
@ -800,4 +999,164 @@ mod tests {
};
assert_eq!("{\"grant_type\":\"refresh_token\",\"client_id\":\"bar\",\"refresh_token\":\"foo\",\"scope\":\"bobo\",\"ttl\":123}", serde_json::to_string(&using_code).unwrap());
}
#[test]
fn test_backoff() {
viaduct_reqwest::use_reqwest_backend();
let m = mock("POST", "/v1/account/devices/invoke_command")
.with_status(429)
.with_header("Content-Type", "application/json")
.with_header("retry-after", "1000000")
.with_body(
r#"{
"code": 429,
"errno": 120,
"error": "Too many requests",
"message": "Too many requests",
"retryAfter": 1000000,
"info": "Some information"
}"#,
)
.create();
let client = Client::new();
let path = format!(
"{}/{}",
mockito::server_url(),
"v1/account/devices/invoke_command"
);
let url = Url::parse(&path).unwrap();
let path = url.path().to_string();
let request = Request::post(url);
assert!(client.make_request(request.clone()).is_err());
let state = client.state.lock().unwrap();
if let HttpClientState::Backoff {
backoff_end_duration,
time_since_backoff: _,
} = state.get(&path).unwrap()
{
assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
// Hacky way to drop the mutex gaurd, so that the next call to
// client.make_request doesn't hang or panic
std::mem::drop(state);
assert!(client.make_request(request).is_err());
// We should be backed off, the second "make_request" should not
// send a request to the server
m.expect(1).assert();
} else {
panic!("HttpClientState should be a timeout!");
}
}
#[test]
fn test_backoff_then_ok() {
viaduct_reqwest::use_reqwest_backend();
let m = mock("POST", "/v1/account/devices/invoke_command")
.with_status(429)
.with_header("Content-Type", "application/json")
.with_header("retry-after", "1")
.with_body(
r#"{
"code": 429,
"errno": 120,
"error": "Too many requests",
"message": "Too many requests",
"retryAfter": 1,
"info": "Some information"
}"#,
)
.create();
let client = Client::new();
let path = format!(
"{}/{}",
mockito::server_url(),
"v1/account/devices/invoke_command"
);
let url = Url::parse(&path).unwrap();
let path = url.path().to_string();
let request = Request::post(url);
assert!(client.make_request(request.clone()).is_err());
let state = client.state.lock().unwrap();
if let HttpClientState::Backoff {
backoff_end_duration,
time_since_backoff: _,
} = state.get(&path).unwrap()
{
assert_eq!(*backoff_end_duration, Duration::from_secs(1));
// We sleep for 1 second, so pass the backoff timeout
std::thread::sleep(*backoff_end_duration);
// Hacky way to drop the mutex gaurd, so that the next call to
// client.make_request doesn't hang or panic
std::mem::drop(state);
assert!(client.make_request(request).is_err());
// We backed off, but the time has passed, the second request should have
// went to the server
m.expect(2).assert();
} else {
panic!("HttpClientState should be a timeout!");
}
}
#[test]
fn test_backoff_per_path() {
viaduct_reqwest::use_reqwest_backend();
let m1 = mock("POST", "/v1/account/devices/invoke_command")
.with_status(429)
.with_header("Content-Type", "application/json")
.with_header("retry-after", "1000000")
.with_body(
r#"{
"code": 429,
"errno": 120,
"error": "Too many requests",
"message": "Too many requests",
"retryAfter": 1000000,
"info": "Some information"
}"#,
)
.create();
let m2 = mock("GET", "/v1/account/device/commands")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(
r#"
{
"index": 3,
"last": true,
"messages": []
}"#,
)
.create();
let client = Client::new();
let path = format!(
"{}/{}",
mockito::server_url(),
"v1/account/devices/invoke_command"
);
let url = Url::parse(&path).unwrap();
let path = url.path().to_string();
let request = Request::post(url);
assert!(client.make_request(request).is_err());
let state = client.state.lock().unwrap();
if let HttpClientState::Backoff {
backoff_end_duration,
time_since_backoff: _,
} = state.get(&path).unwrap()
{
assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
let path2 = format!("{}/{}", mockito::server_url(), "v1/account/device/commands");
// Hacky way to drop the mutex guard, so that the next call to
// client.make_request doesn't hang or panic
std::mem::drop(state);
let second_request = Request::get(Url::parse(&path2).unwrap());
assert!(client.make_request(second_request).is_ok());
// The first endpoint is backed off, but the second one is not
// Both endpoint should be hit
m1.expect(1).assert();
m2.expect(1).assert();
} else {
panic!("HttpClientState should be a timeout!");
}
}
}

View File

@ -8,35 +8,38 @@
use crate::{
commands::send_tab::SendTabPayload,
device::Device,
oauth::{OAuthFlow, OAUTH_WEBCHANNEL_REDIRECT},
oauth::{AuthCircuitBreaker, OAuthFlow, OAUTH_WEBCHANNEL_REDIRECT},
scoped_keys::ScopedKey,
state_persistence::State,
};
pub use crate::{
config::Config,
error::*,
oauth::IntrospectInfo,
oauth::{AccessTokenInfo, RefreshToken},
oauth::{AccessTokenInfo, IntrospectInfo, RefreshToken},
profile::Profile,
telemetry::FxaTelemetry,
};
use serde_derive::*;
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
sync::Arc,
};
use url::Url;
#[cfg(feature = "integration_test")]
pub mod auth;
mod commands;
mod config;
pub mod device;
pub mod error;
pub mod ffi;
mod http_client;
pub mod migrator;
// Include the `msg_types` module, which is generated from msg_types.proto.
pub mod msg_types {
include!("mozilla.appservices.fxaclient.protobuf.rs");
}
mod http_client;
mod oauth;
mod profile;
mod push;
@ -44,6 +47,7 @@ mod scoped_keys;
pub mod scopes;
pub mod send_tab;
mod state_persistence;
mod telemetry;
mod util;
type FxAClient = dyn http_client::FxAClient + Sync + Send;
@ -63,6 +67,10 @@ pub struct FirefoxAccount {
flow_store: HashMap<String, OAuthFlow>,
attached_clients_cache: Option<CachedResponse<Vec<http_client::GetAttachedClientResponse>>>,
devices_cache: Option<CachedResponse<Vec<http_client::GetDeviceResponse>>>,
auth_circuit_breaker: AuthCircuitBreaker,
// 'telemetry' is only currently used by `&mut self` functions, but that's
// not something we want to insist on going forward, so RefCell<> it.
telemetry: RefCell<FxaTelemetry>,
}
impl FirefoxAccount {
@ -73,6 +81,8 @@ impl FirefoxAccount {
flow_store: HashMap::new(),
attached_clients_cache: None,
devices_cache: None,
auth_circuit_breaker: Default::default(),
telemetry: RefCell::new(FxaTelemetry::new()),
}
}
@ -92,6 +102,7 @@ impl FirefoxAccount {
last_seen_profile: None,
access_token_cache: HashMap::new(),
in_flight_migration: None,
ecosystem_user_id: None,
})
}
@ -148,6 +159,7 @@ impl FirefoxAccount {
self.state = self.state.start_over();
self.flow_store.clear();
self.clear_devices_and_attached_clients_cache();
self.telemetry.replace(FxaTelemetry::new());
}
/// Get the Sync Token Server endpoint URL.
@ -225,7 +237,7 @@ impl FirefoxAccount {
}
}
/// Disconnect from the account and optionaly destroy our device record. This will
/// Disconnect from the account and optionally destroy our device record. This will
/// leave the account object in a state where it can eventually reconnect to the same user.
/// This is a "best effort" infallible method: e.g. if the network is unreachable,
/// the device could still be in the FxA devices manager.

View File

@ -167,6 +167,7 @@ impl FirefoxAccount {
let scoped_key_data = self.client.scoped_key_data(
&self.state.config,
&migration_session_token,
&self.state.config.client_id,
scopes::OLD_SYNC,
)?;
let oldsync_key_data = scoped_key_data.get(scopes::OLD_SYNC).ok_or_else(|| {
@ -213,20 +214,11 @@ mod tests {
FirefoxAccount::with_config(config)
}
macro_rules! assert_match {
($value:expr, $pattern:pat) => {
assert!(match $value {
$pattern => true,
_ => false,
});
};
}
#[test]
fn test_migration_can_retry_after_network_errors() {
let mut fxa = setup();
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
// Initial attempt fails with a server-side failure, which we can retry.
let mut client = FxAClientMock::new();
@ -245,11 +237,11 @@ mod tests {
let err = fxa
.migrate_from_session_token("session", "aabbcc", "ddeeff", true)
.unwrap_err();
assert_match!(err.kind(), ErrorKind::RemoteError { code: 500, .. });
assert_match!(
assert!(matches!(err.kind(), ErrorKind::RemoteError { code: 500, .. }));
assert!(matches!(
fxa.is_in_migration_state(),
MigrationState::CopySessionToken
);
));
// Retrying can succeed.
// It makes a lot of network requests, so we have a lot to mock!
@ -275,6 +267,7 @@ mod tests {
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("dup_session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(scopes::OLD_SYNC),
)
.returns_once(Ok(key_data));
@ -298,14 +291,14 @@ mod tests {
fxa.set_client(Arc::new(client));
fxa.try_migration().unwrap();
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
}
#[test]
fn test_migration_cannot_retry_after_other_errors() {
let mut fxa = setup();
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
let mut client = FxAClientMock::new();
client
@ -323,32 +316,33 @@ mod tests {
let err = fxa
.migrate_from_session_token("session", "aabbcc", "ddeeff", true)
.unwrap_err();
assert_match!(err.kind(), ErrorKind::RemoteError { code: 400, .. });
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(err.kind(), ErrorKind::RemoteError { code: 400, .. }));
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
}
#[test]
fn try_migration_fails_if_nothing_in_flight() {
let mut fxa = setup();
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
let err = fxa.try_migration().unwrap_err();
assert_match!(err.kind(), ErrorKind::NoMigrationData);
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(err.kind(), ErrorKind::NoMigrationData));
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
}
#[test]
fn test_migration_state_remembers_whether_to_copy_session_token() {
let mut fxa = setup();
assert_match!(fxa.is_in_migration_state(), MigrationState::None);
assert!(matches!(fxa.is_in_migration_state(), MigrationState::None));
let mut client = FxAClientMock::new();
client
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(scopes::OLD_SYNC),
)
.returns_once(Err(ErrorKind::RemoteError {
@ -364,11 +358,11 @@ mod tests {
let err = fxa
.migrate_from_session_token("session", "aabbcc", "ddeeff", false)
.unwrap_err();
assert_match!(err.kind(), ErrorKind::RemoteError { code: 500, .. });
assert_match!(
assert!(matches!(err.kind(), ErrorKind::RemoteError { code: 500, .. }));
assert!(matches!(
fxa.is_in_migration_state(),
MigrationState::ReuseSessionToken
);
));
// Retrying should fail again in the same way (as opposed to, say, trying
// to duplicate the sessionToken rather than reusing it).
@ -377,6 +371,7 @@ mod tests {
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(scopes::OLD_SYNC),
)
.returns_once(Err(ErrorKind::RemoteError {
@ -390,10 +385,10 @@ mod tests {
fxa.set_client(Arc::new(client));
let err = fxa.try_migration().unwrap_err();
assert_match!(err.kind(), ErrorKind::RemoteError { code: 500, .. });
assert_match!(
assert!(matches!(err.kind(), ErrorKind::RemoteError { code: 500, .. }));
assert!(matches!(
fxa.is_in_migration_state(),
MigrationState::ReuseSessionToken
);
));
}
}

View File

@ -179,3 +179,30 @@ pub struct AccountEvents {
#[prost(message, repeated, tag="1")]
pub events: ::std::vec::Vec<AccountEvent>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthorizationPkceParams {
#[prost(string, required, tag="1")]
pub code_challenge: std::string::String,
#[prost(string, required, tag="2")]
pub code_challenge_method: std::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthorizationParams {
#[prost(string, required, tag="1")]
pub client_id: std::string::String,
#[prost(string, required, tag="2")]
pub scope: std::string::String,
#[prost(string, required, tag="3")]
pub state: std::string::String,
#[prost(string, required, tag="4")]
pub access_type: std::string::String,
#[prost(message, optional, tag="5")]
pub pkce_params: ::std::option::Option<AuthorizationPkceParams>,
#[prost(string, optional, tag="6")]
pub keys_jwk: ::std::option::Option<std::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MetricsParams {
#[prost(map="string, string", tag="1")]
pub parameters: ::std::collections::HashMap<std::string::String, std::string::String>,
}

View File

@ -6,19 +6,20 @@ pub mod attached_clients;
use crate::{
error::*,
http_client::OAuthTokenResponse,
http_client::{AuthorizationRequestParameters, OAuthTokenResponse},
scoped_keys::{ScopedKey, ScopedKeysFlow},
util, FirefoxAccount,
};
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
use rc_crypto::digest;
use serde_derive::*;
use std::convert::TryFrom;
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
iter::FromIterator,
time::{SystemTime, UNIX_EPOCH},
};
use url::Url;
// If a cached token has less than `OAUTH_MIN_TIME_LEFT` seconds left to live,
// it will be considered already expired.
const OAUTH_MIN_TIME_LEFT: u64 = 60;
@ -92,11 +93,13 @@ impl FirefoxAccount {
}
/// Check whether user is authorized using our refresh token.
pub fn check_authorization_status(&self) -> Result<IntrospectInfo> {
pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
let resp = match self.state.refresh_token {
Some(ref refresh_token) => self
.client
.oauth_introspect_refresh_token(&self.state.config, &refresh_token.token)?,
Some(ref refresh_token) => {
self.auth_circuit_breaker.check()?;
self.client
.oauth_introspect_refresh_token(&self.state.config, &refresh_token.token)?
}
None => return Err(ErrorKind::NoRefreshToken.into()),
};
Ok(IntrospectInfo {
@ -109,8 +112,20 @@ impl FirefoxAccount {
/// * `pairing_url` - A pairing URL obtained by scanning a QR code produced by
/// the pairing authority.
/// * `scopes` - Space-separated list of requested scopes by the pairing supplicant.
pub fn begin_pairing_flow(&mut self, pairing_url: &str, scopes: &[&str]) -> Result<String> {
/// * `entrypoint` - The entrypoint to be used for data collection
/// * `metrics` - Optional parameters for metrics
pub fn begin_pairing_flow(
&mut self,
pairing_url: &str,
scopes: &[&str],
entrypoint: &str,
metrics: Option<MetricsParams>,
) -> Result<String> {
let mut url = self.state.config.pair_supp_url()?;
url.query_pairs_mut().append_pair("entrypoint", entrypoint);
if let Some(metrics) = metrics {
metrics.append_params_to_url(&mut url);
}
let pairing_url = Url::parse(pairing_url)?;
if url.host_str() != pairing_url.host_str() {
return Err(ErrorKind::OriginMismatch.into());
@ -122,7 +137,14 @@ impl FirefoxAccount {
/// Initiate an OAuth login flow and return a URL that should be navigated to.
///
/// * `scopes` - Space-separated list of requested scopes.
pub fn begin_oauth_flow(&mut self, scopes: &[&str]) -> Result<String> {
/// * `entrypoint` - The entrypoint to be used for metrics
/// * `metrics` - Optional metrics parameters
pub fn begin_oauth_flow(
&mut self,
scopes: &[&str],
entrypoint: &str,
metrics: Option<MetricsParams>,
) -> Result<String> {
let mut url = if self.state.last_seen_profile.is_some() {
self.state.config.oauth_force_auth_url()?
} else {
@ -131,7 +153,11 @@ impl FirefoxAccount {
url.query_pairs_mut()
.append_pair("action", "email")
.append_pair("response_type", "code");
.append_pair("response_type", "code")
.append_pair("entrypoint", entrypoint);
if let Some(metrics) = metrics {
metrics.append_params_to_url(&mut url);
}
if let Some(ref cached_profile) = self.state.last_seen_profile {
url.query_pairs_mut()
@ -156,27 +182,87 @@ impl FirefoxAccount {
}
/// Fetch an OAuth code for a particular client using a session token from the account state.
/// This method doesn't support OAuth public clients at this time.
///
/// * `client_id` - OAuth client id.
/// * `scopes` - Space-separated list of requested scopes.
/// * `state` - OAuth state.
/// * `access_type` - Type of OAuth access, can be "offline" and "online.
/// * `auth_params` Authorization parameters which includes:
/// * `client_id` - OAuth client id.
/// * `scope` - list of requested scopes.
/// * `state` - OAuth state.
/// * `access_type` - Type of OAuth access, can be "offline" and "online"
/// * `pkce_params` - Optional PKCE parameters for public clients (`code_challenge` and `code_challenge_method`)
/// * `keys_jwk` - Optional JWK used to encrypt scoped keys
pub fn authorize_code_using_session_token(
&self,
client_id: &str,
scope: &str,
state: &str,
access_type: &str,
auth_params: AuthorizationParameters,
) -> Result<String> {
let session_token = self.get_session_token()?;
// Validate request to ensure that the client is actually allowed to request
// the scopes they requested
let allowed_scopes = self.client.scoped_key_data(
&self.state.config,
&session_token,
&auth_params.client_id,
&auth_params.scope.join(" "),
)?;
if let Some(not_allowed_scope) = auth_params
.scope
.iter()
.find(|scope| !allowed_scopes.contains_key(*scope))
{
return Err(ErrorKind::ScopeNotAllowed(
auth_params.client_id.clone(),
not_allowed_scope.clone(),
)
.into());
}
let keys_jwe = if let Some(keys_jwk) = auth_params.keys_jwk {
let mut scoped_keys = HashMap::new();
allowed_scopes
.iter()
.try_for_each(|(scope, _)| -> Result<()> {
scoped_keys.insert(
scope,
self.state
.scoped_keys
.get(scope)
.ok_or_else(|| ErrorKind::NoScopedKey(scope.clone()))?,
);
Ok(())
})?;
let scoped_keys = serde_json::to_string(&scoped_keys)?;
let keys_jwk = base64::decode_config(keys_jwk, base64::URL_SAFE_NO_PAD)?;
let jwk = serde_json::from_slice(&keys_jwk)?;
Some(jwcrypto::encrypt_to_jwe(
scoped_keys.as_bytes(),
EncryptionParameters::ECDH_ES {
enc: EncryptionAlgorithm::A256GCM,
peer_jwk: &jwk,
},
)?)
} else {
None
};
let auth_request_params = AuthorizationRequestParameters {
client_id: auth_params.client_id,
scope: auth_params.scope.join(" "),
state: auth_params.state,
access_type: auth_params.access_type,
code_challenge: auth_params
.pkce_params
.as_ref()
.map(|param| param.code_challenge.clone()),
code_challenge_method: auth_params
.pkce_params
.map(|param| param.code_challenge_method),
keys_jwe,
};
let resp = self.client.authorization_code_using_session_token(
&self.state.config,
&client_id,
&session_token,
&scope,
&state,
&access_type,
auth_request_params,
)?;
Ok(resp.code)
@ -189,7 +275,8 @@ impl FirefoxAccount {
let code_challenge = digest::digest(&digest::SHA256, &code_verifier.as_bytes())?;
let code_challenge = base64::encode_config(&code_challenge, base64::URL_SAFE_NO_PAD);
let scoped_keys_flow = ScopedKeysFlow::with_random_key()?;
let jwk_json = scoped_keys_flow.generate_keys_jwk()?;
let jwk = scoped_keys_flow.get_public_key_jwk()?;
let jwk_json = serde_json::to_string(&jwk)?;
let keys_jwk = base64::encode_config(&jwk_json, base64::URL_SAFE_NO_PAD);
url.query_pairs_mut()
.append_pair("client_id", &self.state.config.client_id)
@ -273,7 +360,7 @@ impl FirefoxAccount {
let old_refresh_token = self.state.refresh_token.clone();
let new_refresh_token = resp
.refresh_token
.ok_or_else(|| ErrorKind::RefreshTokenNotPresent)?;
.ok_or_else(|| ErrorKind::UnrecoverableServerError("No refresh token in response"))?;
// Destroying a refresh token also destroys its associated device,
// grab the device information for replication later.
let old_device_info = match old_refresh_token {
@ -337,13 +424,14 @@ impl FirefoxAccount {
)?;
let new_refresh_token = resp
.refresh_token
.ok_or_else(|| ErrorKind::RefreshTokenNotPresent)?;
.ok_or_else(|| ErrorKind::UnrecoverableServerError("No refresh token in response"))?;
self.state.refresh_token = Some(RefreshToken {
token: new_refresh_token,
scopes: HashSet::from_iter(resp.scope.split(' ').map(ToString::to_string)),
});
self.state.session_token = Some(session_token.to_owned());
self.clear_access_token_cache();
self.clear_devices_and_attached_clients_cache();
// When our keys change, we might need to re-register device capabilities with the server.
// Ensure that this happens on the next call to ensure_capabilities.
self.state.device_capabilities.clear();
@ -354,6 +442,147 @@ impl FirefoxAccount {
pub fn clear_access_token_cache(&mut self) {
self.state.access_token_cache.clear();
}
#[cfg(feature = "integration_test")]
pub fn new_logged_in(
config: crate::Config,
session_token: &str,
scoped_keys: HashMap<String, ScopedKey>,
) -> Self {
let mut fxa = FirefoxAccount::with_config(config);
fxa.state.session_token = Some(session_token.to_owned());
scoped_keys.iter().for_each(|(key, val)| {
fxa.state.scoped_keys.insert(key.to_string(), val.clone());
});
fxa
}
}
const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; // 3 tokens every minute.
// The auth circuit breaker rate-limits access to the `oauth_introspect_refresh_token`
// using a fairly naively implemented token bucket algorithm.
#[derive(Clone, Copy)]
pub(crate) struct AuthCircuitBreaker {
tokens: u8,
last_refill: u64, // in ms.
}
impl Default for AuthCircuitBreaker {
fn default() -> Self {
AuthCircuitBreaker {
tokens: AUTH_CIRCUIT_BREAKER_CAPACITY,
last_refill: Self::now(),
}
}
}
impl AuthCircuitBreaker {
pub(crate) fn check(&mut self) -> Result<()> {
self.refill();
if self.tokens == 0 {
return Err(ErrorKind::AuthCircuitBreakerError.into());
}
self.tokens -= 1;
Ok(())
}
fn refill(&mut self) {
let now = Self::now();
let new_tokens =
((now - self.last_refill) as f64 * AUTH_CIRCUIT_BREAKER_RENEWAL_RATE as f64) as u8; // `as` is a truncating/saturing cast.
if new_tokens > 0 {
self.last_refill = now;
self.tokens = std::cmp::min(
AUTH_CIRCUIT_BREAKER_CAPACITY,
self.tokens.saturating_add(new_tokens),
);
}
}
#[cfg(not(test))]
#[inline]
fn now() -> u64 {
util::now()
}
#[cfg(test)]
fn now() -> u64 {
1600000000000
}
}
#[derive(Clone)]
pub struct AuthorizationPKCEParams {
pub code_challenge: String,
pub code_challenge_method: String,
}
#[derive(Clone)]
pub struct AuthorizationParameters {
pub client_id: String,
pub scope: Vec<String>,
pub state: String,
pub access_type: String,
pub pkce_params: Option<AuthorizationPKCEParams>,
pub keys_jwk: Option<String>,
}
impl TryFrom<Url> for AuthorizationParameters {
type Error = Error;
fn try_from(url: Url) -> Result<Self> {
let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
let scope = query_map
.get("scope")
.cloned()
.ok_or_else(|| ErrorKind::MissingUrlParameter("scope"))?;
let client_id = query_map
.get("client_id")
.cloned()
.ok_or_else(|| ErrorKind::MissingUrlParameter("client_id"))?;
let state = query_map
.get("state")
.cloned()
.ok_or_else(|| ErrorKind::MissingUrlParameter("state"))?;
let access_type = query_map
.get("access_type")
.cloned()
.ok_or_else(|| ErrorKind::MissingUrlParameter("access_type"))?;
let code_challenge = query_map.get("code_challenge").cloned();
let code_challenge_method = query_map.get("code_challenge_method").cloned();
let pkce_params = match (code_challenge, code_challenge_method) {
(Some(code_challenge), Some(code_challenge_method)) => Some(AuthorizationPKCEParams {
code_challenge,
code_challenge_method,
}),
_ => None,
};
let keys_jwk = query_map.get("keys_jwk").cloned();
Ok(Self {
client_id,
scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
state,
access_type,
pkce_params,
keys_jwk,
})
}
}
pub struct MetricsParams {
pub parameters: std::collections::HashMap<String, String>,
}
impl MetricsParams {
fn append_params_to_url(&self, url: &mut Url) {
self.parameters
.iter()
.for_each(|(parameter_name, parameter_value)| {
url.query_pairs_mut()
.append_pair(parameter_name, parameter_value);
});
}
}
#[derive(Clone, Serialize, Deserialize)]
@ -427,15 +656,20 @@ mod tests {
"12345678",
"https://foo.bar",
);
let mut params = HashMap::new();
params.insert("flow_id".to_string(), "87654321".to_string());
let metrics_params = MetricsParams { parameters: params };
let mut fxa = FirefoxAccount::with_config(config);
let url = fxa.begin_oauth_flow(&["profile"]).unwrap();
let url = fxa
.begin_oauth_flow(&["profile"], "test_oauth_flow_url", Some(metrics_params))
.unwrap();
let flow_url = Url::parse(&url).unwrap();
assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
assert_eq!(flow_url.path(), "/authorization");
let mut pairs = flow_url.query_pairs();
assert_eq!(pairs.count(), 10);
assert_eq!(pairs.count(), 12);
assert_eq!(
pairs.next(),
Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
@ -444,7 +678,17 @@ mod tests {
pairs.next(),
Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
);
assert_eq!(
pairs.next(),
Some((
Cow::Borrowed("entrypoint"),
Cow::Borrowed("test_oauth_flow_url")
))
);
assert_eq!(
pairs.next(),
Some((Cow::Borrowed("flow_id"), Cow::Borrowed("87654321")))
);
assert_eq!(
pairs.next(),
Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
@ -490,7 +734,9 @@ mod tests {
let mut fxa = FirefoxAccount::with_config(config);
let email = "test@example.com";
fxa.add_cached_profile("123", email);
let url = fxa.begin_oauth_flow(&["profile"]).unwrap();
let url = fxa
.begin_oauth_flow(&["profile"], "test_force_auth_url", None)
.unwrap();
let url = Url::parse(&url).unwrap();
assert_eq!(url.path(), "/oauth/force_auth");
let mut pairs = url.query_pairs();
@ -511,7 +757,9 @@ mod tests {
"urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
);
let mut fxa = FirefoxAccount::with_config(config);
let url = fxa.begin_oauth_flow(&SCOPES).unwrap();
let url = fxa
.begin_oauth_flow(&SCOPES, "test_webchannel_context_url", None)
.unwrap();
let url = Url::parse(&url).unwrap();
let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
let context = &query_params["context"];
@ -530,7 +778,14 @@ mod tests {
"urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
);
let mut fxa = FirefoxAccount::with_config(config);
let url = fxa.begin_pairing_flow(&PAIRING_URL, &SCOPES).unwrap();
let url = fxa
.begin_pairing_flow(
&PAIRING_URL,
&SCOPES,
"test_webchannel_pairing_context_url",
None,
)
.unwrap();
let url = Url::parse(&url).unwrap();
let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
let context = &query_params["context"];
@ -549,8 +804,19 @@ mod tests {
"12345678",
"https://foo.bar",
);
let mut params = HashMap::new();
params.insert("flow_id".to_string(), "87654321".to_string());
let metrics_params = MetricsParams { parameters: params };
let mut fxa = FirefoxAccount::with_config(config);
let url = fxa.begin_pairing_flow(&PAIRING_URL, &SCOPES).unwrap();
let url = fxa
.begin_pairing_flow(
&PAIRING_URL,
&SCOPES,
"test_pairing_flow_url",
Some(metrics_params),
)
.unwrap();
let flow_url = Url::parse(&url).unwrap();
let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
@ -559,7 +825,18 @@ mod tests {
assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
let mut pairs = flow_url.query_pairs();
assert_eq!(pairs.count(), 8);
assert_eq!(pairs.count(), 10);
assert_eq!(
pairs.next(),
Some((
Cow::Borrowed("entrypoint"),
Cow::Borrowed("test_pairing_flow_url")
))
);
assert_eq!(
pairs.next(),
Some((Cow::Borrowed("flow_id"), Cow::Borrowed("87654321")))
);
assert_eq!(
pairs.next(),
Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
@ -607,8 +884,12 @@ mod tests {
static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
let url =
fxa.begin_pairing_flow(&PAIRING_URL, &["https://identity.mozilla.com/apps/oldsync"]);
let url = fxa.begin_pairing_flow(
&PAIRING_URL,
&["https://identity.mozilla.com/apps/oldsync"],
"test_pairiong_flow_origin_mismatch",
None,
);
assert!(url.is_err());
@ -646,4 +927,220 @@ mod tests {
let auth_status = fxa.check_authorization_status().unwrap();
assert_eq!(auth_status.active, true);
}
#[test]
fn test_check_authorization_status_circuit_breaker() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
let refresh_token_scopes = std::collections::HashSet::new();
fxa.state.refresh_token = Some(RefreshToken {
token: "refresh_token".to_owned(),
scopes: refresh_token_scopes,
});
let mut client = FxAClientMock::new();
// This copy-pasta (equivalent to `.returns(..).times(5)`) is there
// because `Error` is not cloneable :/
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client.expect_oauth_introspect_refresh_token_calls_in_order();
fxa.set_client(Arc::new(client));
for _ in 0..5 {
assert!(fxa.check_authorization_status().is_ok());
}
match fxa.check_authorization_status() {
Ok(_) => unreachable!("should not happen"),
Err(err) => assert!(matches!(err.kind(), ErrorKind::AuthCircuitBreakerError)),
}
}
#[test]
fn test_auth_circuit_breaker_unit_recovery() {
let mut breaker = AuthCircuitBreaker::default();
// AuthCircuitBreaker::now is fixed for tests, let's assert that for sanity.
assert_eq!(AuthCircuitBreaker::now(), 1600000000000);
for _ in 0..AUTH_CIRCUIT_BREAKER_CAPACITY {
assert!(breaker.check().is_ok());
}
assert!(breaker.check().is_err());
// Jump back in time (1 min).
breaker.last_refill -= 60 * 1000;
let expected_tokens_before_check: u8 =
(AUTH_CIRCUIT_BREAKER_RENEWAL_RATE * 60.0 * 1000.0) as u8;
assert!(breaker.check().is_ok());
assert_eq!(breaker.tokens, expected_tokens_before_check - 1);
}
use crate::scopes;
#[test]
fn test_auth_code_pair_valid_not_allowed_scope() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.set_session_token("session");
let mut client = FxAClientMock::new();
let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
let expected_scopes = scopes::OLD_SYNC
.chars()
.chain(std::iter::once(' '))
.chain(not_allowed_scope.chars())
.collect::<String>();
client
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(expected_scopes),
)
.returns_once(Err(ErrorKind::RemoteError {
code: 400,
errno: 163,
error: "Invalid Scopes".to_string(),
message: "Not allowed to request scopes".to_string(),
info: "fyi, there was a server error".to_string(),
}
.into()));
fxa.set_client(Arc::new(client));
let auth_params = AuthorizationParameters {
client_id: "12345678".to_string(),
scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
keys_jwk: None,
};
let res = fxa.authorize_code_using_session_token(auth_params);
assert!(res.is_err());
let err = res.unwrap_err();
if let ErrorKind::RemoteError {
code,
errno,
error: _,
message: _,
info: _,
} = err.kind()
{
assert_eq!(*code, 400);
assert_eq!(*errno, 163); // Requested scopes not allowed
} else {
panic!("Should return an error from the server specifying that the requested scopes are not allowed");
}
}
#[test]
fn test_auth_code_pair_invalid_scope_not_allowed() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.set_session_token("session");
let mut client = FxAClientMock::new();
let invalid_scope = "IamAnInvalidScope";
let expected_scopes = scopes::OLD_SYNC
.chars()
.chain(std::iter::once(' '))
.chain(invalid_scope.chars())
.collect::<String>();
let mut server_ret = HashMap::new();
server_ret.insert(
scopes::OLD_SYNC.to_string(),
ScopedKeyDataResponse {
key_rotation_secret: "IamASecret".to_string(),
key_rotation_timestamp: 100,
identifier: "".to_string(),
},
);
client
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(expected_scopes),
)
.returns_once(Ok(server_ret));
fxa.set_client(Arc::new(client));
let auth_params = AuthorizationParameters {
client_id: "12345678".to_string(),
scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
keys_jwk: None,
};
let res = fxa.authorize_code_using_session_token(auth_params);
assert!(res.is_err());
let err = res.unwrap_err();
if let ErrorKind::ScopeNotAllowed(client_id, scope) = err.kind() {
assert_eq!(client_id.clone(), "12345678");
assert_eq!(scope.clone(), "IamAnInvalidScope");
} else {
panic!("Should return an error that specifies the scope that is not allowed");
}
}
#[test]
fn test_auth_code_pair_scope_not_in_state() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.set_session_token("session");
let mut client = FxAClientMock::new();
let mut server_ret = HashMap::new();
server_ret.insert(
scopes::OLD_SYNC.to_string(),
ScopedKeyDataResponse {
key_rotation_secret: "IamASecret".to_string(),
key_rotation_timestamp: 100,
identifier: "".to_string(),
},
);
client
.expect_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
|arg| arg.partial_eq(scopes::OLD_SYNC),
)
.returns_once(Ok(server_ret));
fxa.set_client(Arc::new(client));
let auth_params = AuthorizationParameters {
client_id: "12345678".to_string(),
scope: vec![scopes::OLD_SYNC.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
};
let res = fxa.authorize_code_using_session_token(auth_params);
assert!(res.is_err());
let err = res.unwrap_err();
if let ErrorKind::NoScopedKey(scope) = err.kind() {
assert_eq!(scope.clone(), scopes::OLD_SYNC.to_string());
} else {
panic!("Should return an error that specifies the scope that is not in the state");
}
}
}

View File

@ -27,6 +27,7 @@ impl FirefoxAccount {
"Access token rejected, clearing the tokens cache and trying again."
);
self.clear_access_token_cache();
self.clear_devices_and_attached_clients_cache();
self.get_profile_helper(ignore_cache)
}
_ => Err(e),
@ -95,12 +96,10 @@ mod tests {
response: Profile {
uid: uid.into(),
email: email.into(),
locale: "en-US".into(),
display_name: None,
avatar: "".into(),
avatar_default: true,
amr_values: vec![],
two_factor_authentication: false,
ecosystem_anon_id: None,
},
cached_at: util::now(),
etag: "fake etag".into(),
@ -135,12 +134,10 @@ mod tests {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
locale: "fr-FR".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
amr_values: vec![],
two_factor_authentication: false,
ecosystem_anon_id: None,
},
etag: None,
})));
@ -216,12 +213,10 @@ mod tests {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
locale: "fr-FR".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
amr_values: vec![],
two_factor_authentication: false,
ecosystem_anon_id: None,
},
etag: None,
})));

View File

@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::device::CommandFetchReason;
use crate::{error::*, AccountEvent, FirefoxAccount};
use serde_derive::Deserialize;
@ -13,21 +14,34 @@ impl FirefoxAccount {
/// Since FxA sends one push notification per command received,
/// we must only retrieve 1 command per push message,
/// otherwise we risk receiving push messages for which the UI has already been shown.
/// However, note that this means iOS currently risks losing messages for
/// which a push notification doesn't arrive.
///
/// **💾 This method alters the persisted account state.**
pub fn handle_push_message(&mut self, payload: &str) -> Result<Vec<AccountEvent>> {
let payload = serde_json::from_str(payload)?;
let payload = serde_json::from_str(payload).or_else(|err| {
// Due to a limitation of serde (https://github.com/serde-rs/serde/issues/1714)
// we can't parse some payloads with an unknown "command" value. Try doing a
// less-strongly-validating parse so we can silently ignore such messages, while
// while reporting errors if the payload is completely unintelligible.
let v: serde_json::Value = serde_json::from_str(payload)?;
match v.get("command") {
Some(_) => Ok(PushPayload::Unknown),
None => Err(err),
}
})?;
match payload {
PushPayload::CommandReceived(CommandReceivedPushPayload { index, .. }) => {
if cfg!(target_os = "ios") {
self.fetch_device_command(index)
self.ios_fetch_device_command(index)
.map(|cmd| vec![AccountEvent::IncomingDeviceCommand(Box::new(cmd))])
} else {
self.poll_device_commands().map(|cmds| {
cmds.into_iter()
.map(|cmd| AccountEvent::IncomingDeviceCommand(Box::new(cmd)))
.collect()
})
self.poll_device_commands(CommandFetchReason::Push(index))
.map(|cmds| {
cmds.into_iter()
.map(|cmd| AccountEvent::IncomingDeviceCommand(Box::new(cmd)))
.collect()
})
}
}
PushPayload::ProfileUpdated => {
@ -66,6 +80,8 @@ impl FirefoxAccount {
}
PushPayload::PasswordChanged | PushPayload::PasswordReset => {
let status = self.check_authorization_status()?;
// clear any device or client data due to password change.
self.clear_devices_and_attached_clients_cache();
Ok(if !status.active {
vec![AccountEvent::AccountAuthStateChanged]
} else {
@ -130,6 +146,10 @@ pub struct AccountDestroyedPushPayload {
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::FxAClientMock;
use crate::http_client::IntrospectResponse;
use crate::CachedResponse;
use std::sync::Arc;
#[test]
fn test_deserialize_send_tab_command() {
@ -178,6 +198,35 @@ mod tests {
};
}
#[test]
fn test_push_password_reset() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let mut client = FxAClientMock::new();
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.times(1)
.returns_once(Ok(IntrospectResponse { active: true }));
fxa.set_client(Arc::new(client));
let refresh_token_scopes = std::collections::HashSet::new();
fxa.state.refresh_token = Some(crate::oauth::RefreshToken {
token: "refresh_token".to_owned(),
scopes: refresh_token_scopes,
});
fxa.state.current_device_id = Some("my_id".to_owned());
fxa.devices_cache = Some(CachedResponse {
response: vec![],
cached_at: 0,
etag: "".to_string(),
});
let json = "{\"version\":1,\"command\":\"fxaccounts:password_reset\"}";
assert!(fxa.devices_cache.is_some());
fxa.handle_push_message(json).unwrap();
assert!(fxa.devices_cache.is_none());
}
#[test]
fn test_push_device_disconnected_remote() {
let mut fxa =
@ -198,11 +247,28 @@ mod tests {
}
#[test]
fn test_handle_push_message_unknown_command() {
fn test_handle_push_message_ignores_unknown_command() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"version\":1,\"command\":\"huh\"}";
let events = fxa.handle_push_message(json).unwrap();
assert!(events.is_empty());
}
#[test]
fn test_handle_push_message_ignores_unknown_command_with_data() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"version\":1,\"command\":\"huh\",\"data\":{\"value\":42}}";
let events = fxa.handle_push_message(json).unwrap();
assert!(events.is_empty());
}
#[test]
fn test_handle_push_message_errors_on_garbage_data() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"wtf\":\"bbq\"}";
fxa.handle_push_message(json).unwrap_err();
}
}

View File

@ -3,14 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::{error::*, FirefoxAccount};
use byteorder::{BigEndian, ByteOrder};
use rc_crypto::{
aead, agreement,
agreement::{Ephemeral, KeyPair},
digest,
};
use serde_derive::*;
use serde_json::{self, json};
use jwcrypto::{self, DecryptionParameters, Jwk};
use rc_crypto::{agreement, agreement::EphemeralKeyPair};
use serde_derive::{Deserialize, Serialize};
impl FirefoxAccount {
pub(crate) fn get_scoped_key(&self, scope: &str) -> Result<&ScopedKey> {
@ -47,144 +42,40 @@ impl std::fmt::Debug for ScopedKey {
}
pub struct ScopedKeysFlow {
key_pair: KeyPair<Ephemeral>,
key_pair: EphemeralKeyPair,
}
/// Theorically, everything done in this file could and should be done in a JWT library.
/// However, none of the existing rust JWT libraries can handle ECDH-ES encryption, and API choices
/// made by their authors make it difficult to add this feature.
/// In the past, we chose cjose to do that job, but it added three C dependencies to build and link
/// against: jansson, openssl and cjose itself.
impl ScopedKeysFlow {
pub fn with_random_key() -> Result<Self> {
let key_pair = KeyPair::<Ephemeral>::generate(&agreement::ECDH_P256)
.map_err(|_| ErrorKind::KeyGenerationFailed)?;
let key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256)?;
Ok(Self { key_pair })
}
#[cfg(test)]
pub fn from_static_key_pair(key_pair: KeyPair<agreement::Static>) -> Result<Self> {
pub fn from_static_key_pair(key_pair: agreement::KeyPair<agreement::Static>) -> Result<Self> {
let (private_key, _) = key_pair.split();
let ephemeral_prv_key = private_key._tests_only_dangerously_convert_to_ephemeral();
let key_pair = KeyPair::from_private_key(ephemeral_prv_key)?;
let key_pair = agreement::KeyPair::from_private_key(ephemeral_prv_key)?;
Ok(Self { key_pair })
}
pub fn generate_keys_jwk(&self) -> Result<String> {
let pub_key_bytes = self.key_pair.public_key().to_bytes()?;
// Uncompressed form (see SECG SEC1 section 2.3.3).
// First byte is 4, then 32 bytes for x, and 32 bytes for y.
assert_eq!(pub_key_bytes.len(), 1 + 32 + 32);
assert_eq!(pub_key_bytes[0], 0x04);
let x = Vec::from(&pub_key_bytes[1..33]);
let x = base64::encode_config(&x, base64::URL_SAFE_NO_PAD);
let y = Vec::from(&pub_key_bytes[33..]);
let y = base64::encode_config(&y, base64::URL_SAFE_NO_PAD);
Ok(json!({
"crv": "P-256",
"kty": "EC",
"x": x,
"y": y,
})
.to_string())
pub fn get_public_key_jwk(&self) -> Result<Jwk> {
Ok(jwcrypto::ec::extract_pub_key_jwk(&self.key_pair)?)
}
pub fn decrypt_keys_jwe(self, jwe: &str) -> Result<String> {
let segments: Vec<&str> = jwe.split('.').collect();
let header = base64::decode_config(&segments[0], base64::URL_SAFE_NO_PAD)?;
let protected_header: serde_json::Value = serde_json::from_slice(&header)?;
if protected_header["epk"]["kty"] != "EC" {
return Err(ErrorKind::UnrecoverableServerError("Only EC keys are supported.").into());
}
if protected_header["epk"]["crv"] != "P-256" {
return Err(
ErrorKind::UnrecoverableServerError("Only P-256 curves are supported.").into(),
);
}
let alg = protected_header["enc"]
.as_str()
.ok_or_else(|| ErrorKind::UnrecoverableServerError("enc is not a string."))?;
let apu = protected_header["apu"].as_str().unwrap_or("");
let apv = protected_header["apv"].as_str().unwrap_or("");
// Part 1: Grab the x/y from the other party and construct the secret.
let x = base64::decode_config(
&protected_header["epk"]["x"]
.as_str()
.ok_or_else(|| ErrorKind::UnrecoverableServerError("x is not a string."))?,
base64::URL_SAFE_NO_PAD,
)?;
let y = base64::decode_config(
&protected_header["epk"]["y"]
.as_str()
.ok_or_else(|| ErrorKind::UnrecoverableServerError("y is not a string."))?,
base64::URL_SAFE_NO_PAD,
)?;
if x.len() != (256 / 8) {
return Err(ErrorKind::UnrecoverableServerError("X must be 32 bytes long.").into());
}
if y.len() != (256 / 8) {
return Err(ErrorKind::UnrecoverableServerError("Y must be 32 bytes long.").into());
}
let mut peer_pub_key: Vec<u8> = vec![0x04];
peer_pub_key.extend_from_slice(&x);
peer_pub_key.extend_from_slice(&y);
let (private_key, _) = self.key_pair.split();
let ikm = private_key.agree(&agreement::ECDH_P256, &peer_pub_key)?;
let secret = ikm.derive(|z| {
// ConcatKDF (1 iteration since keyLen <= hashLen).
// See rfc7518 section 4.6 for reference.
let counter = 1;
let mut buf: Vec<u8> = vec![];
buf.extend_from_slice(&to_32b_buf(counter));
buf.extend_from_slice(&z);
// otherinfo
buf.extend_from_slice(&to_32b_buf(alg.len() as u32));
buf.extend_from_slice(alg.as_bytes());
buf.extend_from_slice(&to_32b_buf(apu.len() as u32));
buf.extend_from_slice(apu.as_bytes());
buf.extend_from_slice(&to_32b_buf(apv.len() as u32));
buf.extend_from_slice(apv.as_bytes());
buf.extend_from_slice(&to_32b_buf(256));
digest::digest(&digest::SHA256, &buf)
})?;
// Part 2: decrypt the payload with the obtained secret
if !segments[1].is_empty() {
return Err(
ErrorKind::UnrecoverableServerError("The Encrypted Key must be empty.").into(),
);
}
let iv = base64::decode_config(&segments[2], base64::URL_SAFE_NO_PAD)?;
let ciphertext = base64::decode_config(&segments[3], base64::URL_SAFE_NO_PAD)?;
let auth_tag = base64::decode_config(&segments[4], base64::URL_SAFE_NO_PAD)?;
if auth_tag.len() != (128 / 8) {
return Err(
ErrorKind::UnrecoverableServerError("The auth tag must be 16 bytes long.").into(),
);
}
let opening_key = aead::OpeningKey::new(&aead::AES_256_GCM, &secret.as_ref())
.map_err(|_| ErrorKind::KeyImportFailed)?;
let mut ciphertext_and_tag = ciphertext.to_vec();
ciphertext_and_tag.extend(&auth_tag.to_vec());
let nonce = aead::Nonce::try_assume_unique_for_key(&aead::AES_256_GCM, &iv)?;
let aad = aead::Aad::from(segments[0].as_bytes());
let plaintext = aead::open(&opening_key, nonce, aad, &ciphertext_and_tag)
.map_err(|_| ErrorKind::AEADOpenFailure)?;
String::from_utf8(plaintext.to_vec()).map_err(Into::into)
let params = DecryptionParameters::ECDH_ES {
local_key_pair: self.key_pair,
};
Ok(jwcrypto::decrypt_jwe(jwe, params)?)
}
}
fn to_32b_buf(n: u32) -> Vec<u8> {
let mut buf = [0; 4];
BigEndian::write_u32(&mut buf, n);
buf.to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
use rc_crypto::agreement::PrivateKey;
use jwcrypto::JwkKeyParameters;
use rc_crypto::agreement::{KeyPair, PrivateKey};
#[test]
fn test_flow() {
@ -208,8 +99,17 @@ mod tests {
let private_key = PrivateKey::<rc_crypto::agreement::Static>::import(&ec_key).unwrap();
let key_pair = KeyPair::from(private_key).unwrap();
let flow = ScopedKeysFlow::from_static_key_pair(key_pair).unwrap();
let json = flow.generate_keys_jwk().unwrap();
assert_eq!(json, "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"ARvGIPJ5eIFdp6YTM-INVDqwfun2R9FfCUvXbH7QCIU\",\"y\":\"hk8gP0Po8nBh-WSiTsvsyesC5c1L6fGOEVuX8FHsvTs\"}");
let jwk = flow.get_public_key_jwk().unwrap();
let JwkKeyParameters::EC(ec_key_params) = jwk.key_parameters;
assert_eq!(ec_key_params.crv, "P-256");
assert_eq!(
ec_key_params.x,
"ARvGIPJ5eIFdp6YTM-INVDqwfun2R9FfCUvXbH7QCIU"
);
assert_eq!(
ec_key_params.y,
"hk8gP0Po8nBh-WSiTsvsyesC5c1L6fGOEVuX8FHsvTs"
);
let jwe = "eyJhbGciOiJFQ0RILUVTIiwia2lkIjoiNFBKTTl5dGVGeUtsb21ILWd2UUtyWGZ0a0N3ak9HNHRfTmpYVXhLM1VqSSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlB3eG9Na1RjSVZ2TFlKWU4wM2R0Y3o2TEJrR0FHaU1hZWlNQ3lTZXEzb2MiLCJ5IjoiLUYtTllRRDZwNUdSQ2ZoYm1hN3NvNkhxdExhVlNub012S0pFcjFBeWlaSSJ9LCJlbmMiOiJBMjU2R0NNIn0..b9FPhjjpmAmo_rP8.ur9jTry21Y2trvtcanSFmAtiRfF6s6qqyg6ruRal7PCwa7PxDzAuMN6DZW5BiK8UREOH08-FyRcIgdDOm5Zq8KwVAn56PGfcH30aNDGQNkA_mpfjx5Tj2z8kI6ryLWew4PGZb-PsL1g-_eyXhktq7dAhetjNYttKwSREWQFokv7N3nJGpukBqnwL1ost-MjDXlINZLVJKAiMHDcu-q7Epitwid2c2JVGOSCJjbZ4-zbxVmZ4o9xhFb2lbvdiaMygH6bPlrjEK99uT6XKtaIZmyDwftbD6G3x4On-CqA2TNL6ILRaJMtmyX--ctL0IrngUIHg_F0Wz94v.zBD8NACkUcZTPLH0tceGnA";
let keys = flow.decrypt_keys_jwe(jwe).unwrap();

View File

@ -3,4 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub const PROFILE: &str = "profile";
pub const PROFILE_WRITE: &str = "profile:write";
pub const OLD_SYNC: &str = "https://identity.mozilla.com/apps/oldsync";

View File

@ -9,7 +9,7 @@ use crate::{
},
error::*,
http_client::GetDeviceResponse,
scopes, FirefoxAccount, IncomingDeviceCommand,
scopes, telemetry, FirefoxAccount, IncomingDeviceCommand,
};
impl FirefoxAccount {
@ -38,22 +38,30 @@ impl FirefoxAccount {
}
/// Send a single tab to another device designated by its device ID.
/// XXX - We need a new send_tabs_to_devices() so we can correctly record
/// telemetry for these cases.
/// This probably requires a new "Tab" struct with the title and url.
/// android-components has SendToAllUseCase(), so this isn't just theoretical.
/// See https://github.com/mozilla/application-services/issues/3402
pub fn send_tab(&mut self, target_device_id: &str, title: &str, url: &str) -> Result<()> {
let devices = self.get_devices(false)?;
let target = devices
.iter()
.find(|d| d.id == target_device_id)
.ok_or_else(|| ErrorKind::UnknownTargetDevice(target_device_id.to_owned()))?;
let payload = SendTabPayload::single_tab(title, url);
let (payload, sent_telemetry) = SendTabPayload::single_tab(title, url);
let oldsync_key = self.get_scoped_key(scopes::OLD_SYNC)?;
let command_payload = send_tab::build_send_command(&oldsync_key, target, &payload)?;
self.invoke_command(send_tab::COMMAND_NAME, target, &command_payload)
self.invoke_command(send_tab::COMMAND_NAME, target, &command_payload)?;
self.telemetry.borrow_mut().record_tab_sent(sent_telemetry);
Ok(())
}
pub(crate) fn handle_send_tab_command(
&mut self,
sender: Option<GetDeviceResponse>,
payload: serde_json::Value,
reason: telemetry::ReceivedReason,
) -> Result<IncomingDeviceCommand> {
let send_tab_key: PrivateSendTabKeys =
match self.state.commands_data.get(send_tab::COMMAND_NAME) {
@ -67,8 +75,24 @@ impl FirefoxAccount {
};
let encrypted_payload: EncryptedSendTabPayload = serde_json::from_value(payload)?;
match encrypted_payload.decrypt(&send_tab_key) {
Ok(payload) => Ok(IncomingDeviceCommand::TabReceived { sender, payload }),
Ok(payload) => {
// It's an incoming tab, which we record telemetry for.
let recd_telemetry = telemetry::ReceivedCommand {
flow_id: payload.flow_id.clone(),
stream_id: payload.stream_id.clone(),
reason,
};
self.telemetry
.borrow_mut()
.record_tab_received(recd_telemetry);
// The telemetry IDs escape to the consumer, but that's OK...
Ok(IncomingDeviceCommand::TabReceived { sender, payload })
}
Err(e) => {
// XXX - this seems ripe for telemetry collection!?
// It also seems like it might be possible to recover - ie, one
// of the reasons is that there are key mismatches. Doesn't that
// mean the "other" key might work?
log::error!("Could not decrypt Send Tab payload. Diagnosing then resetting the Send Tab keys.");
match self.diagnose_remote_keys(send_tab_key) {
Ok(_) => log::error!("Could not find the cause of the Send Tab keys issue."),

View File

@ -105,6 +105,7 @@ pub(crate) struct StateV2 {
pub(crate) session_token: Option<String>, // Hex-formatted string.
pub(crate) last_seen_profile: Option<CachedResponse<Profile>>,
pub(crate) in_flight_migration: Option<MigrationData>,
pub(crate) ecosystem_user_id: Option<String>,
}
impl StateV2 {
@ -124,6 +125,7 @@ impl StateV2 {
device_capabilities: HashSet::new(),
session_token: None,
in_flight_migration: None,
ecosystem_user_id: None,
}
}
}
@ -189,6 +191,7 @@ impl From<StateV1> for Result<StateV2> {
last_seen_profile: None,
in_flight_migration: None,
access_token_cache: HashMap::new(),
ecosystem_user_id: None,
})
}
}

View File

@ -0,0 +1,360 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::{error::*, scopes, FirefoxAccount};
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters, Jwk};
use rand_rccrypto::rand::seq::SliceRandom;
use rc_crypto::rand;
use serde_derive::*;
use sync_guid::Guid;
impl FirefoxAccount {
/// Get the ecosystem anon id, generating it if necessary.
///
/// **💾 This method alters the persisted account state.**
pub fn get_ecosystem_anon_id(&mut self) -> Result<String> {
self.get_ecosystem_anon_id_helper(true)
}
fn get_ecosystem_anon_id_helper(&mut self, generate_placeholder: bool) -> Result<String> {
let profile = self.get_profile(false)?;
// Default case: the ecosystem anon ID was generated during login.
if let Some(ecosystem_anon_id) = profile.ecosystem_anon_id {
return Ok(ecosystem_anon_id);
}
if !generate_placeholder {
return Err(ErrorKind::IllegalState("ecosystem_anon_id should be present").into());
}
// For older clients, we generate an ecosystem_user_id,
// persist it and then return ecosystem_anon_id.
let mut ecosystem_user_id = vec![0u8; 32];
rand::fill(&mut ecosystem_user_id)?;
// Will end up as a len 64 hex string.
let ecosystem_user_id = hex::encode(ecosystem_user_id);
let anon_id_key = self.fetch_random_ecosystem_anon_id_key()?;
let ecosystem_anon_id = jwcrypto::encrypt_to_jwe(
&ecosystem_user_id.as_bytes(),
EncryptionParameters::ECDH_ES {
enc: EncryptionAlgorithm::A256GCM,
peer_jwk: &anon_id_key,
},
)?;
let token = self.get_access_token(scopes::PROFILE_WRITE, None)?.token;
if let Err(err) =
self.client
.set_ecosystem_anon_id(&self.state.config, &token, &ecosystem_anon_id)
{
if let ErrorKind::RemoteError { code: 412, .. } = err.kind() {
// Another client beat us, fetch the new ecosystem_anon_id.
return self.get_ecosystem_anon_id_helper(false);
}
}
// Persist the unencrypted ecosystem_user_id for possible future use.
self.state.ecosystem_user_id = Some(ecosystem_user_id);
Ok(ecosystem_anon_id)
}
fn fetch_random_ecosystem_anon_id_key(&self) -> Result<Jwk> {
let config = self.client.fxa_client_configuration(&self.state.config)?;
let keys = config
.ecosystem_anon_id_keys
.ok_or_else(|| ErrorKind::NoAnonIdKey)?;
let mut rng = rand_rccrypto::RcCryptoRng;
Ok(keys
.choose(&mut rng)
.ok_or_else(|| ErrorKind::NoAnonIdKey)?
.clone())
}
/// Gathers and resets telemetry for this account instance.
/// This should be considered a short-term solution to telemetry gathering
/// and should called whenever consumers expect there might be telemetry,
/// and it should submit the telemetry to whatever telemetry system is in
/// use (probably glean).
///
/// The data is returned as a JSON string, which consumers should parse
/// forgivingly (eg, be tolerant of things not existing) to try and avoid
/// too many changes as telemetry comes and goes.
pub fn gather_telemetry(&mut self) -> Result<String> {
let telem = self.telemetry.replace(FxaTelemetry::new());
Ok(serde_json::to_string(&telem)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{http_client::*, oauth::AccessTokenInfo, Config};
use jwcrypto::{ec::ECKeysParameters, JwkKeyParameters};
use std::sync::Arc;
fn fxa_setup() -> FirefoxAccount {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.add_cached_token(
"profile",
AccessTokenInfo {
scope: "profile".to_string(),
token: "profiletok".to_string(),
key: None,
expires_at: u64::max_value(),
},
);
fxa.add_cached_token(
"profile:write",
AccessTokenInfo {
scope: "profile".to_string(),
token: "profilewritetok".to_string(),
key: None,
expires_at: u64::max_value(),
},
);
fxa
}
#[test]
fn get_ecosystem_anon_id_in_profile() {
let mut fxa = fxa_setup();
let ecosystem_anon_id = "bobo".to_owned();
let mut client = FxAClientMock::new();
client
.expect_profile(
mockiato::Argument::any,
|token| token.partial_eq("profiletok"),
mockiato::Argument::any,
)
.times(1)
.returns_once(Ok(Some(ResponseAndETag {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
ecosystem_anon_id: Some(ecosystem_anon_id.to_owned()),
},
etag: None,
})));
fxa.set_client(Arc::new(client));
assert_eq!(fxa.get_ecosystem_anon_id().unwrap(), ecosystem_anon_id);
}
#[test]
fn get_ecosystem_anon_id_generate_anon_id() {
let mut fxa = fxa_setup();
let mut client = FxAClientMock::new();
client
.expect_profile(
mockiato::Argument::any,
|token| token.partial_eq("profiletok"),
mockiato::Argument::any,
)
.times(1)
.returns_once(Ok(Some(ResponseAndETag {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
ecosystem_anon_id: None,
},
etag: None,
})));
client
.expect_fxa_client_configuration(mockiato::Argument::any)
.times(1)
.returns_once(Ok(ClientConfigurationResponse {
auth_server_base_url: "https://foo.bar".to_owned(),
oauth_server_base_url: "https://foo.bar".to_owned(),
profile_server_base_url: "https://foo.bar".to_owned(),
sync_tokenserver_base_url: "https://foo.bar".to_owned(),
ecosystem_anon_id_keys: Some(vec![Jwk {
kid: Some("LlU4keOmhTuq9fCNnpIldYGT9vT9dIDwnu_SBtTgeEQ".to_owned()),
key_parameters: JwkKeyParameters::EC(ECKeysParameters {
crv: "P-256".to_owned(),
x: "i3FM3OFSCZEoqu-jtelXwKt6AL4ODQ75NUdHbcLWQSo".to_owned(),
y: "nW-S3QiHDo-9hwfBhKnGKarkt_PVqVyIPUytjutTunY".to_owned(),
}),
}]),
}));
client
.expect_set_ecosystem_anon_id(
mockiato::Argument::any,
|token| token.partial_eq("profilewritetok"),
mockiato::Argument::any,
)
.times(1)
.returns_once(Ok(()));
fxa.set_client(Arc::new(client));
let ecosystem_anon_id = fxa.get_ecosystem_anon_id().unwrap();
// Well, it looks like a jwe folks.
assert!(ecosystem_anon_id.chars().filter(|c| c == &'.').count() == 4);
assert!(fxa.state.ecosystem_user_id.unwrap().len() == 64);
}
#[test]
fn get_ecosystem_anon_id_generate_anon_id_412() {
let mut fxa = fxa_setup();
let ecosystem_anon_id = "bobo".to_owned();
let mut client = FxAClientMock::new();
client
.expect_profile(
mockiato::Argument::any,
|token| token.partial_eq("profiletok"),
mockiato::Argument::any,
)
.returns_once(Ok(Some(ResponseAndETag {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
ecosystem_anon_id: None,
},
etag: None,
})));
// 2nd profile call after we get the 412.
client
.expect_profile(
mockiato::Argument::any,
|token| token.partial_eq("profiletok"),
mockiato::Argument::any,
)
.returns_once(Ok(Some(ResponseAndETag {
response: ProfileResponse {
uid: "12345ab".to_string(),
email: "foo@bar.com".to_string(),
display_name: None,
avatar: "https://foo.avatar".to_string(),
avatar_default: true,
ecosystem_anon_id: Some(ecosystem_anon_id.clone()),
},
etag: None,
})));
client.expect_profile_calls_in_order();
client
.expect_fxa_client_configuration(mockiato::Argument::any)
.times(1)
.returns_once(Ok(ClientConfigurationResponse {
auth_server_base_url: "https://foo.bar".to_owned(),
oauth_server_base_url: "https://foo.bar".to_owned(),
profile_server_base_url: "https://foo.bar".to_owned(),
sync_tokenserver_base_url: "https://foo.bar".to_owned(),
ecosystem_anon_id_keys: Some(vec![Jwk {
kid: Some("LlU4keOmhTuq9fCNnpIldYGT9vT9dIDwnu_SBtTgeEQ".to_owned()),
key_parameters: JwkKeyParameters::EC(ECKeysParameters {
crv: "P-256".to_owned(),
x: "i3FM3OFSCZEoqu-jtelXwKt6AL4ODQ75NUdHbcLWQSo".to_owned(),
y: "nW-S3QiHDo-9hwfBhKnGKarkt_PVqVyIPUytjutTunY".to_owned(),
}),
}]),
}));
client
.expect_set_ecosystem_anon_id(
mockiato::Argument::any,
|token| token.partial_eq("profilewritetok"),
mockiato::Argument::any,
)
.times(1)
.returns_once(Err(ErrorKind::RemoteError {
code: 412,
errno: 500,
error: "precondition failed".to_string(),
message: "another user did it".to_string(),
info: "".to_string(),
}
.into()));
fxa.set_client(Arc::new(client));
assert_eq!(fxa.get_ecosystem_anon_id().unwrap(), ecosystem_anon_id);
assert!(fxa.state.ecosystem_user_id.is_none());
}
}
// A somewhat mixed-bag of all telemetry we want to collect. The idea is that
// the app will "pull" telemetry via a new API whenever it thinks there might
// be something to record.
// It's considered a temporary solution until either we can record it directly
// (eg, via glean) or we come up with something better.
// Note that this means we'll lose telemetry if we crash between gathering it
// here and the app submitting it, but that should be rare (in practice,
// apps will submit it directly after an operation that generated telememtry)
/// The reason a tab/command was received.
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReceivedReason {
/// A push notification for the command was received.
Push,
/// Discovered while handling a push notification for a later message.
PushMissed,
/// Explicit polling for missed commands.
Poll,
}
#[derive(Debug, Serialize)]
pub struct SentCommand {
pub flow_id: String,
pub stream_id: String,
}
impl Default for SentCommand {
fn default() -> Self {
Self {
flow_id: Guid::random().to_string(),
stream_id: Guid::random().to_string(),
}
}
}
#[derive(Debug, Serialize)]
pub struct ReceivedCommand {
pub flow_id: String,
pub stream_id: String,
pub reason: ReceivedReason,
}
// We have a naive strategy to avoid unbounded memory growth - the intention
// is that if any platform lets things grow to hit these limits, it's probably
// never going to consume anything - so it doesn't matter what we discard (ie,
// there's no good reason to have a smarter circular buffer etc)
const MAX_TAB_EVENTS: usize = 200;
#[derive(Debug, Default, Serialize)]
pub struct FxaTelemetry {
commands_sent: Vec<SentCommand>,
commands_received: Vec<ReceivedCommand>,
}
impl FxaTelemetry {
pub fn new() -> Self {
FxaTelemetry {
..Default::default()
}
}
pub fn record_tab_sent(&mut self, sent: SentCommand) {
if self.commands_sent.len() < MAX_TAB_EVENTS {
self.commands_sent.push(sent);
}
}
pub fn record_tab_received(&mut self, recd: ReceivedCommand) {
if self.commands_received.len() < MAX_TAB_EVENTS {
self.commands_received.push(recd);
}
}
}

View File

@ -23,7 +23,7 @@ pub fn now_secs() -> u64 {
pub fn random_base64_url_string(len: usize) -> Result<String> {
let mut out = vec![0u8; len];
rand::fill(&mut out).map_err(|_| ErrorKind::RngFailure)?;
rand::fill(&mut out)?;
Ok(base64::encode_config(&out, base64::URL_SAFE_NO_PAD))
}

View File

@ -1 +1 @@
{"files":{"CHANGELOG.md":"73eb4c894a83e99234b1e6e33ea83150bdfca833f4795e1842f9d925738fd8ee","CODE_OF_CONDUCT.md":"902d5357af363426631d907e641e220b3ec89039164743f8442b3f120479b7cf","CONTRIBUTING.md":"2f395c6bff5805ada946b38d407bedea743230c845fd69cbd004da36871b9580","Cargo.toml":"cd9f9e8449caf1db8ac0858d6edc8c13ead85158cf15f018a390a3bdb46e17aa","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"43f86b667ba065459d185ec47e5a48f943ce6dfe04dc02e1cfff0baf58714243","build.rs":"ae11c573f1edf605d7a6dc740e48f2af9ec0a8c0578bf5d381d52126582fb67e","clippy.toml":"20c46fb795c2ef5317874716faa5c8ddc1ff076f3028c85c38dc560d71347ee5","src/bewit.rs":"b09d26497e3f934253578fe67026c382f3224875b30c4ce62e998af327f06a09","src/credentials.rs":"95758518cc82ecdedbc71bcea081d5e2f764e57b8e133aedcc00118b8b2a0d3a","src/crypto/holder.rs":"1d9f8eec15bd8fe12d459fad753758fc6b3de7b3c35d2644cba992458c4e5e19","src/crypto/mod.rs":"7baca935936802828508f8c2fbad90ba0de803b13389e588f8c1edd4e0149347","src/crypto/openssl.rs":"ced672fd59b70912095a718f112e4c02f63caf006680aa0db2f79306781d0cc9","src/crypto/ring.rs":"3f95da19ad39bdb5c40cb6efb53437df007cf1488eb5221ae6f193832008c66e","src/error.rs":"c06b0fa0b6963708f8096a92627b1b3a59d7d861ae3e840d4274aa34a9d2066d","src/header.rs":"558d7c0fc1cf83cf6ed4878d974858116e64e7c99aecf21d8af6a7a3c32739ff","src/lib.rs":"817d35fbd019f4ca6b65c581f6c5db09110d67aa0286d9503acf915904d57653","src/mac.rs":"1bd72295376cba0bfa9ebc1da7a49324380fcf243fd5dfc1ce7513aab06e7340","src/payload.rs":"fb70b564296050ff3e86d9199f0c5f2a02ebde5ca9770a95a5d174a9c2409d7b","src/request.rs":"62be42782d6a11b604c508bb6fb9bc7a5da542f7ba98cacc69142168438e6289","src/response.rs":"b0193fece1d827c3bae6a16d953d275c551951d4be3c4b76067996592b38fb1e"},"package":"57528ce5133f688e1bc4daadc3e50bf9093d40e8a1f64c6e506ccbae005e57e6"}
{"files":{"CHANGELOG.md":"73eb4c894a83e99234b1e6e33ea83150bdfca833f4795e1842f9d925738fd8ee","CODE_OF_CONDUCT.md":"902d5357af363426631d907e641e220b3ec89039164743f8442b3f120479b7cf","CONTRIBUTING.md":"2f395c6bff5805ada946b38d407bedea743230c845fd69cbd004da36871b9580","Cargo.toml":"cf901ec963d8dec408dea76cbc9105de87bd97cacf1061ef17c598b676cfd28f","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"43f86b667ba065459d185ec47e5a48f943ce6dfe04dc02e1cfff0baf58714243","build.rs":"ae11c573f1edf605d7a6dc740e48f2af9ec0a8c0578bf5d381d52126582fb67e","clippy.toml":"20c46fb795c2ef5317874716faa5c8ddc1ff076f3028c85c38dc560d71347ee5","src/bewit.rs":"b09d26497e3f934253578fe67026c382f3224875b30c4ce62e998af327f06a09","src/credentials.rs":"95758518cc82ecdedbc71bcea081d5e2f764e57b8e133aedcc00118b8b2a0d3a","src/crypto/holder.rs":"c0ad1269bb9b98a9f1abc17453813cc2983e958d7d3c0c95943ce74580c9fe97","src/crypto/mod.rs":"8ca9ba36f7584525f82068521dc1d8adf1a4ea95969970df155e2136e662450d","src/crypto/openssl.rs":"ced672fd59b70912095a718f112e4c02f63caf006680aa0db2f79306781d0cc9","src/crypto/ring.rs":"a6efd23f9f48596388d2242da563350cc736a5df58244796e7dbf062230a81fe","src/error.rs":"6539921e7cca19b8f62a9c2fcf5163cac872f6e537f20dc6e9b4fa6ef87aa2ae","src/header.rs":"558d7c0fc1cf83cf6ed4878d974858116e64e7c99aecf21d8af6a7a3c32739ff","src/lib.rs":"817d35fbd019f4ca6b65c581f6c5db09110d67aa0286d9503acf915904d57653","src/mac.rs":"1bd72295376cba0bfa9ebc1da7a49324380fcf243fd5dfc1ce7513aab06e7340","src/payload.rs":"fb70b564296050ff3e86d9199f0c5f2a02ebde5ca9770a95a5d174a9c2409d7b","src/request.rs":"62be42782d6a11b604c508bb6fb9bc7a5da542f7ba98cacc69142168438e6289","src/response.rs":"b0193fece1d827c3bae6a16d953d275c551951d4be3c4b76067996592b38fb1e"},"package":"7539c8d8699bae53238aacd3f93cfb0bcaef77b85dc963902b9367c5d7a84c48"}

View File

@ -13,7 +13,7 @@
[package]
edition = "2018"
name = "hawk"
version = "3.1.1"
version = "3.2.1"
authors = ["Jonas Finnemann Jensen <jopsen@gmail.com>", "Dustin J. Mitchell <dustin@mozilla.com>"]
build = "build.rs"
exclude = ["docker/*", ".taskcluster.yml", ".git*"]
@ -23,32 +23,31 @@ documentation = "https://docs.rs/hawk/"
readme = "README.md"
license = "MPL-2.0"
repository = "https://github.com/taskcluster/rust-hawk"
[dependencies.base64]
version = "0.12.0"
[dependencies.anyhow]
version = "1.0"
[dependencies.failure]
version = "0.1.5"
features = ["derive"]
[dependencies.base64]
version = "0.12"
[dependencies.log]
version = "0.4.8"
version = "0.4"
[dependencies.once_cell]
version = "1.0.1"
version = "1.4"
[dependencies.openssl]
version = "0.10.20"
optional = true
[dependencies.rand]
version = "0.7.0"
[dependencies.ring]
version = "0.16.0"
optional = true
[dependencies.thiserror]
version = "1.0"
[dependencies.url]
version = "2.0.0"
version = "2.1"
[dev-dependencies.pretty_assertions]
version = "^0.6.1"

View File

@ -1,11 +1,10 @@
use super::Cryptographer;
use failure::Fail;
use once_cell::sync::OnceCell;
static CRYPTOGRAPHER: OnceCell<&'static dyn Cryptographer> = OnceCell::new();
#[derive(Debug, Fail)]
#[fail(display = "Cryptographer already initialized")]
#[derive(Debug, thiserror::Error)]
#[error("Cryptographer already initialized")]
pub struct SetCryptographerError(());
/// Sets the global object that will be used for cryptographic operations.

View File

@ -9,7 +9,6 @@
//! [`Cryptographer`] and using the [`set_cryptographer`] or
//! [`set_boxed_cryptographer`] functions.
use crate::DigestAlgorithm;
use failure::Fail;
pub(crate) mod holder;
pub(crate) use holder::get_crypographer;
@ -22,20 +21,17 @@ mod ring;
#[cfg(not(any(feature = "use_ring", feature = "use_openssl")))]
pub use self::holder::{set_boxed_cryptographer, set_cryptographer};
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum CryptoError {
/// The configured cryptographer does not support the digest algorithm
/// specified. This should only happen for custom `Cryptographer` implementations
#[fail(
display = "Digest algorithm {:?} is unsupported by this Cryptographer",
_0
)]
#[error("Digest algorithm {0:?} is unsupported by this Cryptographer")]
UnsupportedDigest(DigestAlgorithm),
/// The configured cryptographer implementation failed to perform an
/// operation in some way.
#[fail(display = "{}", _0)]
Other(#[fail(cause)] failure::Error),
#[error("{0}")]
Other(#[source] anyhow::Error),
}
/// A trait encapsulating the cryptographic operations required by this library.

View File

@ -1,13 +1,12 @@
use super::{CryptoError, Cryptographer, Hasher, HmacKey};
use crate::DigestAlgorithm;
use failure::err_msg;
use ring::{digest, hmac};
use std::convert::{TryFrom, TryInto};
impl From<ring::error::Unspecified> for CryptoError {
// Ring's errors are entirely opaque
fn from(_: ring::error::Unspecified) -> Self {
CryptoError::Other(err_msg("Unspecified ring error"))
CryptoError::Other(anyhow::Error::msg("Unspecified ring error"))
}
}

View File

@ -1,48 +1,47 @@
use crate::crypto::CryptoError;
use failure::Fail;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Fail, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[fail(display = "Unparseable Hawk header: {}", _0)]
#[error("Unparseable Hawk header: {0}")]
HeaderParseError(String),
#[fail(display = "Invalid url: {}", _0)]
#[error("Invalid url: {0}")]
InvalidUrl(String),
#[fail(display = "Missing `ts` attribute in Hawk header")]
#[error("Missing `ts` attribute in Hawk header")]
MissingTs,
#[fail(display = "Missing `nonce` attribute in Hawk header")]
#[error("Missing `nonce` attribute in Hawk header")]
MissingNonce,
#[fail(display = "{}", _0)]
InvalidBewit(#[fail(cause)] InvalidBewit),
#[error("{0}")]
InvalidBewit(#[source] InvalidBewit),
#[fail(display = "{}", _0)]
Io(#[fail(cause)] std::io::Error),
#[error("{0}")]
Io(#[source] std::io::Error),
#[fail(display = "Base64 Decode error: {}", _0)]
Decode(#[fail(cause)] base64::DecodeError),
#[error("Base64 Decode error: {0}")]
Decode(#[source] base64::DecodeError),
#[fail(display = "Crypto error: {}", _0)]
Crypto(#[fail(cause)] CryptoError),
#[error("Crypto error: {0}")]
Crypto(#[source] CryptoError),
}
#[derive(Fail, Debug, PartialEq)]
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum InvalidBewit {
#[fail(display = "Multiple bewits in URL")]
#[error("Multiple bewits in URL")]
Multiple,
#[fail(display = "Invalid bewit format")]
#[error("Invalid bewit format")]
Format,
#[fail(display = "Invalid bewit id")]
#[error("Invalid bewit id")]
Id,
#[fail(display = "Invalid bewit exp")]
#[error("Invalid bewit exp")]
Exp,
#[fail(display = "Invalid bewit mac")]
#[error("Invalid bewit mac")]
Mac,
#[fail(display = "Invalid bewit ext")]
#[error("Invalid bewit ext")]
Ext,
}

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"7f3b5ca3efd16142c9f9abd5f03cc4c7bf9e632539560dbe4903467106a8bb41","src/ec.rs":"f5a15409d66260cf747a1f63feab22f37d6305eb155277f108f151b1b27d9ddc","src/error.rs":"dff1a5db3ff467319fcec3924e1e9244e740d0e972e93b7c34d19c7c00ed67bc","src/lib.rs":"e66647d6afa2e24730484e2a4d4502278aced4ed13dcd5dd2a6dfae9f5f691cc"},"package":null}

17
third_party/rust/jwcrypto/Cargo.toml vendored Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "jwcrypto"
version = "0.1.0"
authors = ["Edouard Oger <eoger@fastmail.com>"]
edition = "2018"
license = "MPL-2.0"
[lib]
crate-type = ["lib"]
[dependencies]
base64 = "0.12"
rc_crypto = { path = "../rc_crypto" }
serde = "1"
serde_derive = "1"
serde_json = "1"
thiserror = "1.0"

198
third_party/rust/jwcrypto/src/ec.rs vendored Normal file
View File

@ -0,0 +1,198 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::{
error::{JwCryptoError, Result},
Algorithm, CompactJwe, DecryptionParameters, EncryptionAlgorithm, EncryptionParameters,
JweHeader, Jwk, JwkKeyParameters,
};
use rc_crypto::{
aead,
agreement::{self, EphemeralKeyPair, InputKeyMaterial, UnparsedPublicKey},
digest, rand,
};
use serde_derive::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct ECKeysParameters {
pub crv: String,
pub x: String,
pub y: String,
}
pub(crate) fn encrypt_to_jwe(
data: &[u8],
encryption_params: EncryptionParameters,
) -> Result<CompactJwe> {
let EncryptionParameters::ECDH_ES { enc, peer_jwk } = encryption_params;
let local_key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256)?;
let local_public_key = extract_pub_key_jwk(&local_key_pair)?;
let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters;
let protected_header = JweHeader {
kid: peer_jwk.kid.clone(),
alg: Algorithm::ECDH_ES,
enc,
epk: Some(local_public_key),
apu: None,
apv: None,
};
let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?;
let encryption_algorithm = match protected_header.enc {
EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM,
};
let sealing_key = aead::SealingKey::new(encryption_algorithm, &secret.as_ref())?;
let additional_data = serde_json::to_string(&protected_header)?;
let additional_data =
base64::encode_config(additional_data.as_bytes(), base64::URL_SAFE_NO_PAD);
let additional_data = additional_data.as_bytes();
let aad = aead::Aad::from(additional_data);
let mut iv: Vec<u8> = vec![0; 12];
rand::fill(&mut iv)?;
let nonce = aead::Nonce::try_assume_unique_for_key(encryption_algorithm, &iv)?;
let mut encrypted = aead::seal(&sealing_key, nonce, aad, data)?;
let tag_idx = encrypted.len() - encryption_algorithm.tag_len();
let auth_tag = encrypted.split_off(tag_idx);
let ciphertext = encrypted;
Ok(CompactJwe::new(
Some(protected_header),
None,
Some(iv),
ciphertext,
Some(auth_tag),
)?)
}
pub(crate) fn decrypt_jwe(
jwe: &CompactJwe,
decryption_params: DecryptionParameters,
) -> Result<String> {
let DecryptionParameters::ECDH_ES { local_key_pair } = decryption_params;
let protected_header = jwe
.protected_header()?
.ok_or_else(|| JwCryptoError::IllegalState("protected_header must be present."))?;
if protected_header.alg != Algorithm::ECDH_ES {
return Err(JwCryptoError::IllegalState("alg mismatch."));
}
// Part 1: Reconstruct the secret.
let peer_jwk = protected_header
.epk
.as_ref()
.ok_or_else(|| JwCryptoError::IllegalState("epk not present"))?;
let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters;
let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?;
// Part 2: decrypt the payload
if jwe.encrypted_key()?.is_some() {
return Err(JwCryptoError::IllegalState(
"The Encrypted Key must be empty.",
));
}
let encryption_algorithm = match protected_header.enc {
EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM,
};
let auth_tag = jwe
.auth_tag()?
.ok_or_else(|| JwCryptoError::IllegalState("auth_tag must be present."))?;
if auth_tag.len() != encryption_algorithm.tag_len() {
return Err(JwCryptoError::IllegalState(
"The auth tag must be 16 bytes long.",
));
}
let iv = jwe
.iv()?
.ok_or_else(|| JwCryptoError::IllegalState("iv must be present."))?;
let opening_key = aead::OpeningKey::new(&encryption_algorithm, &secret.as_ref())?;
let ciphertext_and_tag: Vec<u8> = [jwe.ciphertext()?, auth_tag].concat();
let nonce = aead::Nonce::try_assume_unique_for_key(&encryption_algorithm, &iv)?;
let aad = aead::Aad::from(jwe.protected_header_raw().as_bytes());
let plaintext = aead::open(&opening_key, nonce, aad, &ciphertext_and_tag)?;
Ok(String::from_utf8(plaintext.to_vec())?)
}
fn derive_shared_secret(
protected_header: &JweHeader,
local_key_pair: EphemeralKeyPair,
peer_key: &ECKeysParameters,
) -> Result<digest::Digest> {
let (private_key, _) = local_key_pair.split();
let peer_public_key_raw_bytes = public_key_from_ec_params(peer_key)?;
let peer_public_key = UnparsedPublicKey::new(&agreement::ECDH_P256, &peer_public_key_raw_bytes);
// Note: We don't support key-wrapping, but if we did `algorithm_id` would be `alg` instead.
let algorithm_id = protected_header.enc.algorithm_id();
let ikm = private_key.agree(&peer_public_key)?;
let apu = protected_header.apu.as_deref().unwrap_or_default();
let apv = protected_header.apv.as_deref().unwrap_or_default();
get_secret_from_ikm(ikm, &apu, &apv, &algorithm_id)
}
fn public_key_from_ec_params(jwk: &ECKeysParameters) -> Result<Vec<u8>> {
let x = base64::decode_config(&jwk.x, base64::URL_SAFE_NO_PAD)?;
let y = base64::decode_config(&jwk.y, base64::URL_SAFE_NO_PAD)?;
if jwk.crv != "P-256" {
return Err(JwCryptoError::PartialImplementation(
"Only P-256 curves are supported.",
));
}
if x.len() != (256 / 8) {
return Err(JwCryptoError::IllegalState("X must be 32 bytes long."));
}
if y.len() != (256 / 8) {
return Err(JwCryptoError::IllegalState("Y must be 32 bytes long."));
}
let mut peer_pub_key: Vec<u8> = vec![0x04];
peer_pub_key.extend_from_slice(&x);
peer_pub_key.extend_from_slice(&y);
Ok(peer_pub_key)
}
fn get_secret_from_ikm(
ikm: InputKeyMaterial,
apu: &str,
apv: &str,
alg: &str,
) -> Result<digest::Digest> {
let secret = ikm.derive(|z| {
let mut buf: Vec<u8> = vec![];
// ConcatKDF (1 iteration since keyLen <= hashLen).
// See rfc7518 section 4.6 for reference.
buf.extend_from_slice(&1u32.to_be_bytes());
buf.extend_from_slice(&z);
// otherinfo
buf.extend_from_slice(&(alg.len() as u32).to_be_bytes());
buf.extend_from_slice(alg.as_bytes());
buf.extend_from_slice(&(apu.len() as u32).to_be_bytes());
buf.extend_from_slice(apu.as_bytes());
buf.extend_from_slice(&(apv.len() as u32).to_be_bytes());
buf.extend_from_slice(apv.as_bytes());
buf.extend_from_slice(&256u32.to_be_bytes());
digest::digest(&digest::SHA256, &buf)
})?;
Ok(secret)
}
pub fn extract_pub_key_jwk(key_pair: &EphemeralKeyPair) -> Result<Jwk> {
let pub_key_bytes = key_pair.public_key().to_bytes()?;
// Uncompressed form (see SECG SEC1 section 2.3.3).
// First byte is 4, then 32 bytes for x, and 32 bytes for y.
assert_eq!(pub_key_bytes.len(), 1 + 32 + 32);
assert_eq!(pub_key_bytes[0], 0x04);
let x = Vec::from(&pub_key_bytes[1..33]);
let x = base64::encode_config(&x, base64::URL_SAFE_NO_PAD);
let y = Vec::from(&pub_key_bytes[33..]);
let y = base64::encode_config(&y, base64::URL_SAFE_NO_PAD);
Ok(Jwk {
kid: None,
key_parameters: JwkKeyParameters::EC(ECKeysParameters {
crv: "P-256".to_owned(),
x,
y,
}),
})
}

25
third_party/rust/jwcrypto/src/error.rs vendored Normal file
View File

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use thiserror::Error;
pub(crate) type Result<T> = std::result::Result<T, JwCryptoError>;
#[derive(Error, Debug)]
pub enum JwCryptoError {
#[error("Deserialization error")]
DeserializationError,
#[error("Illegal state error: {0}")]
IllegalState(&'static str),
#[error("Partial implementation error: {0}")]
PartialImplementation(&'static str),
#[error("Base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("Crypto error: {0}")]
CryptoError(#[from] rc_crypto::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("UTF8 decode error: {0}")]
UTF8DecodeError(#[from] std::string::FromUtf8Error),
}

243
third_party/rust/jwcrypto/src/lib.rs vendored Normal file
View File

@ -0,0 +1,243 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! Theorically, everything done in this crate could and should be done in a JWT library.
//! However, none of the existing rust JWT libraries can handle ECDH-ES encryption, and API choices
//! made by their authors make it difficult to add this feature.
//! In the past, we chose cjose to do that job, but it added three C dependencies to build and link
//! against: jansson, openssl and cjose itself.
pub use error::JwCryptoError;
use error::Result;
use rc_crypto::agreement::EphemeralKeyPair;
use serde_derive::{Deserialize, Serialize};
use std::str::FromStr;
pub mod ec;
mod error;
pub enum EncryptionParameters<'a> {
// ECDH-ES in Direct Key Agreement mode.
#[allow(non_camel_case_types)]
ECDH_ES {
enc: EncryptionAlgorithm,
peer_jwk: &'a Jwk,
},
}
pub enum DecryptionParameters {
// ECDH-ES in Direct Key Agreement mode.
#[allow(non_camel_case_types)]
ECDH_ES { local_key_pair: EphemeralKeyPair },
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum Algorithm {
#[serde(rename = "ECDH-ES")]
#[allow(non_camel_case_types)]
ECDH_ES,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum EncryptionAlgorithm {
A256GCM,
}
impl EncryptionAlgorithm {
fn algorithm_id(&self) -> &'static str {
match self {
Self::A256GCM => "A256GCM",
}
}
}
#[derive(Serialize, Deserialize, Debug)]
struct JweHeader {
alg: Algorithm,
enc: EncryptionAlgorithm,
#[serde(skip_serializing_if = "Option::is_none")]
kid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
epk: Option<Jwk>,
#[serde(skip_serializing_if = "Option::is_none")]
apu: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
apv: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Jwk {
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
#[serde(flatten)]
pub key_parameters: JwkKeyParameters,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "kty")]
pub enum JwkKeyParameters {
EC(ec::ECKeysParameters),
}
#[derive(Debug)]
pub struct CompactJwe {
jwe_segments: Vec<String>,
}
impl CompactJwe {
// A builder pattern would be nicer, but this will do for now.
fn new(
protected_header: Option<JweHeader>,
encrypted_key: Option<Vec<u8>>,
iv: Option<Vec<u8>>,
ciphertext: Vec<u8>,
auth_tag: Option<Vec<u8>>,
) -> Result<Self> {
let protected_header = protected_header
.as_ref()
.map(|h| serde_json::to_string(&h))
.transpose()?
.map(|h| base64::encode_config(&h, base64::URL_SAFE_NO_PAD))
.unwrap_or_default();
let encrypted_key = encrypted_key
.as_ref()
.map(|k| base64::encode_config(&k, base64::URL_SAFE_NO_PAD))
.unwrap_or_default();
let iv = iv
.as_ref()
.map(|iv| base64::encode_config(&iv, base64::URL_SAFE_NO_PAD))
.unwrap_or_default();
let ciphertext = base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD);
let auth_tag = auth_tag
.as_ref()
.map(|t| base64::encode_config(&t, base64::URL_SAFE_NO_PAD))
.unwrap_or_default();
let jwe_segments = vec![protected_header, encrypted_key, iv, ciphertext, auth_tag];
Ok(Self { jwe_segments })
}
fn protected_header(&self) -> Result<Option<JweHeader>> {
Ok(self
.try_deserialize_base64_segment(0)?
.map(|s| serde_json::from_slice(&s))
.transpose()?)
}
fn protected_header_raw(&self) -> &str {
&self.jwe_segments[0]
}
fn encrypted_key(&self) -> Result<Option<Vec<u8>>> {
self.try_deserialize_base64_segment(1)
}
fn iv(&self) -> Result<Option<Vec<u8>>> {
self.try_deserialize_base64_segment(2)
}
fn ciphertext(&self) -> Result<Vec<u8>> {
Ok(self
.try_deserialize_base64_segment(3)?
.ok_or_else(|| JwCryptoError::IllegalState("Ciphertext is empty"))?)
}
fn auth_tag(&self) -> Result<Option<Vec<u8>>> {
self.try_deserialize_base64_segment(4)
}
fn try_deserialize_base64_segment(&self, index: usize) -> Result<Option<Vec<u8>>> {
Ok(match self.jwe_segments[index].is_empty() {
true => None,
false => Some(base64::decode_config(
&self.jwe_segments[index],
base64::URL_SAFE_NO_PAD,
)?),
})
}
}
impl FromStr for CompactJwe {
type Err = JwCryptoError;
fn from_str(str: &str) -> Result<Self> {
let jwe_segments: Vec<String> = str.split('.').map(|s| s.to_owned()).collect();
if jwe_segments.len() != 5 {
return Err(JwCryptoError::DeserializationError);
}
Ok(Self { jwe_segments })
}
}
impl ToString for CompactJwe {
fn to_string(&self) -> String {
assert!(self.jwe_segments.len() == 5);
self.jwe_segments.join(".")
}
}
/// Encrypt and serialize data in the JWE compact form.
pub fn encrypt_to_jwe(data: &[u8], encryption_params: EncryptionParameters) -> Result<String> {
let jwe = match encryption_params {
EncryptionParameters::ECDH_ES { .. } => ec::encrypt_to_jwe(data, encryption_params)?,
};
Ok(jwe.to_string())
}
/// Deserialize and decrypt data in the JWE compact form.
pub fn decrypt_jwe(jwe: &str, decryption_params: DecryptionParameters) -> Result<String> {
let jwe = jwe.parse()?;
Ok(match decryption_params {
DecryptionParameters::ECDH_ES { .. } => ec::decrypt_jwe(&jwe, decryption_params)?,
})
}
#[test]
fn test_encrypt_decrypt_jwe_ecdh_es() {
use rc_crypto::agreement;
let key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256).unwrap();
let jwk = ec::extract_pub_key_jwk(&key_pair).unwrap();
let data = b"The big brown fox jumped over... What?";
let encrypted = encrypt_to_jwe(
data,
EncryptionParameters::ECDH_ES {
enc: EncryptionAlgorithm::A256GCM,
peer_jwk: &jwk,
},
)
.unwrap();
let decrypted = decrypt_jwe(
&encrypted,
DecryptionParameters::ECDH_ES {
local_key_pair: key_pair,
},
)
.unwrap();
assert_eq!(decrypted, std::str::from_utf8(data).unwrap());
}
#[test]
fn test_compact_jwe_roundtrip() {
let mut iv = [0u8; 16];
rc_crypto::rand::fill(&mut iv).unwrap();
let mut ciphertext = [0u8; 243];
rc_crypto::rand::fill(&mut ciphertext).unwrap();
let mut auth_tag = [0u8; 16];
rc_crypto::rand::fill(&mut auth_tag).unwrap();
let jwe = CompactJwe::new(
Some(JweHeader {
alg: Algorithm::ECDH_ES,
enc: EncryptionAlgorithm::A256GCM,
kid: None,
epk: None,
apu: None,
apv: None,
}),
None,
Some(iv.to_vec()),
ciphertext.to_vec(),
Some(auth_tag.to_vec()),
)
.unwrap();
let compacted = jwe.to_string();
let jwe2: CompactJwe = compacted.parse().unwrap();
assert_eq!(jwe.jwe_segments, jwe2.jwe_segments);
}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"079319f9f7b8f3faf7f423f029f263c42f1c4836a9a9ef78af9733f5e47929bd","README.md":"14dd59e435d179c21c3b4b880bbe3cc6e5999b9f9ac9431f3f9aa3f43902e3fa","src/aes.rs":"820a74d1c1b9b5c818f5e4c4b39afb4346e56b8512a0f280c0bd92b763f50486","src/ec.rs":"3dfb1b2f630e855a37be7b2c03121d069d0b1f0f65e06bcd46493e2a0206be99","src/ecdh.rs":"6a970e6a30dfba4c5f4d113a5b5f3a814ee650a54eba903f8a50b47e180a1ceb","src/error.rs":"de521060e8ec9ad2c125815eec45ef690ad479ab9d41dab5a26294ee6acd9980","src/lib.rs":"7e9e1ebfaf13af124a5226a46e01e70743f3419eb7acc38ffaf202605bb33b89","src/pk11/context.rs":"ab3cdc8949fc1974523f0c6bf376ab933646df499d568a908076ad80b11c7c56","src/pk11/mod.rs":"d78368654f9a8bc12f1403c4a096b63cf9834820ea6ed48418b9afaa0fc2299e","src/pk11/slot.rs":"9f0aa039a55e7b26dc2dd5d2d3451497af71d147513f59e9c89b1166e89b2dda","src/pk11/sym_key.rs":"6dd1bae6e4c97665d0535fd0165736a2174edcb316f068ac3a8c73e5d4c20509","src/pk11/types.rs":"e42789b44e6c783a24d09c4ca955d70a305b20a35320c9c14c54c796e165b93e","src/secport.rs":"b4fbb007963a20cfd3170f37b35aa2816a0d7bf78bae9dbc64c83f5b8f15d2cb","src/util.rs":"236c46206bb6cd130c07f9da4fd603e23166c550a1ba675f4d752b056d13c27f"},"package":null}
{"files":{"Cargo.toml":"17ff6446ce5ef3fb08620171d719241c6315571aa839d4b039f34a2ec9fa6fc4","README.md":"14dd59e435d179c21c3b4b880bbe3cc6e5999b9f9ac9431f3f9aa3f43902e3fa","src/aes.rs":"820a74d1c1b9b5c818f5e4c4b39afb4346e56b8512a0f280c0bd92b763f50486","src/ec.rs":"e5e95504b68f22d949df4c533e35246f0088bc87976fd7d829dcc15f57a84741","src/ecdh.rs":"6a970e6a30dfba4c5f4d113a5b5f3a814ee650a54eba903f8a50b47e180a1ceb","src/error.rs":"da4a39cef14403d3b34f2f4bbf1bb93e07dff0e4fa7e8e3c931604859c922ee4","src/lib.rs":"34950c67f33e6f10e0488fd1d8a4e9ba52b19a48d00b5f0e00b067d33dc60c0d","src/pbkdf2.rs":"d797520182e45fe8d0d076d76c80bcc6fbfaa767dc9ae3670ca9b5938c0bec6c","src/pk11/context.rs":"895dcf08ed59f47c3ae867cf5d8cc79a04df8b61ac702484fc85acf595f71980","src/pk11/mod.rs":"d78368654f9a8bc12f1403c4a096b63cf9834820ea6ed48418b9afaa0fc2299e","src/pk11/slot.rs":"9f0aa039a55e7b26dc2dd5d2d3451497af71d147513f59e9c89b1166e89b2dda","src/pk11/sym_key.rs":"6dd1bae6e4c97665d0535fd0165736a2174edcb316f068ac3a8c73e5d4c20509","src/pk11/types.rs":"60e5899ba89d13d055d529b3e6e355b8d02f0f037b8d0c076671617088833d0c","src/secport.rs":"cd85d4d22f995ed2c3162ec62af093c4b2b1deeb7bac42002d47d7d69e54cb1c","src/util.rs":"e9843ebb2bae1c343da0e5a0840aabfcdd743b83bb836a8b751b43afa6f43cd9"},"package":null}

View File

@ -10,12 +10,12 @@ crate-type = ["lib"]
[dependencies]
base64 = "0.12"
thiserror = "1.0"
error-support = { path = "../../error" }
failure = "0.1"
failure_derive = "0.1"
nss_sys = { path = "nss_sys" }
serde = "1"
serde_derive = "1"
[features]
default = []
gecko = ["nss_sys/gecko"]

View File

@ -5,10 +5,12 @@
use crate::{
error::*,
pk11::{
self,
context::HashAlgorithm,
slot,
types::{Pkcs11Object, PrivateKey as PK11PrivateKey, PublicKey as PK11PublicKey},
},
util::{ensure_nss_initialized, sec_item_as_slice, ScopedPtr},
util::{ensure_nss_initialized, map_nss_secstatus, sec_item_as_slice, ScopedPtr},
};
use serde_derive::{Deserialize, Serialize};
use std::{
@ -23,8 +25,20 @@ use std::{
#[repr(u8)]
pub enum Curve {
P256,
P384,
}
impl Curve {
pub fn get_field_len(&self) -> u32 {
match &self {
Curve::P256 => 32,
Curve::P384 => 48,
}
}
}
const CRV_P256: &str = "P-256";
const CRV_P384: &str = "P-384";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct EcKey {
@ -39,6 +53,7 @@ impl EcKey {
pub fn new(curve: Curve, private_key: &[u8], public_key: &[u8]) -> Self {
let curve = match curve {
Curve::P256 => CRV_P256,
Curve::P384 => CRV_P384,
};
Self {
curve: curve.to_owned(),
@ -55,6 +70,8 @@ impl EcKey {
pub fn curve(&self) -> Curve {
if self.curve == CRV_P256 {
return Curve::P256;
} else if self.curve == CRV_P384 {
return Curve::P384;
}
unimplemented!("It is impossible to create a curve object with a different CRV.")
}
@ -94,9 +111,7 @@ pub fn generate_keypair(curve: Curve) -> Result<(PrivateKey, PublicKey)> {
// 2. Generate the key pair
// The following code is adapted from:
// https://searchfox.org/mozilla-central/rev/f46e2bf881d522a440b30cbf5cf8d76fc212eaf4/dom/crypto/WebCryptoTask.cpp#2389
let mech = match curve {
Curve::P256 => nss_sys::CKM_EC_KEY_PAIR_GEN,
};
let mech = nss_sys::CKM_EC_KEY_PAIR_GEN;
let slot = slot::get_internal_slot()?;
let mut pub_key: *mut nss_sys::SECKEYPublicKey = ptr::null_mut();
let prv_key = PrivateKey::from(curve, unsafe {
@ -132,9 +147,7 @@ impl PrivateKey {
let mut pub_key = self.wrapped.convert_to_public_key()?;
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1562046.
let field_len = match self.curve {
Curve::P256 => 32,
};
let field_len = self.curve.get_field_len();
let expected_len = 2 * field_len + 1;
let mut pub_value = unsafe { (*pub_key.as_ptr()).u.ec.publicValue };
if pub_value.len == expected_len - 2 {
@ -293,6 +306,39 @@ impl PublicKey {
self.curve
}
/// ECDSA verify operation
pub fn verify(
&self,
message: &[u8],
signature: &[u8],
hash_algorithm: HashAlgorithm,
) -> Result<()> {
// The following code is adapted from:
// https://searchfox.org/mozilla-central/rev/b2716c233e9b4398fc5923cbe150e7f83c7c6c5b/dom/crypto/WebCryptoTask.cpp#1144
let signature = nss_sys::SECItem {
len: u32::try_from(signature.len())?,
data: signature.as_ptr() as *mut u8,
type_: 0,
};
let hash = pk11::context::hash_buf(&hash_algorithm, message)?;
let hash = nss_sys::SECItem {
len: u32::try_from(hash.len())?,
data: hash.as_ptr() as *mut u8,
type_: 0,
};
map_nss_secstatus(|| unsafe {
nss_sys::PK11_VerifyWithMechanism(
self.as_mut_ptr(),
nss_sys::PK11_MapSignKeyType((*self.wrapped.as_ptr()).keyType),
ptr::null(),
&signature,
&hash,
ptr::null_mut(),
)
})?;
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
// Some public keys we create do not have an associated PCKS#11 slot
// therefore we cannot use `read_raw_attribute(CKA_EC_POINT)`
@ -340,12 +386,10 @@ impl PublicKey {
}
fn check_pub_key_bytes(bytes: &[u8], curve: Curve) -> Result<()> {
let field_len = match curve {
Curve::P256 => 32,
};
let field_len = curve.get_field_len();
// Check length of uncompressed point coordinates. There are 2 field elements
// and a leading "point form" octet (which must be EC_POINT_FORM_UNCOMPRESSED).
if bytes.len() != (2 * field_len + 1) {
if bytes.len() != usize::try_from(2 * field_len + 1)? {
return Err(ErrorKind::InternalError.into());
}
// No support for compressed points.
@ -359,7 +403,8 @@ fn create_ec_params_for_curve(curve: Curve) -> Result<Vec<u8>> {
// The following code is adapted from:
// https://searchfox.org/mozilla-central/rev/ec489aa170b6486891cf3625717d6fa12bcd11c1/dom/crypto/WebCryptoCommon.h#299
let curve_oid_tag = match curve {
Curve::P256 => nss_sys::SECOidTag::SEC_OID_ANSIX962_EC_PRIME256V1,
Curve::P256 => nss_sys::SECOidTag::SEC_OID_SECG_EC_SECP256R1,
Curve::P384 => nss_sys::SECOidTag::SEC_OID_SECG_EC_SECP384R1,
};
// Retrieve curve data by OID tag.
let oid_data = unsafe { nss_sys::SECOID_FindOIDByTag(curve_oid_tag as u32) };

View File

@ -2,20 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use failure::Fail;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
#[fail(display = "NSS could not be initialized")]
#[error("NSS could not be initialized")]
NSSInitFailure,
#[fail(display = "NSS error: {} {}", _0, _1)]
#[error("NSS error: {0} {1}")]
NSSError(i32, String),
#[fail(display = "Internal crypto error")]
#[error("Internal crypto error")]
InternalError,
#[fail(display = "Conversion error: {}", _0)]
ConversionError(#[fail(cause)] std::num::TryFromIntError),
#[fail(display = "Base64 decode error: {}", _0)]
Base64Decode(#[fail(cause)] base64::DecodeError),
#[error("Conversion error: {0}")]
ConversionError(#[from] std::num::TryFromIntError),
#[error("Base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
}
error_support::define_error! {

View File

@ -10,6 +10,7 @@ pub mod aes;
pub mod ec;
pub mod ecdh;
mod error;
pub mod pbkdf2;
pub mod pk11;
pub mod secport;
pub use crate::error::{Error, ErrorKind, Result};

78
third_party/rust/nss/src/pbkdf2.rs vendored Normal file
View File

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::util::{ensure_nss_initialized, map_nss_secstatus, sec_item_as_slice, ScopedPtr};
use crate::{
error::*,
pk11::{
slot::get_internal_slot,
types::{AlgorithmID, SymKey},
},
};
// Expose for consumers to choose the hashing algorithm
// Currently only SHA256 supported
pub use crate::pk11::context::HashAlgorithm;
use nss_sys::SECOidTag;
use std::convert::TryFrom;
// ***** BASED ON THE FOLLOWING IMPLEMENTATION *****
// https://searchfox.org/mozilla-central/rev/8ccea36c4fb09412609fb738c722830d7098602b/dom/crypto/WebCryptoTask.cpp#2567
pub fn pbkdf2_key_derive(
password: &[u8],
salt: &[u8],
iterations: u32,
hash_algorithm: HashAlgorithm,
out: &mut [u8],
) -> Result<()> {
ensure_nss_initialized();
let oid_tag = match hash_algorithm {
HashAlgorithm::SHA256 => SECOidTag::SEC_OID_HMAC_SHA256 as u32,
HashAlgorithm::SHA384 => SECOidTag::SEC_OID_HMAC_SHA384 as u32,
};
let mut sec_salt = nss_sys::SECItem {
len: u32::try_from(salt.len())?,
data: salt.as_ptr() as *mut u8,
type_: 0,
};
let alg_id = unsafe {
AlgorithmID::from_ptr(nss_sys::PK11_CreatePBEV2AlgorithmID(
SECOidTag::SEC_OID_PKCS5_PBKDF2 as u32,
SECOidTag::SEC_OID_HMAC_SHA1 as u32,
oid_tag,
i32::try_from(out.len())?,
i32::try_from(iterations)?,
&mut sec_salt as *mut nss_sys::SECItem,
))?
};
let slot = get_internal_slot()?;
let mut sec_pw = nss_sys::SECItem {
len: u32::try_from(password.len())?,
data: password.as_ptr() as *mut u8,
type_: 0,
};
let sym_key = unsafe {
SymKey::from_ptr(nss_sys::PK11_PBEKeyGen(
slot.as_mut_ptr(),
alg_id.as_mut_ptr(),
&mut sec_pw as *mut nss_sys::SECItem,
nss_sys::PR_FALSE,
std::ptr::null_mut(),
))?
};
map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?;
// This doesn't leak, because the SECItem* returned by PK11_GetKeyData
// just refers to a buffer managed by `sym_key` which we copy into `buf`
let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) };
let buf = unsafe { sec_item_as_slice(&mut key_data)? };
// Stop panic in swap_with_slice by returning an error if the sizes mismatch
if buf.len() != out.len() {
return Err(ErrorKind::InternalError.into());
}
out.swap_with_slice(buf);
Ok(())
}

View File

@ -12,28 +12,32 @@ use crate::{
};
use std::{convert::TryFrom, ptr};
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
pub enum HashAlgorithm {
SHA256,
SHA384,
}
impl HashAlgorithm {
fn result_len(&self) -> u32 {
match self {
HashAlgorithm::SHA256 => nss_sys::SHA256_LENGTH,
HashAlgorithm::SHA384 => nss_sys::SHA384_LENGTH,
}
}
fn as_hmac_mechanism(&self) -> u32 {
match self {
HashAlgorithm::SHA256 => nss_sys::CKM_SHA256_HMAC,
HashAlgorithm::SHA384 => nss_sys::CKM_SHA384_HMAC,
}
}
pub(crate) fn as_hkdf_mechanism(&self) -> u32 {
match self {
HashAlgorithm::SHA256 => nss_sys::CKM_NSS_HKDF_SHA256,
HashAlgorithm::SHA384 => nss_sys::CKM_NSS_HKDF_SHA384,
}
}
}
@ -42,6 +46,7 @@ impl From<&HashAlgorithm> for nss_sys::SECOidTag {
fn from(alg: &HashAlgorithm) -> Self {
match alg {
HashAlgorithm::SHA256 => nss_sys::SECOidTag::SEC_OID_SHA256,
HashAlgorithm::SHA384 => nss_sys::SECOidTag::SEC_OID_SHA384,
}
}
}

View File

@ -33,6 +33,17 @@ scoped_ptr!(
scoped_ptr!(Context, nss_sys::PK11Context, pk11_destroy_context_true);
scoped_ptr!(Slot, nss_sys::PK11SlotInfo, nss_sys::PK11_FreeSlot);
scoped_ptr!(
AlgorithmID,
nss_sys::SECAlgorithmID,
secoid_destroy_algorithm_id_true
);
#[inline]
unsafe fn secoid_destroy_algorithm_id_true(alg_id: *mut nss_sys::SECAlgorithmID) {
nss_sys::SECOID_DestroyAlgorithmID(alg_id, nss_sys::PR_TRUE);
}
#[inline]
unsafe fn pk11_destroy_context_true(context: *mut nss_sys::PK11Context) {
nss_sys::PK11_DestroyContext(context, nss_sys::PR_TRUE);

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::util::ensure_nss_initialized;
use std::{convert::TryInto, os::raw::c_void};
use std::os::raw::c_void;
pub fn secure_memcmp(a: &[u8], b: &[u8]) -> bool {
ensure_nss_initialized();
@ -16,7 +16,7 @@ pub fn secure_memcmp(a: &[u8], b: &[u8]) -> bool {
nss_sys::NSS_SecureMemcmp(
a.as_ptr() as *const c_void,
b.as_ptr() as *const c_void,
a.len().try_into().unwrap(),
a.len(),
)
};
result == 0

View File

@ -96,7 +96,7 @@ macro_rules! scoped_ptr {
#[allow(dead_code)]
unsafe fn from_ptr(ptr: *mut $target) -> crate::error::Result<$scoped> {
if !ptr.is_null() {
Ok($scoped { ptr: ptr })
Ok($scoped { ptr })
} else {
Err(crate::error::ErrorKind::InternalError.into())
}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"4f1d37d926e853eb9f3d8074b45c00a317e2b4aafbc339a471430d28526716e9","src/lib.rs":"d2f95eaafabb8d1eb7decbd10bcf881da7af7be4c4aadb8f7d68cf0fd806dcb1"},"package":null}
{"files":{"Cargo.toml":"4f1d37d926e853eb9f3d8074b45c00a317e2b4aafbc339a471430d28526716e9","src/lib.rs":"a9077862fc7c45044178fa2675a04d0b31a27574d93a328e03df3108342dd6e4"},"package":null}

View File

@ -51,6 +51,9 @@ pub fn link_nss() -> Result<(), NoNssDir> {
fn get_nss() -> Result<(PathBuf, PathBuf), NoNssDir> {
let nss_dir = env("NSS_DIR").ok_or(NoNssDir)?;
let nss_dir = Path::new(&nss_dir);
if !nss_dir.exists() {
panic!("It looks like NSS is not built. Please run `libs/verify-[platform]-environment.sh` first!");
}
let lib_dir = nss_dir.join("lib");
let include_dir = nss_dir.join("include");
Ok((lib_dir, include_dir))

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"cf266ecf7564b0134e863832a4919f839ca5dd7078ac546690165c478fc54132","README.md":"ba37bdd9c7c8f0a49448b814451816967b9e2068328e692ce06775b3e4ff9c7f","build.rs":"b541496d108a9e85545b9ee28c84790aa5b361805f691a082403233588423fd0","src/bindings/blapit.rs":"be1b3a97ab6182c0dddbddc4399622992bcab5778f0b6deb301a6e726383651f","src/bindings/keyhi.rs":"cdf9c3735343a718f86cfe5f822530dc7c7e4fc2c36e2a11797d9054dd0bfd05","src/bindings/keythi.rs":"ea9a1a8c33c3f2b8b78bd58d8d627d9f8d8c22ee4e1cd26c78701106bf0db69f","src/bindings/mod.rs":"0ace07c6f0d9c2faafe879c23be7d1f6c0ffdf6a42a9e715319cd5fbb17d84c5","src/bindings/nss.rs":"13533f85d2bdfe778a7612d49684d9d375f6063ea4b6c2f56b7d2f705a08a85f","src/bindings/pk11pub.rs":"b685657681055207b6e229451b4b3d7bbff941edbc2e21d73b2c2c7d83eec7a6","src/bindings/pkcs11n.rs":"4a3f9d275c30d7be8d2899e65833c2e67b2c1c86e75b9aaa7f210b223c242729","src/bindings/pkcs11t.rs":"c84d0bf8ac715959580205040468a94a08aa6d4ee19a7dcc7ef378bd2557bca8","src/bindings/plarena.rs":"8de09e3c378df457988729ca4d58e1ef1f2883dfa68e62acb79a55fb19a9d6f5","src/bindings/prerror.rs":"b7bda8a6511c43f59351a17f4311ceb272231a55473895b999a34e3a3ff76722","src/bindings/prtypes.rs":"f4ead8756ff9659cc49586923f0480862794b01dd67dda0e82a20b7796a87cdc","src/bindings/secasn1t.rs":"5a79f0a4057fb934786ef9407c7b134c7bc2f3560f9af0d58dd27ede62c66391","src/bindings/seccomon.rs":"556b45de49b496983ed4d4ef57650d6acdbba68e557711a684f89c5dbdc30c83","src/bindings/secitem.rs":"7a1593f87dcbb4d9ef462fda9932486d169bea9f12b4ed83e3a7102d0b33127e","src/bindings/secmodt.rs":"f1c002df25b598e6fbed5285c98c0d8cfe4188254ca31f829cb993d321a4f6d0","src/bindings/secoid.rs":"0748c4a3078c5e622403ed36ec4de3c13fc994643ff57503576a4987029eca55","src/bindings/secoidt.rs":"adf9c286829accf70520b2689f411819ed7cf9fd709b2023518b37391e848a16","src/bindings/secport.rs":"66e66ea2ccae1bce68b69e06c6856310c2fb726bf76df4e9d873739a8fd3f41e","src/lib.rs":"a48f9077706a47f8dd4ddf01a6870e8a89002f3cc23c0fbe012c2aa30cc99cce"},"package":null}
{"files":{"Cargo.toml":"cc96e2500ca486bae9fc333900297184c3608a89bdd78fe7790095aa34564c9f","README.md":"ba37bdd9c7c8f0a49448b814451816967b9e2068328e692ce06775b3e4ff9c7f","build.rs":"b541496d108a9e85545b9ee28c84790aa5b361805f691a082403233588423fd0","src/bindings/blapit.rs":"0be43d1c57ac35f490012d0916b8cbcee3e3d911d8eccfd139f328b9623a10ee","src/bindings/keyhi.rs":"cdf9c3735343a718f86cfe5f822530dc7c7e4fc2c36e2a11797d9054dd0bfd05","src/bindings/keythi.rs":"ea9a1a8c33c3f2b8b78bd58d8d627d9f8d8c22ee4e1cd26c78701106bf0db69f","src/bindings/mod.rs":"0ace07c6f0d9c2faafe879c23be7d1f6c0ffdf6a42a9e715319cd5fbb17d84c5","src/bindings/nss.rs":"13533f85d2bdfe778a7612d49684d9d375f6063ea4b6c2f56b7d2f705a08a85f","src/bindings/pk11pub.rs":"d2266bb3586e2bf66c93761317b29955bf55e923e16a7a98f13f64d89a692512","src/bindings/pkcs11n.rs":"bc1ba0d903891d5331aeb6b1921fde7c2cd31cbbe75338e145b5aaff5c94c147","src/bindings/pkcs11t.rs":"0114bbabe8bed71585975be5e1b232f28c1b85461001d2fc1b49e8abf93f8b8a","src/bindings/plarena.rs":"8de09e3c378df457988729ca4d58e1ef1f2883dfa68e62acb79a55fb19a9d6f5","src/bindings/prerror.rs":"b7bda8a6511c43f59351a17f4311ceb272231a55473895b999a34e3a3ff76722","src/bindings/prtypes.rs":"5afd17e4d24880609320f8cc5a9c06f57ac766524ca5f6cbc5edc65195974c6e","src/bindings/secasn1t.rs":"5a79f0a4057fb934786ef9407c7b134c7bc2f3560f9af0d58dd27ede62c66391","src/bindings/seccomon.rs":"556b45de49b496983ed4d4ef57650d6acdbba68e557711a684f89c5dbdc30c83","src/bindings/secitem.rs":"7a1593f87dcbb4d9ef462fda9932486d169bea9f12b4ed83e3a7102d0b33127e","src/bindings/secmodt.rs":"f1c002df25b598e6fbed5285c98c0d8cfe4188254ca31f829cb993d321a4f6d0","src/bindings/secoid.rs":"1a1e3d8106c26d081daa56b22f6214b6b2456e14f6d5b34db77bb428e7dc4525","src/bindings/secoidt.rs":"d3841fa00100d081fd355ef65d8ff10e2341440715c937017d795fc7efd0d31d","src/bindings/secport.rs":"6b9c691f7a80467ad2db76e2168d9dceee781e5edaadd48b76e66852f632db12","src/lib.rs":"3081488f34b747cbe852e6692389db5df3dae65b180558aa7af9bf6ae809faa2"},"package":null}

View File

@ -8,5 +8,12 @@ license = "MPL-2.0"
[lib]
crate-type = ["lib"]
[dependencies]
libsqlite3-sys = { version = "0.20.1", features = ["bundled"] }
[build-dependencies]
nss_build_common = {path = "../nss_build_common"}
[features]
default = []
gecko = []

View File

@ -4,5 +4,6 @@
pub const EC_POINT_FORM_UNCOMPRESSED: u32 = 4;
pub const SHA256_LENGTH: u32 = 32;
pub const SHA384_LENGTH: u32 = 48;
pub const HASH_LENGTH_MAX: u32 = 64;
pub const AES_BLOCK_SIZE: u32 = 16;

View File

@ -76,6 +76,15 @@ extern "C" {
data: *const c_uchar,
dataLen: c_uint,
) -> SECStatus;
pub fn PK11_VerifyWithMechanism(
key: *mut SECKEYPublicKey,
mechanism: CK_MECHANISM_TYPE,
param: *const SECItem,
sig: *const SECItem,
hash: *const SECItem,
wincx: *mut c_void,
) -> SECStatus;
pub fn PK11_MapSignKeyType(keyType: u32 /* KeyType */) -> CK_MECHANISM_TYPE;
pub fn PK11_DestroyContext(context: *mut PK11Context, freeit: PRBool);
pub fn PK11_CreateContextBySymKey(
type_: CK_MECHANISM_TYPE,
@ -110,4 +119,20 @@ extern "C" {
attr: CK_ATTRIBUTE_TYPE,
item: *mut SECItem,
) -> SECStatus;
pub fn PK11_CreatePBEV2AlgorithmID(
pbeAlgTag: u32, /* SECOidTag */
cipherAlgTag: u32, /* SECOidTag */
prfAlgTag: u32, /* SECOidTag */
keyLength: c_int,
iteration: c_int,
salt: *mut SECItem,
) -> *mut SECAlgorithmID;
pub fn PK11_PBEKeyGen(
slot: *mut PK11SlotInfo,
algid: *mut SECAlgorithmID,
pwitem: *mut SECItem,
faulty3DES: PRBool,
wincx: *mut c_void,
) -> *mut PK11SymKey;
}

View File

@ -4,7 +4,12 @@
pub use crate::*;
pub const CKM_NSS_HKDF_SHA256: u32 = 3_461_563_220; // (CKM_NSS + 4)
// https://searchfox.org/nss/rev/4d480919bbf204df5e199b9fdedec8f2a6295778/lib/util/pkcs11n.h#27
pub const NSSCK_VENDOR_NSS: u32 = 0x4E534350;
pub const CKM_NSS: u32 = CKM_VENDOR_DEFINED | NSSCK_VENDOR_NSS;
pub const CKM_NSS_HKDF_SHA256: u32 = CKM_NSS + 4;
pub const CKM_NSS_HKDF_SHA384: u32 = CKM_NSS + 5;
pub type CK_GCM_PARAMS = CK_GCM_PARAMS_V3;
#[repr(C)]

View File

@ -39,7 +39,10 @@ pub const CKA_WRAP: u32 = 262;
pub const CKA_SIGN: u32 = 264;
pub const CKA_EC_PARAMS: u32 = 384;
pub const CKA_EC_POINT: u32 = 385;
// https://searchfox.org/nss/rev/4d480919bbf204df5e199b9fdedec8f2a6295778/lib/util/pkcs11t.h#1244
pub const CKM_VENDOR_DEFINED: u32 = 0x80000000;
pub const CKM_SHA256_HMAC: u32 = 593;
pub const CKM_SHA384_HMAC: u32 = 609;
pub const CKM_SHA512_HMAC: u32 = 625;
pub const CKM_EC_KEY_PAIR_GEN: u32 = 4160;
pub const CKM_ECDH1_DERIVE: u32 = 4176;

View File

@ -2,11 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::os::raw::{c_int, c_uint, c_ulong};
use std::os::raw::{c_int, c_uint};
pub type PRIntn = c_int;
pub type PRBool = PRIntn;
pub type PRUword = c_ulong;
pub type PRUword = usize;
pub type PRInt32 = c_int;
pub type PRUint32 = c_uint;
pub const PR_FALSE: PRBool = 0;

View File

@ -6,4 +6,5 @@ pub use crate::*;
extern "C" {
pub fn SECOID_FindOIDByTag(tagnum: u32 /* SECOidTag */) -> *mut SECOidData;
pub fn SECOID_DestroyAlgorithmID(aid: *mut SECAlgorithmID, freeit: PRBool);
}

View File

@ -5,6 +5,15 @@
pub use crate::*;
use std::os::raw::{c_char, c_ulong};
#[repr(C)]
#[derive(Copy, Clone)]
pub struct SECAlgorithmIDStr {
pub algorithm: SECItem,
pub parameters: SECItem,
}
pub type SECAlgorithmID = SECAlgorithmIDStr;
#[repr(C)]
#[derive(Copy, Clone)]
pub struct SECOidDataStr {
@ -232,7 +241,7 @@ pub enum SECOidTag {
SEC_OID_ANSIX962_EC_PRIME239V1 = 205,
SEC_OID_ANSIX962_EC_PRIME239V2 = 206,
SEC_OID_ANSIX962_EC_PRIME239V3 = 207,
SEC_OID_ANSIX962_EC_PRIME256V1 = 208,
SEC_OID_SECG_EC_SECP256R1 = 208,
SEC_OID_SECG_EC_SECP112R1 = 209,
SEC_OID_SECG_EC_SECP112R2 = 210,
SEC_OID_SECG_EC_SECP128R1 = 211,

View File

@ -3,11 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::*;
use std::os::raw::{c_int, c_ulong, c_void};
use std::os::raw::{c_int, c_void};
pub type size_t = usize;
extern "C" {
pub fn PORT_FreeArena(arena: *mut PLArenaPool, zero: PRBool);
pub fn NSS_SecureMemcmp(a: *const c_void, b: *const c_void, n: size_t) -> c_int;
}
pub type size_t = c_ulong;

View File

@ -8,3 +8,9 @@
mod bindings;
pub use bindings::*;
// So we link against the SQLite lib imported by parent crates
// such as places and logins.
#[allow(unused_extern_crates)]
#[cfg(any(not(feature = "gecko"), __appsvc_ci_hack))]
extern crate libsqlite3_sys;

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"affbe7c78de624bfdf07d0afc97b99377f36e69e77fc3cfc16c377bbdd7e4417","src/lib.rs":"7d2661e7f2be3c29b3e0878bef5aa6abc1930360050ea1191b6487bef0a2182a"},"package":null}

View File

@ -0,0 +1,16 @@
[package]
name = "rand_rccrypto"
version = "0.1.0"
authors = ["Edouard Oger <eoger@fastmail.com>"]
edition = "2018"
license = "MPL-2.0"
[lib]
crate-type = ["lib"]
[dependencies]
rc_crypto = { path = "../rc_crypto" }
# We do not need the rand default features as we provide
# our own Rng implementation backed by rc_crypto.
rand = { version = "0.7", default-features = false, features = ["std"] }
rand_core = "0.5"

View File

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub use rand;
pub use rand_core;
use rand_core::{impls, CryptoRng, Error, RngCore};
pub struct RcCryptoRng;
impl RngCore for RcCryptoRng {
fn next_u32(&mut self) -> u32 {
impls::next_u32_via_fill(self)
}
fn next_u64(&mut self) -> u64 {
impls::next_u64_via_fill(self)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.try_fill_bytes(dest).unwrap()
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
rc_crypto::rand::fill(dest).map_err(Error::new)
}
}
// NSS's `PK11_GenerateRandom` is considered a CSPRNG.
impl CryptoRng for RcCryptoRng {}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"6517b01c6ee4dede6752ac507a17954900a46d0bcc10ef6ce5a3f107924ce5be","README.md":"0c208185ac719f9a2c1cdf62c1e6cdf65ce3d65407f0e99feef4933233843a5b","src/aead.rs":"cf7082c25ee981f5bbe304c5127c908f66753e31f44d9c96a62264c156f9db95","src/aead/aes_cbc.rs":"80461cddfa6e99f982d855af599393136dd11eb9b50fb11ac7c75427d9f24c14","src/aead/aes_gcm.rs":"7aa5651e4246532bb1ffcbdece0fb99e9e08094700ba0048ba239c2543db4376","src/agreement.rs":"6c65f5cdc9636fe425531b4b48b8e0625d0dd480b4f8b78a5504f99b033ccefd","src/constant_time.rs":"2ca0f8274227c88566f015fe0d143ba57d1413d01f9eb2a03535ea8105bf5b70","src/digest.rs":"526ac46c43b410164fb4330e4ee20705b240fc72f47e24d725ef6f0d132bd371","src/ece_crypto.rs":"25df8cbb1bb483e454979aa43d2f6900614b4ce07d92be29adbd824d513601b2","src/error.rs":"517bd8e26295e61a065e9f2ca1e6bed817c9e5fd17d010e4c93cf21e581ba380","src/hawk_crypto.rs":"a64fcebb8228c291e5e5718b1e6519c2e959a257c46cdfa7dc40b8d68968a959","src/hkdf.rs":"d535bd716873fecfe243fbe10a21c6da99eacc59ae5777390f229158c4baee2f","src/hmac.rs":"e67a551e2266f310e05c4a21924479e471bc00217605ce9f4ed3cdcc0034a5a4","src/lib.rs":"1ffe5815521cf4199360cd93a2ecc3b6070296fe4ebe991b28611aafca9604e0","src/rand.rs":"7daa4d3c06b469f50e8c6ae7e2f2f651250440ea4bede5e5a8dfe5a4c5a079cb"},"package":null}
{"files":{"Cargo.toml":"c1a8724425dd53b41e364330c0037f7613777a6c1e53b78155b81d150aa4ea08","README.md":"110e6647522bf94adb22a8659ac93b5f5213b0832d3610acc8f137964962311a","src/aead.rs":"cf7082c25ee981f5bbe304c5127c908f66753e31f44d9c96a62264c156f9db95","src/aead/aes_cbc.rs":"80461cddfa6e99f982d855af599393136dd11eb9b50fb11ac7c75427d9f24c14","src/aead/aes_gcm.rs":"7aa5651e4246532bb1ffcbdece0fb99e9e08094700ba0048ba239c2543db4376","src/agreement.rs":"d39851eabd6edeffddbf626422dc424c62ffea196a90a8221ecdacb3ef4d57ee","src/constant_time.rs":"2ca0f8274227c88566f015fe0d143ba57d1413d01f9eb2a03535ea8105bf5b70","src/digest.rs":"8109c4d59dea6f8fd6f505c3c151528ff9ec5fc2fc153b98bfa55c0ea0892f56","src/ece_crypto.rs":"bca64f9e190bc945ed280de7685b489cc4ba2209e4599069f029681773e9dc4d","src/error.rs":"b8c40ca29b1f40510e0ac84d8addd2bc3daa783c6b0a7eb3d7dd58cf111bc3f0","src/hawk_crypto.rs":"a64fcebb8228c291e5e5718b1e6519c2e959a257c46cdfa7dc40b8d68968a959","src/hkdf.rs":"d535bd716873fecfe243fbe10a21c6da99eacc59ae5777390f229158c4baee2f","src/hmac.rs":"808e613e19e0160957952b47cddf0c6b3103a936ac216584f229ae9dad61e043","src/lib.rs":"0650e733407792560d2292737197fab3bb54563688399053e76694753e7693d4","src/pbkdf2.rs":"d17128fa7d1db826045dfab35be66e3c90d842cb7b990e70e93690518f725a46","src/rand.rs":"7daa4d3c06b469f50e8c6ae7e2f2f651250440ea4bede5e5a8dfe5a4c5a079cb","src/signature.rs":"9e93cbc97cf70cb5923ec716ecea89be57a6d97db4072c1f65ecd5a4d961d0d8"},"package":null}

View File

@ -10,17 +10,15 @@ crate-type = ["lib"]
[dependencies]
base64 = "0.12"
failure = "0.1"
failure_derive = "0.1"
thiserror = "1.0"
error-support = { path = "../error" }
nss = { path = "nss" }
libsqlite3-sys = { version = "0.20.1", features = ["bundled"] }
hawk = { version = "3.1", default-features = false, optional = true }
ece = { version = "1.1", default-features = false, features = ["serializable-keys"], optional = true }
hawk = { version = "3.2", default-features = false, optional = true }
ece = { version = "1.2" , default-features = false, features = ["serializable-keys"], optional = true }
[dev-dependencies]
hex = "0.4"
[features]
default = []
gecko = []
gecko = ["nss/gecko"]

View File

@ -9,6 +9,7 @@ offers the following functionality:
* Cryptographic [digests](./src/digest.rs), [hmac](./src/hmac.rs), and [hkdf](./src/hkdf.rs).
* Authenticated encryption ([AEAD](./src/aead.rs)) routines.
* ECDH [key agreement](./src/agreement.rs).
* ECDSA [signature verification](./src/signature.rs).
* Constant-time [string comparison](./src/constant_time.rs).
* HTTP [Hawk Authentication](./src/hawk_crypto.rs) through the [rust-hawk crate](https://github.com/taskcluster/rust-hawk/).
* HTTP [Encrypted Content-Encoding](./src/ece.rs) through the [ece crate](https://github.com/mozilla/rust-ece).

View File

@ -24,6 +24,8 @@ use core::marker::PhantomData;
pub use ec::{Curve, EcKey};
use nss::{ec, ecdh};
pub type EphemeralKeyPair = KeyPair<Ephemeral>;
/// A key agreement algorithm.
#[derive(PartialEq)]
pub struct Algorithm {
@ -34,6 +36,10 @@ pub static ECDH_P256: Algorithm = Algorithm {
curve_id: ec::Curve::P256,
};
pub static ECDH_P384: Algorithm = Algorithm {
curve_id: ec::Curve::P384,
};
/// How many times the key may be used.
pub trait Lifetime {}
@ -118,6 +124,29 @@ impl PublicKey {
}
}
/// An unparsed public key for key agreement.
pub struct UnparsedPublicKey<'a> {
alg: &'static Algorithm,
bytes: &'a [u8],
}
impl<'a> UnparsedPublicKey<'a> {
pub fn new(algorithm: &'static Algorithm, bytes: &'a [u8]) -> Self {
Self {
alg: algorithm,
bytes,
}
}
pub fn algorithm(&self) -> &'static Algorithm {
self.alg
}
pub fn bytes(&self) -> &'a [u8] {
&self.bytes
}
}
/// A private key for key agreement.
pub struct PrivateKey<U: Lifetime> {
wrapped: ec::PrivateKey,
@ -142,17 +171,8 @@ impl<U: Lifetime> PrivateKey<U> {
/// Ephemeral agreement.
/// This consumes `self`, ensuring that the private key can
/// only be used for a single agreement operation.
pub fn agree(
self,
peer_public_key_alg: &Algorithm,
peer_public_key: &[u8],
) -> Result<InputKeyMaterial> {
agree_(
&self.wrapped,
self.alg,
peer_public_key_alg,
peer_public_key,
)
pub fn agree(self, peer_public_key: &UnparsedPublicKey<'_>) -> Result<InputKeyMaterial> {
agree_(&self.wrapped, self.alg, peer_public_key)
}
}
@ -162,21 +182,16 @@ impl PrivateKey<Static> {
/// be used for a multiple agreement operations.
pub fn agree_static(
&self,
peer_public_key_alg: &Algorithm,
peer_public_key: &[u8],
peer_public_key: &UnparsedPublicKey<'_>,
) -> Result<InputKeyMaterial> {
agree_(
&self.wrapped,
self.alg,
peer_public_key_alg,
peer_public_key,
)
agree_(&self.wrapped, self.alg, peer_public_key)
}
pub fn import(ec_key: &EcKey) -> Result<Self> {
// XXX: we should just let ec::PrivateKey own alg.
let alg = match ec_key.curve() {
Curve::P256 => &ECDH_P256,
Curve::P384 => &ECDH_P384,
};
let private_key = ec::PrivateKey::import(ec_key)?;
Ok(Self {
@ -205,14 +220,13 @@ impl PrivateKey<Static> {
fn agree_(
my_private_key: &ec::PrivateKey,
my_alg: &Algorithm,
peer_public_key_alg: &Algorithm,
peer_public_key: &[u8],
peer_public_key: &UnparsedPublicKey<'_>,
) -> Result<InputKeyMaterial> {
let alg = &my_alg;
if peer_public_key_alg != *alg {
if peer_public_key.algorithm() != *alg {
return Err(ErrorKind::InternalError.into());
}
let pub_key = ec::PublicKey::from_bytes(my_private_key.curve(), peer_public_key)?;
let pub_key = ec::PublicKey::from_bytes(my_private_key.curve(), peer_public_key.bytes())?;
let value = ecdh::ecdh_agreement(my_private_key, &pub_key)?;
Ok(InputKeyMaterial { value })
}
@ -277,9 +291,10 @@ mod tests {
#[test]
fn test_static_agreement() {
let pub_key = base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD).unwrap();
let pub_key_raw = base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD).unwrap();
let peer_pub_key = UnparsedPublicKey::new(&ECDH_P256, &pub_key_raw);
let prv_key = load_priv_key_2();
let ikm = prv_key.agree_static(&ECDH_P256, &pub_key).unwrap();
let ikm = prv_key.agree_static(&peer_pub_key).unwrap();
let secret = ikm
.derive(|z| -> Result<Vec<u8>> { Ok(z.to_vec()) })
.unwrap();
@ -293,15 +308,15 @@ mod tests {
KeyPair::<Ephemeral>::generate(&ECDH_P256).unwrap().split();
let (their_prv_key, their_pub_key) =
KeyPair::<Ephemeral>::generate(&ECDH_P256).unwrap().split();
let ikm_1 = our_prv_key
.agree(&ECDH_P256, &their_pub_key.to_bytes().unwrap())
.unwrap();
let their_pub_key_raw = their_pub_key.to_bytes().unwrap();
let peer_public_key_1 = UnparsedPublicKey::new(&ECDH_P256, &their_pub_key_raw);
let ikm_1 = our_prv_key.agree(&peer_public_key_1).unwrap();
let secret_1 = ikm_1
.derive(|z| -> Result<Vec<u8>> { Ok(z.to_vec()) })
.unwrap();
let ikm_2 = their_prv_key
.agree(&ECDH_P256, &our_pub_key.to_bytes().unwrap())
.unwrap();
let our_pub_key_raw = our_pub_key.to_bytes().unwrap();
let peer_public_key_2 = UnparsedPublicKey::new(&ECDH_P256, &our_pub_key_raw);
let ikm_2 = their_prv_key.agree(&peer_public_key_2).unwrap();
let secret_2 = ikm_2
.derive(|z| -> Result<Vec<u8>> { Ok(z.to_vec()) })
.unwrap();
@ -355,33 +370,45 @@ mod tests {
let mut invalid_pub_key =
base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD).unwrap();
invalid_pub_key[0] = invalid_pub_key[0].wrapping_add(1);
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
let mut invalid_pub_key =
base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD).unwrap();
invalid_pub_key[0] = 0x02;
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
let mut invalid_pub_key =
base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD).unwrap();
invalid_pub_key[64] = invalid_pub_key[0].wrapping_add(1);
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
let mut invalid_pub_key = [0u8; 65];
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
invalid_pub_key[0] = 0x04;
let mut invalid_pub_key = base64::decode_config(PUB_KEY_1_B64, base64::URL_SAFE_NO_PAD)
.unwrap()
.to_vec();
invalid_pub_key = invalid_pub_key[0..64].to_vec();
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
// From FxA tests at https://github.com/mozilla/fxa-crypto-relier/blob/04f61dc/test/deriver/DeriverUtils.js#L78
// We trust that NSS will do the right thing here, but it seems worthwhile to confirm for completeness.
let invalid_pub_key_b64 = "BEogZ-rnm44oJkKsOE6Tc7NwFMgmntf7Btm_Rc4atxcqq99Xq1RWNTFpk99pdQOSjUvwELss51PkmAGCXhLfMV0";
let invalid_pub_key =
base64::decode_config(invalid_pub_key_b64, base64::URL_SAFE_NO_PAD).unwrap();
assert!(prv_key.agree_static(&ECDH_P256, &invalid_pub_key).is_err());
assert!(prv_key
.agree_static(&UnparsedPublicKey::new(&ECDH_P256, &invalid_pub_key))
.is_err());
}
}

View File

@ -47,7 +47,7 @@ pub fn digest(algorithm: &Algorithm, data: &[u8]) -> Result<Digest> {
let value = nss::pk11::context::hash_buf(algorithm, data)?;
Ok(Digest {
value,
algorithm: algorithm.clone(),
algorithm: *algorithm,
})
}

View File

@ -4,14 +4,14 @@
use crate::{
aead,
agreement::{self, Curve, EcKey},
agreement::{self, Curve, EcKey, UnparsedPublicKey},
digest, hkdf, hmac, rand,
};
use ece::crypto::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey};
impl From<crate::Error> for ece::Error {
fn from(_: crate::Error) -> Self {
ece::ErrorKind::CryptoError.into()
ece::Error::CryptoError
}
}
@ -39,9 +39,12 @@ impl RcCryptoLocalKeyPair {
}
fn agree(&self, peer: &RcCryptoRemotePublicKey) -> Result<Vec<u8>, ece::Error> {
let peer_public_key_raw_bytes = &peer.as_raw()?;
let peer_public_key =
UnparsedPublicKey::new(&agreement::ECDH_P256, &peer_public_key_raw_bytes);
self.wrapped
.private_key()
.agree_static(&agreement::ECDH_P256, &peer.as_raw()?)?
.agree_static(&peer_public_key)?
.derive(|z| Ok(z.to_vec()))
}
}
@ -380,8 +383,8 @@ mod tests {
"45b74d2b69be9b074de3b35aa87e7c15611d",
)
.unwrap_err();
match err.kind() {
ErrorKind::HeaderTooShort => {}
match err {
Error::HeaderTooShort => {}
_ => panic!("Unexpected error type!"),
};
}
@ -396,8 +399,8 @@ mod tests {
"de5b696b87f1a15cb6adebdd79d6f99e000000120100b6bc1826c37c9f73dd6b4859c2b505181952",
)
.unwrap_err();
match err.kind() {
ErrorKind::InvalidKeyLength => {}
match err {
Error::InvalidKeyLength => {}
_ => panic!("Unexpected error type!"),
};
}
@ -411,8 +414,8 @@ mod tests {
"355a38cd6d9bef15990e2d3308dbd600",
"8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd"
).unwrap_err();
match err.kind() {
ece::ErrorKind::CryptoError => {}
match err {
Error::CryptoError => {}
_ => panic!("Unexpected error type!"),
};
}
@ -426,8 +429,8 @@ mod tests {
"40c241fde4269ee1e6d725592d982718",
"dbe215507d1ad3d2eaeabeae6e874d8f0000001241047bc4343f34a8348cdc4e462ffc7c40aa6a8c61a739c4c41d45125505f70e9fc5f9efa86852dd488dcf8e8ea2cafb75e07abd5ee7c9d5c038bafef079571b0bda294411ce98c76dd031c0e580577a4980a375e45ed30429be0e2ee9da7e6df8696d01b8ec"
).unwrap_err();
match err.kind() {
ErrorKind::DecryptPadding => {}
match err {
Error::DecryptPadding => {}
_ => panic!("Unexpected error type!"),
};
}

View File

@ -2,16 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use failure::Fail;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
#[fail(display = "NSS error: {}", _0)]
NSSError(#[fail(cause)] nss::Error),
#[fail(display = "Internal crypto error")]
#[error("NSS error: {0}")]
NSSError(#[from] nss::Error),
#[error("Internal crypto error")]
InternalError,
#[fail(display = "Conversion error: {}", _0)]
ConversionError(#[fail(cause)] std::num::TryFromIntError),
#[error("Conversion error: {0}")]
ConversionError(#[from] std::num::TryFromIntError),
}
error_support::define_error! {

View File

@ -87,7 +87,7 @@ pub fn sign(key: &SigningKey, data: &[u8]) -> Result<Signature> {
let value = nss::pk11::context::hmac_sign(key.digest_alg, &key.key_value, data)?;
Ok(Signature(digest::Digest {
value,
algorithm: key.digest_alg.clone(),
algorithm: *key.digest_alg,
}))
}

View File

@ -35,7 +35,9 @@ mod error;
mod hawk_crypto;
pub mod hkdf;
pub mod hmac;
pub mod pbkdf2;
pub mod rand;
pub mod signature;
// Expose `hawk` if the hawk feature is on. This avoids consumers needing to
// configure this separately, which is more or less trivial to do incorrectly.
@ -49,12 +51,6 @@ pub use ece;
pub use crate::error::{Error, ErrorKind, Result};
// So we link against the SQLite lib imported by parent crates
// such as places and logins.
#[allow(unused_extern_crates)]
#[cfg(not(feature = "gecko"))]
extern crate libsqlite3_sys;
/// Only required to be called if you intend to use this library in conjunction
/// with the `hawk` or the `ece` crate.
pub fn ensure_initialized() {

196
third_party/rust/rc_crypto/src/pbkdf2.rs vendored Normal file
View File

@ -0,0 +1,196 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::error::*;
use nss::pbkdf2::pbkdf2_key_derive;
pub use nss::pbkdf2::HashAlgorithm;
/// Extend passwords using pbkdf2, based on the following [rfc](https://www.ietf.org/rfc/rfc2898.txt) it runs the NSS implementation
/// # Arguments
///
/// * `passphrase` - The password to stretch
/// * `salt` - A salt to use in the generation process
/// * `iterations` - The number of iterations the hashing algorithm will run on each section of the key
/// * `hash_algorithm` - The hash algorithm to use
/// * `out` - The slice the algorithm will populate
///
/// # Examples
///
/// ```
/// use rc_crypto::pbkdf2;
/// let password = b"password";
/// let salt = b"salt";
/// let mut out = vec![0u8; 32];
/// let iterations = 2; // Real code should have a MUCH higher number of iterations (Think 1000+)
/// pbkdf2::derive(password, salt, iterations, pbkdf2::HashAlgorithm::SHA256, &mut out).unwrap(); // Oh oh should handle the error!
/// assert_eq!(hex::encode(out), "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43");
//
///```
///
/// # Errors
///
/// Could possibly return an error if the HMAC algorithm fails, or if the NSS algorithm returns an error
pub fn derive(
passphrase: &[u8],
salt: &[u8],
iterations: u32,
hash_algorithm: HashAlgorithm,
out: &mut [u8],
) -> Result<()> {
pbkdf2_key_derive(passphrase, salt, iterations, hash_algorithm, out)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_correct_out() {
let expected = "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b";
let mut out = vec![0u8; 32];
let password = b"password";
let salt = b"salt";
derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_longer_key() {
let expected = "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b4dbf3a2f3dad3377264bb7b8e8330d4efc7451418617dabef683735361cdc18c";
let password = b"password";
let salt = b"salt";
let mut out = vec![0u8; 64];
derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_more_iterations() {
let expected = "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43";
let password = b"password";
let salt = b"salt";
let mut out = vec![0u8; 32];
derive(password, salt, 2, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_odd_length() {
let expected = "ad35240ac683febfaf3cd49d845473fbbbaa2437f5f82d5a415ae00ac76c6bfccf";
let password = b"password";
let salt = b"salt";
let mut out = vec![0u8; 33];
derive(password, salt, 3, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_nulls() {
let expected = "e25d526987819f966e324faa4a";
let password = b"passw\x00rd";
let salt = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let mut out = vec![0u8; 13];
derive(password, salt, 5, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_password_null() {
let expected = "62384466264daadc4144018c6bd864648272b34da8980d31521ffcce92ae003b";
let password = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let salt = b"salt";
let mut out = vec![0u8; 32];
derive(password, salt, 2, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_empty_password() {
let expected = "f135c27993baf98773c5cdb40a5706ce6a345cde61b000a67858650cd6a324d7";
let mut out = vec![0u8; 32];
let password = b"";
let salt = b"salt";
derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_empty_salt() {
let expected = "c1232f10f62715fda06ae7c0a2037ca19b33cf103b727ba56d870c11f290a2ab";
let mut out = vec![0u8; 32];
let password = b"password";
let salt = b"";
derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_tiny_out() {
let expected = "12";
let mut out = vec![0u8; 1];
let password = b"password";
let salt = b"salt";
derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).unwrap();
assert_eq!(expected, hex::encode(out));
}
#[test]
fn test_rejects_zero_iterations() {
let mut out = vec![0u8; 32];
let password = b"password";
let salt = b"salt";
assert!(derive(password, salt, 0, HashAlgorithm::SHA256, &mut out).is_err());
}
#[test]
fn test_rejects_empty_out() {
let mut out = vec![0u8; 0];
let password = b"password";
let salt = b"salt";
assert!(derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).is_err());
}
#[test]
fn test_rejects_gaigantic_salt() {
if (std::u32::MAX as usize) < std::usize::MAX {
let salt = vec![0; (std::u32::MAX as usize) + 1];
let mut out = vec![0u8; 1];
let password = b"password";
assert!(derive(password, &salt, 1, HashAlgorithm::SHA256, &mut out).is_err());
}
}
#[test]
fn test_rejects_gaigantic_password() {
if (std::u32::MAX as usize) < std::usize::MAX {
let password = vec![0; (std::u32::MAX as usize) + 1];
let mut out = vec![0u8; 1];
let salt = b"salt";
assert!(derive(&password, salt, 1, HashAlgorithm::SHA256, &mut out).is_err());
}
}
#[test]
fn test_rejects_gaigantic_out() {
if (std::u32::MAX as usize) < std::usize::MAX {
let password = b"password";
let mut out = vec![0; (std::u32::MAX as usize) + 1];
let salt = b"salt";
assert!(derive(password, salt, 1, HashAlgorithm::SHA256, &mut out).is_err());
}
}
#[test]
fn test_rejects_gaigantic_iterations() {
let password = b"password";
let mut out = vec![0; 32];
let salt = b"salt";
assert!(derive(
password,
salt,
std::u32::MAX,
HashAlgorithm::SHA256,
&mut out
)
.is_err());
}
}

View File

@ -0,0 +1,111 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file contains code that was copied from the ring crate which is under
// the ISC license, reproduced below:
// Copyright 2015-2017 Brian Smith.
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use crate::Result;
use nss::{ec::Curve, ec::PublicKey, pbkdf2::HashAlgorithm};
/// A signature verification algorithm.
pub struct VerificationAlgorithm {
curve: Curve,
digest_alg: HashAlgorithm,
}
pub static ECDSA_P256_SHA256: VerificationAlgorithm = VerificationAlgorithm {
curve: Curve::P256,
digest_alg: HashAlgorithm::SHA256,
};
pub static ECDSA_P384_SHA384: VerificationAlgorithm = VerificationAlgorithm {
curve: Curve::P384,
digest_alg: HashAlgorithm::SHA384,
};
/// An unparsed public key for signature operations.
pub struct UnparsedPublicKey<'a> {
alg: &'static VerificationAlgorithm,
bytes: &'a [u8],
}
impl<'a> UnparsedPublicKey<'a> {
pub fn new(algorithm: &'static VerificationAlgorithm, bytes: &'a [u8]) -> Self {
Self {
alg: algorithm,
bytes,
}
}
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
let pub_key = PublicKey::from_bytes(self.alg.curve, self.bytes)?;
Ok(pub_key.verify(message, signature, self.alg.digest_alg)?)
}
pub fn algorithm(&self) -> &'static VerificationAlgorithm {
self.alg
}
pub fn bytes(&self) -> &'a [u8] {
&self.bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecdsa_p384_sha384_verify() {
// Test generated with JS DOM's WebCrypto.
let pub_key_bytes = base64::decode_config(
"BMZj_xHOfLQn5DIEQcYUkyASDWo8O30gWdkWXHHHWN5owKhGWplYHEb4PLf3DkFTg_smprr-ApdULy3NV10x8IZ0EfVaUZdXvTquH1kiw2PxD7fhqiozMXUaSuZI5KBE6w",
base64::URL_SAFE_NO_PAD
).unwrap();
let message = base64::decode_config(
"F9MQDmEEdvOfm-NkCRrXqG-aVA9kq0xqtjvtWLndmmt6bO2gfLE2CVDDLzJYds0n88uz27c5JkzdsLpm5HP3aLFgD8bgnGm-EgdBpm99CRiIm7mAMbb0-NRAyUxeoGmdgJPVQLWFNoHRwzKV2wZ0Bk-Bq7jkeDHmDfnx-CJKVMQ",
base64::URL_SAFE_NO_PAD,
)
.unwrap();
let signature = base64::decode_config(
"XLZmtJweW4qx0u0l6EpfmB5z-S-CNj4mrl9d7U0MuftdNPhmlNacV4AKR-i4uNn0TUIycU7GsfIjIqxuiL9WdAnfq_KH_SJ95mduqXgWNKlyt8JgMLd4h-jKOllh4erh",
base64::URL_SAFE_NO_PAD,
)
.unwrap();
let public_key =
crate::signature::UnparsedPublicKey::new(&ECDSA_P384_SHA384, &pub_key_bytes);
// Failure case: Wrong key algorithm.
let public_key_wrong_alg =
crate::signature::UnparsedPublicKey::new(&ECDSA_P256_SHA256, &pub_key_bytes);
assert!(public_key_wrong_alg.verify(&message, &signature).is_err());
// Failure case: Add garbage to signature.
let mut garbage_signature = signature.clone();
garbage_signature.push(42);
assert!(public_key.verify(&message, &garbage_signature).is_err());
// Failure case: Flip a bit in message.
let mut garbage_message = message.clone();
garbage_message[42] = 42;
assert!(public_key.verify(&garbage_message, &signature).is_err());
// Happy case.
assert!(public_key.verify(&message, &signature).is_ok());
}
}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"56ac849a71df0e1d9f323cd2ebbdba4d16922fbc0617f8a1d2c3c3254f4056b5","doc/query-plan.md":"fc877e6cbf1b0e089ec99ee4f34673cd9b3fe1a23c8fcfec20cf286cdc0cd0d0","src/conn_ext.rs":"1126009dd562a333d336c6230814b03de970e2eceaef51b3a3ecd23484a3e23b","src/each_chunk.rs":"8aaba842e43b002fbc0fee95d14ce08faa7187b1979c765b2e270cd4802607a5","src/interrupt.rs":"76c829dce08673e06cf1273030a134cd38f713f9b8a9c80982e753a1fe1437a2","src/lib.rs":"cceb1d597dfc01e1141b89351bc875d7b2a680c272642eee53221c3aab9a70e0","src/maybe_cached.rs":"0b18425595055883a98807fbd62ff27a79c18af34e7cb3439f8c3438463ef2dd","src/query_plan.rs":"c0cc296ddf528a949f683317cea2da67ff5caee8042cf20ff00d9f8f54272ad8","src/repeat.rs":"1885f4dd36cc21fabad1ba28ad2ff213ed17707c57564e1c0d7b0349112118bb"},"package":null}
{"files":{"Cargo.toml":"56ac849a71df0e1d9f323cd2ebbdba4d16922fbc0617f8a1d2c3c3254f4056b5","doc/query-plan.md":"fc877e6cbf1b0e089ec99ee4f34673cd9b3fe1a23c8fcfec20cf286cdc0cd0d0","src/conn_ext.rs":"1126009dd562a333d336c6230814b03de970e2eceaef51b3a3ecd23484a3e23b","src/each_chunk.rs":"8aaba842e43b002fbc0fee95d14ce08faa7187b1979c765b2e270cd4802607a5","src/interrupt.rs":"76c829dce08673e06cf1273030a134cd38f713f9b8a9c80982e753a1fe1437a2","src/lib.rs":"cceb1d597dfc01e1141b89351bc875d7b2a680c272642eee53221c3aab9a70e0","src/maybe_cached.rs":"0b18425595055883a98807fbd62ff27a79c18af34e7cb3439f8c3438463ef2dd","src/query_plan.rs":"eb2b0d0031d52dbfaf107eaed8dc9afa650c40bc223162b279689705b686008a","src/repeat.rs":"1885f4dd36cc21fabad1ba28ad2ff213ed17707c57564e1c0d7b0349112118bb"},"package":null}

Some files were not shown because too many files have changed in this diff Show More