mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
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:
parent
67c1c99d93
commit
6c3e1b850d
@ -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 = "8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
|
||||
[source."https://github.com/mozilla-spidermonkey/jsparagus"]
|
||||
git = "https://github.com/mozilla-spidermonkey/jsparagus"
|
||||
|
97
Cargo.lock
generated
97
Cargo.lock
generated
@ -1252,16 +1252,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]]
|
||||
@ -1342,9 +1341,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"failure",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1413,9 +1412,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",
|
||||
@ -1713,23 +1712,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
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",
|
||||
]
|
||||
@ -2230,15 +2232,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",
|
||||
]
|
||||
|
||||
@ -2442,7 +2444,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
|
||||
[[package]]
|
||||
name = "intl-memoizer"
|
||||
@ -2624,6 +2626,19 @@ dependencies = [
|
||||
"smoosh",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwcrypto"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"base64 0.12.0",
|
||||
"rc_crypto",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
@ -3442,27 +3457,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
|
||||
[[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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"libsqlite3-sys",
|
||||
"nss_build_common",
|
||||
]
|
||||
|
||||
@ -4076,6 +4091,16 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_rccrypto"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"rand_core",
|
||||
"rc_crypto",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.1"
|
||||
@ -4129,16 +4154,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"base64 0.12.0",
|
||||
"ece",
|
||||
"error-support",
|
||||
"failure",
|
||||
"failure_derive",
|
||||
"hawk",
|
||||
"libsqlite3-sys",
|
||||
"nss",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4741,7 +4764,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"ffi-support",
|
||||
"interrupt-support",
|
||||
@ -4939,7 +4962,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"base64 0.12.0",
|
||||
"rand",
|
||||
@ -4950,12 +4973,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base16",
|
||||
"base64 0.12.0",
|
||||
"error-support",
|
||||
"failure",
|
||||
"ffi-support",
|
||||
"interrupt-support",
|
||||
"lazy_static",
|
||||
@ -4966,6 +4989,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sync-guid",
|
||||
"sync15-traits",
|
||||
"thiserror",
|
||||
"url",
|
||||
"viaduct",
|
||||
]
|
||||
@ -4973,11 +4997,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"failure",
|
||||
"anyhow",
|
||||
"ffi-support",
|
||||
"interrupt-support",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -5582,10 +5605,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"failure",
|
||||
"failure_derive",
|
||||
"ffi-support",
|
||||
"log",
|
||||
"once_cell",
|
||||
@ -5593,6 +5614,7 @@ dependencies = [
|
||||
"prost-derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -5704,10 +5726,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=8a576fbe79199fa8664f64285524017f74ebcc5f#8a576fbe79199fa8664f64285524017f74ebcc5f"
|
||||
dependencies = [
|
||||
"error-support",
|
||||
"failure",
|
||||
"ffi-support",
|
||||
"interrupt-support",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -5719,6 +5741,7 @@ dependencies = [
|
||||
"sql-support",
|
||||
"sync-guid",
|
||||
"sync15-traits",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
@ -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(...)`.
|
||||
|
@ -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 = "8a576fbe79199fa8664f64285524017f74ebcc5f", features = ["gecko"] }
|
||||
|
@ -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(
|
||||
|
@ -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>),
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 = "8a576fbe79199fa8664f64285524017f74ebcc5f" }
|
||||
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 = "8a576fbe79199fa8664f64285524017f74ebcc5f" }
|
||||
xpcom = { path = "../../../xpcom/rust/xpcom" }
|
||||
|
||||
[dependencies.thin-vec]
|
||||
|
2
third_party/rust/ece/.cargo-checksum.json
vendored
2
third_party/rust/ece/.cargo-checksum.json
vendored
@ -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"}
|
21
third_party/rust/ece/Cargo.toml
vendored
21
third_party/rust/ece/Cargo.toml
vendored
@ -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"]
|
||||
|
14
third_party/rust/ece/src/aes128gcm.rs
vendored
14
third_party/rust/ece/src/aes128gcm.rs
vendored
@ -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])
|
||||
}
|
||||
|
11
third_party/rust/ece/src/aesgcm.rs
vendored
11
third_party/rust/ece/src/aesgcm.rs
vendored
@ -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;
|
||||
|
20
third_party/rust/ece/src/common.rs
vendored
20
third_party/rust/ece/src/common.rs
vendored
@ -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())
|
||||
})
|
||||
|
5
third_party/rust/ece/src/crypto/holder.rs
vendored
5
third_party/rust/ece/src/crypto/holder.rs
vendored
@ -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.
|
||||
|
2
third_party/rust/ece/src/crypto/openssl.rs
vendored
2
third_party/rust/ece/src/crypto/openssl.rs
vendored
@ -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)
|
||||
|
115
third_party/rust/ece/src/error.rs
vendored
115
third_party/rust/ece/src/error.rs
vendored
@ -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),
|
||||
}
|
||||
|
53
third_party/rust/ece/src/lib.rs
vendored
53
third_party/rust/ece/src/lib.rs
vendored
@ -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();
|
||||
|
@ -1 +1 @@
|
||||
{"files":{"Cargo.toml":"21b095cc85324ada8cf714deb719e3e892f2f5222538e063db56fde0f81bd17c","src/lib.rs":"4581b12eb58f9fb5275c7af74fbc4521b82ef224b6ba81f0e785c372ba95f8c6"},"package":null}
|
||||
{"files":{"Cargo.toml":"8419c1fdf71e30634e2ca9dbbabf0f83fdb7dde6a945060c31a16acdbec6894f","src/lib.rs":"70fa9bba695574d97a6e1add79c400744cbd0d60639cbcdc0121c89fa20f9c81"},"package":null}
|
5
third_party/rust/error-support/Cargo.toml
vendored
5
third_party/rust/error-support/Cargo.toml
vendored
@ -6,5 +6,8 @@ edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dependencies.backtrace]
|
||||
optional = true
|
||||
version = "0.3"
|
||||
|
120
third_party/rust/error-support/src/lib.rs
vendored
120
third_party/rust/error-support/src/lib.rs
vendored
@ -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]
|
||||
|
@ -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"}
|
14
third_party/rust/ffi-support/Cargo.toml
vendored
14
third_party/rust/ffi-support/Cargo.toml
vendored
@ -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 = []
|
||||
|
2
third_party/rust/ffi-support/README.md
vendored
2
third_party/rust/ffi-support/README.md
vendored
@ -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
|
||||
|
||||
|
213
third_party/rust/ffi-support/src/handle_map.rs
vendored
213
third_party/rust/ffi-support/src/handle_map.rs
vendored
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
183
third_party/rust/ffi-support/src/lib.rs
vendored
183
third_party/rust/ffi-support/src/lib.rs
vendored
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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":"40ccd4fd38a7397ec692ccb078969abf7360124cf94ffb7f6a41da751046dbb8","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}
|
12
third_party/rust/fxa-client/Cargo.toml
vendored
12
third_party/rust/fxa-client/Cargo.toml
vendored
@ -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 = []
|
||||
|
156
third_party/rust/fxa-client/examples/devices_api.rs
vendored
156
third_party/rust/fxa-client/examples/devices_api.rs
vendored
@ -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!"),
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
@ -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
255
third_party/rust/fxa-client/src/auth.rs
vendored
Normal 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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
124
third_party/rust/fxa-client/src/config.rs
vendored
124
third_party/rust/fxa-client/src/config.rs
vendored
@ -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,16 @@ 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.
|
||||
// (ideally we'd use `strip_suffix`, but we currently target a rust version
|
||||
// where this doesn't exist - `trim_end_matches` will repeatedly remove
|
||||
// the suffix, but that seems fine for this use-case)
|
||||
self.token_server_url_override = Some(
|
||||
token_server_url_override
|
||||
.trim_end_matches("/1.0/sync/1.5")
|
||||
.to_owned(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@ -145,32 +135,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 +171,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 +356,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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
55
third_party/rust/fxa-client/src/device.rs
vendored
55
third_party/rust/fxa-client/src/device.rs
vendored
@ -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()),
|
||||
}
|
||||
|
160
third_party/rust/fxa-client/src/error.rs
vendored
160
third_party/rust/fxa-client/src/error.rs
vendored
@ -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),
|
||||
|
80
third_party/rust/fxa-client/src/ffi.rs
vendored
80
third_party/rust/fxa-client/src/ffi.rs
vendored
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
475
third_party/rust/fxa-client/src/http_client.rs
vendored
475
third_party/rust/fxa-client/src/http_client.rs
vendored
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
third_party/rust/fxa-client/src/lib.rs
vendored
22
third_party/rust/fxa-client/src/lib.rs
vendored
@ -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.
|
||||
|
49
third_party/rust/fxa-client/src/migrator.rs
vendored
49
third_party/rust/fxa-client/src/migrator.rs
vendored
@ -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
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
}
|
||||
|
569
third_party/rust/fxa-client/src/oauth.rs
vendored
569
third_party/rust/fxa-client/src/oauth.rs
vendored
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
third_party/rust/fxa-client/src/profile.rs
vendored
13
third_party/rust/fxa-client/src/profile.rs
vendored
@ -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,
|
||||
})));
|
||||
|
82
third_party/rust/fxa-client/src/push.rs
vendored
82
third_party/rust/fxa-client/src/push.rs
vendored
@ -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();
|
||||
}
|
||||
}
|
||||
|
152
third_party/rust/fxa-client/src/scoped_keys.rs
vendored
152
third_party/rust/fxa-client/src/scoped_keys.rs
vendored
@ -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();
|
||||
|
1
third_party/rust/fxa-client/src/scopes.rs
vendored
1
third_party/rust/fxa-client/src/scopes.rs
vendored
@ -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";
|
||||
|
32
third_party/rust/fxa-client/src/send_tab.rs
vendored
32
third_party/rust/fxa-client/src/send_tab.rs
vendored
@ -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."),
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
360
third_party/rust/fxa-client/src/telemetry.rs
vendored
Normal file
360
third_party/rust/fxa-client/src/telemetry.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
2
third_party/rust/fxa-client/src/util.rs
vendored
2
third_party/rust/fxa-client/src/util.rs
vendored
@ -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))
|
||||
}
|
||||
|
||||
|
2
third_party/rust/hawk/.cargo-checksum.json
vendored
2
third_party/rust/hawk/.cargo-checksum.json
vendored
@ -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"}
|
23
third_party/rust/hawk/Cargo.toml
vendored
23
third_party/rust/hawk/Cargo.toml
vendored
@ -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"
|
||||
|
||||
|
5
third_party/rust/hawk/src/crypto/holder.rs
vendored
5
third_party/rust/hawk/src/crypto/holder.rs
vendored
@ -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.
|
||||
|
12
third_party/rust/hawk/src/crypto/mod.rs
vendored
12
third_party/rust/hawk/src/crypto/mod.rs
vendored
@ -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.
|
||||
|
3
third_party/rust/hawk/src/crypto/ring.rs
vendored
3
third_party/rust/hawk/src/crypto/ring.rs
vendored
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
41
third_party/rust/hawk/src/error.rs
vendored
41
third_party/rust/hawk/src/error.rs
vendored
@ -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,
|
||||
}
|
||||
|
||||
|
1
third_party/rust/jwcrypto/.cargo-checksum.json
vendored
Normal file
1
third_party/rust/jwcrypto/.cargo-checksum.json
vendored
Normal 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
17
third_party/rust/jwcrypto/Cargo.toml
vendored
Normal 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
198
third_party/rust/jwcrypto/src/ec.rs
vendored
Normal 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
25
third_party/rust/jwcrypto/src/error.rs
vendored
Normal 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
243
third_party/rust/jwcrypto/src/lib.rs
vendored
Normal 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);
|
||||
}
|
2
third_party/rust/nss/.cargo-checksum.json
vendored
2
third_party/rust/nss/.cargo-checksum.json
vendored
@ -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}
|
4
third_party/rust/nss/Cargo.toml
vendored
4
third_party/rust/nss/Cargo.toml
vendored
@ -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"]
|
||||
|
69
third_party/rust/nss/src/ec.rs
vendored
69
third_party/rust/nss/src/ec.rs
vendored
@ -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) };
|
||||
|
18
third_party/rust/nss/src/error.rs
vendored
18
third_party/rust/nss/src/error.rs
vendored
@ -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! {
|
||||
|
1
third_party/rust/nss/src/lib.rs
vendored
1
third_party/rust/nss/src/lib.rs
vendored
@ -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
78
third_party/rust/nss/src/pbkdf2.rs
vendored
Normal 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(())
|
||||
}
|
7
third_party/rust/nss/src/pk11/context.rs
vendored
7
third_party/rust/nss/src/pk11/context.rs
vendored
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
third_party/rust/nss/src/pk11/types.rs
vendored
11
third_party/rust/nss/src/pk11/types.rs
vendored
@ -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);
|
||||
|
4
third_party/rust/nss/src/secport.rs
vendored
4
third_party/rust/nss/src/secport.rs
vendored
@ -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
|
||||
|
2
third_party/rust/nss/src/util.rs
vendored
2
third_party/rust/nss/src/util.rs
vendored
@ -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())
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"files":{"Cargo.toml":"4f1d37d926e853eb9f3d8074b45c00a317e2b4aafbc339a471430d28526716e9","src/lib.rs":"d2f95eaafabb8d1eb7decbd10bcf881da7af7be4c4aadb8f7d68cf0fd806dcb1"},"package":null}
|
||||
{"files":{"Cargo.toml":"4f1d37d926e853eb9f3d8074b45c00a317e2b4aafbc339a471430d28526716e9","src/lib.rs":"a9077862fc7c45044178fa2675a04d0b31a27574d93a328e03df3108342dd6e4"},"package":null}
|
3
third_party/rust/nss_build_common/src/lib.rs
vendored
3
third_party/rust/nss_build_common/src/lib.rs
vendored
@ -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))
|
||||
|
@ -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}
|
7
third_party/rust/nss_sys/Cargo.toml
vendored
7
third_party/rust/nss_sys/Cargo.toml
vendored
@ -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 = []
|
||||
|
@ -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;
|
||||
|
25
third_party/rust/nss_sys/src/bindings/pk11pub.rs
vendored
25
third_party/rust/nss_sys/src/bindings/pk11pub.rs
vendored
@ -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;
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
11
third_party/rust/nss_sys/src/bindings/secoidt.rs
vendored
11
third_party/rust/nss_sys/src/bindings/secoidt.rs
vendored
@ -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,
|
||||
|
@ -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;
|
||||
|
6
third_party/rust/nss_sys/src/lib.rs
vendored
6
third_party/rust/nss_sys/src/lib.rs
vendored
@ -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;
|
||||
|
1
third_party/rust/rand_rccrypto/.cargo-checksum.json
vendored
Normal file
1
third_party/rust/rand_rccrypto/.cargo-checksum.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"files":{"Cargo.toml":"affbe7c78de624bfdf07d0afc97b99377f36e69e77fc3cfc16c377bbdd7e4417","src/lib.rs":"7d2661e7f2be3c29b3e0878bef5aa6abc1930360050ea1191b6487bef0a2182a"},"package":null}
|
16
third_party/rust/rand_rccrypto/Cargo.toml
vendored
Normal file
16
third_party/rust/rand_rccrypto/Cargo.toml
vendored
Normal 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"
|
30
third_party/rust/rand_rccrypto/src/lib.rs
vendored
Normal file
30
third_party/rust/rand_rccrypto/src/lib.rs
vendored
Normal 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 {}
|
@ -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}
|
10
third_party/rust/rc_crypto/Cargo.toml
vendored
10
third_party/rust/rc_crypto/Cargo.toml
vendored
@ -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"]
|
||||
|
1
third_party/rust/rc_crypto/README.md
vendored
1
third_party/rust/rc_crypto/README.md
vendored
@ -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).
|
||||
|
101
third_party/rust/rc_crypto/src/agreement.rs
vendored
101
third_party/rust/rc_crypto/src/agreement.rs
vendored
@ -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());
|
||||
}
|
||||
}
|
||||
|
2
third_party/rust/rc_crypto/src/digest.rs
vendored
2
third_party/rust/rc_crypto/src/digest.rs
vendored
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
25
third_party/rust/rc_crypto/src/ece_crypto.rs
vendored
25
third_party/rust/rc_crypto/src/ece_crypto.rs
vendored
@ -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!"),
|
||||
};
|
||||
}
|
||||
|
14
third_party/rust/rc_crypto/src/error.rs
vendored
14
third_party/rust/rc_crypto/src/error.rs
vendored
@ -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! {
|
||||
|
2
third_party/rust/rc_crypto/src/hmac.rs
vendored
2
third_party/rust/rc_crypto/src/hmac.rs
vendored
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
8
third_party/rust/rc_crypto/src/lib.rs
vendored
8
third_party/rust/rc_crypto/src/lib.rs
vendored
@ -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
196
third_party/rust/rc_crypto/src/pbkdf2.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
111
third_party/rust/rc_crypto/src/signature.rs
vendored
Normal file
111
third_party/rust/rc_crypto/src/signature.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user