diff --git a/.gitignore b/.gitignore index fff574a..7253f42 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,8 @@ Cargo.lock .idea/ .DS_Store /NDK/ -android/NDK/ \ No newline at end of file +android/NDK/ +.gradle +local.properties +wrappers/android/build +build/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9208460..82d1e84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ 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,8 +53,8 @@ ed25519-dalek = { version = "2.1.0", features = ["rand_core", "pkcs8"], optiona 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.20", default-features = false, optional = true } +[target.'cfg(target_os = "android")'.dependencies] +jni = { version = "0.21.1" } [target.'cfg(target_arch="wasm32")'.dependencies] wasm-bindgen = { version = "0.2.91" } diff --git a/android/build.sh b/android/build.sh deleted file mode 100755 index de74552..0000000 --- a/android/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -cbindgen src/lib.rs -l c > slauth.h - -export PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH -CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release --features="android" -CC=armv7a-linux-androideabi21-clang cargo build --target armv7-linux-androideabi --release --features="android" -CC=i686-linux-android21-clang cargo build --target i686-linux-android --release --features="android" -CC=x86_64-linux-android21-clang cargo build --target x86_64-linux-android --release --features="android" diff --git a/android/setup.sh b/android/setup.sh deleted file mode 100755 index 878f3e1..0000000 --- a/android/setup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -rustup target add \ -x86_64-linux-android \ -aarch64-linux-android \ -armv7-linux-androideabi \ -i686-linux-android \ -arm-linux-androideabi - -cargo install cbindgen \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1d60a09 --- /dev/null +++ b/build.gradle @@ -0,0 +1,59 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.9.6" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + apply plugin: 'maven-publish' + repositories { + google() + jcenter() + + } +} + +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) + +project('slauth') { + ext { + libraryVersion = '0.7.6' + } + publishing { + repositories { + maven { + name = "cloudsmith" + url = "https://maven.cloudsmith.io/devolutions/maven-public/" + credentials { + username = System.getenv('CLOUDSMITH_USERNAME') + password = System.getenv('CLOUDSMITH_API_KEY') + } + } + } + publications { + aar(MavenPublication) { + groupId = 'devolutions' + artifactId = project.getName() + version = project.ext.libraryVersion + // Tell maven to prepare the generated "*.aar" file for publishing + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + } + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..b2b3bb2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5e18ce3 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 28 10:53:22 EDT 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..6e5d9ef --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..319ad08 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include 'slauth' +project(':slauth').projectDir = new File(settingsDir, 'wrappers/android') \ No newline at end of file diff --git a/src/webauthn/authenticator/native.rs b/src/webauthn/authenticator/native.rs index d02c1a9..6a61a1e 100644 --- a/src/webauthn/authenticator/native.rs +++ b/src/webauthn/authenticator/native.rs @@ -175,3 +175,173 @@ mod ios { } } } + +#[cfg(target_os = "android")] +pub mod android { + use crate::{ + strings, + webauthn::{ + authenticator::{responses::AuthenticatorCredentialCreationResponse, WebauthnAuthenticator}, + proto::web_message::{ + PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRaw, PublicKeyCredentialRequestOptions, + }, + }, + }; + use std::{ + ffi::{c_uchar, CString}, + os::raw::c_char, + ptr::null_mut, + }; + use uuid::Uuid; + + #[no_mangle] + pub unsafe extern "C" fn generate_credential_creation_response( + aaguid: *const c_char, + credential_id: *const c_uchar, + credential_id_length: usize, + request_json: *const c_char, + origin: *const c_char, + attestation_flags: u8, + ) -> *mut AuthenticatorCredentialCreationResponse { + let aaguid_str = strings::c_char_to_string(aaguid); + let aaguid = Uuid::parse_str(aaguid_str.as_str()); + if aaguid.is_err() { + return null_mut(); + } + let credential_id: Vec = std::slice::from_raw_parts(credential_id, credential_id_length).into(); + + let options: Result = + serde_json::from_str(strings::c_char_to_string(request_json).as_str()); + + if options.is_err() { + return null_mut(); + } + + let origin_str = if origin.is_null() { + None + } else { + Some(strings::c_char_to_string(origin)) + }; + let options = options.expect("Checked above"); + let response = WebauthnAuthenticator::generate_credential_creation_response( + options, + aaguid.expect("Checked above"), + credential_id, + origin_str, + attestation_flags, + ); + + if response.is_err() { + return null_mut(); + } + + Box::into_raw(Box::new(response.expect("Checked above"))) + } + + #[no_mangle] + pub unsafe extern "C" fn get_private_key_from_response(res: *mut AuthenticatorCredentialCreationResponse) -> *mut c_char { + if res.is_null() { + return null_mut(); + } + + let cstring = CString::new((*res).private_key_response.clone()); + match cstring { + Ok(cstring) => cstring.into_raw(), + Err(_) => null_mut(), + } + } + + #[no_mangle] + pub unsafe extern "C" fn get_json_from_creation_response(res: *mut AuthenticatorCredentialCreationResponse) -> *mut c_char { + if res.is_null() { + return null_mut(); + } + + let public_key_credential = PublicKeyCredential::from((*res).credential_response.clone()); + let json = serde_json::to_string(&public_key_credential); + + if json.is_err() { + return null_mut(); + } + + let cstring = CString::new(json.expect("Checked above")); + match cstring { + Ok(cstring) => cstring.into_raw(), + Err(_) => null_mut(), + } + } + + #[no_mangle] + pub unsafe extern "C" fn get_json_from_request_response(res: *mut PublicKeyCredentialRaw) -> *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); + + if json.is_err() { + return null_mut(); + } + + let cstring = CString::new(json.expect("Checked above")); + match cstring { + Ok(cstring) => cstring.into_raw(), + Err(_) => null_mut(), + } + } + + #[no_mangle] + pub unsafe extern "C" fn generate_credential_request_response( + credential_id: *const c_uchar, + credential_id_length: usize, + request_json: *const c_char, + origin: *const c_char, + attestation_flags: u8, + user_handle: *const c_uchar, + user_handle_length: usize, + private_key: *const c_char, + ) -> *mut PublicKeyCredentialRaw { + 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 + } else { + Some(std::slice::from_raw_parts(user_handle, user_handle_length).into()) + }; + + let options: Result = + serde_json::from_str(strings::c_char_to_string(request_json).as_str()); + + let private_key = strings::c_char_to_string(private_key); + + if options.is_err() { + return null_mut(); + } + + let origin_str = if origin.is_null() { + None + } else { + Some(strings::c_char_to_string(origin)) + }; + let options = options.expect("Checked above"); + let response = WebauthnAuthenticator::generate_credential_request_response( + credential_id, + attestation_flags, + options, + origin_str, + user_handle, + private_key, + ); + + if response.is_err() { + return null_mut(); + } + + Box::into_raw(Box::new(response.expect("Checked above"))) + } + + #[no_mangle] + pub unsafe extern "C" fn response_free(res: *mut AuthenticatorCredentialCreationResponse) { + let _ = Box::from_raw(res); + } +} diff --git a/wrappers/android/build.gradle b/wrappers/android/build.gradle new file mode 100644 index 0000000..d78d97d --- /dev/null +++ b/wrappers/android/build.gradle @@ -0,0 +1,56 @@ +apply plugin: 'com.android.library' + +ext { + libraryName = 'Slauth' + artifact = 'slauth' + + libraryDescription = 'An Android wrapper around the rust Slauth implementation' + + siteUrl = 'https://github.com/Devolutions/Slauth' + gitUrl = 'https://github.com/Devolutions/Slauth.git' + + developerId = 'rarchambault' + developerName = 'Richer Archambault' + developerEmail = 'rarchambault@devolutions.net' + + licenseName = 'MIT License' + licenseUrl = 'https://raw.githubusercontent.com/Devolutions/Slauth/master/LICENSE' + allLicenses = ["MIT"] +} + +android { + compileSdkVersion 33 + + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 28 + versionCode 1 + versionName "0.7.6" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + lintOptions { + abortOnError false + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + runtimeOnly fileTree(dir: 'jniLibs', include: ['*.so']) + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'net.java.dev.jna:jna:5.14.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} \ No newline at end of file diff --git a/wrappers/android/build.sh b/wrappers/android/build.sh new file mode 100755 index 0000000..42c8a3e --- /dev/null +++ b/wrappers/android/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +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 + +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 diff --git a/wrappers/android/proguard-rules.pro b/wrappers/android/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/wrappers/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/wrappers/android/src/androidTest/java/net/devolutions/slauth/ExampleInstrumentedTest.java b/wrappers/android/src/androidTest/java/net/devolutions/slauth/ExampleInstrumentedTest.java new file mode 100644 index 0000000..1475cc4 --- /dev/null +++ b/wrappers/android/src/androidTest/java/net/devolutions/slauth/ExampleInstrumentedTest.java @@ -0,0 +1,70 @@ +package net.devolutions.slauth; + +import android.support.test.runner.AndroidJUnit4; +import android.util.Base64; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void totpUris() { + String baseUri = "otpauth://totp/john.doe@email.com?secret=12a9f88729b3bf4477f76b6c65d0e144d8ddc8f1&algorithm=SHA1&digits=6&period=30&issuer=Slauth"; + Totp totp = null; + try { + totp = new Totp(baseUri); + } catch (Exception e) { + e.printStackTrace(); + } + + String genUri = totp.toUri("john.doe@email.com", "Slauth"); + + System.out.println(genUri); + + //assertEquals(baseUri, genUri); No more equal since the baseuri use hex + + String code1 = totp.gen(); + + try { + Thread.sleep(31000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + String code2 = totp.genWith(31); + + assertEquals(code1, code2); + } + + @Test + public void u2fTest() { + try { + byte[] att_cert = android.util.Base64.decode("MIICODCCAd6gAwIBAgIJAKsa9WC9HvEuMAoGCCqGSM49BAMCMFoxDzANBgNVBAMMBlNsYXV0aDELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzETMBEGA1UEBwwKTGF2YWx0cm91ZTEUMBIGA1UECgwLRGV2b2x1dGlvbnMwHhcNMTkwNzAyMTgwMTUyWhcNMzEwNjI5MTgwMTUyWjBaMQ8wDQYDVQQDDAZTbGF1dGgxCzAJBgNVBAYTAkNBMQ8wDQYDVQQIDAZRdWViZWMxEzARBgNVBAcMCkxhdmFsdHJvdWUxFDASBgNVBAoMC0Rldm9sdXRpb25zMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE15PAnpUUIzbgKxD6RFuNMjjl/cD06vKRBtl0X/CiNzc3igTh1qcc00QICgAQUxdvHSn+DaSRki/kI9OJ8lkPGqOBjDCBiTAdBgNVHQ4EFgQU7iZ4JceUHOuWoMymFGm+ZBUmwwgwHwYDVR0jBBgwFoAU7iZ4JceUHOuWoMymFGm+ZBUmwwgwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAVBgNVHREEDjAMggpzbGF1dGgub3JnMAoGCCqGSM49BAMCA0gAMEUCIEdjPFNsund4FXs/1HpK4AXWQ0asfY6ERhNlg29VGS6pAiEAx8f2lrlVV1tASWbC/edTgH9JsCbANuXW/9FZcWHGl2E=", Base64.DEFAULT); + byte[] att_key = android.util.Base64.decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgzgUSoDttmryF0C+ck4GppKwssha7ngah0dfezfTBzDOhRANCAATXk8CelRQjNuArEPpEW40yOOX9wPTq8pEG2XRf8KI3NzeKBOHWpxzTRAgKABBTF28dKf4NpJGSL+Qj04nyWQ8a", Base64.DEFAULT); + + String json = "{\"appId\":\"https://login.devolutions.com/\",\"registerRequests\":[{\"challenge\":\"UzAxNE0yMTBWM1JDYzA1a1JqWndRUT09\",\"version\":\"U2F_V2\"}],\"registeredKeys\":[],\"requestId\":1,\"timeoutSeconds\":300,\"type\":\"u2f_register_request\"}"; + + WebRequest web_r = new WebRequest(json); + + String origin = web_r.getOrigin(); + + WebResponse rsp = web_r.register(origin, att_cert, att_key); + + SigningKey key = rsp.getSigningKey(); + + System.out.println(key.getKeyHandle()); + System.out.println(key.toString()); + System.out.println(rsp.toJson()); + } catch (InvalidResponseTypeException e) { + e.printStackTrace(); + } + } +} diff --git a/wrappers/android/src/main/AndroidManifest.xml b/wrappers/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1b055db --- /dev/null +++ b/wrappers/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java b/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java new file mode 100644 index 0000000..de5da61 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java @@ -0,0 +1,20 @@ +public enum AttestationFlags { + USER_PRESENT(1), + //Reserved for future (2) + USER_VERIFIED(4), + BACKUP_ELIGIBLE(8), + BACKED_UP(16), + //Reserved for future (32) + ATTESTED_CREDENTIAL_DATA_INCLUDED(64), + EXTENSION_DATA_INCLUDED(128); + + private final int value; + + AttestationFlags(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} \ No newline at end of file diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/Hotp.java b/wrappers/android/src/main/java/net/devolutions/slauth/Hotp.java new file mode 100644 index 0000000..ba49b23 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/Hotp.java @@ -0,0 +1,41 @@ +package net.devolutions.slauth; + +import java.io.IOException; + +public class Hotp extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public Hotp(String uri) throws Exception { + this.raw = JNA.INSTANCE.hotp_from_uri(uri); + if (this.raw == null) { + throw new Exception(); + } + } + + public String gen() { + return JNA.INSTANCE.hotp_gen(raw); + } + + public void inc() { + JNA.INSTANCE.hotp_inc(raw); + } + + public String toUri(String label, String issuer) { + return JNA.INSTANCE.hotp_to_uri(raw, label, issuer); + } + + public Boolean validateCurrent(String code) { + return JNA.INSTANCE.hotp_validate_current(raw, code); + } + + public Boolean verify(String code) { + return JNA.INSTANCE.hotp_verify(raw, code); + } + + @Override + public void close() throws IOException { + JNA.INSTANCE.hotp_free(raw); + } +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/InvalidRequestTypeException.java b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidRequestTypeException.java new file mode 100644 index 0000000..f6aad5c --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidRequestTypeException.java @@ -0,0 +1,4 @@ +package net.devolutions.slauth; + +class InvalidRequestTypeException extends Exception { +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/InvalidResponseTypeException.java b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidResponseTypeException.java new file mode 100644 index 0000000..26218b3 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidResponseTypeException.java @@ -0,0 +1,4 @@ +package net.devolutions.slauth; + +class InvalidResponseTypeException extends Exception { +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/InvalidSigningKeyException.java b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidSigningKeyException.java new file mode 100644 index 0000000..8f5625b --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/InvalidSigningKeyException.java @@ -0,0 +1,4 @@ +package net.devolutions.slauth; + +class InvalidSigningKeyException extends Exception { +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java b/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java new file mode 100644 index 0000000..173903b --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java @@ -0,0 +1,83 @@ +package net.devolutions.slauth; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +public interface JNA extends Library { + String JNA_LIBRARY_NAME = "slauth"; + + JNA INSTANCE = Native.load(JNA_LIBRARY_NAME, JNA.class); + + Pointer hotp_from_uri(String uri); + + void hotp_free(Pointer hotp); + + String hotp_gen(Pointer hotp); + + void hotp_inc(Pointer hotp); + + String hotp_to_uri(Pointer hotp, String label, String issuer); + + Boolean hotp_validate_current(Pointer hotp, String code); + + Boolean hotp_verify(Pointer hotp, String code); + + void totp_free(Pointer totp); + + Pointer totp_from_uri(String uri); + + String totp_gen(Pointer totp); + + String totp_gen_with(Pointer totp, long elapsed); + + String totp_to_uri(Pointer totp, String label, String issuer); + + Boolean totp_validate_current(Pointer totp, String code); + + Boolean totp_verify(Pointer totp, String code); + + void client_web_response_free(Pointer rsp); + + Pointer client_web_response_signing_key(Pointer rsp); + + String client_web_response_to_json(Pointer rsp); + + void signing_key_free(Pointer s); + + Pointer signing_key_from_string(String s); + + String signing_key_to_string(Pointer s); + + String signing_key_get_key_handle(Pointer s); + + void web_request_free(Pointer req); + + Pointer web_request_from_json(String req); + + Boolean web_request_is_register(Pointer req); + + Boolean web_request_is_sign(Pointer req); + + String web_request_key_handle(Pointer req, String origin); + + String web_request_origin(Pointer req); + + Pointer web_request_register(Pointer req, String origin, byte[] attestation_cert, long attestation_cert_len, byte[] attestation_key, long attestation_key_len); + + Pointer web_request_sign(Pointer req, Pointer signing_key, String origin, long counter, Boolean user_presence); + + long web_request_timeout(Pointer req); + + Pointer generate_credential_creation_response(String aaguid, byte[] credential_id, long credential_id_len, String request_json, String origin, byte attestation_flags); + + Pointer generate_credential_request_response(byte[] credential_id, long credential_id_len, String request_json, String origin, byte attestation_flags, byte[] user_handle, long user_handle_length, String private_key); + + void response_free(Pointer req); + + String get_json_from_request_response(Pointer req); + + String get_json_from_creation_response(Pointer req); + + String get_private_key_from_response(Pointer req); +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/RustObject.java b/wrappers/android/src/main/java/net/devolutions/slauth/RustObject.java new file mode 100644 index 0000000..7f2bd37 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/RustObject.java @@ -0,0 +1,8 @@ +package net.devolutions.slauth; + +import com.sun.jna.Pointer; +import java.io.Closeable; + +abstract class RustObject implements Closeable { + Pointer raw; +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/SigningKey.java b/wrappers/android/src/main/java/net/devolutions/slauth/SigningKey.java new file mode 100644 index 0000000..e97c637 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/SigningKey.java @@ -0,0 +1,37 @@ +package net.devolutions.slauth; + +import com.sun.jna.Pointer; + +import java.io.IOException; + +public class SigningKey extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public SigningKey(Pointer raw) { + this.raw = raw; + } + + public SigningKey(String string) throws InvalidSigningKeyException { + Pointer p = JNA.INSTANCE.signing_key_from_string(string); + if (p == null) { + throw new InvalidSigningKeyException(); + } + + this.raw = p; + } + + public String toString() { + return JNA.INSTANCE.signing_key_to_string(raw); + } + + public String getKeyHandle() { + return JNA.INSTANCE.signing_key_get_key_handle(raw); + } + + @Override + public void close() throws IOException { + JNA.INSTANCE.signing_key_free(raw); + } +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/Totp.java b/wrappers/android/src/main/java/net/devolutions/slauth/Totp.java new file mode 100644 index 0000000..4c725bd --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/Totp.java @@ -0,0 +1,41 @@ +package net.devolutions.slauth; + +import java.io.IOException; + +public class Totp extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public Totp(String uri) throws Exception { + this.raw = JNA.INSTANCE.totp_from_uri(uri); + if (this.raw == null) { + throw new Exception(); + } + } + + public String gen() { + return JNA.INSTANCE.totp_gen(raw); + } + + public String genWith(long elapsed) { + return JNA.INSTANCE.totp_gen_with(raw, elapsed); + } + + public String toUri(String label, String issuer) { + return JNA.INSTANCE.totp_to_uri(raw, label, issuer); + } + + public Boolean validateCurrent(String code) { + return JNA.INSTANCE.totp_validate_current(raw, code); + } + + public Boolean verify(String code) { + return JNA.INSTANCE.totp_verify(raw, code); + } + + @Override + public void close() throws IOException { + JNA.INSTANCE.totp_free(raw); + } +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java new file mode 100644 index 0000000..abe67a3 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java @@ -0,0 +1,30 @@ +package net.devolutions.slauth; + +import java.io.IOException; + +public class WebAuthnCreationResponse extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public WebAuthnCreationResponse(String aaguid, byte[] credentialId, String requestJson, String origin, byte attestationFlags) throws Exception { + this.raw = JNA.INSTANCE.generate_credential_creation_response(aaguid, credentialId, credentialId.length, requestJson, origin, attestationFlags); + if (this.raw == null) { + throw new Exception(); + } + } + + public String getJson() { + return JNA.INSTANCE.get_json_from_creation_response(raw); + } + + public String getPrivateKey() { + return JNA.INSTANCE.get_private_key_from_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 new file mode 100644 index 0000000..e55b6b4 --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java @@ -0,0 +1,25 @@ +package net.devolutions.slauth; + +import java.io.IOException; + +public class WebAuthnRequestResponse extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public WebAuthnRequestResponse(byte[] credentialId, String requestJson, String origin, byte attestationFlags, byte[] userHandle, String privateKey) throws Exception { + this.raw = JNA.INSTANCE.generate_credential_request_response(credentialId, credentialId.length, requestJson, origin, attestationFlags, userHandle, userHandle.length, privateKey); + if (this.raw == null) { + throw new Exception(); + } + } + + public String getJson() { + return JNA.INSTANCE.get_json_from_request_response(raw); + } + + @Override + public void close() throws IOException { + JNA.INSTANCE.response_free(raw); + } +} diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/WebRequest.java b/wrappers/android/src/main/java/net/devolutions/slauth/WebRequest.java new file mode 100644 index 0000000..b503c5e --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebRequest.java @@ -0,0 +1,58 @@ +package net.devolutions.slauth; + +import com.sun.jna.Pointer; + +import java.io.IOException; +import java.util.Optional; + +public class WebRequest extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public WebRequest(String json) { + this.raw = JNA.INSTANCE.web_request_from_json(json); + } + + public Boolean isRegister() { + return JNA.INSTANCE.web_request_is_register(raw); + } + + public Boolean isSign() { + return JNA.INSTANCE.web_request_is_sign(raw); + } + + public String getOrigin() { + return JNA.INSTANCE.web_request_origin(raw); + } + + public long getTimeout() { + return JNA.INSTANCE.web_request_timeout(raw); + } + + public String getKeyHandle(String origin) throws InvalidRequestTypeException { + if (this.isSign()) { + return JNA.INSTANCE.web_request_key_handle(raw, origin); + } else { + throw new InvalidRequestTypeException(); + } + } + + public WebResponse register(String origin, byte[] attestationCert, byte[] attestationKey) { + Pointer p = JNA.INSTANCE.web_request_register(raw, origin, attestationCert, attestationCert.length, attestationKey, attestationKey.length); + + return new WebResponse(p); + } + + public WebResponse sign(String origin, SigningKey key, int counter, Boolean userPresence) { + Pointer p = JNA.INSTANCE.web_request_sign(raw, key.raw, origin, counter, userPresence); + + return new WebResponse(p); + } + + @Override + public void close() throws IOException { + JNA.INSTANCE.web_request_free(raw); + } +} + diff --git a/wrappers/android/src/main/java/net/devolutions/slauth/WebResponse.java b/wrappers/android/src/main/java/net/devolutions/slauth/WebResponse.java new file mode 100644 index 0000000..b313c9f --- /dev/null +++ b/wrappers/android/src/main/java/net/devolutions/slauth/WebResponse.java @@ -0,0 +1,36 @@ +package net.devolutions.slauth; + +import com.sun.jna.Pointer; + +import java.io.IOException; + +public class WebResponse extends RustObject { + static { + System.loadLibrary("slauth"); + } + + public WebResponse(Pointer raw) { + this.raw = raw; + } + + public String toJson() { + return JNA.INSTANCE.client_web_response_to_json(raw); + } + + public SigningKey getSigningKey() throws InvalidResponseTypeException { + Pointer p = JNA.INSTANCE.client_web_response_signing_key(raw); + + if (p == null) { + throw new InvalidResponseTypeException(); + } + + return new SigningKey(p); + } + + + @Override + public void close() throws IOException { + JNA.INSTANCE.client_web_response_free(raw); + } +} + diff --git a/wrappers/android/src/main/jniLibs/arm64-v8a/libjnidispatch.so b/wrappers/android/src/main/jniLibs/arm64-v8a/libjnidispatch.so new file mode 100644 index 0000000..0b482c5 Binary files /dev/null and b/wrappers/android/src/main/jniLibs/arm64-v8a/libjnidispatch.so differ diff --git a/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so b/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so new file mode 100755 index 0000000..8d15f16 Binary files /dev/null and b/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so differ diff --git a/wrappers/android/src/main/jniLibs/x86_64/libjnidispatch.so b/wrappers/android/src/main/jniLibs/x86_64/libjnidispatch.so new file mode 100644 index 0000000..9ec622f Binary files /dev/null and b/wrappers/android/src/main/jniLibs/x86_64/libjnidispatch.so differ diff --git a/wrappers/android/src/main/jniLibs/x86_64/libslauth.so b/wrappers/android/src/main/jniLibs/x86_64/libslauth.so new file mode 100755 index 0000000..39b9384 Binary files /dev/null and b/wrappers/android/src/main/jniLibs/x86_64/libslauth.so differ diff --git a/wrappers/android/src/main/res/values/strings.xml b/wrappers/android/src/main/res/values/strings.xml new file mode 100644 index 0000000..59cb192 --- /dev/null +++ b/wrappers/android/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Slauth + diff --git a/wrappers/android/src/test/java/net/devolutions/slauth/ExampleUnitTest.java b/wrappers/android/src/test/java/net/devolutions/slauth/ExampleUnitTest.java new file mode 100644 index 0000000..a719011 --- /dev/null +++ b/wrappers/android/src/test/java/net/devolutions/slauth/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package net.devolutions.slauth; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file