mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
fix(clients): SHA256 external_id to normalize before sending to portal (#1949)
* Normalizes very long or very short device IDs to a predictable length * Ensures uniform distribution for the DB index * Provides some basic level of privacy preservation
This commit is contained in:
@@ -134,4 +134,6 @@ dependencies {
|
||||
// Add the dependencies for the Crashlytics and Analytics libraries
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")}
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
implementation("com.google.firebase:firebase-installations-ktx")
|
||||
}
|
||||
|
||||
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@@ -1821,6 +1821,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rand_core",
|
||||
"ring",
|
||||
"rtnetlink",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -68,7 +68,7 @@ fun copyJniShared(task: Task, buildType: String) = task.apply {
|
||||
jniTargets.forEach { entry ->
|
||||
val soFile = File(
|
||||
project.projectDir.parentFile.parentFile.parentFile.parentFile,
|
||||
"target/${entry.key}/${buildType}/libconnlib.so"
|
||||
"target/${entry.key}/$buildType/libconnlib.so",
|
||||
)
|
||||
val targetDir = File(project.projectDir, "/jniLibs/${entry.value}").apply {
|
||||
if (!exists()) {
|
||||
@@ -77,9 +77,11 @@ fun copyJniShared(task: Task, buildType: String) = task.apply {
|
||||
}
|
||||
|
||||
copy {
|
||||
with(copySpec {
|
||||
from(soFile)
|
||||
})
|
||||
with(
|
||||
copySpec {
|
||||
from(soFile)
|
||||
},
|
||||
)
|
||||
into(targetDir)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +90,7 @@ fun copyJniShared(task: Task, buildType: String) = task.apply {
|
||||
cargo {
|
||||
prebuiltToolchains = true
|
||||
verbose = true
|
||||
module = "../"
|
||||
module = "../"
|
||||
libname = "connlib"
|
||||
targets = listOf("arm", "arm64", "x86", "x86_64")
|
||||
features {
|
||||
@@ -108,13 +110,13 @@ tasks.register("copyJniSharedObjectsRelease") {
|
||||
|
||||
tasks.whenTaskAdded {
|
||||
if (name.startsWith("javaPreCompile")) {
|
||||
val newTasks = arrayOf (
|
||||
val newTasks = arrayOf(
|
||||
tasks.named("cargoBuild"),
|
||||
if (name.endsWith("Debug")) {
|
||||
tasks.named("copyJniSharedObjectsDebug")
|
||||
} else {
|
||||
tasks.named("copyJniSharedObjectsRelease")
|
||||
}
|
||||
},
|
||||
)
|
||||
dependsOn(*newTasks)
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ fn connect(
|
||||
env: &mut JNIEnv,
|
||||
portal_url: JString,
|
||||
portal_token: JString,
|
||||
external_id: JString,
|
||||
device_id: JString,
|
||||
callback_handler: GlobalRef,
|
||||
) -> Result<Session<CallbackHandler>, ConnectError> {
|
||||
let portal_url = String::from(env.get_string(&portal_url).map_err(|source| {
|
||||
@@ -309,17 +309,17 @@ fn connect(
|
||||
vm: env.get_java_vm().map_err(ConnectError::GetJavaVmFailed)?,
|
||||
callback_handler,
|
||||
};
|
||||
let external_id = env
|
||||
.get_string(&external_id)
|
||||
let device_id = env
|
||||
.get_string(&device_id)
|
||||
.map_err(|source| ConnectError::StringInvalid {
|
||||
name: "external_id",
|
||||
name: "device_id",
|
||||
source,
|
||||
})?
|
||||
.into();
|
||||
Session::connect(
|
||||
portal_url.as_str(),
|
||||
portal_token,
|
||||
external_id,
|
||||
device_id,
|
||||
callback_handler,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
@@ -335,13 +335,13 @@ pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_con
|
||||
_class: JClass,
|
||||
portal_url: JString,
|
||||
portal_token: JString,
|
||||
external_id: JString,
|
||||
device_id: JString,
|
||||
callback_handler: JObject,
|
||||
) -> *const Session<CallbackHandler> {
|
||||
let Ok(callback_handler) = env.new_global_ref(callback_handler) else { return std::ptr::null() };
|
||||
|
||||
if let Some(result) = catch_and_throw(&mut env, |env| {
|
||||
connect(env, portal_url, portal_token, external_id, callback_handler)
|
||||
connect(env, portal_url, portal_token, device_id, callback_handler)
|
||||
}) {
|
||||
match result {
|
||||
Ok(session) => return Box::into_raw(Box::new(session)),
|
||||
|
||||
@@ -19,7 +19,7 @@ mod ffi {
|
||||
fn connect(
|
||||
portal_url: String,
|
||||
token: String,
|
||||
external_id: String,
|
||||
device_id: String,
|
||||
callback_handler: CallbackHandler,
|
||||
) -> Result<WrappedSession, String>;
|
||||
|
||||
@@ -154,14 +154,14 @@ impl WrappedSession {
|
||||
fn connect(
|
||||
portal_url: String,
|
||||
token: String,
|
||||
external_id: String,
|
||||
device_id: String,
|
||||
callback_handler: ffi::CallbackHandler,
|
||||
) -> Result<Self, String> {
|
||||
init_logging();
|
||||
Session::connect(
|
||||
portal_url.as_str(),
|
||||
token,
|
||||
external_id,
|
||||
device_id,
|
||||
CallbackHandler(callback_handler.into()),
|
||||
)
|
||||
.map(|session| Self { session })
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
};
|
||||
|
||||
use firezone_client_connlib::{
|
||||
get_external_id, get_user_agent, Callbacks, Error, ResourceDescription, Session,
|
||||
get_device_id, get_user_agent, Callbacks, Error, ResourceDescription, Session,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
@@ -82,8 +82,8 @@ fn main() -> Result<()> {
|
||||
// TODO: allow passing as arg vars
|
||||
let url = parse_env_var::<Url>(URL_ENV_VAR)?;
|
||||
let secret = parse_env_var::<String>(SECRET_ENV_VAR)?;
|
||||
let external_id = get_external_id();
|
||||
let mut session = Session::connect(url, secret, external_id, CallbackHandler).unwrap();
|
||||
let device_id = get_device_id();
|
||||
let mut session = Session::connect(url, secret, device_id, CallbackHandler).unwrap();
|
||||
tracing::info!("Started new session");
|
||||
|
||||
block_on_ctrl_c();
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use firezone_gateway_connlib::{get_external_id, Callbacks, Error, ResourceDescription, Session};
|
||||
use firezone_gateway_connlib::{get_device_id, Callbacks, Error, ResourceDescription, Session};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -66,8 +66,8 @@ fn main() -> Result<()> {
|
||||
// TODO: allow passing as arg vars
|
||||
let url = parse_env_var::<Url>(URL_ENV_VAR)?;
|
||||
let secret = parse_env_var::<String>(SECRET_ENV_VAR)?;
|
||||
let external_id = get_external_id();
|
||||
let mut session = Session::connect(url, secret, external_id, CallbackHandler).unwrap();
|
||||
let device_id = get_device_id();
|
||||
let mut session = Session::connect(url, secret, device_id, CallbackHandler).unwrap();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
ctrlc::set_handler(move || tx.send(()).expect("Could not send stop signal on channel."))
|
||||
|
||||
@@ -19,7 +19,7 @@ pub type Session<CB> = libs_common::Session<
|
||||
>;
|
||||
|
||||
pub use libs_common::{
|
||||
get_external_id, get_user_agent, messages::ResourceDescription, Callbacks, Error,
|
||||
get_device_id, get_user_agent, messages::ResourceDescription, Callbacks, Error,
|
||||
};
|
||||
use messages::Messages;
|
||||
use messages::ReplyMessages;
|
||||
|
||||
@@ -30,6 +30,7 @@ os_info = { version = "3", default-features = false }
|
||||
rand = { version = "0.8", default-features = false, features = ["std"] }
|
||||
chrono = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
ring = "0.16"
|
||||
|
||||
# Needed for Android logging until tracing is working
|
||||
log = "0.4"
|
||||
|
||||
@@ -27,8 +27,8 @@ pub fn get_user_agent() -> String {
|
||||
format!("{os_type}/{os_version} {lib_name}/{lib_version}")
|
||||
}
|
||||
|
||||
/// Returns the SMBios Serial of the device, or a random UUIDv4 if it can't be found.
|
||||
pub fn get_external_id() -> String {
|
||||
/// Returns the SMBios Serial of the device or a random UUIDv4 if the SMBios is not available.
|
||||
pub fn get_device_id() -> String {
|
||||
// smbios fails to build on mobile, but it works for other platforms.
|
||||
#[cfg(not(any(target_os = "ios", target_os = "android")))]
|
||||
match smbioslib::table_load_from_device() {
|
||||
|
||||
@@ -4,6 +4,7 @@ use boringtun::x25519::{PublicKey, StaticSecret};
|
||||
use ip_network::IpNetwork;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use rand_core::OsRng;
|
||||
use ring::digest::{Context, SHA256};
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{Debug, Display},
|
||||
@@ -213,7 +214,7 @@ where
|
||||
pub fn connect(
|
||||
portal_url: impl TryInto<Url>,
|
||||
token: String,
|
||||
external_id: String,
|
||||
device_id: String,
|
||||
callbacks: CB,
|
||||
) -> Result<Self> {
|
||||
// TODO: We could use tokio::runtime::current() to get the current runtime
|
||||
@@ -255,7 +256,7 @@ where
|
||||
tx,
|
||||
portal_url.try_into().map_err(|_| Error::UriError)?,
|
||||
token,
|
||||
external_id,
|
||||
device_id,
|
||||
this.callbacks.clone(),
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
@@ -271,12 +272,13 @@ where
|
||||
runtime_stopper: tokio::sync::mpsc::Sender<StopRuntime>,
|
||||
portal_url: Url,
|
||||
token: String,
|
||||
external_id: String,
|
||||
device_id: String,
|
||||
callbacks: CallbackErrorFacade<CB>,
|
||||
) {
|
||||
runtime.spawn(async move {
|
||||
let private_key = StaticSecret::random_from_rng(OsRng);
|
||||
let name_suffix: String = thread_rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect();
|
||||
let external_id = sha256(device_id);
|
||||
|
||||
let connect_url = fatal_error!(
|
||||
get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), &external_id, &name_suffix),
|
||||
@@ -394,6 +396,18 @@ fn set_ws_scheme(url: &mut Url) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sha256(input: String) -> String {
|
||||
let mut ctx = Context::new(&SHA256);
|
||||
ctx.update(input.as_bytes());
|
||||
let digest = ctx.finish();
|
||||
|
||||
digest
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_websocket_path(
|
||||
mut url: Url,
|
||||
secret: String,
|
||||
|
||||
@@ -19,4 +19,4 @@ pub type Session<CB> = libs_common::Session<
|
||||
CB,
|
||||
>;
|
||||
|
||||
pub use libs_common::{get_external_id, messages::ResourceDescription, Callbacks, Error};
|
||||
pub use libs_common::{get_device_id, messages::ResourceDescription, Callbacks, Error};
|
||||
|
||||
@@ -5,7 +5,3 @@ DEVELOPMENT_TEAM = <team_id>
|
||||
// Should be an app id created at developer.apple.com
|
||||
// with Network Extensions capability.
|
||||
PRODUCT_BUNDLE_IDENTIFIER = <app_id>
|
||||
|
||||
// If you want to build Connlib with mocks,
|
||||
// enable it here.
|
||||
// CONNLIB_MOCK=1
|
||||
|
||||
@@ -130,7 +130,7 @@ public class Adapter {
|
||||
do {
|
||||
self.state = .startingTunnel(
|
||||
session: try WrappedSession.connect(
|
||||
self.controlPlaneURLString, self.token, self.getExternalId(), self.callbackHandler),
|
||||
self.controlPlaneURLString, self.token, self.getDeviceId(), self.callbackHandler),
|
||||
onStarted: completionHandler
|
||||
)
|
||||
} catch let error {
|
||||
@@ -200,23 +200,25 @@ public class Adapter {
|
||||
}
|
||||
|
||||
// MARK: Device unique identifiers
|
||||
|
||||
extension Adapter {
|
||||
func getExternalId() -> String {
|
||||
func getDeviceId() -> String {
|
||||
#if os(iOS)
|
||||
guard let uuid = UIDevice.current.identifierForVendor?.uuidString else {
|
||||
guard let extId = UIDevice.current.identifierForVendor?.uuidString else {
|
||||
// Send a blank string, letting either connlib or the portal handle this
|
||||
return ""
|
||||
}
|
||||
return uuid
|
||||
|
||||
#elseif os(macOS)
|
||||
guard let macBytes = PrimaryMacAddress.copy_mac_address() else {
|
||||
guard let extId = PrimaryMacAddress.copy_mac_address() as? String else {
|
||||
// Send a blank string, letting either connlib or the portal handle this
|
||||
return ""
|
||||
}
|
||||
return (macBytes as Data).base64EncodedString()
|
||||
#else
|
||||
#error("Unsupported platform")
|
||||
#endif
|
||||
|
||||
return extId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +273,7 @@ extension Adapter {
|
||||
do {
|
||||
self.state = .startingTunnel(
|
||||
session: try WrappedSession.connect(
|
||||
controlPlaneURLString, token, self.getExternalId(), self.callbackHandler),
|
||||
controlPlaneURLString, token, self.getDeviceId(), self.callbackHandler),
|
||||
onStarted: { error in
|
||||
if let error = error {
|
||||
self.logger.error(
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
// Believe it or not, this is Apple's recommended way of doing things for macOS
|
||||
// See https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device#//apple_ref/doc/uid/TP40010573-CH1-SW14
|
||||
|
||||
import IOKit
|
||||
import Foundation
|
||||
import IOKit
|
||||
import OSLog
|
||||
|
||||
public class PrimaryMacAddress {
|
||||
// Returns an object with a +1 retain count; the caller needs to release.
|
||||
private static func io_service(named name: String, wantBuiltIn: Bool) -> io_service_t? {
|
||||
let default_port = kIOMainPortDefault
|
||||
let defaultPort = kIOMainPortDefault
|
||||
var iterator = io_iterator_t()
|
||||
defer {
|
||||
if iterator != IO_OBJECT_NULL {
|
||||
@@ -23,21 +23,24 @@ public class PrimaryMacAddress {
|
||||
}
|
||||
}
|
||||
|
||||
guard let matchingDict = IOBSDNameMatching(default_port, 0, name),
|
||||
IOServiceGetMatchingServices(default_port,
|
||||
matchingDict as CFDictionary,
|
||||
&iterator) == KERN_SUCCESS,
|
||||
iterator != IO_OBJECT_NULL
|
||||
guard let matchingDict = IOBSDNameMatching(defaultPort, 0, name),
|
||||
IOServiceGetMatchingServices(
|
||||
defaultPort,
|
||||
matchingDict as CFDictionary,
|
||||
&iterator) == KERN_SUCCESS,
|
||||
iterator != IO_OBJECT_NULL
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var candidate = IOIteratorNext(iterator)
|
||||
while candidate != IO_OBJECT_NULL {
|
||||
if let cftype = IORegistryEntryCreateCFProperty(candidate,
|
||||
"IOBuiltin" as CFString,
|
||||
kCFAllocatorDefault,
|
||||
0) {
|
||||
if let cftype = IORegistryEntryCreateCFProperty(
|
||||
candidate,
|
||||
"IOBuiltin" as CFString,
|
||||
kCFAllocatorDefault,
|
||||
0)
|
||||
{
|
||||
let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean
|
||||
if wantBuiltIn == CFBooleanGetValue(isBuiltIn) {
|
||||
return candidate
|
||||
@@ -55,23 +58,23 @@ public class PrimaryMacAddress {
|
||||
// Prefer built-in network interfaces.
|
||||
// For example, an external Ethernet adaptor can displace
|
||||
// the built-in Wi-Fi as en0.
|
||||
guard let service = io_service(named: "en0", wantBuiltIn: true)
|
||||
?? io_service(named: "en1", wantBuiltIn: true)
|
||||
?? io_service(named: "en0", wantBuiltIn: false)
|
||||
guard
|
||||
let service = io_service(named: "en0", wantBuiltIn: true)
|
||||
?? io_service(named: "en1", wantBuiltIn: true)
|
||||
?? io_service(named: "en0", wantBuiltIn: false)
|
||||
else { return nil }
|
||||
defer { IOObjectRelease(service) }
|
||||
|
||||
|
||||
if let cftype = IORegistryEntrySearchCFProperty(
|
||||
service,
|
||||
kIOServicePlane,
|
||||
"IOMACAddress" as CFString,
|
||||
kCFAllocatorDefault,
|
||||
IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) {
|
||||
IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents))
|
||||
{
|
||||
return (cftype as! CFData)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user