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:
Jamil
2023-08-28 20:24:01 -07:00
committed by GitHub
parent 79021a7f25
commit ce11fa29f0
15 changed files with 80 additions and 59 deletions

View File

@@ -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
View File

@@ -1821,6 +1821,7 @@ dependencies = [
"parking_lot",
"rand",
"rand_core",
"ring",
"rtnetlink",
"serde",
"serde_json",

View File

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

View File

@@ -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)),

View File

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

View File

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

View File

@@ -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."))

View File

@@ -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;

View File

@@ -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"

View File

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

View File

@@ -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,

View File

@@ -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};

View File

@@ -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

View File

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

View File

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