diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/Cargo.toml b/Cargo.toml index 82d1e84..eb88ea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ u2f = ["auth-base", "untrusted", "serde_repr", ] webauthn-server = [ "webauthn", "webpki" ] webauthn = [ "auth-base", "bytes", "serde_cbor", "uuid", "http", "ed25519-dalek", "p256", "indexmap" ] auth-base = ["base64", "byteorder", "ring", "serde", "serde_derive", "serde_json", "serde_bytes"] +android = ["jni"] [dependencies] sha2 = {version = "0.10" , features = ["oid"]} @@ -54,7 +55,7 @@ p256 = { version = "0.13.2", optional = true } indexmap = { version = "2.2.6", features = ["serde"], optional = true } [target.'cfg(target_os = "android")'.dependencies] -jni = { version = "0.21.1" } +jni = { version = "0.21.1", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] wasm-bindgen = { version = "0.2.91" } diff --git a/src/oath/hotp.rs b/src/oath/hotp.rs index 8276957..20aef29 100644 --- a/src/oath/hotp.rs +++ b/src/oath/hotp.rs @@ -140,7 +140,7 @@ impl OtpAuth for HOTPContext { "otpauth://hotp/{}?secret={}&algorithm={}&digits={}&counter={}", label.unwrap_or("slauth"), base32::encode(base32::Alphabet::RFC4648 { padding: false }, self.secret.as_slice()), - self.alg.to_string(), + self.alg, self.digits, self.counter ); diff --git a/src/oath/mod.rs b/src/oath/mod.rs index 6fdaf21..ea57b41 100644 --- a/src/oath/mod.rs +++ b/src/oath/mod.rs @@ -4,6 +4,7 @@ use hmac::{ }; use sha1::Sha1; use sha2::{Sha256, Sha512}; +use std::fmt::Display; pub mod hotp; pub mod totp; @@ -71,13 +72,14 @@ impl HashesAlgorithm { } } -impl ToString for HashesAlgorithm { - fn to_string(&self) -> String { - match self { +impl Display for HashesAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { HashesAlgorithm::SHA1 => "SHA1".to_string(), HashesAlgorithm::SHA256 => "SHA256".to_string(), HashesAlgorithm::SHA512 => "SHA512".to_string(), - } + }; + write!(f, "{}", str) } } diff --git a/src/oath/totp.rs b/src/oath/totp.rs index 1de71ed..ae7f645 100644 --- a/src/oath/totp.rs +++ b/src/oath/totp.rs @@ -203,7 +203,7 @@ impl OtpAuth for TOTPContext { "otpauth://totp/{}?secret={}&algorithm={}&digits={}&period={}", label.unwrap_or("slauth"), base32::encode(base32::Alphabet::RFC4648 { padding: false }, self.secret.as_slice()), - self.alg.to_string(), + self.alg, self.digits, self.period ); diff --git a/src/u2f/proto/web_message.rs b/src/u2f/proto/web_message.rs index 6978e50..ecfe8be 100644 --- a/src/u2f/proto/web_message.rs +++ b/src/u2f/proto/web_message.rs @@ -33,7 +33,6 @@ pub struct Registration { pub attestation_cert: Vec, } -/// #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RegisterRequest { @@ -43,7 +42,6 @@ pub struct RegisterRequest { pub challenge: String, } -/// #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RegisteredKey { @@ -97,7 +95,6 @@ impl<'a> From<&'a U2fRequestType> for U2fResponseType { } } -/// #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct U2fRequest { @@ -121,17 +118,14 @@ pub struct U2fRequest { pub data: Request, } -/// #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct U2fRegisterRequest { - /// pub register_requests: Vec, /// An array of RegisteredKeys representing the U2F tokens registered to this user. pub registered_keys: Vec, } -/// #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct U2fSignRequest { @@ -141,7 +135,6 @@ pub struct U2fSignRequest { pub registered_keys: Vec, } -/// #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum Request { @@ -234,7 +227,6 @@ pub struct U2fSignResponse { pub client_data: String, } -/// #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum Response { diff --git a/src/u2f/server/mod.rs b/src/u2f/server/mod.rs index 1df8fbb..ffcbdd7 100644 --- a/src/u2f/server/mod.rs +++ b/src/u2f/server/mod.rs @@ -208,7 +208,6 @@ impl Registration { } impl U2fSignResponse { - /// pub fn validate_signature(&self, public_key: &[u8]) -> Result { let U2fSignResponse { signature_data, diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index a1f2aab..3353de3 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -180,7 +180,7 @@ impl WebauthnAuthenticator { pub fn generate_attestation_object( alg: CoseAlgorithmIdentifier, aaguid: Uuid, - credential_id: &Vec, + credential_id: &[u8], rp_id: &str, attestation_flags: u8, ) -> Result<(AttestationObject, String, Vec), WebauthnCredentialRequestError> { @@ -250,7 +250,7 @@ impl WebauthnAuthenticator { let attested_credential_data = if attestation_flags & AttestationFlags::AttestedCredentialDataIncluded as u8 != 0 { Some(AttestedCredentialData { aaguid: aaguid.into_bytes(), - credential_id: credential_id.clone(), + credential_id: credential_id.to_owned(), credential_public_key: CredentialPublicKey { key_type: key_info.key_type(), alg: alg.into(), @@ -302,7 +302,8 @@ impl WebauthnAuthenticator { let auth_data_bytes = Self::generate_authenticator_data(rp_id, attestation_flags, None)?.to_vec()?; - let challenge = base64::decode(credential_request_options.challenge)?; + let challenge = base64::decode(credential_request_options.challenge.as_str()) + .or(base64::decode_config(credential_request_options.challenge, URL_SAFE_NO_PAD))?; let collected_client_data = CollectedClientData { request_type: WEBAUTHN_REQUEST_TYPE_GET.to_owned(), challenge: base64::encode_config(challenge, URL_SAFE_NO_PAD), @@ -374,7 +375,7 @@ impl WebauthnAuthenticator { let signing_key = rsa::pkcs1v15::SigningKey::::new(key); Ok(signing_key.sign([auth_data_bytes, client_data_hash].concat().as_slice()).to_vec()) } - _ => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), + _ => Err(WebauthnCredentialRequestError::AlgorithmNotSupported), } } @@ -422,9 +423,9 @@ fn test_best_alg() { assert_eq!(alg, CoseAlgorithmIdentifier::Ed25519); let params2 = vec![ - CoseAlgorithmIdentifier::ES256.into(), - CoseAlgorithmIdentifier::RS1.into(), - CoseAlgorithmIdentifier::RSA.into(), + CoseAlgorithmIdentifier::ES256, + CoseAlgorithmIdentifier::RS1, + CoseAlgorithmIdentifier::RSA, ]; let alg = WebauthnAuthenticator::find_best_supported_algorithm(¶ms2).unwrap(); diff --git a/src/webauthn/authenticator/native.rs b/src/webauthn/authenticator/native.rs index 6a61a1e..eb2f21a 100644 --- a/src/webauthn/authenticator/native.rs +++ b/src/webauthn/authenticator/native.rs @@ -176,24 +176,103 @@ mod ios { } } -#[cfg(target_os = "android")] +#[cfg(feature = "android")] pub mod android { use crate::{ strings, webauthn::{ - authenticator::{responses::AuthenticatorCredentialCreationResponse, WebauthnAuthenticator}, + authenticator::{ + responses::{AuthenticatorCredentialCreationResponse, AuthenticatorCredentialCreationResponseAdditionalData}, + WebauthnAuthenticator, + }, proto::web_message::{ - PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRaw, PublicKeyCredentialRequestOptions, + AuthenticatorAttestationResponse, PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRaw, + PublicKeyCredentialRequestOptions, }, }, }; + use serde_derive::{Deserialize, Serialize}; use std::{ ffi::{c_uchar, CString}, os::raw::c_char, ptr::null_mut, }; + use std::collections::HashMap; + use base64::URL_SAFE_NO_PAD; use uuid::Uuid; + #[derive(Serialize, Deserialize, Clone, Debug)] + #[serde(rename_all = "camelCase")] + pub struct PublicKeyCredentialAndroid { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_id: Option, + pub client_extension_results: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_attachment: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "type")] + pub credential_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub response: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + } + + impl From for PublicKeyCredentialAndroid { + fn from(raw: PublicKeyCredentialRaw) -> Self { + PublicKeyCredentialAndroid { + id: Some(raw.id.clone()), + raw_id: Some(raw.id), + authenticator_attachment: Some("cross-platform".to_owned()), + client_extension_results: HashMap::new(), + response: raw.response.map(|response| AuthenticatorAttestationResponse { + attestation_object: response.attestation_object.map(|ad|base64::encode_config(ad, URL_SAFE_NO_PAD)), + client_data_json: base64::encode(&response.client_data_json), + authenticator_data: response.authenticator_data.map(|ad|base64::encode_config(ad, URL_SAFE_NO_PAD)), + signature: response.signature.map(|ad|base64::encode_config(ad, URL_SAFE_NO_PAD)), + user_handle: response.user_handle.map(|ad|base64::encode_config(ad, URL_SAFE_NO_PAD)), + }), + credential_type: Some("public-key".to_owned()), + error: None, + } + } + } + + #[derive(Serialize, Clone)] + pub struct AuthenticatorCredentialCreationResponseAndroid { + #[serde(skip_serializing_if = "Option::is_none")] + pub credential_response: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub private_key_response: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + } + + impl AuthenticatorCredentialCreationResponseAndroid { + pub fn from_error(error: String) -> Self { + AuthenticatorCredentialCreationResponseAndroid { + credential_response: None, + private_key_response: None, + additional_data: None, + error: Some(error), + } + } + } + + impl From for AuthenticatorCredentialCreationResponseAndroid { + fn from(value: AuthenticatorCredentialCreationResponse) -> Self { + AuthenticatorCredentialCreationResponseAndroid { + credential_response: Some(value.credential_response), + private_key_response: Some(value.private_key_response), + additional_data: Some(value.additional_data), + error: None, + } + } + } + #[no_mangle] pub unsafe extern "C" fn generate_credential_creation_response( aaguid: *const c_char, @@ -202,7 +281,7 @@ pub mod android { request_json: *const c_char, origin: *const c_char, attestation_flags: u8, - ) -> *mut AuthenticatorCredentialCreationResponse { + ) -> *mut AuthenticatorCredentialCreationResponseAndroid { let aaguid_str = strings::c_char_to_string(aaguid); let aaguid = Uuid::parse_str(aaguid_str.as_str()); if aaguid.is_err() { @@ -213,8 +292,11 @@ pub mod android { let options: Result = serde_json::from_str(strings::c_char_to_string(request_json).as_str()); - if options.is_err() { - return null_mut(); + if let Err(e) = options.as_ref() { + return Box::into_raw(Box::new(AuthenticatorCredentialCreationResponseAndroid::from_error(format!( + "Error reading options: {:?}", + e + )))); } let origin_str = if origin.is_null() { @@ -231,11 +313,13 @@ pub mod android { attestation_flags, ); - if response.is_err() { - return null_mut(); + match response { + Ok(response) => Box::into_raw(Box::new(AuthenticatorCredentialCreationResponseAndroid::from(response))), + Err(e) => Box::into_raw(Box::new(AuthenticatorCredentialCreationResponseAndroid::from_error(format!( + "Error creating key: {:?}", + e + )))), } - - Box::into_raw(Box::new(response.expect("Checked above"))) } #[no_mangle] @@ -272,13 +356,36 @@ pub mod android { } #[no_mangle] - pub unsafe extern "C" fn get_json_from_request_response(res: *mut PublicKeyCredentialRaw) -> *mut c_char { + pub unsafe extern "C" fn get_error_from_creation_response(res: *mut AuthenticatorCredentialCreationResponseAndroid) -> *mut c_char { if res.is_null() { return null_mut(); } - let public_key_credential = PublicKeyCredential::from((*res).clone()); - let json = serde_json::to_string(&public_key_credential); + let error = (*res).error.as_ref(); + match error { + Some(e) => { + let cstring = CString::new(e.as_str()); + match cstring { + Ok(cstring) => cstring.into_raw(), + Err(_) => null_mut(), + } + } + + None => null_mut(), + } + } + + #[no_mangle] + pub unsafe extern "C" fn get_json_from_request_response(res: *mut PublicKeyCredentialAndroid) -> *mut c_char { + if res.is_null() { + return null_mut(); + } + + if (*res).error.is_some() { + return null_mut(); + } + + let json = serde_json::to_string(&(*res)); if json.is_err() { return null_mut(); @@ -291,6 +398,26 @@ pub mod android { } } + #[no_mangle] + pub unsafe extern "C" fn get_error_from_request_response(res: *mut PublicKeyCredentialAndroid) -> *mut c_char { + if res.is_null() { + return null_mut(); + } + + let error = (*res).error.as_ref(); + match error { + Some(e) => { + let cstring = CString::new(e.as_str()); + match cstring { + Ok(cstring) => cstring.into_raw(), + Err(_) => null_mut(), + } + } + + None => null_mut(), + } + } + #[no_mangle] pub unsafe extern "C" fn generate_credential_request_response( credential_id: *const c_uchar, @@ -301,7 +428,7 @@ pub mod android { user_handle: *const c_uchar, user_handle_length: usize, private_key: *const c_char, - ) -> *mut PublicKeyCredentialRaw { + ) -> *mut PublicKeyCredentialAndroid { let credential_id: Vec = std::slice::from_raw_parts(credential_id, credential_id_length).into(); let user_handle: Option> = if user_handle.is_null() { None @@ -314,8 +441,16 @@ pub mod android { let private_key = strings::c_char_to_string(private_key); - if options.is_err() { - return null_mut(); + if let Err(e) = options.as_ref() { + return Box::into_raw(Box::new(PublicKeyCredentialAndroid { + id: None, + raw_id: None, + client_extension_results: HashMap::new(), + authenticator_attachment: None, + response: None, + credential_type: None, + error: Some(format!("Error reading options: {:?}", e)), + })); } let origin_str = if origin.is_null() { @@ -326,18 +461,25 @@ pub mod android { let options = options.expect("Checked above"); let response = WebauthnAuthenticator::generate_credential_request_response( credential_id, - attestation_flags, + u8::from_be(attestation_flags), options, origin_str, user_handle, private_key, ); - if response.is_err() { - return null_mut(); + match response { + Ok(response) => Box::into_raw(Box::new(PublicKeyCredentialAndroid::from(response))), + Err(e) => Box::into_raw(Box::new(PublicKeyCredentialAndroid { + id: None, + raw_id: None, + client_extension_results: HashMap::new(), + authenticator_attachment: None, + response: None, + credential_type: None, + error: Some(format!("Error generating response: {:?}", e)), + })), } - - Box::into_raw(Box::new(response.expect("Checked above"))) } #[no_mangle] diff --git a/src/webauthn/proto/raw_message.rs b/src/webauthn/proto/raw_message.rs index be51766..96becf1 100644 --- a/src/webauthn/proto/raw_message.rs +++ b/src/webauthn/proto/raw_message.rs @@ -16,6 +16,7 @@ use serde_cbor::{to_vec, Value}; use serde_derive::*; use std::{ collections::BTreeMap, + fmt::Display, io::{Cursor, Read, Write}, str::FromStr, }; @@ -595,8 +596,8 @@ impl Coordinates { } } -impl ToString for Coordinates { - fn to_string(&self) -> String { +impl Display for Coordinates { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut key = Vec::new(); match self { Coordinates::Compressed { x, y } => { @@ -613,7 +614,7 @@ impl ToString for Coordinates { _ => {} } - base64::encode_config(&key, base64::URL_SAFE_NO_PAD) + write!(f, "{}", base64::encode_config(&key, base64::URL_SAFE_NO_PAD)) } } diff --git a/src/webauthn/proto/web_message.rs b/src/webauthn/proto/web_message.rs index df47c81..75b9e45 100644 --- a/src/webauthn/proto/web_message.rs +++ b/src/webauthn/proto/web_message.rs @@ -212,7 +212,7 @@ pub struct CollectedClientData { pub request_type: String, pub challenge: String, pub origin: String, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] + #[serde(default)] pub cross_origin: bool, #[serde(skip_serializing_if = "Option::is_none")] pub token_binding: Option, diff --git a/wrappers/android/build.sh b/wrappers/android/build.sh index 42c8a3e..5a1d02f 100755 --- a/wrappers/android/build.sh +++ b/wrappers/android/build.sh @@ -1,8 +1,15 @@ #!/bin/bash +CURRENT_FOLDER=$(basename "$PWD") +if [ $CURRENT_FOLDER != "slauth" ]; +then + echo "Please run this script from the root of the project" + exit 1 +fi + export PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH -CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release -CC=x86_64-linux-android21-clang cargo build --target x86_64-linux-android --release +CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release --features "android" +CC=x86_64-linux-android21-clang cargo build --target x86_64-linux-android --release --features "android" -cp ../../target/aarch64-linux-android/release/libslauth.so src/main/jniLibs/arm64-v8a/libslauth.so -cp ../../target/x86_64-linux-android/release/libslauth.so src/main/jniLibs/x86_64/libslauth.so \ No newline at end of file +cp target/aarch64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so +cp target/x86_64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/x86_64/libslauth.so \ No newline at end of file diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java b/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java index de5da61..b66b934 100644 --- a/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java +++ b/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java @@ -1,3 +1,5 @@ +package net.devolutions.slauth; + public enum AttestationFlags { USER_PRESENT(1), //Reserved for future (2) diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java b/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java index 173903b..35c6385 100644 --- a/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java +++ b/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java @@ -77,7 +77,11 @@ public interface JNA extends Library { String get_json_from_request_response(Pointer req); + String get_error_from_request_response(Pointer req); + String get_json_from_creation_response(Pointer req); + String get_error_from_creation_response(Pointer req); + String get_private_key_from_response(Pointer req); } diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java index abe67a3..2f3caeb 100644 --- a/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java @@ -12,6 +12,11 @@ public WebAuthnCreationResponse(String aaguid, byte[] credentialId, String reque if (this.raw == null) { throw new Exception(); } + + String json = this.getJson(); + if (json == null || json.isEmpty()) { + throw new Exception(this.getError()); + } } public String getJson() { @@ -22,6 +27,10 @@ public String getPrivateKey() { return JNA.INSTANCE.get_private_key_from_response(raw); } + public String getError() { + return JNA.INSTANCE.get_error_from_creation_response(raw); + } + @Override public void close() throws IOException { JNA.INSTANCE.response_free(raw); diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java index e55b6b4..11d7b62 100644 --- a/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java @@ -12,12 +12,21 @@ public WebAuthnRequestResponse(byte[] credentialId, String requestJson, String o if (this.raw == null) { throw new Exception(); } + + String json = this.getJson(); + if (json == null || json.isEmpty()) { + throw new Exception(this.getError()); + } } public String getJson() { return JNA.INSTANCE.get_json_from_request_response(raw); } + public String getError() { + return JNA.INSTANCE.get_error_from_request_response(raw); + } + @Override public void close() throws IOException { JNA.INSTANCE.response_free(raw); diff --git a/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so b/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so index 8d15f16..c6318e6 100755 Binary files a/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so and b/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so differ diff --git a/wrappers/android/src/main/jniLibs/x86_64/libslauth.so b/wrappers/android/src/main/jniLibs/x86_64/libslauth.so index 39b9384..6736325 100755 Binary files a/wrappers/android/src/main/jniLibs/x86_64/libslauth.so and b/wrappers/android/src/main/jniLibs/x86_64/libslauth.so differ