├── wrappers
├── wasm
│ ├── example
│ │ └── otp-example
│ │ │ ├── src
│ │ │ ├── assets
│ │ │ │ └── .gitkeep
│ │ │ ├── app
│ │ │ │ ├── app.component.css
│ │ │ │ ├── app-routing.module.ts
│ │ │ │ ├── app.module.ts
│ │ │ │ ├── services
│ │ │ │ │ └── otp.service.ts
│ │ │ │ ├── app.component.spec.ts
│ │ │ │ └── app.component.ts
│ │ │ ├── environments
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── styles.css
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ ├── test.ts
│ │ │ └── polyfills.ts
│ │ │ ├── e2e
│ │ │ ├── tsconfig.json
│ │ │ ├── src
│ │ │ │ ├── app.po.ts
│ │ │ │ └── app.e2e-spec.ts
│ │ │ └── protractor.conf.js
│ │ │ ├── tsconfig.app.json
│ │ │ ├── .editorconfig
│ │ │ ├── tsconfig.spec.json
│ │ │ ├── browserslist
│ │ │ ├── tsconfig.json
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── karma.conf.js
│ │ │ ├── package.json
│ │ │ ├── tslint.json
│ │ │ └── angular.json
│ ├── build.sh
│ └── build-web.sh
├── swift
│ ├── ffi
│ │ ├── module.modulemap
│ │ └── framework.modulemap
│ ├── classes
│ │ ├── RustObject.swift
│ │ ├── SlauthUtils.swift
│ │ ├── Hotp.swift
│ │ ├── Totp.swift
│ │ ├── WebAuthnRequestResponse.swift
│ │ ├── WebAuthnCreationResponse.swift
│ │ └── U2f.swift
│ ├── Package.swift
│ └── build.sh
└── android
│ ├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ ├── jniLibs
│ │ │ ├── x86_64
│ │ │ │ └── libslauth.so
│ │ │ └── arm64-v8a
│ │ │ │ └── libslauth.so
│ │ └── java
│ │ │ └── net
│ │ │ └── devolutions
│ │ │ └── slauth
│ │ │ ├── InvalidRequestTypeException.java
│ │ │ ├── InvalidResponseTypeException.java
│ │ │ ├── InvalidSigningKeyException.java
│ │ │ ├── RustObject.java
│ │ │ ├── SlauthUtils.java
│ │ │ ├── AttestationFlags.java
│ │ │ ├── WebResponse.java
│ │ │ ├── SigningKey.java
│ │ │ ├── Hotp.java
│ │ │ ├── Totp.java
│ │ │ ├── WebAuthnRequestResponse.java
│ │ │ ├── WebAuthnCreationResponse.java
│ │ │ ├── WebRequest.java
│ │ │ └── JNA.java
│ ├── test
│ │ └── java
│ │ │ └── net
│ │ │ └── devolutions
│ │ │ └── slauth
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── net
│ │ └── devolutions
│ │ └── slauth
│ │ └── ExampleInstrumentedTest.java
│ ├── build.sh
│ ├── proguard-rules.pro
│ └── build.gradle
├── fuzz
├── .gitignore
├── fuzz_targets
│ ├── fuzz_webauthn_messages.rs
│ └── fuzz_messages.rs
└── Cargo.toml
├── settings.gradle
├── src
├── u2f
│ ├── proto
│ │ ├── mod.rs
│ │ ├── constants.rs
│ │ ├── hid.rs
│ │ └── web_message.rs
│ ├── error.rs
│ ├── mod.rs
│ ├── client
│ │ └── token.rs
│ └── server
│ │ └── mod.rs
├── webauthn
│ ├── proto
│ │ ├── mod.rs
│ │ ├── constants.rs
│ │ └── web_message.rs
│ ├── mod.rs
│ ├── authenticator
│ │ └── responses.rs
│ └── error.rs
├── base64.rs
├── lib.rs
├── oath
│ ├── mod.rs
│ └── hotp.rs
└── wasm.rs
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── rustfmt.toml
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── rust.yml
│ └── publish.yml
├── .cargo
└── config.toml
├── .gitignore
├── gradle.properties
├── LICENSE
├── README.md
├── Cargo.toml
├── gradlew
├── examples
└── web-server.rs
├── slauth.h
└── deny.toml
/wrappers/wasm/example/otp-example/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fuzz/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | target
3 | corpus
4 | artifacts
5 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/app.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'slauth'
2 | project(':slauth').projectDir = new File(settingsDir, 'wrappers/android')
--------------------------------------------------------------------------------
/src/u2f/proto/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod constants;
2 | pub mod hid;
3 | pub mod raw_message;
4 | pub mod web_message;
5 |
--------------------------------------------------------------------------------
/src/webauthn/proto/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod constants;
2 | pub mod raw_message;
3 | pub mod tpm;
4 | pub mod web_message;
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Devolutions/slauth/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/wrappers/swift/ffi/module.modulemap:
--------------------------------------------------------------------------------
1 | module SlauthFFI [system] {
2 | header "../headers/slauth.h"
3 | export *
4 | }
5 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Slauth
3 |
4 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Devolutions/slauth/HEAD/wrappers/wasm/example/otp-example/src/favicon.ico
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | condense_wildcard_suffixes = true
2 | reorder_impl_items = true
3 | reorder_imports = true
4 | imports_granularity = "Crate"
5 | max_width = 140
6 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/jniLibs/x86_64/libslauth.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Devolutions/slauth/HEAD/wrappers/android/src/main/jniLibs/x86_64/libslauth.so
--------------------------------------------------------------------------------
/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Devolutions/slauth/HEAD/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so
--------------------------------------------------------------------------------
/wrappers/wasm/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | wasm-pack build --scope devolutions --out-dir ./dist/bundler --target bundler -- --no-default-features --features "webauthn"
3 |
--------------------------------------------------------------------------------
/wrappers/swift/ffi/framework.modulemap:
--------------------------------------------------------------------------------
1 | framework module SlauthFFI {
2 | umbrella header "../headers/slauth.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/InvalidRequestTypeException.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | class InvalidRequestTypeException extends Exception {
4 | }
5 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/InvalidResponseTypeException.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | class InvalidResponseTypeException extends Exception {
4 | }
5 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/InvalidSigningKeyException.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | class InvalidSigningKeyException extends Exception {
4 | }
5 |
--------------------------------------------------------------------------------
/src/webauthn/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "webauthn")]
2 | pub mod authenticator;
3 | #[cfg(feature = "webauthn")]
4 | pub mod error;
5 | #[cfg(feature = "webauthn")]
6 | pub mod proto;
7 |
8 | #[cfg(feature = "webauthn-server")]
9 | pub mod server;
10 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/RustObject.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import com.sun.jna.Pointer;
4 | import java.io.Closeable;
5 |
6 | abstract class RustObject implements Closeable {
7 | Pointer raw;
8 | }
9 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # File auto-generated and managed by Devops
2 | /.github/workflows @devolutions/devops
3 | /.github/randy.yml @devolutions/devops
4 | /.github/CODEOWNERS @devolutions/devops
5 | /.github/scripts/ @devolutions/devops
6 | /.github/dependabot.yml @devolutions/security-managers
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 28 10:53:22 EDT 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
7 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "jasminewd2",
10 | "node"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "cargo"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 | assignees:
9 | - "devolutions/lucid-maintainers"
10 | # Disable version updates, we only want security updates.
11 | open-pull-requests-limit: 0
--------------------------------------------------------------------------------
/fuzz/fuzz_targets/fuzz_webauthn_messages.rs:
--------------------------------------------------------------------------------
1 | #![no_main]
2 | #[macro_use] extern crate libfuzzer_sys;
3 | extern crate slauth;
4 |
5 | use slauth::webauthn::proto::raw_message::*;
6 |
7 | fuzz_target!(|data: &[u8]| {
8 | let _ = AttestationObject::from_bytes(data);
9 | let _ = AuthenticatorData::from_vec(data.to_vec());
10 | });
11 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 |
4 |
5 | const routes: Routes = [];
6 |
7 | @NgModule({
8 | imports: [RouterModule.forRoot(routes)],
9 | exports: [RouterModule]
10 | })
11 | export class AppRoutingModule { }
12 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo(): Promise {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText(): Promise {
9 | return element(by.css('app-root .content span')).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OtpExample
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/RustObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RustObject.swift
3 | // firebase
4 | //
5 | // Created by Richer Archambault on 2019-04-26.
6 | // Copyright © 2019 Sebastien Aubin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol RustObject {
12 | init(raw: OpaquePointer)
13 | func intoRaw() -> OpaquePointer
14 | }
15 |
16 | struct Err: Error {
17 | let message: String
18 | }
19 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.aarch64-linux-android]
2 | ar = "aarch64-linux-android-ar"
3 | linker = "aarch64-linux-android21-clang"
4 |
5 | [target.armv7-linux-androideabi]
6 | ar = "arm-linux-androideabi-ar"
7 | linker = "armv7a-linux-androideabi21-clang"
8 |
9 | [target.i686-linux-android]
10 | ar = "i686-linux-android-ar"
11 | linker = "i686-linux-android21-clang"
12 |
13 | [target.x86_64-linux-android]
14 | ar = "x86_64-linux-android-ar"
15 | linker = "x86_64-linux-android21-clang"
16 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/SlauthUtils.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | public class SlauthUtils {
4 | static {
5 | System.loadLibrary("slauth_jni");
6 | }
7 |
8 | public String privateKeyToPkcs8Der(String key) {
9 | return JNA.INSTANCE.private_key_to_pkcs8_der(key);
10 | }
11 |
12 | public String Pkcs8ToCustomPrivateKey(String key) {
13 | return JNA.INSTANCE.pkcs8_to_custom_private_key(key);
14 | }
15 | }
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/wrappers/android/src/test/java/net/devolutions/slauth/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7 | Cargo.lock
8 |
9 | # These are backup files generated by rustfmt
10 | **/*.rs.bk
11 |
12 | /target
13 | **/*.rs.bk
14 | Cargo.lock
15 | .idea/
16 | .DS_Store
17 | /NDK/
18 | android/NDK/
19 | .gradle
20 | local.properties
21 | wrappers/android/build
22 | build/
23 | libslauth.xcframework/
24 | .swiftpm/
25 | package/
26 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/AttestationFlags.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | public enum AttestationFlags {
4 | USER_PRESENT(1),
5 | //Reserved for future (2)
6 | USER_VERIFIED(4),
7 | BACKUP_ELIGIBLE(8),
8 | BACKED_UP(16),
9 | //Reserved for future (32)
10 | ATTESTED_CREDENTIAL_DATA_INCLUDED(64),
11 | EXTENSION_DATA_INCLUDED(128);
12 |
13 | private final int value;
14 |
15 | AttestationFlags(int value) {
16 | this.value = value;
17 | }
18 |
19 | public int getValue() {
20 | return value;
21 | }
22 | }
--------------------------------------------------------------------------------
/wrappers/wasm/build-web.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # https://stackoverflow.com/a/246128/1775923
4 | SOURCE=${BASH_SOURCE[0]}
5 | while [ -L "$SOURCE" ]; do
6 | DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )
7 | SOURCE=$(readlink "$SOURCE")
8 | [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE
9 | done
10 | DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )
11 |
12 | wasm-pack build --scope devolutions --out-dir ./dist/web --target web -- --no-default-features --features "webauthn"
13 | sed -i 's/"@devolutions\/slauth"/"@devolutions\/slauth-web"/' ${DIR}/../../dist/web/package.json
14 |
--------------------------------------------------------------------------------
/fuzz/Cargo.toml:
--------------------------------------------------------------------------------
1 |
2 | [package]
3 | name = "slauth-fuzz"
4 | version = "0.0.1"
5 | authors = ["Automatically generated"]
6 | publish = false
7 |
8 | [package.metadata]
9 | cargo-fuzz = true
10 |
11 | [dependencies.slauth]
12 | path = ".."
13 | [dependencies.libfuzzer-sys]
14 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git"
15 |
16 | # Prevent this from interfering with workspaces
17 | [workspace]
18 | members = ["."]
19 |
20 | [[bin]]
21 | name = "fuzz_messages"
22 | path = "fuzz_targets/fuzz_messages.rs"
23 |
24 | [[bin]]
25 | name = "fuzz_webauthn_messages"
26 | path = "fuzz_targets/fuzz_webauthn_messages.rs"
27 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "lib": [
15 | "es2018",
16 | "dom"
17 | ]
18 | },
19 | "angularCompilerOptions": {
20 | "fullTemplateTypeCheck": true,
21 | "strictInjectionParameters": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/SlauthUtils.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if canImport(SlauthFFI)
4 | import SlauthFFI
5 | #endif
6 |
7 | public class SlauthUtils {
8 | public static func convertPkcs8ToPrivateKey(pkcs8String: String) -> String {
9 | let cString = pkcs8_to_custom_private_key(pkcs8String)
10 | let privateKey = String(cString: cString!)
11 | free(cString)
12 | return privateKey
13 | }
14 |
15 | public static func convertPrivateKeyToPkcs8(privateKey: String) -> String {
16 | let cString = private_key_to_pkcs8_der(privateKey)
17 | let pkcs8String = String(cString: cString!)
18 | free(cString)
19 | return pkcs8String
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {NgModule} from '@angular/core';
3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms';
4 | import {AppRoutingModule} from './app-routing.module';
5 | import {QRCodeModule} from 'angularx-qrcode';
6 | import {AppComponent} from './app.component';
7 |
8 | @NgModule({
9 | declarations: [
10 | AppComponent
11 | ],
12 | imports: [
13 | BrowserModule,
14 | AppRoutingModule,
15 | FormsModule,
16 | ReactiveFormsModule,
17 | QRCodeModule
18 | ],
19 | providers: [],
20 | bootstrap: [AppComponent]
21 | })
22 | export class AppModule {
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 | import { browser, logging } from 'protractor';
3 |
4 | describe('workspace-project App', () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it('should display welcome message', () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual('otp-example app is running!');
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER);
19 | expect(logs).not.toContain(jasmine.objectContaining({
20 | level: logging.Level.SEVERE,
21 | } as logging.Entry));
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/base64.rs:
--------------------------------------------------------------------------------
1 | pub use base64::Engine as _;
2 | use base64::{
3 | alphabet,
4 | engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
5 | };
6 |
7 | const CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new()
8 | .with_encode_padding(true)
9 | .with_decode_padding_mode(DecodePaddingMode::Indifferent)
10 | .with_decode_allow_trailing_bits(true);
11 |
12 | const CONFIG_NO_PAD: GeneralPurposeConfig = GeneralPurposeConfig::new()
13 | .with_encode_padding(false)
14 | .with_decode_padding_mode(DecodePaddingMode::Indifferent)
15 | .with_decode_allow_trailing_bits(true);
16 |
17 | pub const BASE64: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, CONFIG);
18 | pub const BASE64_URLSAFE_NOPAD: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, CONFIG_NO_PAD);
19 |
--------------------------------------------------------------------------------
/wrappers/swift/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.10
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Slauth",
6 | platforms: [
7 | .iOS(.v16)
8 | ],
9 | products: [
10 | .library(
11 | name: "Slauth",
12 | targets: ["Slauth"]
13 | )
14 | ],
15 | targets: [
16 | .systemLibrary(
17 | name: "SlauthFFI",
18 | path: "ffi"
19 | ),
20 | .binaryTarget(
21 | name: "libslauth",
22 | path: "libslauth.xcframework"
23 | ),
24 | .target(
25 | name: "Slauth",
26 | dependencies: [
27 | "SlauthFFI",
28 | "libslauth"
29 | ],
30 | path: "classes"
31 | )
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/wrappers/android/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CURRENT_FOLDER=$(basename "$PWD")
4 | if [ $CURRENT_FOLDER != "slauth" ];
5 | then
6 | echo "Please run this script from the root of the project"
7 | exit 1
8 | fi
9 |
10 | export RUSTFLAGS="-C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"
11 | export PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH
12 | CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release --features "android"
13 | CC=x86_64-linux-android21-clang cargo build --target x86_64-linux-android --release --features "android"
14 |
15 | cp target/aarch64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so
16 | cp target/x86_64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/x86_64/libslauth.so
--------------------------------------------------------------------------------
/src/webauthn/authenticator/responses.rs:
--------------------------------------------------------------------------------
1 | use crate::webauthn::proto::{raw_message::CoseAlgorithmIdentifier, web_message::PublicKeyCredentialRaw};
2 | use serde_derive::{Deserialize, Serialize};
3 |
4 | #[derive(Serialize, Clone)]
5 | pub struct AuthenticatorCredentialCreationResponse {
6 | pub credential_response: PublicKeyCredentialRaw,
7 | pub private_key_response: String,
8 | pub additional_data: AuthenticatorCredentialCreationResponseAdditionalData,
9 | }
10 |
11 | #[derive(Serialize, Clone)]
12 | pub struct AuthenticatorCredentialCreationResponseAdditionalData {
13 | pub public_key_der: Vec,
14 | pub public_key_alg: i64,
15 | }
16 |
17 | #[derive(Serialize, Deserialize)]
18 | pub struct PrivateKeyResponse {
19 | pub private_key: Vec,
20 | #[serde(default)]
21 | pub key_alg: CoseAlgorithmIdentifier,
22 | }
23 |
--------------------------------------------------------------------------------
/wrappers/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: {
11 | context(path: string, deep?: boolean, filter?: RegExp): {
12 | keys(): string[];
13 | (id: string): T;
14 | };
15 | };
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting()
21 | );
22 | // Then we find all the tests.
23 | const context = require.context('./', true, /\.spec\.ts$/);
24 | // And load the modules.
25 | context.keys().map(context);
26 |
--------------------------------------------------------------------------------
/fuzz/fuzz_targets/fuzz_messages.rs:
--------------------------------------------------------------------------------
1 | #![no_main]
2 | #[macro_use] extern crate libfuzzer_sys;
3 | extern crate slauth;
4 |
5 | use slauth::u2f::proto::raw_message::{Message, AuthenticateRequest, AuthenticateResponse, RegisterRequest,
6 | RegisterResponse, VersionRequest, VersionResponse};
7 | use slauth::u2f::proto::raw_message::apdu::{ApduFrame, Request, Response};
8 |
9 | fuzz_target!(|data: &[u8]| {
10 | if let Ok(req) = Request::read_from(data) {
11 | let _ = AuthenticateRequest::from_apdu(req.clone());
12 | let _ = RegisterRequest::from_apdu(req.clone());
13 | let _ = VersionRequest::from_apdu(req);
14 | };
15 |
16 | if let Ok(rsp) = Response::read_from(data) {
17 | let _ = AuthenticateResponse::from_apdu(rsp.clone());
18 | let _ = RegisterResponse::from_apdu(rsp.clone());
19 | let _ = VersionResponse::from_apdu(rsp);
20 | };
21 |
22 | });
23 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/WebResponse.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import com.sun.jna.Pointer;
4 |
5 | import java.io.IOException;
6 |
7 | public class WebResponse extends RustObject {
8 | static {
9 | System.loadLibrary("slauth");
10 | }
11 |
12 | public WebResponse(Pointer raw) {
13 | this.raw = raw;
14 | }
15 |
16 | public String toJson() {
17 | return JNA.INSTANCE.client_web_response_to_json(raw);
18 | }
19 |
20 | public SigningKey getSigningKey() throws InvalidResponseTypeException {
21 | Pointer p = JNA.INSTANCE.client_web_response_signing_key(raw);
22 |
23 | if (p == null) {
24 | throw new InvalidResponseTypeException();
25 | }
26 |
27 | return new SigningKey(p);
28 | }
29 |
30 |
31 | @Override
32 | public void close() throws IOException {
33 | JNA.INSTANCE.client_web_response_free(raw);
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: [
13 | './src/**/*.e2e-spec.ts'
14 | ],
15 | capabilities: {
16 | browserName: 'chrome'
17 | },
18 | directConnect: true,
19 | baseUrl: 'http://localhost:4200/',
20 | framework: 'jasmine',
21 | jasmineNodeOpts: {
22 | showColors: true,
23 | defaultTimeoutInterval: 30000,
24 | print: function() {}
25 | },
26 | onPrepare() {
27 | require('ts-node').register({
28 | project: require('path').join(__dirname, './tsconfig.json')
29 | });
30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
31 | }
32 | };
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/SigningKey.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import com.sun.jna.Pointer;
4 |
5 | import java.io.IOException;
6 |
7 | public class SigningKey extends RustObject {
8 | static {
9 | System.loadLibrary("slauth");
10 | }
11 |
12 | public SigningKey(Pointer raw) {
13 | this.raw = raw;
14 | }
15 |
16 | public SigningKey(String string) throws InvalidSigningKeyException {
17 | Pointer p = JNA.INSTANCE.signing_key_from_string(string);
18 | if (p == null) {
19 | throw new InvalidSigningKeyException();
20 | }
21 |
22 | this.raw = p;
23 | }
24 |
25 | public String toString() {
26 | return JNA.INSTANCE.signing_key_to_string(raw);
27 | }
28 |
29 | public String getKeyHandle() {
30 | return JNA.INSTANCE.signing_key_get_key_handle(raw);
31 | }
32 |
33 | @Override
34 | public void close() throws IOException {
35 | JNA.INSTANCE.signing_key_free(raw);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/wrappers/swift/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | rm -rf ./package
5 |
6 | # Build Rust libraries for iOS targets
7 | cargo build --target aarch64-apple-ios --release
8 | cargo build --target aarch64-apple-ios-sim --release
9 | cargo build --target aarch64-apple-darwin --release
10 | cargo build --target x86_64-apple-darwin --release
11 |
12 | lipo target/x86_64-apple-darwin/release/libslauth.a target/aarch64-apple-darwin/release/libslauth.a -create -output target/libslauth.a
13 |
14 | mkdir package
15 | mkdir ./package/headers
16 | cp slauth.h ./package/headers
17 |
18 | # Create XCFramework
19 | xcodebuild -create-xcframework \
20 | -library target/aarch64-apple-ios/release/libslauth.a -headers ./package/headers \
21 | -library target/aarch64-apple-ios-sim/release/libslauth.a -headers ./package/headers \
22 | -library target/libslauth.a -headers ./package/headers \
23 | -output ./package/libslauth.xcframework
24 |
25 | cp wrappers/swift/Package.swift ./package
26 | cp -R wrappers/swift/classes ./package
27 | cp -R wrappers/swift/ffi ./package
28 |
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Devolutions
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/Hotp.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import java.io.IOException;
4 |
5 | public class Hotp extends RustObject {
6 | static {
7 | System.loadLibrary("slauth");
8 | }
9 |
10 | public Hotp(String uri) throws Exception {
11 | this.raw = JNA.INSTANCE.hotp_from_uri(uri);
12 | if (this.raw == null) {
13 | throw new Exception();
14 | }
15 | }
16 |
17 | public String gen() {
18 | return JNA.INSTANCE.hotp_gen(raw);
19 | }
20 |
21 | public void inc() {
22 | JNA.INSTANCE.hotp_inc(raw);
23 | }
24 |
25 | public String toUri(String label, String issuer) {
26 | return JNA.INSTANCE.hotp_to_uri(raw, label, issuer);
27 | }
28 |
29 | public Boolean validateCurrent(String code) {
30 | return JNA.INSTANCE.hotp_validate_current(raw, code);
31 | }
32 |
33 | public Boolean verify(String code) {
34 | return JNA.INSTANCE.hotp_verify(raw, code);
35 | }
36 |
37 | @Override
38 | public void close() throws IOException {
39 | JNA.INSTANCE.hotp_free(raw);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/README.md:
--------------------------------------------------------------------------------
1 | # OtpExample
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.5.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/otp-example'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/Totp.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import java.io.IOException;
4 |
5 | public class Totp extends RustObject {
6 | static {
7 | System.loadLibrary("slauth");
8 | }
9 |
10 | public Totp(String uri) throws Exception {
11 | this.raw = JNA.INSTANCE.totp_from_uri(uri);
12 | if (this.raw == null) {
13 | throw new Exception();
14 | }
15 | }
16 |
17 | public String gen() {
18 | return JNA.INSTANCE.totp_gen(raw);
19 | }
20 |
21 | public String genWith(long elapsed) {
22 | return JNA.INSTANCE.totp_gen_with(raw, elapsed);
23 | }
24 |
25 | public String toUri(String label, String issuer) {
26 | return JNA.INSTANCE.totp_to_uri(raw, label, issuer);
27 | }
28 |
29 | public Boolean validateCurrent(String code) {
30 | return JNA.INSTANCE.totp_validate_current(raw, code);
31 | }
32 |
33 | public Boolean verify(String code) {
34 | return JNA.INSTANCE.totp_verify(raw, code);
35 | }
36 |
37 | @Override
38 | public void close() throws IOException {
39 | JNA.INSTANCE.totp_free(raw);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/services/otp.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {ReplaySubject} from 'rxjs';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class OtpService {
8 | module: typeof import('slauth');
9 |
10 | ready = new ReplaySubject(1);
11 |
12 | constructor() {
13 | if (this.isWebAssemblySupported()) {
14 | // @ts-ignore
15 | import('slauth').then(module => {
16 | this.module = module;
17 | this.ready.next(!!this.module);
18 | });
19 | }
20 | }
21 |
22 | isWebAssemblySupported(): boolean {
23 | try {
24 | if (typeof WebAssembly === 'object'
25 | && typeof WebAssembly.instantiate === 'function') {
26 | const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
27 | if (module instanceof WebAssembly.Module) {
28 | return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
29 | }
30 | }
31 | } catch (e) {
32 | }
33 | return false;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async(() => {
7 | TestBed.configureTestingModule({
8 | imports: [
9 | RouterTestingModule
10 | ],
11 | declarations: [
12 | AppComponent
13 | ],
14 | }).compileComponents();
15 | }));
16 |
17 | it('should create the app', () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app).toBeTruthy();
21 | });
22 |
23 | it(`should have as title 'otp-example'`, () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | const app = fixture.componentInstance;
26 | expect(app.title).toEqual('otp-example');
27 | });
28 |
29 | it('should render title', () => {
30 | const fixture = TestBed.createComponent(AppComponent);
31 | fixture.detectChanges();
32 | const compiled = fixture.nativeElement;
33 | expect(compiled.querySelector('.content span').textContent).toContain('otp-example app is running!');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnRequestResponse.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import java.io.IOException;
4 |
5 | public class WebAuthnRequestResponse extends RustObject {
6 | static {
7 | System.loadLibrary("slauth");
8 | }
9 |
10 | public WebAuthnRequestResponse(byte[] credentialId, String requestJson, String origin, byte attestationFlags, byte[] userHandle, String privateKey) throws Exception {
11 | this.raw = JNA.INSTANCE.generate_credential_request_response(credentialId, credentialId.length, requestJson, origin, attestationFlags, userHandle, userHandle.length, privateKey);
12 | if (this.raw == null) {
13 | throw new Exception();
14 | }
15 |
16 | String json = this.getJson();
17 | if (json == null || json.isEmpty()) {
18 | throw new Exception(this.getError());
19 | }
20 | }
21 |
22 | public String getJson() {
23 | return JNA.INSTANCE.get_json_from_request_response(raw);
24 | }
25 |
26 | public String getError() {
27 | return JNA.INSTANCE.get_error_from_request_response(raw);
28 | }
29 |
30 | @Override
31 | public void close() throws IOException {
32 | JNA.INSTANCE.response_free(raw);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/WebAuthnCreationResponse.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import java.io.IOException;
4 |
5 | public class WebAuthnCreationResponse extends RustObject {
6 | static {
7 | System.loadLibrary("slauth");
8 | }
9 |
10 | public WebAuthnCreationResponse(String aaguid, byte[] credentialId, String requestJson, String origin, byte attestationFlags) throws Exception {
11 | this.raw = JNA.INSTANCE.generate_credential_creation_response(aaguid, credentialId, credentialId.length, requestJson, origin, attestationFlags);
12 | if (this.raw == null) {
13 | throw new Exception();
14 | }
15 |
16 | String json = this.getJson();
17 | if (json == null || json.isEmpty()) {
18 | throw new Exception(this.getError());
19 | }
20 | }
21 |
22 | public String getJson() {
23 | return JNA.INSTANCE.get_json_from_creation_response(raw);
24 | }
25 |
26 | public String getPrivateKey() {
27 | return JNA.INSTANCE.get_private_key_from_response(raw);
28 | }
29 |
30 | public String getError() {
31 | return JNA.INSTANCE.get_error_from_creation_response(raw);
32 | }
33 |
34 | @Override
35 | public void close() throws IOException {
36 | JNA.INSTANCE.response_free(raw);
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/Hotp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Hotp.swift
3 | // firebase
4 | //
5 | // Created by Richer Archambault on 2019-04-26.
6 | // Copyright © 2019 Sebastien Aubin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if canImport(SlauthFFI)
12 | import SlauthFFI
13 | #endif
14 |
15 | public class Hotp: NSObject, RustObject {
16 | var raw: OpaquePointer
17 |
18 | required init(raw: OpaquePointer) {
19 | self.raw = raw
20 | }
21 |
22 | func intoRaw() -> OpaquePointer {
23 | return self.raw
24 | }
25 |
26 | public convenience init(uri: String) throws {
27 | let r = hotp_from_uri(uri)
28 | if r == nil {
29 | throw Err(message: "InvalidUri")
30 | } else {
31 | self.init(raw: r!)
32 | }
33 | }
34 |
35 | deinit {
36 | hotp_free(raw)
37 | }
38 |
39 | public func to_uri(label: String, issuer: String) -> String {
40 | let uri = hotp_to_uri(raw, label, issuer)
41 | let s = String(cString: uri!)
42 | free(uri)
43 | return s
44 | }
45 |
46 | public func inc() {
47 | hotp_inc(raw)
48 | }
49 |
50 | public func gen() -> String {
51 | let code = hotp_gen(raw)
52 | let s_code = String(cString: code!)
53 | free(code)
54 | return s_code
55 | }
56 |
57 | public func verify(code: String) -> Bool {
58 | return hotp_verify(raw, code)
59 | }
60 |
61 | public func validate_current(code: String) -> Bool {
62 | return hotp_validate_current(raw, code)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/Totp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Totp.swift
3 | // firebase
4 | //
5 | // Created by Richer Archambault on 2019-04-26.
6 | // Copyright © 2019 Sebastien Aubin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if canImport(SlauthFFI)
12 | import SlauthFFI
13 | #endif
14 |
15 | public class Totp: NSObject, RustObject {
16 | var raw: OpaquePointer
17 |
18 | required init(raw: OpaquePointer) {
19 | self.raw = raw
20 | }
21 |
22 | func intoRaw() -> OpaquePointer {
23 | return self.raw
24 | }
25 |
26 | public convenience init(uri: String) throws {
27 | let r = totp_from_uri(uri)
28 | if r == nil {
29 | throw Err(message: "InvalidUri")
30 | } else {
31 | self.init(raw: r!)
32 | }
33 | }
34 |
35 | deinit {
36 | totp_free(raw)
37 | }
38 |
39 | public func to_uri(label: String, issuer: String) -> String {
40 | let uri = totp_to_uri(raw, label, issuer)
41 | let s = String(cString: uri!)
42 | free(uri)
43 | return s
44 | }
45 |
46 | public func gen() -> String {
47 | let code = totp_gen(raw)
48 | let s = String(cString: code!)
49 | free(code)
50 | return s
51 | }
52 |
53 | public func gen_with(elapsed: UInt) -> String {
54 | let code = totp_gen_with(raw, elapsed)
55 | let s = String(cString: code!)
56 | free(code)
57 | return s
58 | }
59 |
60 | public func verify(code: String) -> Bool {
61 | return totp_verify(raw, code)
62 | }
63 |
64 | public func validate_current(code: String) -> Bool {
65 | return totp_validate_current(raw, code)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "otp-example",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "~9.0.5",
15 | "@angular/common": "~9.0.5",
16 | "@angular/compiler": "~9.0.5",
17 | "@angular/core": "~9.0.5",
18 | "@angular/forms": "~9.0.5",
19 | "@angular/platform-browser": "~9.0.5",
20 | "@angular/platform-browser-dynamic": "~9.0.5",
21 | "@angular/router": "~9.0.5",
22 | "slauth": "^0.6.3",
23 | "rxjs": "~6.5.4",
24 | "tslib": "^1.10.0",
25 | "zone.js": "~0.10.2",
26 | "angularx-qrcode": "^2.3.5"
27 | },
28 | "devDependencies": {
29 | "@angular-devkit/build-angular": "~0.900.5",
30 | "@angular/cli": "~9.0.5",
31 | "@angular/compiler-cli": "~9.0.5",
32 | "@angular/language-service": "~9.0.5",
33 | "@types/node": "^12.11.1",
34 | "@types/jasmine": "~3.5.0",
35 | "@types/jasminewd2": "~2.0.3",
36 | "codelyzer": "^5.1.2",
37 | "jasmine-core": "~3.5.0",
38 | "jasmine-spec-reporter": "~4.2.1",
39 | "karma": "~4.3.0",
40 | "karma-chrome-launcher": "~3.1.0",
41 | "karma-coverage-istanbul-reporter": "~2.1.0",
42 | "karma-jasmine": "~2.0.1",
43 | "karma-jasmine-html-reporter": "^1.4.2",
44 | "protractor": "~5.4.3",
45 | "ts-node": "~8.3.0",
46 | "tslint": "~5.18.0",
47 | "typescript": "~3.7.5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/wrappers/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | ext {
4 | libraryName = 'Slauth'
5 | artifact = 'slauth'
6 |
7 | libraryDescription = 'An Android wrapper around the rust Slauth implementation'
8 |
9 | siteUrl = 'https://github.com/Devolutions/Slauth'
10 | gitUrl = 'https://github.com/Devolutions/Slauth.git'
11 |
12 | developerId = 'rarchambault'
13 | developerName = 'Richer Archambault'
14 | developerEmail = 'rarchambault@devolutions.net'
15 |
16 | licenseName = 'MIT License'
17 | licenseUrl = 'https://raw.githubusercontent.com/Devolutions/Slauth/master/LICENSE'
18 | allLicenses = ["MIT"]
19 | }
20 |
21 | android {
22 | compileSdkVersion 33
23 |
24 |
25 | defaultConfig {
26 | minSdkVersion 23
27 | targetSdkVersion 28
28 | versionCode 1
29 | versionName "0.7.20"
30 |
31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
32 |
33 | }
34 |
35 | lintOptions {
36 | abortOnError false
37 | }
38 |
39 | buildTypes {
40 | release {
41 | minifyEnabled false
42 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
43 | }
44 | }
45 |
46 | }
47 |
48 | dependencies {
49 | implementation fileTree(dir: 'libs', include: ['*.jar'])
50 | runtimeOnly fileTree(dir: 'jniLibs', include: ['*.so'])
51 | implementation 'com.android.support:appcompat-v7:28.0.0'
52 | implementation 'net.java.dev.jna:jna:5.16.0@aar'
53 | testImplementation 'junit:junit:4.12'
54 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
55 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
56 | }
57 |
--------------------------------------------------------------------------------
/src/webauthn/proto/constants.rs:
--------------------------------------------------------------------------------
1 | use x509_parser::der_parser::{oid, Oid};
2 |
3 | pub const WEBAUTHN_CHALLENGE_LENGTH: usize = 32;
4 | pub const WEBAUTHN_CREDENTIAL_ID_LENGTH: usize = 16;
5 |
6 | pub const WEBAUTHN_USER_PRESENT_FLAG: u8 = 0b00000001;
7 | pub const WEBAUTHN_USER_VERIFIED_FLAG: u8 = 0b00000100;
8 | pub const WEBAUTHN_ATTESTED_CREDENTIAL_DATA_FLAG: u8 = 0b01000000;
9 | pub const WEBAUTHN_EXTENSION_DATA_FLAG: u8 = 0b10000000;
10 |
11 | pub const WEBAUTHN_FORMAT_PACKED: &str = "packed";
12 | pub const WEBAUTHN_FORMAT_FIDO_U2F: &str = "fido-u2f";
13 | pub const WEBAUTHN_FORMAT_NONE: &str = "none";
14 | pub const WEBAUTHN_FORMAT_ANDROID_SAFETYNET: &str = "android-safetynet";
15 | pub const WEBAUTHN_FORMAT_ANDROID_KEY: &str = "android-key";
16 | pub const WEBAUTHN_FORMAT_TPM: &str = "tpm";
17 |
18 | pub const WEBAUTH_PUBLIC_KEY_TYPE_OKP: i64 = 1;
19 | pub const WEBAUTH_PUBLIC_KEY_TYPE_EC2: i64 = 2;
20 | pub const WEBAUTH_PUBLIC_KEY_TYPE_RSA: i64 = 3;
21 |
22 | pub const WEBAUTHN_REQUEST_TYPE_CREATE: &str = "webauthn.create";
23 | pub const WEBAUTHN_REQUEST_TYPE_GET: &str = "webauthn.get";
24 |
25 | pub const ECDSA_Y_PREFIX_POSITIVE: u8 = 2;
26 | pub const ECDSA_Y_PREFIX_NEGATIVE: u8 = 3;
27 | pub const ECDSA_Y_PREFIX_UNCOMPRESSED: u8 = 4;
28 |
29 | pub const ECDSA_CURVE_P256: i64 = 1;
30 | pub const ECDSA_CURVE_P384: i64 = 2;
31 | pub const ECDSA_CURVE_P521: i64 = 3;
32 | pub const ECDAA_CURVE_ED25519: i64 = 6;
33 |
34 | pub const TPM_GENERATED_VALUE: u32 = 0xff544347; // https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation
35 |
36 | pub const TCG_AT_TPM_MANUFACTURER: &[u8] = &oid!(raw 2.23.133.2.1);
37 | pub const TCG_AT_TPM_MODEL: &[u8] = &oid!(raw 2.23.133.2.2);
38 | pub const TCG_AT_TPM_VERSION: &[u8] = &oid!(raw 2.23.133.2.3);
39 |
40 | pub const TCG_KP_AIK_CERTIFICATE: &Oid = &oid!(2.23.133 .8 .3);
41 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(warnings)]
2 |
3 | //! # Slauth
4 | //!
5 | //! Auth utils for MFA algorithms
6 |
7 | extern crate core;
8 |
9 | /// Module for hotp algorithms
10 | pub mod oath;
11 |
12 | #[cfg(feature = "u2f")]
13 | pub mod u2f;
14 |
15 | #[cfg(feature = "webauthn")]
16 | pub mod webauthn;
17 |
18 | #[cfg(target_arch = "wasm32")]
19 | pub mod wasm;
20 |
21 | pub mod base64;
22 |
23 | #[cfg(feature = "native-bindings")]
24 | pub mod strings {
25 | use std::{
26 | ffi::{CStr, CString},
27 | os::raw::c_char,
28 | };
29 |
30 | /// # Safety
31 | /// Needed to cast string in FFY context
32 | pub unsafe fn c_char_to_string_checked(cchar: *const c_char) -> Option {
33 | let c_str = CStr::from_ptr(cchar);
34 | match c_str.to_str() {
35 | Ok(string) => Some(string.to_string()),
36 | Err(_) => None,
37 | }
38 | }
39 |
40 | /// # Safety
41 | /// Needed to cast string in FFY context
42 | pub unsafe fn c_char_to_string(cchar: *const c_char) -> String {
43 | let c_str = CStr::from_ptr(cchar);
44 | let r_str = c_str.to_str().unwrap_or("");
45 | r_str.to_string()
46 | }
47 |
48 | pub fn string_to_c_char(r_string: String) -> *mut c_char {
49 | CString::new(r_string)
50 | .expect("Converting a string into a c_char should not fail")
51 | .into_raw()
52 | }
53 |
54 | /// # Safety
55 | /// Needed to cast string in FFY context
56 | pub unsafe fn mut_c_char_to_string(cchar: *mut c_char) -> String {
57 | let c_string = if cchar.is_null() {
58 | CString::from_vec_unchecked(vec![])
59 | } else {
60 | CString::from_raw(cchar)
61 | };
62 | let c_str = c_string.as_c_str();
63 | let r_str = c_str.to_str().unwrap_or_default();
64 | r_str.to_string()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/WebRequest.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import com.sun.jna.Pointer;
4 |
5 | import java.io.IOException;
6 | import java.util.Optional;
7 |
8 | public class WebRequest extends RustObject {
9 | static {
10 | System.loadLibrary("slauth");
11 | }
12 |
13 | public WebRequest(String json) {
14 | this.raw = JNA.INSTANCE.web_request_from_json(json);
15 | }
16 |
17 | public Boolean isRegister() {
18 | return JNA.INSTANCE.web_request_is_register(raw);
19 | }
20 |
21 | public Boolean isSign() {
22 | return JNA.INSTANCE.web_request_is_sign(raw);
23 | }
24 |
25 | public String getOrigin() {
26 | return JNA.INSTANCE.web_request_origin(raw);
27 | }
28 |
29 | public long getTimeout() {
30 | return JNA.INSTANCE.web_request_timeout(raw);
31 | }
32 |
33 | public String getKeyHandle(String origin) throws InvalidRequestTypeException {
34 | if (this.isSign()) {
35 | return JNA.INSTANCE.web_request_key_handle(raw, origin);
36 | } else {
37 | throw new InvalidRequestTypeException();
38 | }
39 | }
40 |
41 | public WebResponse register(String origin, byte[] attestationCert, byte[] attestationKey) {
42 | Pointer p = JNA.INSTANCE.web_request_register(raw, origin, attestationCert, attestationCert.length, attestationKey, attestationKey.length);
43 |
44 | return new WebResponse(p);
45 | }
46 |
47 | public WebResponse sign(String origin, SigningKey key, int counter, Boolean userPresence) {
48 | Pointer p = JNA.INSTANCE.web_request_sign(raw, key.raw, origin, counter, userPresence);
49 |
50 | return new WebResponse(p);
51 | }
52 |
53 | @Override
54 | public void close() throws IOException {
55 | JNA.INSTANCE.web_request_free(raw);
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/WebAuthnRequestResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if canImport(SlauthFFI)
4 | import SlauthFFI
5 | #endif
6 |
7 | @available(iOS 15.0, *)
8 | public class WebAuthnRequestResponse: NSObject, RustObject {
9 |
10 | var raw: OpaquePointer
11 |
12 | required init(raw: OpaquePointer) {
13 | self.raw = raw
14 | }
15 |
16 | func intoRaw() -> OpaquePointer {
17 | return self.raw
18 | }
19 |
20 | public func getAuthData() -> Data {
21 | let buffer = get_auth_data_from_response(self.raw)
22 | return Data(bytes: buffer.data, count: Int(buffer.len))
23 | }
24 |
25 | public func getSignature() -> Data {
26 | let buffer = get_signature_from_response(self.raw)
27 | return Data(bytes: buffer.data, count: Int(buffer.len))
28 | }
29 |
30 | public func isSuccess() -> Bool {
31 | return is_success(self.raw)
32 | }
33 |
34 | public func getErrorMessage() -> String {
35 | let cString = get_error_message(self.raw)
36 | let errorMessage = String(cString: cString!)
37 | free(cString)
38 | return errorMessage
39 | }
40 |
41 | public convenience init(
42 | rpId: String, attestationFlags: UInt8, clientDataHash: Data, privateKey: String
43 | ) throws {
44 | let clientDataHashPointer = UnsafeMutablePointer.allocate(
45 | capacity: clientDataHash.count)
46 | clientDataHash.copyBytes(to: clientDataHashPointer, count: clientDataHash.count)
47 |
48 | let r = generate_credential_request_response(
49 | rpId, privateKey, attestationFlags.bigEndian, clientDataHashPointer,
50 | UInt(clientDataHash.count))
51 | if r == nil {
52 | throw Err(message: "Invalid parameters")
53 | } else {
54 | self.init(raw: r!)
55 | }
56 | clientDataHashPointer.deallocate()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "array-type": false,
5 | "arrow-parens": false,
6 | "deprecation": {
7 | "severity": "warning"
8 | },
9 | "component-class-suffix": true,
10 | "contextual-lifecycle": true,
11 | "directive-class-suffix": true,
12 | "directive-selector": [
13 | true,
14 | "attribute",
15 | "app",
16 | "camelCase"
17 | ],
18 | "component-selector": [
19 | true,
20 | "element",
21 | "app",
22 | "kebab-case"
23 | ],
24 | "import-blacklist": [
25 | true,
26 | "rxjs/Rx"
27 | ],
28 | "interface-name": false,
29 | "max-classes-per-file": false,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-consecutive-blank-lines": false,
47 | "no-console": [
48 | true,
49 | "debug",
50 | "info",
51 | "time",
52 | "timeEnd",
53 | "trace"
54 | ],
55 | "no-empty": false,
56 | "no-inferrable-types": [
57 | true,
58 | "ignore-params"
59 | ],
60 | "no-non-null-assertion": true,
61 | "no-redundant-jsdoc": true,
62 | "no-switch-case-fall-through": true,
63 | "no-var-requires": false,
64 | "object-literal-key-quotes": [
65 | true,
66 | "as-needed"
67 | ],
68 | "object-literal-sort-keys": false,
69 | "ordered-imports": false,
70 | "quotemark": [
71 | true,
72 | "single"
73 | ],
74 | "trailing-comma": false,
75 | "no-conflicting-lifecycle": true,
76 | "no-host-metadata-property": true,
77 | "no-input-rename": true,
78 | "no-inputs-metadata-property": true,
79 | "no-output-native": true,
80 | "no-output-on-prefix": true,
81 | "no-output-rename": true,
82 | "no-outputs-metadata-property": true,
83 | "template-banana-in-box": true,
84 | "template-no-negated-async": true,
85 | "use-lifecycle-interface": true,
86 | "use-pipe-transform-interface": true
87 | },
88 | "rulesDirectory": [
89 | "codelyzer"
90 | ]
91 | }
--------------------------------------------------------------------------------
/wrappers/swift/classes/WebAuthnCreationResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import AuthenticationServices
3 |
4 | #if canImport(SlauthFFI)
5 | import SlauthFFI
6 | #endif
7 |
8 | @available(iOS 15.0, *)
9 | public class WebAuthnCreationResponse: NSObject {
10 |
11 | var raw: OpaquePointer
12 | var aaguid: String
13 |
14 | required init(raw: OpaquePointer, aaguid: String) {
15 | self.raw = raw
16 | self.aaguid = aaguid
17 | }
18 |
19 | func intoRaw() -> OpaquePointer {
20 | return self.raw
21 | }
22 |
23 | public func getPrivateKey() -> String {
24 | let cString = get_private_key_from_response(self.raw)
25 | let privateKey = String(cString: cString!)
26 | free(cString)
27 | return privateKey
28 | }
29 |
30 | public func getAttestationObject() -> Data {
31 | let buffer = get_attestation_object_from_response(self.raw)
32 | return Data(bytes: buffer.data, count: Int(buffer.len))
33 | }
34 |
35 | public convenience init(aaguid: String, credentialId: Data, rpId: String, attestationFlags: UInt8, cose_algorithm_identifiers: [ASCOSEAlgorithmIdentifier]) throws {
36 | let credentialPointer = UnsafeMutablePointer.allocate(capacity: credentialId.count)
37 | credentialId.copyBytes(to: credentialPointer, count: credentialId.count)
38 |
39 | let cose_algorithm_identifiers_pointer = UnsafeMutablePointer.allocate(capacity: cose_algorithm_identifiers.count)
40 | for i in 0...(cose_algorithm_identifiers.count - 1) {
41 | cose_algorithm_identifiers_pointer[i] = Int32(cose_algorithm_identifiers[i].rawValue)
42 | }
43 |
44 | let r = generate_credential_creation_response(aaguid, credentialPointer, UInt(credentialId.count), rpId, attestationFlags.bigEndian, cose_algorithm_identifiers_pointer, UInt(cose_algorithm_identifiers.count))
45 | if r == nil {
46 | throw Err(message: "Invalid parameters")
47 | } else {
48 | self.init(raw: r!, aaguid: aaguid)
49 | }
50 | credentialPointer.deallocate()
51 | cose_algorithm_identifiers_pointer.deallocate()
52 | }
53 |
54 | deinit {
55 | response_free(raw)
56 | }
57 | }
58 |
59 | public enum AttestationFlags: UInt8 {
60 | case userPresent = 1
61 | //Reserved for future = 2
62 | case userVerified = 4
63 | case backupEligible = 8
64 | case backedUp = 16
65 | //Reserved for future = 32
66 | case attestedCredentialDataIncluded = 64
67 | case extensionDataIncluded = 128
68 | }
69 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnDestroy, OnInit} from '@angular/core';
2 | import {OtpService} from "./services/otp.service";
3 | import {interval, Subject, Subscription} from "rxjs";
4 | import {takeUntil} from "rxjs/operators";
5 | import {Totp} from "slauth";
6 | import {FormControl, FormGroup} from '@angular/forms';
7 |
8 | @Component({
9 | selector: 'app-root',
10 | templateUrl: './app.component.html',
11 | styleUrls: ['./app.component.css']
12 | })
13 | export class AppComponent implements OnInit, OnDestroy {
14 | form: FormGroup;
15 | title = 'otp-example';
16 | code = null;
17 | issuer = 'Devolutions';
18 | account = 'dev@devolutions.net';
19 | otp: Totp = null;
20 | // secret is either base32 or hex
21 | secret = 'GEZDGNBVGY';
22 | // period is the duration time of a generated code
23 | period = 30;
24 | // digit is how many digit the generated code will have
25 | digits = 6;
26 | private unsubscribe$ = new Subject();
27 | private intSub: Subscription = null;
28 |
29 | constructor(private otpService: OtpService) {
30 | }
31 |
32 | ngOnDestroy(): void {
33 | this.unsubscribe$.next();
34 | this.unsubscribe$.complete();
35 | }
36 |
37 | ngOnInit() {
38 | this.form = new FormGroup({
39 | issuer: new FormControl('Devolutions', []),
40 | account: new FormControl('dev@devolutions.net', []),
41 | secret: new FormControl('GEZDGNBVGY', []),
42 | period: new FormControl(30, []),
43 | digits: new FormControl(6, []),
44 | });
45 |
46 | this.applyOtpConfig()
47 | }
48 |
49 | applyOtpConfig() {
50 | if (this.intSub) {
51 | this.intSub.unsubscribe();
52 | }
53 |
54 | this.issuer = this.form.get('issuer').value;
55 | this.account = this.form.get('account').value;
56 | this.secret = this.form.get('secret').value;
57 | this.period = this.form.get('period').value;
58 | this.digits = this.form.get('digits').value;
59 |
60 | this.otpService.ready.pipe(takeUntil(this.unsubscribe$)).subscribe((available) => {
61 | console.log("loaded");
62 | if (!available) {return;}
63 | this.otp = this.otpService.module.Totp.fromParts(this.secret, this.period, this.digits, this.otpService.module.OtpAlgorithm.sha1());
64 | this.intSub = interval(2).pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
65 | this.code = this.otp.generateCode();
66 | })
67 | })
68 | }
69 |
70 | getUri() {
71 | if (this.otp) {
72 | return this.otp.toUri(this.issuer, this.account);
73 | } else {
74 | return '';
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # slauth
2 | [](https://docs.rs/slauth/)
3 | [](https://crates.io/crates/slauth)
4 | [](https://github.com/devolutions/slauth/issues)
5 | 
6 | [](https://github.com/devolutions/slauth/blob/master/LICENSE)
7 | [](https://deps.rs/repo/github/devolutions/slauth)
8 |
9 | ## Slauth is a Rust only, OpenSource implementation of Multiple authenticator utils / specification
10 |
11 | ### Current Implementation Status
12 | Status is describe by : ✔ as implemented, ❌ as not implemented and ⚠️ as partially implemented.
13 |
14 | ### OATH Authentication ([specs](https://openauthentication.org/specifications-technical-resources/))
15 |
16 | #### Authentication Methods
17 |
18 | | Name | Status | Ref |
19 | |:----:|:------:|:-------------------------------------------------:|
20 | | HOTP | ✔ | [RFC 4226](https://tools.ietf.org/html/rfc4226) |
21 | | TOTP | ✔ | [RFC 6238](https://tools.ietf.org/html/rfc6238) |
22 | | OCRA | ❌ | [RFC 6287](https://tools.ietf.org/html/rfc6287) |
23 |
24 | #### Provisioning
25 |
26 | | Name | Status | Ref |
27 | |:----:|:------:|:-------------------------------------------------:|
28 | | PSKC | ❌ | [RFC 6030](https://tools.ietf.org/html/rfc6030) |
29 | | DSKPP | ❌ | [RFC 6063](https://tools.ietf.org/html/rfc6063) |
30 |
31 |
32 | ### FIDO & W3C Specification ([specs](https://fidoalliance.org/specifications/download/))
33 |
34 | #### Universal 2nd Factor (U2F)
35 |
36 | | Name | Status | Ref |
37 | |:----:|:------:|:-------------------------------------------------:|
38 | | Server-Side Verification | ✔ | |
39 | | Raw Message | ✔ | [Spec](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html) |
40 | | HID Protocol | ❌ | [Spec](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-hid-protocol-v1.2-ps-20170411.html) |
41 |
42 | #### WebAuthN
43 |
44 | | Name | Status | Ref |
45 | |:----:|:------:|:-------------------------------------------------:|
46 | | Server-Side Verification | ⚠️ | [Spec](https://www.w3.org/TR/webauthn/) |
47 | | Raw Message | ✔ | [Spec](https://www.w3.org/TR/webauthn/) |
48 | | COSE | ⚠️ | [Spec](https://tools.ietf.org/html/rfc8152) |
49 |
50 | For the server side validation, the following algorithm are implemented:
51 | - `ES256`
52 | - `ES384`
53 | - `ED25519`
54 | - `RS256`
55 |
56 | #### Universal Authentication Framework (UAF)
57 |
58 | Not Implemented
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test Package
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | build-rust:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Build
17 | run: cargo build --verbose
18 |
19 | - name: Run tests
20 | run: cargo test --verbose
21 |
22 | build-wasm:
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - name: Checkout repo
27 | uses: actions/checkout@v4
28 |
29 | - name: Setup wasm
30 | run: |
31 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
32 | wasm-pack --version
33 |
34 | - name: Build
35 | run: sh build.sh
36 | working-directory: wrappers/wasm
37 |
38 | build-wasm-web:
39 | runs-on: ubuntu-latest
40 |
41 | steps:
42 | - name: Checkout repo
43 | uses: actions/checkout@v4
44 |
45 | - name: Setup wasm
46 | run: |
47 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
48 | wasm-pack --version
49 |
50 | - name: Build
51 | run: bash build-web.sh
52 | working-directory: wrappers/wasm
53 |
54 | build-android:
55 | runs-on: ubuntu-latest
56 |
57 | steps:
58 | - name: Checkout repo
59 | uses: actions/checkout@v4
60 |
61 | - name: Set up JDK 1.8
62 | uses: actions/setup-java@v4
63 | with:
64 | java-version: 8
65 | distribution: adopt
66 |
67 | - name: Setup Android
68 | run: |
69 | wget https://dl.google.com/android/repository/android-ndk-r23b-linux.zip
70 | unzip android-ndk-r23b-linux.zip
71 | export ANDROID_NDK_HOME=$GITHUB_WORKSPACE/android-ndk-r23b
72 | echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV
73 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
74 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android" >> $GITHUB_ENV::LIBRARY_PATH
75 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android" >> $GITHUB_ENV::LD_LIBRARY_PATH
76 | rustup target add aarch64-linux-android
77 | rustup target add x86_64-linux-android
78 | rustup target add x86_64-unknown-linux-gnu
79 |
80 | - name: Build
81 | run: sh wrappers/android/build.sh
82 |
83 | build-swift:
84 | runs-on: macos-latest
85 |
86 | steps:
87 | - name: Checkout repo
88 | uses: actions/checkout@v4
89 |
90 | - name: Setup rust
91 | run: |
92 | rustup target add aarch64-apple-ios
93 | rustup target add x86_64-apple-darwin
94 | rustup target add aarch64-apple-ios-sim
95 | rustup target add aarch64-apple-darwin
96 |
97 | - name: Generate package
98 | run: sh wrappers/swift/build.sh
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/u2f/error.rs:
--------------------------------------------------------------------------------
1 | use ring::error::{KeyRejected, Unspecified};
2 | #[cfg(feature = "u2f-server")]
3 | use serde_json::error::Error as SJsonError;
4 | use std::{
5 | error::Error as StdError,
6 | fmt::{Display, Formatter},
7 | io::Error as IoError,
8 | };
9 |
10 | #[derive(Debug)]
11 | pub enum Error {
12 | IoError(IoError),
13 | U2FErrorCode(u16),
14 | UnexpectedApdu(String),
15 | AsnFormatError(String),
16 | MalformedApdu,
17 | Version,
18 | RingKeyRejected(KeyRejected),
19 | Registration(String),
20 | Sign(String),
21 | Other(String),
22 | #[cfg(feature = "u2f-server")]
23 | EndEntityError(webpki::Error),
24 | #[cfg(feature = "u2f-server")]
25 | SerdeJsonError(SJsonError),
26 | }
27 |
28 | impl StdError for Error {}
29 |
30 | impl Display for Error {
31 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
32 | use Error::*;
33 | match self {
34 | IoError(io_e) => io_e.fmt(f),
35 | U2FErrorCode(code) => write!(f, "U2f Error Code: {}", code),
36 | UnexpectedApdu(s) => write!(f, "{}", s),
37 | AsnFormatError(s) => write!(f, "{}", s),
38 | MalformedApdu => write!(f, "Unsupported version"),
39 | Version => write!(f, "Unsupported version"),
40 | RingKeyRejected(key_r_e) => key_r_e.fmt(f),
41 | Registration(s) => write!(f, "{}", s),
42 | Sign(s) => write!(f, "{}", s),
43 | Other(s) => write!(f, "{}", s),
44 | #[cfg(feature = "u2f-server")]
45 | EndEntityError(webpki_e) => webpki_e.fmt(f),
46 | #[cfg(feature = "u2f-server")]
47 | SerdeJsonError(s_j_e) => s_j_e.fmt(f),
48 | }
49 | }
50 | }
51 |
52 | #[cfg(feature = "u2f-server")]
53 | impl From for Error {
54 | fn from(e: webpki::Error) -> Self {
55 | Error::EndEntityError(e)
56 | }
57 | }
58 |
59 | impl From for Error {
60 | fn from(e: IoError) -> Self {
61 | Error::IoError(e)
62 | }
63 | }
64 |
65 | impl From for Error {
66 | fn from(sw: u16) -> Self {
67 | Error::U2FErrorCode(sw)
68 | }
69 | }
70 |
71 | impl From for Error {
72 | fn from(_: Unspecified) -> Self {
73 | Error::Other("Unspecified".to_string())
74 | }
75 | }
76 |
77 | impl From for Error {
78 | fn from(e: KeyRejected) -> Self {
79 | Error::RingKeyRejected(e)
80 | }
81 | }
82 |
83 | #[cfg(feature = "u2f-server")]
84 | impl From for Error {
85 | fn from(e: SJsonError) -> Self {
86 | Error::SerdeJsonError(e)
87 | }
88 | }
89 |
90 | pub trait ResultExt {
91 | fn then(self, op: F) -> Result
92 | where
93 | F: FnOnce(Result) -> Result;
94 | }
95 |
96 | impl ResultExt for Result {
97 | fn then(self, op: F) -> Result
98 | where
99 | F: FnOnce(Result) -> Result,
100 | {
101 | op(self)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/wrappers/android/src/main/java/net/devolutions/slauth/JNA.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import com.sun.jna.Library;
4 | import com.sun.jna.Native;
5 | import com.sun.jna.Pointer;
6 |
7 | public interface JNA extends Library {
8 | String JNA_LIBRARY_NAME = "slauth";
9 |
10 | JNA INSTANCE = Native.load(JNA_LIBRARY_NAME, JNA.class);
11 |
12 | Pointer hotp_from_uri(String uri);
13 |
14 | void hotp_free(Pointer hotp);
15 |
16 | String hotp_gen(Pointer hotp);
17 |
18 | void hotp_inc(Pointer hotp);
19 |
20 | String hotp_to_uri(Pointer hotp, String label, String issuer);
21 |
22 | Boolean hotp_validate_current(Pointer hotp, String code);
23 |
24 | Boolean hotp_verify(Pointer hotp, String code);
25 |
26 | void totp_free(Pointer totp);
27 |
28 | Pointer totp_from_uri(String uri);
29 |
30 | String totp_gen(Pointer totp);
31 |
32 | String totp_gen_with(Pointer totp, long elapsed);
33 |
34 | String totp_to_uri(Pointer totp, String label, String issuer);
35 |
36 | Boolean totp_validate_current(Pointer totp, String code);
37 |
38 | Boolean totp_verify(Pointer totp, String code);
39 |
40 | void client_web_response_free(Pointer rsp);
41 |
42 | Pointer client_web_response_signing_key(Pointer rsp);
43 |
44 | String client_web_response_to_json(Pointer rsp);
45 |
46 | void signing_key_free(Pointer s);
47 |
48 | Pointer signing_key_from_string(String s);
49 |
50 | String signing_key_to_string(Pointer s);
51 |
52 | String signing_key_get_key_handle(Pointer s);
53 |
54 | void web_request_free(Pointer req);
55 |
56 | Pointer web_request_from_json(String req);
57 |
58 | Boolean web_request_is_register(Pointer req);
59 |
60 | Boolean web_request_is_sign(Pointer req);
61 |
62 | String web_request_key_handle(Pointer req, String origin);
63 |
64 | String web_request_origin(Pointer req);
65 |
66 | Pointer web_request_register(Pointer req, String origin, byte[] attestation_cert, long attestation_cert_len, byte[] attestation_key, long attestation_key_len);
67 |
68 | Pointer web_request_sign(Pointer req, Pointer signing_key, String origin, long counter, Boolean user_presence);
69 |
70 | long web_request_timeout(Pointer req);
71 |
72 | Pointer generate_credential_creation_response(String aaguid, byte[] credential_id, long credential_id_len, String request_json, String origin, byte attestation_flags);
73 |
74 | 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);
75 |
76 | void response_free(Pointer req);
77 |
78 | String get_json_from_request_response(Pointer req);
79 |
80 | String get_error_from_request_response(Pointer req);
81 |
82 | String get_json_from_creation_response(Pointer req);
83 |
84 | String get_error_from_creation_response(Pointer req);
85 |
86 | String get_private_key_from_response(Pointer req);
87 |
88 | String private_key_to_pkcs8_der(String private_key);
89 |
90 | String pkcs8_to_custom_private_key(String pkcs8_key);
91 | }
92 |
--------------------------------------------------------------------------------
/wrappers/android/src/androidTest/java/net/devolutions/slauth/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package net.devolutions.slauth;
2 |
3 | import android.support.test.runner.AndroidJUnit4;
4 | import android.util.Base64;
5 |
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 |
9 | import static org.junit.Assert.*;
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * @see Testing documentation
15 | */
16 | @RunWith(AndroidJUnit4.class)
17 | public class ExampleInstrumentedTest {
18 | @Test
19 | public void totpUris() {
20 | String baseUri = "otpauth://totp/john.doe@email.com?secret=12a9f88729b3bf4477f76b6c65d0e144d8ddc8f1&algorithm=SHA1&digits=6&period=30&issuer=Slauth";
21 | Totp totp = null;
22 | try {
23 | totp = new Totp(baseUri);
24 | } catch (Exception e) {
25 | e.printStackTrace();
26 | }
27 |
28 | String genUri = totp.toUri("john.doe@email.com", "Slauth");
29 |
30 | System.out.println(genUri);
31 |
32 | //assertEquals(baseUri, genUri); No more equal since the baseuri use hex
33 |
34 | String code1 = totp.gen();
35 |
36 | try {
37 | Thread.sleep(31000);
38 | } catch (InterruptedException e) {
39 | e.printStackTrace();
40 | }
41 |
42 | String code2 = totp.genWith(31);
43 |
44 | assertEquals(code1, code2);
45 | }
46 |
47 | @Test
48 | public void u2fTest() {
49 | try {
50 | byte[] att_cert = android.util.Base64.decode("MIICODCCAd6gAwIBAgIJAKsa9WC9HvEuMAoGCCqGSM49BAMCMFoxDzANBgNVBAMMBlNsYXV0aDELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzETMBEGA1UEBwwKTGF2YWx0cm91ZTEUMBIGA1UECgwLRGV2b2x1dGlvbnMwHhcNMTkwNzAyMTgwMTUyWhcNMzEwNjI5MTgwMTUyWjBaMQ8wDQYDVQQDDAZTbGF1dGgxCzAJBgNVBAYTAkNBMQ8wDQYDVQQIDAZRdWViZWMxEzARBgNVBAcMCkxhdmFsdHJvdWUxFDASBgNVBAoMC0Rldm9sdXRpb25zMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE15PAnpUUIzbgKxD6RFuNMjjl/cD06vKRBtl0X/CiNzc3igTh1qcc00QICgAQUxdvHSn+DaSRki/kI9OJ8lkPGqOBjDCBiTAdBgNVHQ4EFgQU7iZ4JceUHOuWoMymFGm+ZBUmwwgwHwYDVR0jBBgwFoAU7iZ4JceUHOuWoMymFGm+ZBUmwwgwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAVBgNVHREEDjAMggpzbGF1dGgub3JnMAoGCCqGSM49BAMCA0gAMEUCIEdjPFNsund4FXs/1HpK4AXWQ0asfY6ERhNlg29VGS6pAiEAx8f2lrlVV1tASWbC/edTgH9JsCbANuXW/9FZcWHGl2E=", Base64.DEFAULT);
51 | byte[] att_key = android.util.Base64.decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgzgUSoDttmryF0C+ck4GppKwssha7ngah0dfezfTBzDOhRANCAATXk8CelRQjNuArEPpEW40yOOX9wPTq8pEG2XRf8KI3NzeKBOHWpxzTRAgKABBTF28dKf4NpJGSL+Qj04nyWQ8a", Base64.DEFAULT);
52 |
53 | String json = "{\"appId\":\"https://login.devolutions.com/\",\"registerRequests\":[{\"challenge\":\"UzAxNE0yMTBWM1JDYzA1a1JqWndRUT09\",\"version\":\"U2F_V2\"}],\"registeredKeys\":[],\"requestId\":1,\"timeoutSeconds\":300,\"type\":\"u2f_register_request\"}";
54 |
55 | WebRequest web_r = new WebRequest(json);
56 |
57 | String origin = web_r.getOrigin();
58 |
59 | WebResponse rsp = web_r.register(origin, att_cert, att_key);
60 |
61 | SigningKey key = rsp.getSigningKey();
62 |
63 | System.out.println(key.getKeyHandle());
64 | System.out.println(key.toString());
65 | System.out.println(rsp.toJson());
66 | } catch (InvalidResponseTypeException e) {
67 | e.printStackTrace();
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/u2f/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod client;
2 | pub mod error;
3 | pub mod proto;
4 |
5 | #[cfg(feature = "u2f-server")]
6 | pub mod server;
7 |
8 | #[test]
9 | fn test() {
10 | use crate::{
11 | base64::*,
12 | u2f::proto::web_message::{Response, U2fRequest},
13 | };
14 | use server::*;
15 | const ATT_PKEY: &str = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgzgUSoDttmryF0C+ck4GppKwssha7ngah0dfezfTBzDOhRANCAATXk8CelRQjNuArEPpEW40yOOX9wPTq8pEG2XRf8KI3NzeKBOHWpxzTRAgKABBTF28dKf4NpJGSL+Qj04nyWQ8a";
16 | const ATT_CERT: &str = "MIICODCCAd6gAwIBAgIJAKsa9WC9HvEuMAoGCCqGSM49BAMCMFoxDzANBgNVBAMMBlNsYXV0aDELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzETMBEGA1UEBwwKTGF2YWx0cm91ZTEUMBIGA1UECgwLRGV2b2x1dGlvbnMwHhcNMTkwNzAyMTgwMTUyWhcNMzEwNjI5MTgwMTUyWjBaMQ8wDQYDVQQDDAZTbGF1dGgxCzAJBgNVBAYTAkNBMQ8wDQYDVQQIDAZRdWViZWMxEzARBgNVBAcMCkxhdmFsdHJvdWUxFDASBgNVBAoMC0Rldm9sdXRpb25zMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE15PAnpUUIzbgKxD6RFuNMjjl/cD06vKRBtl0X/CiNzc3igTh1qcc00QICgAQUxdvHSn+DaSRki/kI9OJ8lkPGqOBjDCBiTAdBgNVHQ4EFgQU7iZ4JceUHOuWoMymFGm+ZBUmwwgwHwYDVR0jBBgwFoAU7iZ4JceUHOuWoMymFGm+ZBUmwwgwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAVBgNVHREEDjAMggpzbGF1dGgub3JnMAoGCCqGSM49BAMCA0gAMEUCIEdjPFNsund4FXs/1HpK4AXWQ0asfY6ERhNlg29VGS6pAiEAx8f2lrlVV1tASWbC/edTgH9JsCbANuXW/9FZcWHGl2E=";
17 | const APP_ID: &str = "https://example.com/login/";
18 |
19 | let server_request = U2fRequestBuilder::register()
20 | .app_id(APP_ID.to_string())
21 | .challenge("1234567".to_string())
22 | .timeout_sec(81)
23 | .build()
24 | .expect("Unable to build U2fRequest register");
25 |
26 | let json_req = serde_json::to_string(&server_request).expect("Unable to serialize request"); //r#"{"appId":"http://localhost:4242","registerRequests":[{"challenge":"UzAxNE0yMTBWM1JDYzA1a1JqWndRUT09","version":"U2F_V2"}],"registeredKeys":[],"requestId":1,"timeoutSeconds":300,"type":"u2f_register_request"}"#;
27 |
28 | let web_req = serde_json::from_str::(&json_req).expect("Unable to deserialize req");
29 |
30 | let origin = web_req.app_id.as_ref().expect("Missing origin");
31 |
32 | let (rsp, signing_key) = web_req
33 | .register(
34 | origin.to_string(),
35 | BASE64.decode(ATT_CERT).unwrap().as_slice(),
36 | BASE64.decode(ATT_PKEY).unwrap().as_slice(),
37 | )
38 | .expect("Unable to register");
39 |
40 | let registration_rsp = if let Response::Register(reg) = rsp { reg } else { panic!() };
41 |
42 | let registration = registration_rsp.get_registration().expect("Unable to verify registration response");
43 |
44 | let server_sign_request = U2fRequestBuilder::sign()
45 | .app_id(APP_ID.to_string())
46 | .challenge("987654321".to_string())
47 | .timeout_sec(82)
48 | .registered_keys(vec![registration.get_registered_key()])
49 | .build()
50 | .expect("Unable to build U2fRequest Sign");
51 |
52 | let json_sign_req = serde_json::to_string(&server_sign_request).expect("Unable to serialize request");
53 |
54 | let web_sign_req = serde_json::from_str::(&json_sign_req).expect("Unable to deserialize req");
55 |
56 | let origin = web_sign_req.app_id.as_ref().expect("Missing origin");
57 |
58 | let rsp = web_sign_req
59 | .sign(&signing_key, origin.to_string(), 1, true)
60 | .expect("Unable to sign");
61 |
62 | let sign_rsp = if let Response::Sign(sig) = rsp { sig } else { panic!() };
63 |
64 | assert!(sign_rsp
65 | .validate_signature(registration.pub_key.as_slice())
66 | .expect("Unable to validate signature"));
67 | }
68 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "slauth"
3 | version = "0.7.20"
4 | authors = [
5 | "richer ",
6 | "LucFauvel ",
7 | ]
8 | edition = "2021"
9 | description = "oath HOTP and TOTP complient implementation"
10 | documentation = "https://docs.rs/slauth"
11 | homepage = "https://github.com/devolutions/slauth"
12 | repository = "https://github.com/devolutions/slauth"
13 | readme = "README.md"
14 | keywords = ["TOTP", "HOTP", "2FA", "MFA", "WebAuthn"]
15 | license = "MIT"
16 |
17 | [lib]
18 | name = "slauth"
19 | crate-type = ["lib", "staticlib", "cdylib"]
20 |
21 | [features]
22 | default = ["u2f-server", "webauthn-server", "native-bindings"]
23 | native-bindings = []
24 | u2f-server = ["u2f", "webpki"]
25 | u2f = ["auth-base", "untrusted", "serde_repr"]
26 | webauthn-server = ["webauthn", "webpki"]
27 | webauthn = [
28 | "auth-base",
29 | "bytes",
30 | "serde_cbor",
31 | "uuid",
32 | "http",
33 | "ed25519-dalek",
34 | "p256",
35 | "indexmap",
36 | ]
37 | auth-base = [
38 | "base64",
39 | "byteorder",
40 | "ring",
41 | "serde",
42 | "serde_derive",
43 | "serde_json",
44 | "serde_bytes",
45 | ]
46 | android = ["jni"]
47 |
48 | [dependencies]
49 | sha2 = { version = "0.10", features = ["oid"] }
50 | hmac = { version = "0.12", features = ["reset"] }
51 | sha-1 = { version = "0.10", features = ["oid"] }
52 | time = "0.3"
53 | base32 = "0.5"
54 | hex = "0.4"
55 | rsa = "0.9.8"
56 | rand_core = "0.6.4"
57 | x509-parser = "0.17.0"
58 |
59 | base64 = { version = "0.22", optional = true }
60 | byteorder = { version = "1.5", optional = true }
61 | ring = { version = "0.17", optional = true }
62 | untrusted = { version = "0.9.0", optional = true }
63 | serde = { version = "1.0", optional = true }
64 | serde_repr = { version = "0.1", optional = true }
65 | serde_derive = { version = "1.0", optional = true }
66 | serde_bytes = { version = "0.11", optional = true }
67 | serde_json = { version = "1.0", optional = true }
68 | serde_cbor = { version = "0.11", optional = true }
69 | webpki = { version = "0.22", optional = true, features = ["alloc"] }
70 | bytes = { version = "1.10", optional = true }
71 | http = { version = "1.3", optional = true }
72 | uuid = { version = "1.18", optional = true }
73 | ed25519-dalek = { version = "2.2.0", features = [
74 | "rand_core",
75 | "pkcs8",
76 | ], optional = true }
77 | p256 = { version = "0.13.2", optional = true }
78 | indexmap = { version = "2.12.0", features = ["serde"], optional = true }
79 |
80 | [target.'cfg(target_os = "android")'.dependencies]
81 | jni = { version = "0.21.1", optional = true }
82 |
83 | [target.'cfg(target_arch="wasm32")'.dependencies]
84 | wasm-bindgen = { version = "0.2.104" }
85 | js-sys = "0.3.81"
86 | # FIXME: https://docs.rs/getrandom/0.2.2/getrandom/#webassembly-support
87 | # let `getrandom` know that JavaScript is available for our targets
88 | # `getrandom` is not used directly, but by adding the right feature here
89 | # it will be compiled with it in our dependencies as well (since union of
90 | # all the features selected is used when building a Cargo project)
91 | getrandom = { version = "0.2", features = ["js"] }
92 | serde-wasm-bindgen = "0.6.5"
93 |
94 | [target.'cfg(target_arch="wasm32")'.dev-dependencies]
95 | wasm-bindgen-test = "0.3.54"
96 |
97 | [target.'cfg(not(target_arch="wasm32"))'.dev-dependencies]
98 | saphir = { version = "3.1.0", git = "https://github.com/richerarc/saphir.git", tag = "v3.1.0", features = [
99 | "full",
100 | ] } # not released on crates.io yet, required for dependancies
101 | tokio = { version = "1", features = ["full"] }
102 | async-stream = ">= 0.3, < 0.3.4" # 0.3.4 and up currently break saphir
103 |
104 | [dev-dependencies]
105 | serde_json = "1.0"
106 | serde_cbor = "0.11"
107 | uuid = "1.18"
108 | rand = "0.9"
109 | bytes = "1.10"
110 |
111 | #[package.metadata.wasm-pack.profile.release]
112 | #wasm-opt = false
113 |
--------------------------------------------------------------------------------
/wrappers/swift/classes/U2f.swift:
--------------------------------------------------------------------------------
1 | //
2 | // U2f.swift
3 | // Slauth
4 | //
5 | // Created by Richer Archambault on 2019-07-01.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(SlauthFFI)
11 | import SlauthFFI
12 | #endif
13 |
14 | public class WebRequest : RustObject {
15 | var raw: OpaquePointer
16 |
17 | required init(raw: OpaquePointer) {
18 | self.raw = raw
19 | }
20 |
21 | public convenience init?(json: String) {
22 | let pointer = web_request_from_json(json)
23 | if pointer == nil {
24 | return nil
25 | }
26 | self.init(raw: pointer!)
27 | }
28 |
29 | public func intoRaw() -> OpaquePointer {
30 | return self.raw
31 | }
32 |
33 | deinit {
34 | if raw != nil {
35 | web_request_free(raw)
36 | }
37 | }
38 |
39 | public func isRegister() -> Bool {
40 | return web_request_is_register(raw)
41 | }
42 |
43 | public func isSign() -> Bool {
44 | return web_request_is_sign(raw)
45 | }
46 |
47 | public func getOrigin() -> Optional {
48 | let cOrigin = web_request_origin(raw)
49 | if cOrigin == nil {
50 | return .none
51 | }
52 |
53 | let origin = String(cString: cOrigin!)
54 | free(cOrigin)
55 | return .some(origin)
56 | }
57 |
58 | public func getTimeout() -> UInt64 {
59 | return web_request_timeout(raw)
60 | }
61 |
62 | public func getKeyHandle(origin: String) -> Optional {
63 | if self.isSign() {
64 | let cKeyHandle = web_request_key_handle(raw, origin)
65 | if cKeyHandle == nil {
66 | return .none
67 | }
68 |
69 | let keyHandle = String(cString: cKeyHandle!)
70 | free(cKeyHandle)
71 | return .some(keyHandle)
72 | } else {
73 | return .none
74 | }
75 | }
76 |
77 | public func register(origin: String, attestationCert: [UInt8], attestationKey: [UInt8]) -> WebResponse {
78 | return WebResponse(raw: web_request_register(raw, origin, attestationCert, UInt64(attestationCert.count), attestationKey, UInt64(attestationKey.count)))
79 | }
80 |
81 | public func sign(origin: String, signingKey: SigningKey, counter: UInt32, userPresence: Bool) -> WebResponse {
82 | return WebResponse(raw: web_request_sign(raw, signingKey.intoRaw(), origin, UInt(counter), userPresence))
83 | }
84 |
85 | }
86 |
87 | public class SigningKey: RustObject {
88 | var raw: OpaquePointer
89 |
90 | public required init(raw: OpaquePointer) {
91 | self.raw = raw
92 | }
93 |
94 | public convenience init?(string: String) {
95 | let pointer = signing_key_from_string(string)
96 |
97 | if pointer == nil {
98 | return nil
99 | }
100 |
101 | self.init(raw: pointer!)
102 | }
103 |
104 | func intoRaw() -> OpaquePointer {
105 | return self.raw
106 | }
107 |
108 | deinit {
109 | signing_key_free(raw)
110 | }
111 |
112 | public func getKeyHandle() -> String {
113 | let cString = signing_key_get_key_handle(raw)
114 | let keyHandle = String(cString: cString!)
115 | free(cString)
116 | return keyHandle
117 | }
118 |
119 | public func toString() -> String {
120 | let csString = signing_key_to_string(raw)
121 | let sign = String(cString: csString!)
122 | free(csString)
123 | return sign
124 | }
125 | }
126 |
127 | public class WebResponse: RustObject {
128 | var raw: OpaquePointer
129 |
130 | public required init(raw: OpaquePointer) {
131 | self.raw = raw
132 | }
133 |
134 | func intoRaw() -> OpaquePointer {
135 | return self.raw
136 | }
137 |
138 | deinit {
139 | client_web_response_free(raw)
140 | }
141 |
142 | public func getSigningKey() -> Optional {
143 | let rawKey = client_web_response_signing_key(raw)
144 | if rawKey == nil {
145 | return .none
146 | }
147 |
148 | return .some(SigningKey(raw: rawKey!))
149 | }
150 |
151 | public func toJson() -> String {
152 | let cJsonString = client_web_response_to_json(raw)
153 | let json = String(cString: cJsonString!)
154 | free(cJsonString)
155 | return json
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/wrappers/wasm/example/otp-example/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "otp-example": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/otp-example",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "tsconfig.app.json",
21 | "aot": true,
22 | "assets": [
23 | "src/favicon.ico",
24 | "src/assets"
25 | ],
26 | "styles": [
27 | "src/styles.css"
28 | ],
29 | "scripts": []
30 | },
31 | "configurations": {
32 | "production": {
33 | "fileReplacements": [
34 | {
35 | "replace": "src/environments/environment.ts",
36 | "with": "src/environments/environment.prod.ts"
37 | }
38 | ],
39 | "optimization": true,
40 | "outputHashing": "all",
41 | "sourceMap": false,
42 | "extractCss": true,
43 | "namedChunks": false,
44 | "extractLicenses": true,
45 | "vendorChunk": false,
46 | "buildOptimizer": true,
47 | "budgets": [
48 | {
49 | "type": "initial",
50 | "maximumWarning": "2mb",
51 | "maximumError": "5mb"
52 | },
53 | {
54 | "type": "anyComponentStyle",
55 | "maximumWarning": "6kb",
56 | "maximumError": "10kb"
57 | }
58 | ]
59 | }
60 | }
61 | },
62 | "serve": {
63 | "builder": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "otp-example:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "otp-example:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "otp-example:build"
77 | }
78 | },
79 | "test": {
80 | "builder": "@angular-devkit/build-angular:karma",
81 | "options": {
82 | "main": "src/test.ts",
83 | "polyfills": "src/polyfills.ts",
84 | "tsConfig": "tsconfig.spec.json",
85 | "karmaConfig": "karma.conf.js",
86 | "assets": [
87 | "src/favicon.ico",
88 | "src/assets"
89 | ],
90 | "styles": [
91 | "src/styles.css"
92 | ],
93 | "scripts": []
94 | }
95 | },
96 | "lint": {
97 | "builder": "@angular-devkit/build-angular:tslint",
98 | "options": {
99 | "tsConfig": [
100 | "tsconfig.app.json",
101 | "tsconfig.spec.json",
102 | "e2e/tsconfig.json"
103 | ],
104 | "exclude": [
105 | "**/node_modules/**"
106 | ]
107 | }
108 | },
109 | "e2e": {
110 | "builder": "@angular-devkit/build-angular:protractor",
111 | "options": {
112 | "protractorConfig": "e2e/protractor.conf.js",
113 | "devServerTarget": "otp-example:serve"
114 | },
115 | "configurations": {
116 | "production": {
117 | "devServerTarget": "otp-example:serve:production"
118 | }
119 | }
120 | }
121 | }
122 | }},
123 | "defaultProject": "otp-example"
124 | }
125 |
--------------------------------------------------------------------------------
/src/oath/mod.rs:
--------------------------------------------------------------------------------
1 | //Have to allow deprecated because hmac crate hasn't been updated yet to GenericArray 1.0
2 | #[allow(deprecated)]
3 | use hmac::{
4 | digest::{generic_array::GenericArray, FixedOutputReset, InvalidLength, OutputSizeUser},
5 | Mac, SimpleHmac,
6 | };
7 | use sha1::Sha1;
8 | use sha2::{Sha256, Sha512};
9 | use std::fmt::Display;
10 |
11 | pub mod hotp;
12 | pub mod totp;
13 |
14 | pub const OTP_DEFAULT_DIGITS_VALUE: usize = 6;
15 | pub const OTP_DEFAULT_ALG_VALUE: HashesAlgorithm = HashesAlgorithm::SHA1;
16 |
17 | #[derive(Clone)]
18 | pub enum HashesAlgorithm {
19 | SHA1,
20 | SHA256,
21 | SHA512,
22 | }
23 |
24 | #[derive(Clone)]
25 | pub(crate) struct MacHashKey {
26 | secret: Vec,
27 | alg: HashesAlgorithm,
28 | }
29 |
30 | impl MacHashKey {
31 | pub(crate) fn sign(&self, data: &[u8]) -> Result {
32 | match self.alg {
33 | HashesAlgorithm::SHA1 => {
34 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?;
35 | context.update(data);
36 | Ok(HmacShaResult::RSHA1(context.finalize_fixed_reset()))
37 | }
38 | HashesAlgorithm::SHA256 => {
39 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?;
40 | context.update(data);
41 | Ok(HmacShaResult::RSHA256(context.finalize_fixed_reset()))
42 | }
43 | HashesAlgorithm::SHA512 => {
44 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?;
45 | context.update(data);
46 | Ok(HmacShaResult::RSHA512(context.finalize_fixed_reset()))
47 | }
48 | }
49 | }
50 | }
51 |
52 | #[allow(deprecated)]
53 | pub(crate) enum HmacShaResult {
54 | RSHA1(GenericArray::OutputSize>),
55 | RSHA256(GenericArray::OutputSize>),
56 | RSHA512(GenericArray::OutputSize>),
57 | }
58 |
59 | impl HmacShaResult {
60 | pub(crate) fn into_vec(self) -> Vec {
61 | match self {
62 | HmacShaResult::RSHA1(res) => res.to_vec(),
63 | HmacShaResult::RSHA256(res) => res.to_vec(),
64 | HmacShaResult::RSHA512(res) => res.to_vec(),
65 | }
66 | }
67 | }
68 |
69 | impl HashesAlgorithm {
70 | pub(crate) fn to_mac_hash_key(&self, key: &[u8]) -> MacHashKey {
71 | MacHashKey {
72 | secret: key.to_vec(),
73 | alg: self.clone(),
74 | }
75 | }
76 | }
77 |
78 | impl Display for HashesAlgorithm {
79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 | let str = match self {
81 | HashesAlgorithm::SHA1 => "SHA1".to_string(),
82 | HashesAlgorithm::SHA256 => "SHA256".to_string(),
83 | HashesAlgorithm::SHA512 => "SHA512".to_string(),
84 | };
85 | write!(f, "{}", str)
86 | }
87 | }
88 |
89 | pub trait OtpAuth {
90 | fn to_uri(&self, label: Option<&str>, issuer: Option<&str>) -> String;
91 | fn from_uri(uri: &str) -> Result
92 | where
93 | Self: Sized;
94 | }
95 |
96 | #[inline]
97 | pub(crate) fn dt(hmac_res: &[u8]) -> u32 {
98 | let offset_val = (hmac_res[hmac_res.len() - 1] & 0x0F) as usize;
99 | let h = &hmac_res[offset_val..offset_val + 4];
100 |
101 | ((h[0] as u32 & 0x7f) << 24) | ((h[1] as u32 & 0xff) << 16) | ((h[2] as u32 & 0xff) << 8) | (h[3] as u32 & 0xff)
102 | }
103 |
104 | #[inline]
105 | pub(crate) fn decode_hex_or_base_32(encoded: &str) -> Option> {
106 | // Try base32 first then is it does not follows RFC4648, try HEX
107 | base32::decode(base32::Alphabet::Rfc4648 { padding: false }, encoded)
108 | .or_else(|| base32::decode(base32::Alphabet::Rfc4648Lower { padding: false }, encoded))
109 | .or_else(|| hex::decode(encoded).ok())
110 | }
111 |
112 | #[cfg(target_arch = "wasm32")]
113 | pub fn get_time() -> u64 {
114 | let dt = js_sys::Date::new_0();
115 | let ut: f64 = dt.get_time();
116 | if ut < 0.0 {
117 | 0
118 | } else {
119 | (ut.floor() as u64) / 1000
120 | }
121 | }
122 |
123 | #[cfg(not(target_arch = "wasm32"))]
124 | pub fn get_time() -> u64 {
125 | time::OffsetDateTime::now_utc().unix_timestamp() as u64
126 | }
127 |
--------------------------------------------------------------------------------
/src/u2f/proto/constants.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | pub const MAX_RESPONSE_LEN_SHORT: usize = 256;
4 | pub const MAX_RESPONSE_LEN_EXTENDED: usize = 65536;
5 |
6 | pub const U2F_V2_VERSION_STR: &str = "U2F_V2";
7 |
8 | // From :Common U2F raw message format header - Review Draft
9 | // 2014-10-08
10 |
11 | // ASN1 constants
12 |
13 | pub const ASN1_SEQ_TYPE: u8 = 0x30;
14 | pub const ASN1_DEFINITE_SHORT_MASK: u8 = 0x80;
15 | pub const ASN1_DEFINITE_LONG_FOLLOWING_MASK: u8 = 0x7f;
16 | pub const ASN1_MAX_FOLLOWING_LEN_BYTES: usize = 126;
17 |
18 | // General constants
19 |
20 | pub const U2F_EC_KEY_SIZE: usize = 32; // EC key size in bytes
21 | pub const U2F_EC_POINT_SIZE: usize = (U2F_EC_KEY_SIZE * 2) + 1; // Size of EC point
22 | pub const U2F_MAX_KH_SIZE: usize = 128; // Max size of key handle
23 | pub const U2F_MAX_ATT_CERT_SIZE: usize = 2048; // Max size of attestation certificate
24 | pub const U2F_MAX_EC_SIG_SIZE: usize = 72; // Max size of DER coded EC signature
25 | pub const U2F_CTR_SIZE: usize = 4; // Size of counter field
26 | pub const U2F_APPID_SIZE: usize = 32; // Size of application id
27 | pub const U2F_CHAL_SIZE: usize = 32; // Size of challenge
28 | pub const U2F_REGISTER_MAX_DATA_TBS_SIZE: usize = 1 + U2F_APPID_SIZE + U2F_CHAL_SIZE + U2F_MAX_KH_SIZE + U2F_EC_POINT_SIZE;
29 | pub const U2F_AUTH_MAX_DATA_TBS_SIZE: usize = 1 + U2F_APPID_SIZE + U2F_CHAL_SIZE + 1 + 4;
30 |
31 | #[inline]
32 | pub const fn enc_size(x: u16) -> u16 {
33 | (x + 7) & 0xfff8
34 | }
35 |
36 | // EC (uncompressed) point
37 |
38 | pub const U2F_POINT_UNCOMPRESSED: u8 = 0x04; // Uncompressed point format
39 |
40 | pub struct U2fEcPoint {
41 | pub point_format: u8,
42 | pub x: [u8; U2F_EC_KEY_SIZE],
43 | pub y: [u8; U2F_EC_KEY_SIZE],
44 | }
45 |
46 | // U2F native commands
47 |
48 | pub const U2F_REGISTER: u8 = 0x01; // Registration command
49 | pub const U2F_AUTHENTICATE: u8 = 0x02; // Authenticate/sign command
50 | pub const U2F_VERSION: u8 = 0x03; // Read version string command
51 |
52 | pub const U2F_VENDOR_FIRST: u8 = 0x40; // First vendor defined command
53 | pub const U2F_VENDOR_LAST: u8 = 0xbf; // Last vendor defined command
54 |
55 | // U2F_CMD_REGISTER command defines
56 |
57 | pub const U2F_REGISTER_ID: u8 = 0x05; // Version 2 registration identifier
58 | pub const U2F_REGISTER_HASH_ID: u8 = 0x00; // Version 2 hash identintifier
59 |
60 | //pub struct U2fRegisterReq {
61 | // pub chal: [u8; U2F_CHAL_SIZE], // Challenge
62 | // pub app_id: [u8; U2F_APPID_SIZE], // Application id
63 | //}
64 | //
65 | //pub struct U2fRegisterRsp {
66 | // pub register_id: u8, // Registration identifier (U2F_REGISTER_ID_V2)
67 | // pub pubkey: U2fEcPoint, // Generated public key
68 | // pub key_handle_len: u8, // Length of key handle
69 | // pub key_handle_cert_sig: [u8;
70 | // U2F_MAX_KH_SIZE + // Key handle
71 | // U2F_MAX_ATT_CERT_SIZE + // Attestation certificate
72 | // U2F_MAX_EC_SIG_SIZE], // Registration signature
73 | //}
74 |
75 | // U2F_CMD_AUTHENTICATE command defines
76 |
77 | // Authentication control byte
78 |
79 | pub const U2F_AUTH_DONT_ENFORCE: u8 = 0x08;
80 | pub const U2F_AUTH_ENFORCE: u8 = 0x03; // Enforce user presence and sign
81 | pub const U2F_AUTH_CHECK_ONLY: u8 = 0x07; // Check only
82 | pub const U2F_AUTH_FLAG_TUP: u8 = 0x01; // Test of user presence set
83 | pub const U2F_AUTH_FLAG_TDOWN: u8 = 0x00; // Test of user presence set
84 |
85 | //pub struct U2fAuthenticateReq {
86 | // pub chal: [u8; U2F_CHAL_SIZE], // Challenge
87 | // pub app_id: [u8; U2F_APPID_SIZE], // Application id
88 | // pub key_handle_len: u8, // Length of key handle
89 | // pub key_handle: [u8; U2F_MAX_KH_SIZE], // Key handle
90 | //}
91 | //
92 | //pub struct U2fAuthenticateRsp {
93 | // pub flags: u8,
94 | // pub ctr: [u8; U2F_CTR_SIZE],
95 | // pub sig: [u8; U2F_MAX_EC_SIG_SIZE],
96 | //}
97 |
98 | // Command status responses
99 |
100 | pub const U2F_SW_NO_ERROR: u16 = 0x9000; // SW_NO_ERROR
101 | pub const U2F_SW_WRONG_DATA: u16 = 0x6A80; // SW_WRONG_DATA
102 | pub const U2F_SW_CONDITIONS_NOT_SATISFIED: u16 = 0x6985; // SW_CONDITIONS_NOT_SATISFIED
103 | pub const U2F_SW_COMMAND_NOT_ALLOWED: u16 = 0x6986; // SW_COMMAND_NOT_ALLOWED
104 | pub const U2F_SW_WRONG_LENGTH: u16 = 0x6700; //SW_WRONG_LENGTH
105 | pub const U2F_SW_CLA_NOT_SUPPORTED: u16 = 0x6E00; //SW_CLA_NOT_SUPPORTED
106 | pub const U2F_SW_INS_NOT_SUPPORTED: u16 = 0x6D00; // SW_INS_NOT_SUPPORTED
107 |
--------------------------------------------------------------------------------
/src/wasm.rs:
--------------------------------------------------------------------------------
1 | use uuid::Uuid;
2 | use wasm_bindgen::prelude::*;
3 |
4 | use crate::{
5 | oath::{
6 | decode_hex_or_base_32,
7 | totp::{TOTPBuilder, TOTPContext},
8 | HashesAlgorithm, OtpAuth,
9 | },
10 | webauthn::{
11 | authenticator::WebauthnAuthenticator,
12 | proto::web_message::{PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions},
13 | },
14 | };
15 |
16 | #[wasm_bindgen]
17 | #[derive(Clone)]
18 | pub struct OtpAlgorithm {
19 | inner: HashesAlgorithm,
20 | }
21 |
22 | #[wasm_bindgen]
23 | impl OtpAlgorithm {
24 | #[wasm_bindgen(js_name = "sha1")]
25 | pub fn sha1() -> OtpAlgorithm {
26 | OtpAlgorithm {
27 | inner: HashesAlgorithm::SHA1,
28 | }
29 | }
30 |
31 | #[wasm_bindgen(js_name = "sha256")]
32 | pub fn sha256() -> OtpAlgorithm {
33 | OtpAlgorithm {
34 | inner: HashesAlgorithm::SHA256,
35 | }
36 | }
37 |
38 | #[wasm_bindgen(js_name = "sha512")]
39 | pub fn sha512() -> OtpAlgorithm {
40 | OtpAlgorithm {
41 | inner: HashesAlgorithm::SHA512,
42 | }
43 | }
44 | }
45 |
46 | #[wasm_bindgen]
47 | #[derive(Clone)]
48 | pub struct Totp {
49 | inner: TOTPContext,
50 | }
51 |
52 | #[wasm_bindgen]
53 | impl Totp {
54 | #[wasm_bindgen(js_name = "fromParts")]
55 | pub fn from_parts(secret: String, period: i32, digits: i32, algo: OtpAlgorithm) -> Result {
56 | let secret = decode_hex_or_base_32(secret.as_str()).ok_or_else(|| "Otpauth uri is malformed, missing secret value".to_string())?;
57 | let inner = TOTPBuilder::new()
58 | .algorithm(algo.inner)
59 | .digits(digits as usize)
60 | .period(period as u64)
61 | .secret(secret.as_slice())
62 | .build();
63 |
64 | Ok(Totp { inner })
65 | }
66 |
67 | #[wasm_bindgen(js_name = "fromUri")]
68 | pub fn from_uri(uri: String) -> Result {
69 | let inner = TOTPContext::from_uri(uri.as_str())?;
70 |
71 | Ok(Totp { inner })
72 | }
73 |
74 | #[wasm_bindgen(js_name = "toUri")]
75 | pub fn to_uri(&self, application: Option, username: Option) -> String {
76 | self.inner.to_uri(username.as_deref(), application.as_deref())
77 | }
78 |
79 | #[wasm_bindgen(js_name = "generateCode")]
80 | pub fn generate_code(&self) -> String {
81 | self.inner.gen()
82 | }
83 | }
84 |
85 | #[cfg(feature = "webauthn")]
86 | #[wasm_bindgen]
87 | #[derive(Clone)]
88 | pub struct PasskeyAuthenticator {
89 | aaguid: Uuid,
90 | }
91 |
92 | #[cfg(feature = "webauthn")]
93 | #[wasm_bindgen]
94 | impl PasskeyAuthenticator {
95 | #[wasm_bindgen(constructor)]
96 | pub fn new(aaguid: String) -> Result {
97 | let aaguid = Uuid::parse_str(aaguid.as_str()).map_err(|_| "Failed to parse aaguid from string")?;
98 | Ok(PasskeyAuthenticator { aaguid })
99 | }
100 |
101 | #[wasm_bindgen(js_name = "generateCredentialCreationResponse")]
102 | pub fn generate_credential_creation_response(
103 | &self,
104 | options: JsValue,
105 | credential_id: Vec,
106 | attestation_flags: u8,
107 | origin: Option,
108 | ) -> Result {
109 | let options: PublicKeyCredentialCreationOptions = serde_wasm_bindgen::from_value(options).map_err(|e| format!("{e:?}"))?;
110 | let cred =
111 | WebauthnAuthenticator::generate_credential_creation_response(options, self.aaguid, credential_id, origin, attestation_flags)
112 | .map_err(|e| format!("{e:?}"))?;
113 | serde_wasm_bindgen::to_value(&cred).map_err(|e| format!("{e:?}"))
114 | }
115 |
116 | #[wasm_bindgen(js_name = "generateCredentialRequestResponse")]
117 | pub fn generate_credential_request_response(
118 | &self,
119 | options: JsValue,
120 | credential_id: Vec,
121 | attestation_flags: u8,
122 | origin: Option,
123 | user_handle: Option>,
124 | private_key: String,
125 | ) -> Result {
126 | let options: PublicKeyCredentialRequestOptions = serde_wasm_bindgen::from_value(options).map_err(|e| format!("{e:?}"))?;
127 | let cred = WebauthnAuthenticator::generate_credential_request_response(
128 | credential_id,
129 | attestation_flags,
130 | options,
131 | origin,
132 | user_handle,
133 | private_key,
134 | )
135 | .map_err(|e| format!("{e:?}"))?;
136 | serde_wasm_bindgen::to_value(&cred).map_err(|e| format!("{e:?}"))
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/u2f/proto/hid.rs:
--------------------------------------------------------------------------------
1 | pub mod hid_const {
2 | // From : Common U2F HID transport header - Review Draft
3 | // 2014-10-08
4 |
5 | // Size of HID reports
6 |
7 | pub const HID_RPT_SIZE: usize = 64; // Default size of raw HID report
8 |
9 | // Frame layout - command- and continuation frames
10 |
11 | pub const CID_BROADCAST: u32 = 0xffff_ffff; // Broadcast channel id
12 | pub const CID_BROADCAST_SLICE: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; // Broadcast channel id
13 |
14 | pub const TYPE_MASK: u8 = 0x80; // Frame type mask
15 | pub const TYPE_INIT: u8 = 0x80; // Initial frame identifier
16 | pub const TYPE_CONT: u8 = 0x00; // Continuation frame identifier
17 |
18 | // HID usage- and usage-page definitions
19 |
20 | pub const FIDO_USAGE_PAGE: u16 = 0xf1d0; // FIDO alliance HID usage page
21 | pub const FIDO_USAGE_U2FHID: u8 = 0x01; // U2FHID usage for top-level collection
22 | pub const FIDO_USAGE_DATA_IN: u8 = 0x20; // Raw IN data report
23 | pub const FIDO_USAGE_DATA_OUT: u8 = 0x21; // Raw OUT data report
24 |
25 | // General constants
26 |
27 | pub const U2FHID_IF_VERSION: usize = 2; // Current interface implementation version
28 | pub const U2FHID_TRANS_TIMEOUT: usize = 3000; // Default message timeout in ms
29 |
30 | // U2FHID native commands
31 |
32 | pub const U2FHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only
33 | pub const U2FHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame
34 | pub const U2FHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command
35 | pub const U2FHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization
36 | pub const U2FHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink
37 | pub const U2FHID_SYNC: u8 = TYPE_INIT | 0x3c; // Protocol resync command
38 | pub const U2FHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response
39 |
40 | pub const U2FHID_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command
41 | pub const U2FHID_VENDOR_LAST: u8 = TYPE_INIT | 0x7f; // Last vendor defined command
42 |
43 | // U2FHID_INIT command defines
44 |
45 | pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge
46 | pub const CAPFLAG_WINK: u8 = 0x01; // Device supports WINK command
47 |
48 | // Low-level error codes. Return as negatives.
49 |
50 | pub const ERR_NONE: u8 = 0x00; // No error
51 | pub const ERR_INVALID_CMD: u8 = 0x01; // Invalid command
52 | pub const ERR_INVALID_PAR: u8 = 0x02; // Invalid parameter
53 | pub const ERR_INVALID_LEN: u8 = 0x03; // Invalid message length
54 | pub const ERR_INVALID_SEQ: u8 = 0x04; // Invalid message sequencing
55 | pub const ERR_MSG_TIMEOUT: u8 = 0x05; // Message has timed out
56 | pub const ERR_CHANNEL_BUSY: u8 = 0x06; // Channel busy
57 | pub const ERR_LOCK_REQUIRED: u8 = 0x0a; // Command requires channel lock
58 | pub const ERR_SYNC_FAIL: u8 = 0x0b; // SYNC command failed
59 | pub const ERR_OTHER: u8 = 0x7f; // Other unspecified error
60 | }
61 |
62 | pub mod hid_type {
63 | #![allow(dead_code)]
64 |
65 | use crate::u2f::proto::hid::hid_const::*;
66 |
67 | pub enum Packet {
68 | Init {
69 | cmd: u8, // Frame type - b7 defines type
70 | bcnth: u8, // Message byte count - high part
71 | bcntl: u8, // Message byte count - low part
72 | data: [u8; HID_RPT_SIZE - 7], // Data payload
73 | },
74 | Cont {
75 | seq: u8, // Frame type - b7 defines type
76 | data: [u8; HID_RPT_SIZE - 5], // Data payload
77 | },
78 | }
79 |
80 | pub struct U2fHidFrame {
81 | cid: u32, // Channel identifier
82 | packet: Packet,
83 | }
84 |
85 | impl U2fHidFrame {
86 | #[inline]
87 | pub fn frame_type(&self) -> u8 {
88 | match self.packet {
89 | Packet::Init { cmd, .. } => cmd & TYPE_MASK,
90 | Packet::Cont { seq, .. } => seq & TYPE_MASK,
91 | }
92 | }
93 |
94 | #[inline]
95 | pub fn frame_cmd(&self) -> Option {
96 | match self.packet {
97 | Packet::Init { cmd, .. } => Some(cmd & !TYPE_MASK),
98 | _ => None,
99 | }
100 | }
101 |
102 | #[inline]
103 | pub fn frame_seq(&self) -> Option {
104 | match self.packet {
105 | Packet::Cont { seq, .. } => Some(seq & !TYPE_MASK),
106 | _ => None,
107 | }
108 | }
109 |
110 | #[inline]
111 | pub fn msg_len(&self) -> Option {
112 | match self.packet {
113 | Packet::Init { bcnth, bcntl, .. } => Some(bcnth as u16 * 256 + bcntl as u16),
114 | _ => None,
115 | }
116 | }
117 | }
118 |
119 | pub struct U2fHidInitReq {
120 | nonce: [u8; INIT_NONCE_SIZE], // Client application nonce
121 | }
122 |
123 | pub struct U2fHidInitRsp {
124 | nonce: [u8; INIT_NONCE_SIZE], // Client application nonce
125 | cid: u32, // Client application nonce
126 | interface_version: u8, // Channel identifier
127 | major_version: u8, // Interface version
128 | minor_version: u8, // Major version number
129 | build_version: u8, // Minor version number
130 | cap_flags: u8, // Build version number
131 | }
132 |
133 | // U2FHID_SYNC command defines
134 |
135 | pub struct U2fHidSyncReq {
136 | nonce: u8, // Client application nonce
137 | }
138 |
139 | pub struct U2fHidSyncRsp {
140 | nonce: u8, // Client application nonce
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
--------------------------------------------------------------------------------
/src/webauthn/error.rs:
--------------------------------------------------------------------------------
1 | use base64::DecodeError;
2 | #[cfg(feature = "webauthn-server")]
3 | use ring::error::Unspecified;
4 | use serde_cbor::Error as CborError;
5 | use serde_json::Error as JsonError;
6 | use std::{
7 | error::Error as StdError,
8 | fmt::{Debug, Display, Formatter},
9 | io::Error as IoError,
10 | };
11 | #[cfg(feature = "webauthn-server")]
12 | use webpki::Error as WebPkiError;
13 |
14 | #[derive(Debug)]
15 | pub enum CredentialError {
16 | RequestType,
17 | Challenge,
18 | Origin,
19 | Rp,
20 | UserPresentFlag,
21 | UserVerifiedFlag,
22 | Extensions,
23 | KeyType,
24 | CertificateMissing,
25 | CertificateNotSupported,
26 | AttestationMissing,
27 | AttestationNotSupported,
28 | Other(String),
29 | }
30 |
31 | #[derive(Debug)]
32 | pub enum TpmError {
33 | AlgorithmNotSupported,
34 | AttestationVersionNotSupported,
35 | AttestedNamePubAreaMismatch,
36 | AttToBeSignedHashAlgorithmInvalid(i64),
37 | AttToBeSignedMismatch,
38 | AttestationTypeInvalid,
39 | CertificateMissing,
40 | CertificateParsing,
41 | CertificateVersionInvalid,
42 | CertificateSubjectInvalid,
43 | CertificateExtensionNotCritical,
44 | CertificateExtensionRequirementNotMet(String),
45 | CertificateRequirementNotMet(String),
46 | MagicInvalid,
47 | PubAreaHashUnknown(u16),
48 | PubAreaMismatch,
49 | PublicKeyParametersMismatch(i64),
50 | PublicKeyCoordinatesMismatch,
51 | SignatureHashInvalid(i64),
52 | SignatureValidationFailed,
53 | TpmVendorNotFound,
54 | }
55 |
56 | #[derive(Debug)]
57 | pub enum Error {
58 | IoError(IoError),
59 | Base64Error(DecodeError),
60 | CborError(CborError),
61 | JsonError(JsonError),
62 | #[cfg(feature = "webauthn-server")]
63 | WebPkiError(WebPkiError),
64 | #[cfg(feature = "webauthn-server")]
65 | RingError(Unspecified),
66 | Version,
67 | CredentialError(CredentialError),
68 | TpmError(TpmError),
69 | Other(String),
70 | }
71 |
72 | impl From for Error {
73 | fn from(e: DecodeError) -> Self {
74 | Error::Base64Error(e)
75 | }
76 | }
77 |
78 | impl From for Error {
79 | fn from(e: CborError) -> Self {
80 | Error::CborError(e)
81 | }
82 | }
83 |
84 | impl From for Error {
85 | fn from(e: JsonError) -> Self {
86 | Error::JsonError(e)
87 | }
88 | }
89 |
90 | #[cfg(feature = "webauthn-server")]
91 | impl From for Error {
92 | fn from(e: WebPkiError) -> Self {
93 | Error::WebPkiError(e)
94 | }
95 | }
96 |
97 | #[cfg(feature = "webauthn-server")]
98 | impl From for Error {
99 | fn from(e: Unspecified) -> Self {
100 | Error::RingError(e)
101 | }
102 | }
103 |
104 | impl StdError for Error {}
105 |
106 | impl Display for CredentialError {
107 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
108 | use CredentialError::*;
109 | match self {
110 | RequestType => write!(f, "Wrong request type"),
111 | Challenge => write!(f, "Challenges do not match"),
112 | Origin => write!(f, "Wrong origin"),
113 | Rp => write!(f, "Wrong rp ID"),
114 | UserPresentFlag => write!(f, "Missing user present flag"),
115 | UserVerifiedFlag => write!(f, "Missing user verified flag"),
116 | Extensions => write!(f, "Extensions should not be present"),
117 | KeyType => write!(f, "wrong key type"),
118 | CertificateMissing => write!(f, "Certificate is missing"),
119 | CertificateNotSupported => write!(f, "Ecdaaa certificate is not supported"),
120 | AttestationMissing => write!(f, "Missing attested credential data"),
121 | AttestationNotSupported => write!(f, "Attestation format is not supported"),
122 | Other(s) => write!(f, "{}", s),
123 | }
124 | }
125 | }
126 |
127 | impl Display for Error {
128 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
129 | use Error::*;
130 | match self {
131 | IoError(io_e) => std::fmt::Display::fmt(io_e, f),
132 | Version => write!(f, "Unsupported version"),
133 | CredentialError(ce) => std::fmt::Display::fmt(ce, f),
134 | Other(s) => write!(f, "{}", s),
135 | Base64Error(e) => std::fmt::Display::fmt(e, f),
136 | CborError(cb_e) => std::fmt::Display::fmt(cb_e, f),
137 | JsonError(js_e) => std::fmt::Display::fmt(js_e, f),
138 | #[cfg(feature = "webauthn-server")]
139 | WebPkiError(wp_e) => std::fmt::Display::fmt(wp_e, f),
140 | #[cfg(feature = "webauthn-server")]
141 | RingError(r_e) => std::fmt::Display::fmt(r_e, f),
142 | TpmError(tpm_e) => std::fmt::Display::fmt(tpm_e, f),
143 | }
144 | }
145 | }
146 |
147 | impl Display for TpmError {
148 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
149 | match self {
150 | TpmError::AlgorithmNotSupported => write!(f, "Algorithm not supported"),
151 | TpmError::AttestationVersionNotSupported => write!(f, "Attestation version not supported"),
152 | TpmError::AttestedNamePubAreaMismatch => write!(f, "Attested name does not match with hash of PubArea"),
153 | TpmError::AttToBeSignedHashAlgorithmInvalid(hash) => write!(f, "Invalid hash algorithm for AttToBeSigned: {}", hash),
154 | TpmError::AttToBeSignedMismatch => write!(f, "AttToBeSigned does not match with CertInfo.extra_data"),
155 | TpmError::AttestationTypeInvalid => write!(f, "Attestation type is invalid"),
156 | TpmError::CertificateMissing => write!(f, "Aik certificate is missing"),
157 | TpmError::CertificateParsing => write!(f, "Error parsing aik certificate"),
158 | TpmError::CertificateVersionInvalid => write!(f, "Certificate version is not supported. Expected v3"),
159 | TpmError::CertificateSubjectInvalid => write!(f, "Certificate subject is not empty"),
160 | TpmError::CertificateExtensionNotCritical => write!(f, "Certificate extension is not critical"),
161 | TpmError::CertificateExtensionRequirementNotMet(ext) => write!(f, "Requirements for {} certificate extension are not met", ext),
162 | TpmError::CertificateRequirementNotMet(field) => write!(f, "Requirements for {} certificate field are not met", field),
163 | TpmError::MagicInvalid => write!(f, "CertInfo.magic is different then TPM_GENERATED_VALUE"),
164 | TpmError::PubAreaHashUnknown(hash) => write!(f, "PubArea's Tpm Algorithm ID {} is not supported", hash),
165 | TpmError::PubAreaMismatch => write!(f, "PubArea public key information does not match with CredentialPublicKey"),
166 | TpmError::PublicKeyParametersMismatch(alg) => write!(
167 | f,
168 | "PubArea public key parameters does not match with CredentialPublicKey with algorithm {}",
169 | alg
170 | ),
171 | TpmError::PublicKeyCoordinatesMismatch => {
172 | write!(f, "PubArea public key coordinates does not match with EC2 CredentialPublicKey")
173 | }
174 | TpmError::SignatureHashInvalid(hash) => write!(f, "Signature hash not supported {}", hash),
175 | TpmError::SignatureValidationFailed => write!(f, "Signature validation failed"),
176 | TpmError::TpmVendorNotFound => write!(f, "TPM Vendor not found"),
177 | }
178 | }
179 | }
180 |
181 | impl From for Error {
182 | fn from(e: IoError) -> Self {
183 | Error::IoError(e)
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/u2f/client/token.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io::Read,
3 | sync::atomic::{AtomicU32, Ordering},
4 | time::Duration,
5 | };
6 |
7 | use ring::{
8 | rand::{self, SystemRandom},
9 | signature::{self, KeyPair},
10 | };
11 |
12 | use crate::u2f::{
13 | client::SigningKey,
14 | error::Error,
15 | proto::{
16 | constants::*,
17 | raw_message::{
18 | apdu, AuthenticateRequest, AuthenticateResponse, Message, RegisterRequest, RegisterResponse, VersionRequest, VersionResponse,
19 | },
20 | },
21 | };
22 |
23 | pub(crate) fn gen_key_handle(app_id: &[u8], chall: &[u8]) -> String {
24 | let mut data = Vec::with_capacity(app_id.len() + chall.len());
25 | data.extend_from_slice(app_id);
26 | data.extend_from_slice(chall);
27 | base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &data)
28 | }
29 |
30 | pub fn register(req: RegisterRequest, attestation_cert: &[u8], attestation_key: &[u8]) -> Result<(RegisterResponse, SigningKey), Error> {
31 | let RegisterRequest { challenge, application } = req;
32 |
33 | // Generate a key pair in PKCS#8 (v2) format.
34 | let rng = rand::SystemRandom::new();
35 | let registered_key_pkcs8_doc = signature::EcdsaKeyPair::generate_pkcs8(&signature::ECDSA_P256_SHA256_ASN1_SIGNING, &rng)?;
36 |
37 | let registered_key_pkcs8_bytes = registered_key_pkcs8_doc.as_ref();
38 |
39 | let key_handle = gen_key_handle(&application, &challenge);
40 |
41 | let random = SystemRandom::new();
42 | let registered_key_pair =
43 | signature::EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_ASN1_SIGNING, registered_key_pkcs8_bytes, &random)?;
44 | let registered_pub_key = registered_key_pair.public_key();
45 | let mut user_public_key = [0u8; U2F_EC_POINT_SIZE];
46 |
47 | registered_pub_key.as_ref().read_exact(&mut user_public_key)?;
48 |
49 | let key_handle_length = key_handle.len() as u8;
50 |
51 | let mut tbs_vec = Vec::with_capacity(U2F_REGISTER_MAX_DATA_TBS_SIZE);
52 |
53 | tbs_vec.push(0x00);
54 | tbs_vec.extend_from_slice(&application);
55 | tbs_vec.extend_from_slice(&challenge);
56 | tbs_vec.extend_from_slice(key_handle.as_bytes());
57 | tbs_vec.extend_from_slice(&user_public_key);
58 |
59 | let att_key_pair = signature::EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_ASN1_SIGNING, attestation_key, &random)?;
60 |
61 | let sig = att_key_pair.sign(&rng, tbs_vec.as_slice())?;
62 |
63 | let signature = sig.as_ref().to_vec();
64 |
65 | Ok((
66 | RegisterResponse {
67 | reserved: U2F_REGISTER_ID,
68 | user_public_key,
69 | key_handle_length,
70 | key_handle: key_handle.clone(),
71 | attestation_cert: attestation_cert.to_vec(),
72 | signature,
73 | },
74 | SigningKey {
75 | key_handle,
76 | private_key: registered_key_pkcs8_bytes.to_vec(),
77 | },
78 | ))
79 | }
80 |
81 | pub fn sign(req: AuthenticateRequest, signing_key: &SigningKey, counter: u32, user_presence: bool) -> Result {
82 | let AuthenticateRequest {
83 | control,
84 | challenge,
85 | application,
86 | ..
87 | } = req;
88 |
89 | if !user_presence && control == U2F_AUTH_ENFORCE {
90 | return Err(Error::U2FErrorCode(U2F_SW_CONDITIONS_NOT_SATISFIED));
91 | }
92 |
93 | let user_presence = if user_presence { U2F_AUTH_FLAG_TUP } else { U2F_AUTH_FLAG_TDOWN };
94 |
95 | match control {
96 | U2F_AUTH_CHECK_ONLY => Err(Error::U2FErrorCode(U2F_SW_CONDITIONS_NOT_SATISFIED)),
97 | U2F_AUTH_ENFORCE | U2F_AUTH_DONT_ENFORCE => {
98 | let rng = rand::SystemRandom::new();
99 | let key_pair = signature::EcdsaKeyPair::from_pkcs8(
100 | &signature::ECDSA_P256_SHA256_ASN1_SIGNING,
101 | signing_key.private_key.as_slice(),
102 | &SystemRandom::new(),
103 | )?;
104 |
105 | let mut tbs_vec = Vec::with_capacity(U2F_AUTH_MAX_DATA_TBS_SIZE);
106 | tbs_vec.extend_from_slice(&application);
107 | tbs_vec.push(user_presence);
108 | tbs_vec.extend_from_slice(&counter.to_be_bytes());
109 | tbs_vec.extend_from_slice(&challenge);
110 |
111 | let sig = key_pair.sign(&rng, tbs_vec.as_slice())?;
112 | let signature = sig.as_ref().to_vec();
113 |
114 | Ok(AuthenticateResponse {
115 | user_presence,
116 | counter,
117 | signature,
118 | })
119 | }
120 | _ => Err(Error::U2FErrorCode(U2F_SW_INS_NOT_SUPPORTED)),
121 | }
122 | }
123 |
124 | pub struct U2FSToken {
125 | pub(crate) store: Box,
126 | pub(crate) presence_validator: Box,
127 | pub(crate) counter: AtomicU32,
128 | }
129 |
130 | impl U2FSToken {
131 | pub fn handle_apdu_request_with_timeout(&self, req: apdu::Request, timeout: Option) -> apdu::Response {
132 | let res = match req.command_mode {
133 | U2F_REGISTER => RegisterRequest::from_apdu(req).and_then(|reg| self.register(reg, timeout).and_then(|rsp| rsp.into_apdu())),
134 | U2F_AUTHENTICATE => {
135 | AuthenticateRequest::from_apdu(req).and_then(|auth| self.authenticate(auth, timeout).and_then(|rsp| rsp.into_apdu()))
136 | }
137 | U2F_VERSION => VersionRequest::from_apdu(req).and_then(|vers| self.version(vers).into_apdu()),
138 | com if (U2F_VENDOR_FIRST..=U2F_VENDOR_LAST).contains(&com) => Err(Error::U2FErrorCode(U2F_SW_INS_NOT_SUPPORTED)),
139 | _ => Err(Error::U2FErrorCode(U2F_SW_COMMAND_NOT_ALLOWED)),
140 | };
141 |
142 | match res {
143 | Ok(rsp) => rsp,
144 | Err(e) => match e {
145 | Error::U2FErrorCode(sw) => apdu::Response::from_status(sw),
146 | _ => apdu::Response::from_status(U2F_SW_WRONG_LENGTH),
147 | },
148 | }
149 | }
150 |
151 | pub fn handle_apdu_request(&self, req: apdu::Request) -> apdu::Response {
152 | self.handle_apdu_request_with_timeout(req, Some(Duration::from_secs(10)))
153 | }
154 |
155 | fn register(&self, req: RegisterRequest, timeout: Option) -> Result {
156 | if self
157 | .presence_validator
158 | .check_user_presence(timeout.unwrap_or_else(|| Duration::from_secs(10)))
159 | {
160 | let (rsp, signing_key) = register(req, self.store.attestation_cert(), self.store.attestation_key())?;
161 |
162 | if self.store.save(signing_key.key_handle, signing_key.private_key) {
163 | Ok(rsp)
164 | } else {
165 | Err(Error::Other("U2F Register: Unable to save private key".to_string()))
166 | }
167 | } else {
168 | Err(Error::U2FErrorCode(U2F_SW_CONDITIONS_NOT_SATISFIED))
169 | }
170 | }
171 |
172 | fn authenticate(&self, req: AuthenticateRequest, timeout: Option) -> Result {
173 | let expected_key_handle = String::from_utf8_lossy(req.key_handle.as_slice()).to_string();
174 |
175 | if let Some(pk_bytes) = self.store.load(expected_key_handle.as_str()) {
176 | return sign(
177 | req,
178 | &SigningKey {
179 | key_handle: expected_key_handle,
180 | private_key: pk_bytes.to_vec(),
181 | },
182 | self.counter.fetch_add(1, Ordering::Relaxed),
183 | self.presence_validator
184 | .check_user_presence(timeout.unwrap_or_else(|| Duration::from_secs(10))),
185 | );
186 | }
187 |
188 | Err(Error::U2FErrorCode(U2F_SW_WRONG_DATA))
189 | }
190 |
191 | fn version(&self, _: VersionRequest) -> VersionResponse {
192 | VersionResponse {
193 | version: U2F_V2_VERSION_STR.to_string(),
194 | }
195 | }
196 | }
197 |
198 | pub trait KeyStore {
199 | fn contains(&self, handle: &str) -> bool;
200 | fn load(&self, handle: &str) -> Option<&[u8]>;
201 | fn save(&self, handle: String, key: Vec) -> bool;
202 | fn attestation_cert(&self) -> &[u8];
203 | fn attestation_key(&self) -> &[u8];
204 | }
205 |
206 | pub trait PresenceValidator {
207 | fn check_user_presence(&self, timeout: Duration) -> bool;
208 | }
209 |
--------------------------------------------------------------------------------
/examples/web-server.rs:
--------------------------------------------------------------------------------
1 | use rand::seq::IteratorRandom;
2 | use saphir::prelude::*;
3 | use serde_json::{json, Value};
4 | use slauth::{
5 | base64::*,
6 | webauthn::{
7 | error::{CredentialError as CredE, Error::CredentialError},
8 | proto::{
9 | constants::WEBAUTHN_CHALLENGE_LENGTH,
10 | raw_message::CredentialPublicKey,
11 | web_message::{PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions},
12 | },
13 | server::{CredentialCreationBuilder, CredentialCreationVerifier, CredentialRequestBuilder, CredentialRequestVerifier},
14 | },
15 | };
16 | use std::{collections::HashMap, sync::RwLock};
17 |
18 | struct TestController {
19 | creds: RwLock>,
20 | reg_contexts: RwLock>,
21 | sign_contexts: RwLock>,
22 | }
23 |
24 | impl TestController {
25 | pub fn new() -> Self {
26 | TestController {
27 | creds: RwLock::new(HashMap::new()),
28 | reg_contexts: RwLock::new(HashMap::new()),
29 | sign_contexts: RwLock::new(HashMap::new()),
30 | }
31 | }
32 | }
33 |
34 | #[derive(Debug)]
35 | enum TestError {
36 | Slauth(slauth::webauthn::error::Error),
37 | Internal,
38 | }
39 |
40 | impl From for TestError {
41 | fn from(e: slauth::webauthn::error::Error) -> Self {
42 | TestError::Slauth(e)
43 | }
44 | }
45 |
46 | impl Responder for TestError {
47 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder {
48 | match self {
49 | TestError::Slauth(e) => builder.status(500).body(e.to_string()),
50 | TestError::Internal => builder.status(500),
51 | }
52 | }
53 | }
54 |
55 | #[controller(name = "webauthn")]
56 | impl TestController {
57 | #[get("/register")]
58 | async fn register_request(&self) -> Result, TestError> {
59 | let uuid = BASE64.encode("e1aea4d6-d2ee-4218-9f1c-5ccddadaa1a7");
60 | let builder = CredentialCreationBuilder::new()
61 | .challenge(gen_challenge(WEBAUTHN_CHALLENGE_LENGTH))
62 | .user(uuid.clone(), "lfauvel@devolutions.net".to_string(), "Luc Fauvel".to_string(), None)
63 | .rp("localhost".to_string(), None, Some("localhost".to_string()))
64 | .build();
65 |
66 | match builder {
67 | Ok(pubkey) => {
68 | if let Ok(mut contexts) = self.reg_contexts.write() {
69 | contexts.insert(uuid, pubkey.clone());
70 | }
71 | Ok(Json(json!({ "publicKey": pubkey })))
72 | }
73 | Err(e) => {
74 | dbg!(e);
75 | Err(TestError::Internal)
76 | }
77 | }
78 | }
79 |
80 | #[post("/register")]
81 | async fn complete_register(&self, cred: Json) -> Result<(), TestError> {
82 | let cred = cred.into_inner();
83 | let uuid = BASE64.encode("e1aea4d6-d2ee-4218-9f1c-5ccddadaa1a7");
84 | if let Some(context) = self.reg_contexts.read().expect("should be ok").get(&uuid) {
85 | let mut verifier = CredentialCreationVerifier::new(cred.clone(), context.clone(), "http://localhost");
86 | if let Ok(result) = verifier.verify() {
87 | self.creds.write().unwrap().insert(cred.id, (result.public_key, result.sign_count));
88 | }
89 | }
90 |
91 | Ok(())
92 | }
93 |
94 | #[get("/sign")]
95 | async fn sign_request(&self) -> Result, TestError> {
96 | let mut builder = CredentialRequestBuilder::new()
97 | .rp("localhost".to_string())
98 | .challenge(gen_challenge(WEBAUTHN_CHALLENGE_LENGTH));
99 | let uuid = BASE64.encode("e1aea4d6-d2ee-4218-9f1c-5ccddadaa1a7");
100 | for (cred, _) in self.creds.read().unwrap().iter() {
101 | builder = builder.allow_credential(cred.clone());
102 | }
103 | match builder.build() {
104 | Ok(pubkey) => {
105 | self.sign_contexts.write().unwrap().insert(uuid, pubkey.clone());
106 | Ok(Json(json!({ "publicKey": pubkey })))
107 | }
108 | Err(e) => {
109 | dbg!(e);
110 | Err(TestError::Internal)
111 | }
112 | }
113 | }
114 |
115 | #[post("/sign")]
116 | async fn complete_sign(&self, req: Json) -> Result<(u16, String), TestError> {
117 | let cred = req.into_inner();
118 | let uuid = BASE64.encode("e1aea4d6-d2ee-4218-9f1c-5ccddadaa1a7");
119 |
120 | let ctx_lock = self
121 | .sign_contexts
122 | .read()
123 | .map_err(|_| CredentialError(CredE::Other("Synchronization error".to_string())))?;
124 | let context = ctx_lock
125 | .get(&uuid)
126 | .ok_or(CredentialError(CredE::Other("Context not found".to_string())))?;
127 |
128 | let creds_lock = self
129 | .creds
130 | .read()
131 | .map_err(|_| CredentialError(CredE::Other("Synchronization error".to_string())))?;
132 | let (cred_pub, sign_count) = creds_lock
133 | .get(&cred.id)
134 | .ok_or(CredentialError(CredE::Other("Credential not found".to_string())))?;
135 |
136 | let mut verifier = CredentialRequestVerifier::new(
137 | cred,
138 | cred_pub.clone(),
139 | context.clone(),
140 | "http://localhost",
141 | uuid.as_bytes(),
142 | *sign_count,
143 | );
144 | let res = verifier.verify()?;
145 | self.creds.write().unwrap().insert(uuid, (cred_pub.clone(), res.sign_count));
146 | Ok((200, "it works".to_string()))
147 | }
148 | }
149 |
150 | pub struct CorsMiddleware;
151 |
152 | impl CorsMiddleware {
153 | pub fn new() -> Self {
154 | CorsMiddleware {}
155 | }
156 | }
157 |
158 | impl Default for CorsMiddleware {
159 | fn default() -> Self {
160 | Self::new()
161 | }
162 | }
163 |
164 | #[middleware]
165 | impl CorsMiddleware {
166 | // fn resolve(&self, req: &mut SyncRequest, res: &mut SyncResponse) -> RequestContinuation {
167 | async fn next(&self, mut ctx: HttpContext, chain: &dyn MiddlewareChain) -> Result {
168 | let req = ctx.state.request_unchecked();
169 | let headers = req.headers().clone();
170 | let is_auth = req.uri().path().contains("/auth");
171 |
172 | if req.method() == Method::OPTIONS.as_ref() {
173 | ctx.after(Builder::new()
174 | .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
175 | .header("Access-Control-Allow-Headers", "Auth-ID, WWW-Authenticate, auth-id, www-authenticate, authorization, Authorization, Origin, origin, Set-Cookie, set-cookie, Cookie, cookie, Code, Content-Type, content-type")
176 | .status(StatusCode::NO_CONTENT)
177 | .build()?);
178 | } else {
179 | ctx = chain.next(ctx).await?;
180 | }
181 |
182 | let res = ctx.state.response_unchecked_mut();
183 | let res_headers = res.headers_mut();
184 |
185 | if let Some(Ok(origin)) = headers.get("Origin").map(|c| c.to_str()) {
186 | res_headers.insert("Access-Control-Allow-Origin", origin.parse()?);
187 | } else {
188 | res_headers.insert("Access-Control-Allow-Origin", "*".parse()?);
189 | }
190 |
191 | res_headers.insert("Access-Control-Expose-Headers", "Auth-ID, WWW-Authenticate, auth-id, www-authenticate, authorization, Authorization, Origin, origin, Set-Cookie, set-cookie, Cookie, cookie".parse()?);
192 |
193 | if is_auth {
194 | res_headers.insert("Access-Control-Allow-Credentials", "true".parse()?);
195 | }
196 |
197 | Ok(ctx)
198 | }
199 | }
200 |
201 | #[tokio::main]
202 | async fn main() -> Result<(), SaphirError> {
203 | let server = Server::builder()
204 | .configure_middlewares(|stack| stack.apply(CorsMiddleware::new(), vec!["/"], None))
205 | .configure_router(|router| router.controller(TestController::new()))
206 | .configure_listener(|listener_config| listener_config.interface("0.0.0.0:12345"))
207 | .build();
208 |
209 | server.run().await
210 | }
211 |
212 | pub fn gen_challenge(len: usize) -> String {
213 | let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
214 |
215 | let mut rng = rand::rng();
216 | let value = (0..len)
217 | .map(|_| charset.chars().choose(&mut rng).unwrap() as u8)
218 | .collect::>();
219 | BASE64_URLSAFE_NOPAD.encode(value.as_slice())
220 | }
221 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | beta:
7 | description: Publish a beta version (npmjs)
8 | default: false
9 | required: true
10 | type: boolean
11 | android:
12 | description: Publish an android version (maven)
13 | default: true
14 | required: true
15 | type: boolean
16 | rust:
17 | description: Publish a rust version (crates.io)
18 | default: true
19 | required: true
20 | type: boolean
21 | swift:
22 | description: Publish a swift version (swift package)
23 | default: true
24 | required: true
25 | type: boolean
26 | wasm:
27 | description: Publish a wasm (bundler) version (npmjs)
28 | default: true
29 | required: true
30 | type: boolean
31 | wasm_web:
32 | description: Publish a wasm (web) version (npmjs)
33 | default: true
34 | required: true
35 | type: boolean
36 |
37 | jobs:
38 | build-wasm:
39 | environment: npm-publish
40 | if: ${{ inputs.wasm }}
41 | runs-on: ubuntu-latest
42 | permissions:
43 | contents: read
44 | id-token: write
45 |
46 | steps:
47 | - name: Checkout repo
48 | uses: actions/checkout@v4
49 |
50 | - name: Setup wasm
51 | run: |
52 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
53 | wasm-pack --version
54 |
55 | - name: Build
56 | run: sh build.sh
57 | working-directory: wrappers/wasm
58 |
59 | - name: Upload artifact
60 | uses: actions/upload-artifact@v4.3.6
61 | with:
62 | name: wasm
63 | path: dist/bundler
64 |
65 | - name: Publish
66 | run: npm publish --tag ${{ inputs.beta && 'beta' || 'latest' }}
67 | working-directory: dist/bundler
68 |
69 | - name: Update Artifactory Cache
70 | run: gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="slauth"
71 | env:
72 | GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}
73 |
74 | build-wasm-web:
75 | environment: npm-publish
76 | if: ${{ inputs.wasm_web }}
77 | runs-on: ubuntu-latest
78 | permissions:
79 | contents: read
80 | id-token: write
81 |
82 | steps:
83 | - name: Checkout repo
84 | uses: actions/checkout@v4
85 |
86 | - name: Setup wasm
87 | run: |
88 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
89 | wasm-pack --version
90 |
91 | - name: Build
92 | run: bash build-web.sh
93 | working-directory: wrappers/wasm
94 |
95 | - name: Upload artifact
96 | uses: actions/upload-artifact@v4.3.6
97 | with:
98 | name: wasm-web
99 | path: dist/web
100 |
101 | - name: Publish
102 | run: npm publish --tag ${{ inputs.beta && 'beta' || 'latest' }}
103 | working-directory: dist/web
104 |
105 | - name: Update Artifactory Cache
106 | run: gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="slauth-web"
107 | env:
108 | GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}
109 |
110 | build-android:
111 | environment: cloudsmith-publish
112 | if: ${{ inputs.android }}
113 | runs-on: ubuntu-latest
114 |
115 | steps:
116 | - name: Checkout repo
117 | uses: actions/checkout@v4
118 |
119 | - name: Set up JDK 1.8
120 | uses: actions/setup-java@v4
121 | with:
122 | java-version: 8
123 | distribution: adopt
124 |
125 | - name: Setup Android
126 | run: |
127 | wget https://dl.google.com/android/repository/android-ndk-r23b-linux.zip
128 | unzip android-ndk-r23b-linux.zip
129 | export ANDROID_NDK_HOME=$GITHUB_WORKSPACE/android-ndk-r23b
130 | echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV
131 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
132 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android" >> $GITHUB_ENV::LIBRARY_PATH
133 | echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android" >> $GITHUB_ENV::LD_LIBRARY_PATH
134 | rustup target add aarch64-linux-android
135 | rustup target add x86_64-linux-android
136 | rustup target add x86_64-unknown-linux-gnu
137 |
138 | - name: Build
139 | run: sh wrappers/android/build.sh
140 |
141 | - name: Create local.properties
142 | run: echo "sdk.dir=$ANDROID_HOME" > local.properties
143 |
144 | - name: Allow gradlew to run
145 | run: chmod +x gradlew
146 |
147 | - name: Package .aar
148 | run: ./gradlew clean assembleRelease
149 | env:
150 | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
151 | CLOUDSMITH_USERNAME: bot-devolutions
152 |
153 | - run: ./gradlew publish
154 | env:
155 | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
156 | CLOUDSMITH_USERNAME: bot-devolutions
157 |
158 | - name: Upload .aar artifact
159 | uses: actions/upload-artifact@v4.3.6
160 | with:
161 | name: android
162 | path: wrappers/android/build/outputs/aar/slauth-release.aar
163 |
164 | build-rust:
165 | environment: crates-publish
166 | if: ${{ inputs.rust }}
167 | runs-on: ubuntu-latest
168 | permissions:
169 | contents: read
170 | id-token: write
171 |
172 | steps:
173 | - name: Checkout repo
174 | uses: actions/checkout@v4
175 |
176 | - name: Authenticate with crates.io
177 | id: auth
178 | uses: rust-lang/crates-io-auth-action@v1
179 |
180 | - name: Publish
181 | run: cargo publish
182 | env:
183 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
184 |
185 | build-swift:
186 | if: ${{ inputs.swift }}
187 | environment: cloudsmith-publish
188 | runs-on: macos-latest
189 |
190 | steps:
191 | - name: Checkout repo
192 | uses: actions/checkout@v4
193 |
194 | - name: Setup rust
195 | run: |
196 | rustup target add aarch64-apple-ios
197 | rustup target add x86_64-apple-darwin
198 | rustup target add aarch64-apple-ios-sim
199 | rustup target add aarch64-apple-darwin
200 |
201 | - name: Setup version
202 | id: version
203 | run: |
204 | VERSION=$(grep -E "^version\s*=" Cargo.toml | head -1 | awk -F'"' '{print $2}')
205 | echo "version=$VERSION" >> $GITHUB_OUTPUT
206 |
207 | - name: Setup code signing
208 | run: |
209 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
210 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
211 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
212 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
213 |
214 | CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
215 | echo -n "$CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
216 | security import $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
217 | security list-keychain -d user -s $KEYCHAIN_PATH
218 |
219 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
220 | env:
221 | CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
222 | CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
223 | KEYCHAIN_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
224 |
225 | - name: Generate package
226 | run: sh wrappers/swift/build.sh
227 |
228 | - name: Sign XCFramework
229 | run: |
230 | codesign --timestamp --sign "$SIGNING_IDENTITY" package/libslauth.xcframework
231 | codesign --verify --verbose package/libslauth.xcframework
232 | env:
233 | SIGNING_IDENTITY: "Developer ID Application: Devolutions inc."
234 |
235 | - name: Package Swift Package
236 | run: |
237 | VERSION=${{ steps.version.outputs.version }}
238 | mv package Slauth-$VERSION
239 | zip -r Slauth-$VERSION.zip Slauth-$VERSION
240 |
241 | - name: Upload package
242 | uses: actions/upload-artifact@v4.3.6
243 | with:
244 | name: swift-zip
245 | path: ./Slauth-${{ steps.version.outputs.version }}.zip
246 |
247 | - name: Install Cloudsmith CLI
248 | run: pip install --upgrade cloudsmith-cli
249 |
250 | - name: Push package to Cloudsmith
251 | run: cloudsmith push swift devolutions/swift-public Slauth-${{ steps.version.outputs.version }}.zip --name Slauth --version ${{ steps.version.outputs.version }} --scope devolutions
252 | env:
253 | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
254 |
--------------------------------------------------------------------------------
/src/u2f/server/mod.rs:
--------------------------------------------------------------------------------
1 | use std::sync::atomic::{AtomicU64, Ordering};
2 |
3 | use ring::signature;
4 | use sha2::{Digest, Sha256};
5 | use webpki::{EndEntityCert, ECDSA_P256_SHA256};
6 |
7 | use crate::{
8 | base64::*,
9 | u2f::{
10 | error::Error,
11 | proto::{
12 | constants::U2F_V2_VERSION_STR,
13 | raw_message,
14 | raw_message::{apdu::ApduFrame, Message},
15 | web_message::*,
16 | },
17 | },
18 | };
19 |
20 | static REQUESTS_IDS: AtomicU64 = AtomicU64::new(0);
21 |
22 | pub struct U2fRequestBuilder {
23 | rtype: U2fRequestType,
24 | app_id: Option,
25 | challenge: Option,
26 | timeout: Option,
27 | registered_keys: Option>,
28 | }
29 |
30 | impl U2fRequestBuilder {
31 | fn new(typ: U2fRequestType) -> Self {
32 | U2fRequestBuilder {
33 | app_id: None,
34 | challenge: None,
35 | timeout: None,
36 | rtype: typ,
37 | registered_keys: None,
38 | }
39 | }
40 |
41 | pub fn register() -> Self {
42 | Self::new(U2fRequestType::Register)
43 | }
44 |
45 | pub fn sign() -> Self {
46 | Self::new(U2fRequestType::Sign)
47 | }
48 |
49 | pub fn app_id(mut self, app_id: String) -> Self {
50 | self.app_id = Some(app_id);
51 | self
52 | }
53 |
54 | pub fn challenge(mut self, challenge: String) -> Self {
55 | self.challenge = Some(BASE64.encode(challenge));
56 | self
57 | }
58 |
59 | pub fn timeout_sec(mut self, timeout: u64) -> Self {
60 | self.timeout = Some(timeout);
61 | self
62 | }
63 |
64 | pub fn registered_keys(mut self, regk: Vec) -> Self {
65 | self.registered_keys = Some(regk);
66 | self
67 | }
68 |
69 | pub fn build(self) -> Result {
70 | let U2fRequestBuilder {
71 | app_id,
72 | challenge,
73 | timeout,
74 | rtype,
75 | registered_keys,
76 | } = self;
77 |
78 | let challenge = BASE64_URLSAFE_NOPAD.encode(
79 | challenge
80 | .as_ref()
81 | .ok_or_else(|| Error::Other("Unable to build a U2F request without a challenge".to_string()))?,
82 | );
83 |
84 | let data = match rtype {
85 | U2fRequestType::Register => Request::Register(U2fRegisterRequest {
86 | register_requests: vec![RegisterRequest {
87 | version: U2F_V2_VERSION_STR.to_string(),
88 | challenge,
89 | }],
90 | registered_keys: registered_keys.unwrap_or_default(),
91 | }),
92 | U2fRequestType::Sign => {
93 | let registered_keys = registered_keys
94 | .ok_or_else(|| Error::Other("Unable to build a U2F Sign request without at least one registered key".to_string()))?;
95 |
96 | Request::Sign(U2fSignRequest {
97 | challenge,
98 | registered_keys,
99 | })
100 | }
101 | };
102 |
103 | Ok(U2fRequest {
104 | req_type: rtype,
105 | app_id,
106 | timeout_seconds: timeout,
107 | request_id: Some(REQUESTS_IDS.fetch_add(1, Ordering::Relaxed)),
108 | data,
109 | })
110 | }
111 | }
112 |
113 | impl U2fResponse {
114 | pub fn as_register_response(&self) -> Option<&U2fRegisterResponse> {
115 | match self.response_data {
116 | Response::Register(ref reg) => Some(reg),
117 | _ => None,
118 | }
119 | }
120 |
121 | pub fn as_sign_response(&self) -> Option<&U2fSignResponse> {
122 | match self.response_data {
123 | Response::Sign(ref sign) => Some(sign),
124 | _ => None,
125 | }
126 | }
127 |
128 | pub fn is_error_response(&self) -> bool {
129 | matches!(self.response_data, Response::Error(_))
130 | }
131 |
132 | pub fn as_error_response(&self) -> Option<&ClientError> {
133 | match self.response_data {
134 | Response::Error(ref e) => Some(e),
135 | _ => None,
136 | }
137 | }
138 | }
139 |
140 | impl U2fRegisterResponse {
141 | /// Attempt to parse and validate the registration response data and construct a Registration Object
142 | ///
143 | /// Returns a `Registration` struct if all conditions are satisfied and signature is validated, else will return an error
144 | pub fn get_registration(&self) -> Result {
145 | let U2fRegisterResponse {
146 | version,
147 | registration_data,
148 | client_data,
149 | } = &self;
150 |
151 | if version != U2F_V2_VERSION_STR {
152 | return Err(Error::Version);
153 | }
154 |
155 | // Validate that input is consistent with what's expected
156 | let registration_data_bytes = BASE64_URLSAFE_NOPAD
157 | .decode(registration_data)
158 | .map_err(|e| Error::Registration(e.to_string()))?;
159 | let raw_rsp = raw_message::apdu::Response::read_from(®istration_data_bytes)?;
160 | let raw_u2f_reg = raw_message::RegisterResponse::from_apdu(raw_rsp)?;
161 |
162 | let client_data_bytes = BASE64_URLSAFE_NOPAD
163 | .decode(client_data)
164 | .map_err(|e| Error::Registration(e.to_string()))?;
165 |
166 | let client_data: ClientData =
167 | serde_json::from_slice(client_data_bytes.as_slice()).map_err(|e| Error::Registration(e.to_string()))?;
168 |
169 | // Validate signature
170 | let attestation_cert = EndEntityCert::try_from(raw_u2f_reg.attestation_cert.as_slice())?;
171 |
172 | let mut hasher = Sha256::new();
173 |
174 | hasher.update(client_data_bytes.as_slice());
175 |
176 | let challenge_hash = hasher.finalize_reset();
177 |
178 | hasher.update(&client_data.origin);
179 |
180 | let app_id_hash = hasher.finalize_reset();
181 |
182 | let signature_data = {
183 | let mut data = vec![0x00];
184 | data.extend_from_slice(&app_id_hash);
185 | data.extend_from_slice(&challenge_hash);
186 | data.extend_from_slice(raw_u2f_reg.key_handle.as_bytes());
187 | data.extend_from_slice(&raw_u2f_reg.user_public_key);
188 | data
189 | };
190 |
191 | attestation_cert.verify_signature(&ECDSA_P256_SHA256, &signature_data, &raw_u2f_reg.signature)?;
192 |
193 | Ok(Registration {
194 | version: U2F_V2_VERSION_STR.to_string(),
195 | app_id: client_data.origin,
196 | key_handle: raw_u2f_reg.key_handle,
197 | pub_key: raw_u2f_reg.user_public_key.to_vec(),
198 | attestation_cert: raw_u2f_reg.attestation_cert,
199 | })
200 | }
201 | }
202 |
203 | impl Registration {
204 | pub fn get_registered_key(&self) -> RegisteredKey {
205 | RegisteredKey {
206 | version: self.version.clone(),
207 | key_handle: self.key_handle.clone(),
208 | transports: None,
209 | app_id: Some(self.app_id.clone()),
210 | }
211 | }
212 | }
213 |
214 | impl U2fSignResponse {
215 | pub fn validate_signature(&self, public_key: &[u8]) -> Result {
216 | let U2fSignResponse {
217 | signature_data,
218 | client_data,
219 | ..
220 | } = &self;
221 |
222 | let signature_data_byte = BASE64_URLSAFE_NOPAD
223 | .decode(signature_data)
224 | .map_err(|e| Error::Registration(e.to_string()))?;
225 | let raw_rsp = raw_message::apdu::Response::read_from(&signature_data_byte)?;
226 | let raw_u2f_sign = raw_message::AuthenticateResponse::from_apdu(raw_rsp)?;
227 |
228 | let client_data_bytes = BASE64_URLSAFE_NOPAD
229 | .decode(client_data)
230 | .map_err(|e| Error::Registration(e.to_string()))?;
231 |
232 | let client_data: ClientData =
233 | serde_json::from_slice(client_data_bytes.as_slice()).map_err(|e| Error::Registration(e.to_string()))?;
234 |
235 | let mut hasher = Sha256::new();
236 |
237 | hasher.update(client_data_bytes.as_slice());
238 |
239 | let challenge_hash = hasher.finalize_reset();
240 |
241 | hasher.update(&client_data.origin);
242 |
243 | let app_id_hash = hasher.finalize_reset();
244 |
245 | let signature_data = {
246 | let mut data = Vec::new();
247 | data.extend_from_slice(&app_id_hash);
248 | data.push(raw_u2f_sign.user_presence);
249 | data.extend_from_slice(&raw_u2f_sign.counter.to_le_bytes());
250 | data.extend_from_slice(&challenge_hash);
251 | data
252 | };
253 |
254 | let public_key = signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public_key);
255 |
256 | public_key.verify(signature_data.as_slice(), raw_u2f_sign.signature.as_slice())?;
257 |
258 | Ok((raw_u2f_sign.user_presence & 0x01) == 0x01)
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/slauth.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | #define OTP_DEFAULT_DIGITS_VALUE 6
7 |
8 | #define HOTP_DEFAULT_COUNTER_VALUE 0
9 |
10 | #define HOTP_DEFAULT_RESYNC_VALUE 2
11 |
12 | #define TOTP_DEFAULT_PERIOD_VALUE 30
13 |
14 | #define TOTP_DEFAULT_BACK_RESYNC_VALUE 1
15 |
16 | #define TOTP_DEFAULT_FORWARD_RESYNC_VALUE 1
17 |
18 | #define MAX_RESPONSE_LEN_SHORT 256
19 |
20 | #define MAX_RESPONSE_LEN_EXTENDED 65536
21 |
22 | #define ASN1_SEQ_TYPE 48
23 |
24 | #define ASN1_DEFINITE_SHORT_MASK 128
25 |
26 | #define ASN1_DEFINITE_LONG_FOLLOWING_MASK 127
27 |
28 | #define ASN1_MAX_FOLLOWING_LEN_BYTES 126
29 |
30 | #define U2F_EC_KEY_SIZE 32
31 |
32 | #define U2F_EC_POINT_SIZE ((U2F_EC_KEY_SIZE * 2) + 1)
33 |
34 | #define U2F_MAX_KH_SIZE 128
35 |
36 | #define U2F_MAX_ATT_CERT_SIZE 2048
37 |
38 | #define U2F_MAX_EC_SIG_SIZE 72
39 |
40 | #define U2F_CTR_SIZE 4
41 |
42 | #define U2F_APPID_SIZE 32
43 |
44 | #define U2F_CHAL_SIZE 32
45 |
46 | #define U2F_REGISTER_MAX_DATA_TBS_SIZE ((((1 + U2F_APPID_SIZE) + U2F_CHAL_SIZE) + U2F_MAX_KH_SIZE) + U2F_EC_POINT_SIZE)
47 |
48 | #define U2F_AUTH_MAX_DATA_TBS_SIZE ((((1 + U2F_APPID_SIZE) + U2F_CHAL_SIZE) + 1) + 4)
49 |
50 | #define U2F_POINT_UNCOMPRESSED 4
51 |
52 | #define U2F_REGISTER 1
53 |
54 | #define U2F_AUTHENTICATE 2
55 |
56 | #define U2F_VERSION 3
57 |
58 | #define U2F_VENDOR_FIRST 64
59 |
60 | #define U2F_VENDOR_LAST 191
61 |
62 | #define U2F_REGISTER_ID 5
63 |
64 | #define U2F_REGISTER_HASH_ID 0
65 |
66 | #define U2F_AUTH_DONT_ENFORCE 8
67 |
68 | #define U2F_AUTH_ENFORCE 3
69 |
70 | #define U2F_AUTH_CHECK_ONLY 7
71 |
72 | #define U2F_AUTH_FLAG_TUP 1
73 |
74 | #define U2F_AUTH_FLAG_TDOWN 0
75 |
76 | #define U2F_SW_NO_ERROR 36864
77 |
78 | #define U2F_SW_WRONG_DATA 27264
79 |
80 | #define U2F_SW_CONDITIONS_NOT_SATISFIED 27013
81 |
82 | #define U2F_SW_COMMAND_NOT_ALLOWED 27014
83 |
84 | #define U2F_SW_WRONG_LENGTH 26368
85 |
86 | #define U2F_SW_CLA_NOT_SUPPORTED 28160
87 |
88 | #define U2F_SW_INS_NOT_SUPPORTED 27904
89 |
90 | #define HID_RPT_SIZE 64
91 |
92 | #define CID_BROADCAST 4294967295
93 |
94 | #define TYPE_MASK 128
95 |
96 | #define TYPE_INIT 128
97 |
98 | #define TYPE_CONT 0
99 |
100 | #define FIDO_USAGE_PAGE 61904
101 |
102 | #define FIDO_USAGE_U2FHID 1
103 |
104 | #define FIDO_USAGE_DATA_IN 32
105 |
106 | #define FIDO_USAGE_DATA_OUT 33
107 |
108 | #define U2FHID_IF_VERSION 2
109 |
110 | #define U2FHID_TRANS_TIMEOUT 3000
111 |
112 | #define U2FHID_PING (TYPE_INIT | 1)
113 |
114 | #define U2FHID_MSG (TYPE_INIT | 3)
115 |
116 | #define U2FHID_LOCK (TYPE_INIT | 4)
117 |
118 | #define U2FHID_INIT (TYPE_INIT | 6)
119 |
120 | #define U2FHID_WINK (TYPE_INIT | 8)
121 |
122 | #define U2FHID_SYNC (TYPE_INIT | 60)
123 |
124 | #define U2FHID_ERROR (TYPE_INIT | 63)
125 |
126 | #define U2FHID_VENDOR_FIRST (TYPE_INIT | 64)
127 |
128 | #define U2FHID_VENDOR_LAST (TYPE_INIT | 127)
129 |
130 | #define INIT_NONCE_SIZE 8
131 |
132 | #define CAPFLAG_WINK 1
133 |
134 | #define ERR_NONE 0
135 |
136 | #define ERR_INVALID_CMD 1
137 |
138 | #define ERR_INVALID_PAR 2
139 |
140 | #define ERR_INVALID_LEN 3
141 |
142 | #define ERR_INVALID_SEQ 4
143 |
144 | #define ERR_MSG_TIMEOUT 5
145 |
146 | #define ERR_CHANNEL_BUSY 6
147 |
148 | #define ERR_LOCK_REQUIRED 10
149 |
150 | #define ERR_SYNC_FAIL 11
151 |
152 | #define ERR_OTHER 127
153 |
154 | #define WEBAUTHN_CHALLENGE_LENGTH 32
155 |
156 | #define WEBAUTHN_CREDENTIAL_ID_LENGTH 16
157 |
158 | #define WEBAUTHN_USER_PRESENT_FLAG 1
159 |
160 | #define WEBAUTHN_USER_VERIFIED_FLAG 4
161 |
162 | #define WEBAUTHN_ATTESTED_CREDENTIAL_DATA_FLAG 64
163 |
164 | #define WEBAUTHN_EXTENSION_DATA_FLAG 128
165 |
166 | #define WEBAUTH_PUBLIC_KEY_TYPE_OKP 1
167 |
168 | #define WEBAUTH_PUBLIC_KEY_TYPE_EC2 2
169 |
170 | #define WEBAUTH_PUBLIC_KEY_TYPE_RSA 3
171 |
172 | #define ECDSA_Y_PREFIX_POSITIVE 2
173 |
174 | #define ECDSA_Y_PREFIX_NEGATIVE 3
175 |
176 | #define ECDSA_Y_PREFIX_UNCOMPRESSED 4
177 |
178 | #define ECDSA_CURVE_P256 1
179 |
180 | #define ECDSA_CURVE_P384 2
181 |
182 | #define ECDSA_CURVE_P521 3
183 |
184 | #define ECDAA_CURVE_ED25519 6
185 |
186 | #define TPM_GENERATED_VALUE 4283712327
187 |
188 | typedef struct AuthenticatorCreationResponse AuthenticatorCreationResponse;
189 |
190 | typedef struct AuthenticatorRequestResponse AuthenticatorRequestResponse;
191 |
192 | typedef struct ClientWebResponse ClientWebResponse;
193 |
194 | typedef struct HOTPContext HOTPContext;
195 |
196 | typedef struct HashesAlgorithm HashesAlgorithm;
197 |
198 | typedef struct SigningKey SigningKey;
199 |
200 | typedef struct TOTPContext TOTPContext;
201 |
202 | /**
203 | *
204 | */
205 | typedef struct U2fRequest U2fRequest;
206 |
207 | typedef struct U2fRequest WebRequest;
208 |
209 | typedef struct Buffer {
210 | uint8_t *data;
211 | uintptr_t len;
212 | } Buffer;
213 |
214 |
215 |
216 | struct HOTPContext *hotp_from_uri(const char *uri);
217 |
218 | void hotp_free(struct HOTPContext *hotp);
219 |
220 | char *hotp_to_uri(struct HOTPContext *hotp, const char *label, const char *issuer);
221 |
222 | char *hotp_gen(struct HOTPContext *hotp);
223 |
224 | void hotp_inc(struct HOTPContext *hotp);
225 |
226 | bool hotp_verify(struct HOTPContext *hotp, const char *code);
227 |
228 | bool hotp_validate_current(struct HOTPContext *hotp, const char *code);
229 |
230 | struct TOTPContext *totp_from_uri(const char *uri);
231 |
232 | void totp_free(struct TOTPContext *totp);
233 |
234 | char *totp_to_uri(struct TOTPContext *totp, const char *label, const char *issuer);
235 |
236 | char *totp_gen(struct TOTPContext *totp);
237 |
238 | char *totp_gen_with(struct TOTPContext *totp, unsigned long elapsed);
239 |
240 | bool totp_verify(struct TOTPContext *totp, const char *code);
241 |
242 | bool totp_validate_current(struct TOTPContext *totp, const char *code);
243 |
244 | WebRequest *web_request_from_json(const char *req);
245 |
246 | void web_request_free(WebRequest *req);
247 |
248 | bool web_request_is_register(WebRequest *req);
249 |
250 | bool web_request_is_sign(WebRequest *req);
251 |
252 | char *web_request_origin(WebRequest *req);
253 |
254 | unsigned long long web_request_timeout(WebRequest *req);
255 |
256 | char *web_request_key_handle(WebRequest *req, const char *origin);
257 |
258 | struct ClientWebResponse *web_request_sign(WebRequest *req,
259 | struct SigningKey *signing_key,
260 | const char *origin,
261 | unsigned long counter,
262 | bool user_presence);
263 |
264 | struct ClientWebResponse *web_request_register(WebRequest *req,
265 | const char *origin,
266 | const unsigned char *attestation_cert,
267 | unsigned long long attestation_cert_len,
268 | const unsigned char *attestation_key,
269 | unsigned long long attestation_key_len);
270 |
271 | void client_web_response_free(struct ClientWebResponse *rsp);
272 |
273 | char *client_web_response_to_json(struct ClientWebResponse *rsp);
274 |
275 | struct SigningKey *client_web_response_signing_key(struct ClientWebResponse *rsp);
276 |
277 | void signing_key_free(struct SigningKey *s);
278 |
279 | char *signing_key_to_string(struct SigningKey *s);
280 |
281 | char *signing_key_get_key_handle(struct SigningKey *s);
282 |
283 | struct SigningKey *signing_key_from_string(const char *s);
284 |
285 | char *get_private_key_from_response(struct AuthenticatorCreationResponse *res);
286 |
287 | struct Buffer get_attestation_object_from_response(struct AuthenticatorCreationResponse *res);
288 |
289 | void response_free(struct AuthenticatorCreationResponse *res);
290 |
291 | struct AuthenticatorCreationResponse *generate_credential_creation_response(const char *aaguid,
292 | const unsigned char *credential_id,
293 | uintptr_t credential_id_length,
294 | const char *rp_id,
295 | uint8_t attestation_flags,
296 | const int *cose_algorithm_identifiers,
297 | uintptr_t cose_algorithm_identifiers_length);
298 |
299 | struct AuthenticatorRequestResponse *generate_credential_request_response(const char *rp_id,
300 | const char *private_key,
301 | uint8_t attestation_flags,
302 | const unsigned char *client_data_hash,
303 | uintptr_t client_data_hash_length);
304 |
305 | struct Buffer get_auth_data_from_response(struct AuthenticatorRequestResponse *res);
306 |
307 | struct Buffer get_signature_from_response(struct AuthenticatorRequestResponse *res);
308 |
309 | char *get_error_message(struct AuthenticatorRequestResponse *res);
310 |
311 | bool is_success(struct AuthenticatorRequestResponse *res);
312 |
313 | char *private_key_to_pkcs8_der(const char *private_key);
314 |
315 | char *pkcs8_to_custom_private_key(const char *pkcs8_key);
316 |
--------------------------------------------------------------------------------
/src/u2f/proto/web_message.rs:
--------------------------------------------------------------------------------
1 | use serde_derive::*;
2 | use serde_repr::*;
3 |
4 | #[derive(Serialize, Deserialize, Debug, PartialOrd, Eq, PartialEq)]
5 | /// FIDO U2F Transports
6 | pub enum Transport {
7 | /// Bluetooth Classic
8 | #[serde(rename = "bt")]
9 | Bluetooth,
10 | /// Bluetooth Low-Energy
11 | #[serde(rename = "ble")]
12 | BluetoothLE,
13 | /// Near field communication
14 | #[serde(rename = "nfc")]
15 | Nfc,
16 | /// Usb removable device
17 | #[serde(rename = "usb")]
18 | Usb,
19 | /// Usb non-removable device
20 | #[serde(rename = "usb-internal")]
21 | UsbInternal,
22 | }
23 |
24 | #[derive(Serialize, Deserialize, Clone, Debug)]
25 | #[serde(rename_all = "camelCase")]
26 | pub struct Registration {
27 | pub version: String,
28 | pub app_id: String,
29 | pub key_handle: String,
30 | #[serde(with = "serde_bytes")]
31 | pub pub_key: Vec,
32 | #[serde(with = "serde_bytes")]
33 | pub attestation_cert: Vec,
34 | }
35 |
36 | #[derive(Serialize, Deserialize)]
37 | #[serde(rename_all = "camelCase")]
38 | pub struct RegisterRequest {
39 | /// The version of the protocol that the to-be-registered token must speak. E.g. "U2F_V2".
40 | pub version: String,
41 | /// The websafe-base64-encoded challenge.
42 | pub challenge: String,
43 | }
44 |
45 | #[derive(Serialize, Deserialize)]
46 | #[serde(rename_all = "camelCase")]
47 | pub struct RegisteredKey {
48 | /// The version of the protocol that the to-be-registered token must speak. E.g. "U2F_V2".
49 | pub version: String,
50 | /// The registered keyHandle to use for signing, as a websafe-base64 encoding of the key handle bytes returned by the U2F token during registration.
51 | pub key_handle: String,
52 | /// The transport(s) this token supports, if known by the RP.
53 | #[serde(skip_serializing_if = "Option::is_none")]
54 | #[serde(default)]
55 | pub transports: Option>,
56 | /// The application id that the RP would like to assert for this key handle, if it's distinct from the application id for the overall request. (Ordinarily this will be omitted.)
57 | #[serde(skip_serializing_if = "Option::is_none")]
58 | #[serde(default)]
59 | pub app_id: Option,
60 | }
61 |
62 | #[derive(Serialize, Deserialize)]
63 | pub enum U2fRequestType {
64 | #[serde(rename = "u2f_register_request")]
65 | Register,
66 | #[serde(rename = "u2f_sign_request")]
67 | Sign,
68 | }
69 |
70 | #[derive(Serialize, Deserialize)]
71 | pub enum U2fResponseType {
72 | #[serde(rename = "u2f_register_response")]
73 | Register,
74 | #[serde(rename = "u2f_sign_response")]
75 | Sign,
76 | }
77 |
78 | impl From for U2fResponseType {
79 | fn from(t: U2fRequestType) -> Self {
80 | if let U2fRequestType::Register = t {
81 | U2fResponseType::Register
82 | } else {
83 | U2fResponseType::Sign
84 | }
85 | }
86 | }
87 |
88 | impl<'a> From<&'a U2fRequestType> for U2fResponseType {
89 | fn from(t: &'a U2fRequestType) -> Self {
90 | if let U2fRequestType::Register = t {
91 | U2fResponseType::Register
92 | } else {
93 | U2fResponseType::Sign
94 | }
95 | }
96 | }
97 |
98 | #[derive(Serialize, Deserialize)]
99 | #[serde(rename_all = "camelCase")]
100 | pub struct U2fRequest {
101 | /// The type of request, either Register ("u2f_register_request") or Sign ("u2f_sign_request").
102 | #[serde(rename = "type")]
103 | pub req_type: U2fRequestType,
104 | /// An application identifier for the request. If none is given, the origin of the calling web page is used.
105 | #[serde(skip_serializing_if = "Option::is_none")]
106 | #[serde(default)]
107 | pub app_id: Option,
108 | /// A timeout for the FIDO Client's processing, in seconds.
109 | #[serde(skip_serializing_if = "Option::is_none")]
110 | #[serde(default)]
111 | pub timeout_seconds: Option,
112 | /// An integer identifying this request from concurrent requests.
113 | #[serde(skip_serializing_if = "Option::is_none")]
114 | #[serde(default)]
115 | pub request_id: Option,
116 | /// The specific request data
117 | #[serde(flatten)]
118 | pub data: Request,
119 | }
120 |
121 | #[derive(Serialize, Deserialize)]
122 | #[serde(rename_all = "camelCase")]
123 | pub struct U2fRegisterRequest {
124 | pub register_requests: Vec,
125 | /// An array of RegisteredKeys representing the U2F tokens registered to this user.
126 | pub registered_keys: Vec,
127 | }
128 |
129 | #[derive(Serialize, Deserialize)]
130 | #[serde(rename_all = "camelCase")]
131 | pub struct U2fSignRequest {
132 | /// The websafe-base64-encoded challenge.
133 | pub challenge: String,
134 | /// An array of RegisteredKeys representing the U2F tokens registered to this user.
135 | pub registered_keys: Vec,
136 | }
137 |
138 | #[derive(Serialize, Deserialize)]
139 | #[serde(untagged)]
140 | pub enum Request {
141 | Register(U2fRegisterRequest),
142 | Sign(U2fSignRequest),
143 | }
144 |
145 | #[derive(Serialize, Deserialize)]
146 | #[serde(rename_all = "camelCase")]
147 | pub struct U2fResponse {
148 | /// The type of request, either Register ("u2f_register_response") or Sign ("u2f_sign_response").
149 | #[serde(rename = "type")]
150 | pub rsp_type: U2fResponseType,
151 | #[serde(skip_serializing_if = "Option::is_none")]
152 | #[serde(default)]
153 | pub request_id: Option,
154 | pub response_data: Response,
155 | }
156 |
157 | #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug)]
158 | #[repr(u8)]
159 | pub enum ErrorCode {
160 | Ok = 0,
161 | OtherError = 1,
162 | BadRequest = 2,
163 | ConfigurationUnsupported = 3,
164 | DeviceIneligible = 4,
165 | Timeout = 5,
166 | }
167 |
168 | #[derive(Serialize, Deserialize)]
169 | #[serde(rename_all = "camelCase")]
170 | pub struct ClientError {
171 | pub error_code: ErrorCode,
172 | #[serde(skip_serializing_if = "Option::is_none")]
173 | #[serde(default)]
174 | pub error_message: Option,
175 | }
176 |
177 | impl ClientError {
178 | pub fn bad_request(msg: Option) -> ClientError {
179 | ClientError {
180 | error_code: ErrorCode::BadRequest,
181 | error_message: msg,
182 | }
183 | }
184 |
185 | pub fn other_error(msg: Option) -> ClientError {
186 | ClientError {
187 | error_code: ErrorCode::OtherError,
188 | error_message: msg,
189 | }
190 | }
191 |
192 | pub fn configuration_unsupported(msg: Option) -> ClientError {
193 | ClientError {
194 | error_code: ErrorCode::ConfigurationUnsupported,
195 | error_message: msg,
196 | }
197 | }
198 |
199 | pub fn device_ineligible(msg: Option) -> ClientError {
200 | ClientError {
201 | error_code: ErrorCode::DeviceIneligible,
202 | error_message: msg,
203 | }
204 | }
205 |
206 | pub fn timeout(msg: Option) -> ClientError {
207 | ClientError {
208 | error_code: ErrorCode::Timeout,
209 | error_message: msg,
210 | }
211 | }
212 | }
213 |
214 | #[derive(Serialize, Deserialize)]
215 | #[serde(rename_all = "camelCase")]
216 | pub struct U2fRegisterResponse {
217 | pub version: String,
218 | pub registration_data: String,
219 | pub client_data: String,
220 | }
221 |
222 | #[derive(Serialize, Deserialize)]
223 | #[serde(rename_all = "camelCase")]
224 | pub struct U2fSignResponse {
225 | pub key_handle: String,
226 | pub signature_data: String,
227 | pub client_data: String,
228 | }
229 |
230 | #[derive(Serialize, Deserialize)]
231 | #[serde(untagged)]
232 | pub enum Response {
233 | Register(U2fRegisterResponse),
234 | Sign(U2fSignResponse),
235 | Error(ClientError),
236 | }
237 |
238 | #[derive(Serialize, Deserialize)]
239 | pub enum ClientDataType {
240 | #[serde(rename = "navigator.id.getAssertion")]
241 | Authentication,
242 | #[serde(rename = "navigator.id.finishEnrollment")]
243 | Registration,
244 | }
245 |
246 | #[derive(Serialize, Deserialize)]
247 | pub struct ClientData {
248 | pub typ: ClientDataType,
249 | pub challenge: String,
250 | pub origin: String,
251 | #[serde(skip_serializing_if = "Option::is_none")]
252 | #[serde(default)]
253 | pub cid_pubkey: Option,
254 | }
255 |
256 | #[test]
257 | fn request_json_format() {
258 | let sign_req_str = "{\"type\": \"u2f_sign_request\",\"appId\": \"https://example.com\",\"challenge\": \"YWM3OGQ5YWJhODljNzlhMDU0NTZjZDhiNmU3NWY3NGE\",\"registeredKeys\": [{\"version\": \"U2F_V2\", \"keyHandle\": \"test\", \"transports\": [\"usb\", \"nfc\"]}],\"timeoutSeconds\": 30}";
259 |
260 | let sign_req = serde_json::from_str::(sign_req_str).unwrap();
261 |
262 | if let U2fRequestType::Sign = sign_req.req_type {
263 | assert_eq!(sign_req.app_id.unwrap(), "https://example.com");
264 | assert!(sign_req.request_id.is_none());
265 | assert_eq!(sign_req.timeout_seconds, Some(30));
266 |
267 | if let Request::Sign(sign) = &sign_req.data {
268 | assert_eq!(sign.challenge, "YWM3OGQ5YWJhODljNzlhMDU0NTZjZDhiNmU3NWY3NGE");
269 | assert_eq!(sign.registered_keys.len(), 1);
270 |
271 | assert!(sign.registered_keys[0].app_id.is_none());
272 | assert_eq!(sign.registered_keys[0].version, "U2F_V2");
273 | assert_eq!(sign.registered_keys[0].key_handle, "test");
274 | assert_eq!(sign.registered_keys[0].transports, Some(vec![Transport::Usb, Transport::Nfc]));
275 | }
276 | } else {
277 | panic!()
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
1 | # This template contains all of the possible sections and their default values
2 |
3 | # Note that all fields that take a lint level have these possible values:
4 | # * deny - An error will be produced and the check will fail
5 | # * warn - A warning will be produced, but the check will not fail
6 | # * allow - No warning or error will be produced, though in some cases a note
7 | # will be
8 |
9 | # The values provided in this template are the default values that will be used
10 | # when any section or field is not specified in your own configuration
11 |
12 | # If 1 or more target triples (and optionally, target_features) are specified,
13 | # only the specified targets will be checked when running `cargo deny check`.
14 | # This means, if a particular package is only ever used as a target specific
15 | # dependency, such as, for example, the `nix` crate only being used via the
16 | # `target_family = "unix"` configuration, that only having windows targets in
17 | # this list would mean the nix crate, as well as any of its exclusive
18 | # dependencies not shared by any other crates, would be ignored, as the target
19 | # list here is effectively saying which targets you are building for.
20 | targets = [
21 | # The triple can be any string, but only the target triples built in to
22 | # rustc (as of 1.40) can be checked against actual config expressions
23 | #{ triple = "x86_64-unknown-linux-musl" },
24 | # { triple = "x86_64-unknown-linux-gnu", features = ["default"] }
25 | # You can also specify which target_features you promise are enabled for a
26 | # particular target. target_features are currently not validated against
27 | # the actual valid features supported by the target architecture.
28 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
29 | ]
30 |
31 | # This section is considered when running `cargo deny check advisories`
32 | # More documentation for the advisories section can be found here:
33 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
34 | [advisories]
35 | # The path where the advisory database is cloned/fetched into
36 | db-path = "~/.cargo/advisory-db"
37 | # The url(s) of the advisory databases to use
38 | db-urls = ["https://github.com/rustsec/advisory-db"]
39 | # The lint level for security vulnerabilities
40 | vulnerability = "deny"
41 | # The lint level for unmaintained crates
42 | unmaintained = "warn"
43 | # The lint level for crates that have been yanked from their source registry
44 | yanked = "warn"
45 | # The lint level for crates with security notices. Note that as of
46 | # 2019-12-17 there are no security notice advisories in
47 | # https://github.com/rustsec/advisory-db
48 | notice = "warn"
49 | # A list of advisory IDs to ignore. Note that ignored advisories will still
50 | # output a note when they are encountered.
51 | ignore = [
52 | "RUSTSEC-2020-0071" # Used by chrono; chrono have not released a patched version yet (0.4.22 is not patched)
53 | ]
54 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score
55 | # lower than the range specified will be ignored. Note that ignored advisories
56 | # will still output a note when they are encountered.
57 | # * None - CVSS Score 0.0
58 | # * Low - CVSS Score 0.1 - 3.9
59 | # * Medium - CVSS Score 4.0 - 6.9
60 | # * High - CVSS Score 7.0 - 8.9
61 | # * Critical - CVSS Score 9.0 - 10.0
62 | #severity-threshold =
63 |
64 | # If this is true, then cargo deny will use the git executable to fetch advisory database.
65 | # If this is false, then it uses a built-in git library.
66 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
67 | # See Git Authentication for more information about setting up git authentication.
68 | #git-fetch-with-cli = true
69 |
70 | # This section is considered when running `cargo deny check licenses`
71 | # More documentation for the licenses section can be found here:
72 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
73 | [licenses]
74 | # The lint level for crates which do not have a detectable license
75 | unlicensed = "deny"
76 | # List of explicitly allowed licenses
77 | # See https://spdx.org/licenses/ for list of possible licenses
78 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
79 | allow = [
80 | "MIT",
81 | "ISC",
82 | "Apache-2.0",
83 | "BSD-3-Clause",
84 | "Unicode-DFS-2016",
85 | "OpenSSL",
86 | ]
87 | # List of explicitly disallowed licenses
88 | # See https://spdx.org/licenses/ for list of possible licenses
89 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
90 | deny = [
91 | #"Nokia",
92 | ]
93 | # Lint level for licenses considered copyleft
94 | copyleft = "warn"
95 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
96 | # * both - The license will be approved if it is both OSI-approved *AND* FSF
97 | # * either - The license will be approved if it is either OSI-approved *OR* FSF
98 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
99 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
100 | # * neither - This predicate is ignored and the default lint level is used
101 | allow-osi-fsf-free = "neither"
102 | # Lint level used when no other predicates are matched
103 | # 1. License isn't in the allow or deny lists
104 | # 2. License isn't copyleft
105 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
106 | default = "deny"
107 | # The confidence threshold for detecting a license from license text.
108 | # The higher the value, the more closely the license text must be to the
109 | # canonical license text of a valid SPDX license file.
110 | # [possible values: any between 0.0 and 1.0].
111 | confidence-threshold = 0.8
112 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses
113 | # aren't accepted for every possible crate as with the normal allow list
114 | exceptions = [
115 | # Each entry is the crate and version constraint, and its specific allow
116 | # list
117 | #{ allow = ["Zlib"], name = "adler32", version = "*" },
118 | ]
119 |
120 | # Some crates don't have (easily) machine readable licensing information,
121 | # adding a clarification entry for it allows you to manually specify the
122 | # licensing information
123 | [[licenses.clarify]]
124 | name = "ring"
125 | version = "*"
126 | expression = "MIT AND ISC AND OpenSSL"
127 | license-files = []
128 | # The name of the crate the clarification applies to
129 | #name = "ring"
130 | # The optional version constraint for the crate
131 | #version = "*"
132 | # The SPDX expression for the license requirements of the crate
133 | #expression = "MIT AND ISC AND OpenSSL"
134 | # One or more files in the crate's source used as the "source of truth" for
135 | # the license expression. If the contents match, the clarification will be used
136 | # when running the license check, otherwise the clarification will be ignored
137 | # and the crate will be checked normally, which may produce warnings or errors
138 | # depending on the rest of your configuration
139 | #license-files = [
140 | # Each entry is a crate relative path, and the (opaque) hash of its contents
141 | #{ path = "LICENSE", hash = 0xbd0eed23 }
142 | #]
143 |
144 | [licenses.private]
145 | # If true, ignores workspace crates that aren't published, or are only
146 | # published to private registries.
147 | # To see how to mark a crate as unpublished (to the official registry),
148 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
149 | ignore = false
150 | # One or more private registries that you might publish crates to, if a crate
151 | # is only published to private registries, and ignore is true, the crate will
152 | # not have its license(s) checked
153 | registries = [
154 | #"https://sekretz.com/registry
155 | ]
156 |
157 | # This section is considered when running `cargo deny check bans`.
158 | # More documentation about the 'bans' section can be found here:
159 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
160 | [bans]
161 | # Lint level for when multiple versions of the same crate are detected
162 | multiple-versions = "warn"
163 | # Lint level for when a crate version requirement is `*`
164 | wildcards = "allow"
165 | # The graph highlighting used when creating dotgraphs for crates
166 | # with multiple versions
167 | # * lowest-version - The path to the lowest versioned duplicate is highlighted
168 | # * simplest-path - The path to the version with the fewest edges is highlighted
169 | # * all - Both lowest-version and simplest-path are used
170 | highlight = "all"
171 | # List of crates that are allowed. Use with care!
172 | allow = [
173 | #{ name = "ansi_term", version = "=0.11.0" },
174 | ]
175 | # List of crates to deny
176 | deny = [
177 | # Each entry the name of a crate and a version range. If version is
178 | # not specified, all versions will be matched.
179 | #{ name = "ansi_term", version = "=0.11.0" },
180 | #
181 | # Wrapper crates can optionally be specified to allow the crate when it
182 | # is a direct dependency of the otherwise banned crate
183 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
184 | ]
185 | # Certain crates/versions that will be skipped when doing duplicate detection.
186 | skip = [
187 | #{ name = "ansi_term", version = "=0.11.0" },
188 | ]
189 | # Similarly to `skip` allows you to skip certain crates during duplicate
190 | # detection. Unlike skip, it also includes the entire tree of transitive
191 | # dependencies starting at the specified crate, up to a certain depth, which is
192 | # by default infinite
193 | skip-tree = [
194 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
195 | ]
196 |
197 | # This section is considered when running `cargo deny check sources`.
198 | # More documentation about the 'sources' section can be found here:
199 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
200 | [sources]
201 | # Lint level for what to happen when a crate from a crate registry that is not
202 | # in the allow list is encountered
203 | unknown-registry = "warn"
204 | # Lint level for what to happen when a crate from a git repository that is not
205 | # in the allow list is encountered
206 | unknown-git = "warn"
207 | # List of URLs for allowed crate registries. Defaults to the crates.io index
208 | # if not specified. If it is specified but empty, no registries are allowed.
209 | allow-registry = ["https://github.com/rust-lang/crates.io-index"]
210 | # List of URLs for allowed Git repositories
211 | allow-git = []
212 |
213 | [sources.allow-org]
214 | # 1 or more github.com organizations to allow git sources for
215 | #github = [""]
216 | # 1 or more gitlab.com organizations to allow git sources for
217 | #gitlab = [""]
218 | # 1 or more bitbucket.org organizations to allow git sources for
219 | #bitbucket = [""]
220 |
--------------------------------------------------------------------------------
/src/webauthn/proto/web_message.rs:
--------------------------------------------------------------------------------
1 | use base64::Engine;
2 | use http::Uri;
3 | use serde_derive::*;
4 | use std::collections::HashMap;
5 |
6 | use crate::base64::BASE64;
7 |
8 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
9 | #[serde(rename = "publicKey", rename_all = "camelCase")]
10 | pub struct PublicKeyCredentialCreationOptions {
11 | pub rp: PublicKeyCredentialRpEntity,
12 | pub user: PublicKeyCredentialUserEntity,
13 | pub challenge: String,
14 | pub pub_key_cred_params: Vec,
15 | #[serde(skip_serializing_if = "Option::is_none")]
16 | pub timeout: Option,
17 | #[serde(skip_serializing_if = "Vec::is_empty", default)]
18 | pub exclude_credentials: Vec,
19 | #[serde(skip_serializing_if = "Option::is_none")]
20 | pub authenticator_selection: Option,
21 | #[serde(skip_serializing_if = "Option::is_none")]
22 | pub attestation: Option,
23 | #[serde(default, skip_serializing_if = "Extensions::is_empty")]
24 | pub extensions: Extensions,
25 | }
26 |
27 | #[derive(Serialize, Deserialize, Clone, Debug)]
28 | #[serde(rename = "publicKey", rename_all = "camelCase")]
29 | pub struct PublicKeyCredentialRequestOptions {
30 | pub challenge: String,
31 | #[serde(skip_serializing_if = "Option::is_none")]
32 | pub timeout: Option,
33 | #[serde(skip_serializing_if = "Option::is_none")]
34 | pub rp_id: Option,
35 | #[serde(skip_serializing_if = "Vec::is_empty", default)]
36 | pub allow_credentials: Vec,
37 | #[serde(skip_serializing_if = "Option::is_none")]
38 | pub user_verification: Option,
39 | #[serde(default, skip_serializing_if = "Extensions::is_empty")]
40 | pub extensions: Extensions,
41 | }
42 |
43 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
44 | pub struct PublicKeyCredentialRpEntity {
45 | #[serde(skip_serializing_if = "Option::is_none")]
46 | pub id: Option,
47 | pub name: String,
48 | #[serde(skip_serializing_if = "Option::is_none")]
49 | pub icon: Option,
50 | }
51 |
52 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
53 | #[serde(rename_all = "camelCase")]
54 | pub struct PublicKeyCredentialUserEntity {
55 | pub id: String,
56 | pub name: String,
57 | pub display_name: String,
58 | #[serde(skip_serializing_if = "Option::is_none")]
59 | pub icon: Option,
60 | }
61 |
62 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
63 | pub struct PublicKeyCredentialParameters {
64 | #[serde(rename = "type")]
65 | pub auth_type: PublicKeyCredentialType,
66 | pub alg: i64,
67 | }
68 |
69 | #[derive(Serialize, Deserialize, Clone, Debug, Eq)]
70 | pub struct PublicKeyCredentialDescriptor {
71 | #[serde(rename = "type")]
72 | pub cred_type: PublicKeyCredentialType,
73 | pub id: String,
74 | #[serde(skip_serializing_if = "Option::is_none")]
75 | pub transports: Option>,
76 | }
77 |
78 | impl PartialEq for PublicKeyCredentialDescriptor {
79 | fn eq(&self, other: &Self) -> bool {
80 | self.id == other.id
81 | }
82 | }
83 |
84 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
85 | pub enum PublicKeyCredentialType {
86 | #[serde(rename = "public-key")]
87 | PublicKey,
88 | }
89 |
90 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
91 | pub enum AuthenticatorTransport {
92 | #[serde(rename = "usb")]
93 | Usb,
94 | #[serde(rename = "nfc")]
95 | Nfc,
96 | #[serde(rename = "ble")]
97 | BluetoothLE,
98 | #[serde(rename = "internal")]
99 | Internal,
100 | #[serde(rename = "hybrid")]
101 | Hybrid,
102 | #[serde(rename = "smart-card")]
103 | SmartCard,
104 | }
105 |
106 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
107 | #[serde(rename_all = "camelCase")]
108 | pub struct AuthenticatorSelectionCriteria {
109 | #[serde(skip_serializing_if = "Option::is_none")]
110 | pub authenticator_attachment: Option,
111 | #[serde(skip_serializing_if = "Option::is_none")]
112 | pub require_resident_key: Option,
113 | #[serde(skip_serializing_if = "Option::is_none")]
114 | pub user_verification: Option,
115 | }
116 |
117 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
118 | #[serde(rename_all = "camelCase")]
119 | pub enum AuthenticatorAttachment {
120 | Platform,
121 | #[serde(rename = "cross-platform")]
122 | CrossPlatform,
123 | }
124 |
125 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
126 | #[serde(rename_all = "camelCase")]
127 | pub enum UserVerificationRequirement {
128 | Required,
129 | Preferred,
130 | Discouraged,
131 | }
132 |
133 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
134 | #[serde(rename_all = "camelCase")]
135 | pub enum AttestationConveyancePreference {
136 | None,
137 | Indirect,
138 | Direct,
139 | Enterprise,
140 | }
141 |
142 | #[derive(Serialize, Deserialize, Clone, Debug)]
143 | #[serde(rename_all = "camelCase")]
144 | pub struct PublicKeyCredential {
145 | pub id: String,
146 | #[serde(skip_serializing_if = "Option::is_none")]
147 | pub response: Option,
148 | }
149 |
150 | #[derive(Serialize, Deserialize, Clone, Debug)]
151 | #[serde(rename_all = "camelCase")]
152 | pub struct PublicKeyCredentialRaw {
153 | pub id: String,
154 | pub raw_id: Vec,
155 | #[serde(skip_serializing_if = "Option::is_none")]
156 | pub response: Option,
157 | }
158 |
159 | impl From for PublicKeyCredential {
160 | fn from(raw: PublicKeyCredentialRaw) -> Self {
161 | PublicKeyCredential {
162 | id: raw.id,
163 | response: raw.response.map(|response| AuthenticatorAttestationResponse {
164 | attestation_object: response.attestation_object.map(|ao| BASE64.encode(ao)),
165 | client_data_json: BASE64.encode(&response.client_data_json),
166 | authenticator_data: response.authenticator_data.map(|ad| BASE64.encode(ad)),
167 | signature: response.signature.map(|s| BASE64.encode(s)),
168 | user_handle: response.user_handle.map(|uh| BASE64.encode(uh)),
169 | }),
170 | }
171 | }
172 | }
173 |
174 | #[derive(Serialize, Deserialize, Clone, Debug)]
175 | #[serde(rename_all = "camelCase")]
176 | pub struct AuthenticatorAttestationResponse {
177 | #[serde(skip_serializing_if = "Option::is_none")]
178 | pub attestation_object: Option,
179 | #[serde(rename = "clientDataJSON")]
180 | pub client_data_json: String,
181 | #[serde(skip_serializing_if = "Option::is_none")]
182 | pub authenticator_data: Option,
183 | #[serde(skip_serializing_if = "Option::is_none")]
184 | pub signature: Option,
185 | #[serde(skip_serializing_if = "Option::is_none")]
186 | pub user_handle: Option,
187 | }
188 |
189 | #[derive(Serialize, Deserialize, Clone, Debug)]
190 | #[serde(rename_all = "camelCase")]
191 | pub struct AuthenticatorAttestationResponseRaw {
192 | #[serde(skip_serializing_if = "Option::is_none")]
193 | pub attestation_object: Option>,
194 | #[serde(rename = "clientDataJSON")]
195 | pub client_data_json: Vec,
196 | #[serde(skip_serializing_if = "Option::is_none")]
197 | pub authenticator_data: Option>,
198 | #[serde(skip_serializing_if = "Option::is_none")]
199 | pub signature: Option>,
200 | #[serde(skip_serializing_if = "Option::is_none")]
201 | pub user_handle: Option>,
202 | #[serde(skip_serializing_if = "Vec::is_empty")]
203 | pub transports: Vec,
204 | }
205 |
206 | #[derive(Serialize, Deserialize, Clone, Debug)]
207 | #[serde(rename_all = "camelCase")]
208 | pub enum Transport {
209 | Usb,
210 | Nfc,
211 | Ble,
212 | Internal,
213 | Hybrid,
214 | #[serde(rename = "smart-card")]
215 | SmartCard,
216 | }
217 |
218 | #[derive(Serialize, Deserialize, Clone, Debug)]
219 | #[serde(rename_all = "camelCase")]
220 | pub struct CollectedClientData {
221 | #[serde(rename = "type")]
222 | pub request_type: String,
223 | pub challenge: String,
224 | pub origin: String,
225 | #[serde(default)]
226 | pub cross_origin: bool,
227 | #[serde(skip_serializing_if = "Option::is_none")]
228 | pub token_binding: Option,
229 | }
230 |
231 | #[derive(Serialize, Deserialize, Clone, Debug)]
232 | pub struct TokenBinding {
233 | pub status: TokenBindingStatus,
234 | pub id: Option,
235 | }
236 |
237 | #[derive(Serialize, Deserialize, Clone, Debug)]
238 | #[serde(rename_all = "camelCase")]
239 | pub enum TokenBindingStatus {
240 | Present,
241 | Supported,
242 | }
243 |
244 | #[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq)]
245 | #[serde(rename_all = "camelCase")]
246 | pub struct Extensions {
247 | #[serde(skip_serializing_if = "Option::is_none")]
248 | pub prf: Option,
249 | }
250 |
251 | impl Extensions {
252 | pub fn is_empty(&self) -> bool {
253 | self.prf.is_none()
254 | }
255 | }
256 |
257 | // https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfinputs
258 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
259 | #[serde(rename_all = "camelCase")]
260 | pub struct PrfExtension {
261 | #[serde(default, skip_serializing_if = "Option::is_none")]
262 | pub eval: Option,
263 |
264 | // Only supported in authentication, not creation
265 | #[serde(default, skip_serializing_if = "HashMap::is_empty")]
266 | pub eval_by_credential: HashMap,
267 | }
268 |
269 | // https://w3c.github.io/webauthn/#dictdef-authenticationextensionsprfvalues
270 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
271 | #[serde(rename_all = "camelCase")]
272 | pub struct AuthenticationExtensionsPRFValues {
273 | pub first: Vec,
274 | #[serde(default, skip_serializing_if = "Option::is_none")]
275 | pub second: Option>,
276 | }
277 |
278 | pub fn get_default_rp_id(origin: &str) -> String {
279 | origin
280 | .parse::()
281 | .ok()
282 | .and_then(|u| u.authority().map(|a| a.host().to_string()))
283 | .unwrap_or(origin.to_string())
284 | }
285 |
286 | #[test]
287 | fn test_default_rp_id() {
288 | assert_eq!(get_default_rp_id("https://login.example.com:1337"), "login.example.com");
289 | assert_eq!(get_default_rp_id("https://login.example.com"), "login.example.com");
290 | assert_eq!(get_default_rp_id("http://login.example.com:1337"), "login.example.com");
291 | assert_eq!(get_default_rp_id("http://login.example.com"), "login.example.com");
292 | assert_eq!(get_default_rp_id("login.example.com:1337"), "login.example.com");
293 | assert_eq!(get_default_rp_id("login.example.com"), "login.example.com");
294 | }
295 |
--------------------------------------------------------------------------------
/src/oath/hotp.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | pub const HOTP_DEFAULT_COUNTER_VALUE: u64 = 0;
4 | pub const HOTP_DEFAULT_RESYNC_VALUE: u16 = 2;
5 |
6 | #[derive(Default)]
7 | pub struct HOTPBuilder {
8 | alg: Option,
9 | counter: Option,
10 | resync: Option,
11 | digits: Option,
12 | secret: Option>,
13 | }
14 |
15 | impl HOTPBuilder {
16 | pub fn new() -> Self {
17 | HOTPBuilder::default()
18 | }
19 |
20 | pub fn algorithm(mut self, alg: HashesAlgorithm) -> Self {
21 | self.alg = Some(alg);
22 | self
23 | }
24 |
25 | pub fn counter(mut self, c: u64) -> Self {
26 | self.counter = Some(c);
27 | self
28 | }
29 |
30 | pub fn re_sync_parameter(mut self, s: u16) -> Self {
31 | self.resync = Some(s);
32 | self
33 | }
34 |
35 | pub fn digits(mut self, d: usize) -> Self {
36 | self.digits = Some(d);
37 | self
38 | }
39 |
40 | pub fn secret(mut self, secret: &[u8]) -> Self {
41 | self.secret = Some(secret.to_vec());
42 | self
43 | }
44 |
45 | pub fn build(self) -> HOTPContext {
46 | let HOTPBuilder {
47 | alg,
48 | counter,
49 | resync,
50 | digits,
51 | secret,
52 | } = self;
53 |
54 | let alg = alg.unwrap_or(OTP_DEFAULT_ALG_VALUE);
55 | let secret = secret.unwrap_or_default();
56 | let secret_key = alg.to_mac_hash_key(secret.as_slice());
57 |
58 | HOTPContext {
59 | alg,
60 | counter: counter.unwrap_or(HOTP_DEFAULT_COUNTER_VALUE),
61 | resync: resync.unwrap_or(HOTP_DEFAULT_RESYNC_VALUE),
62 | digits: digits.unwrap_or(OTP_DEFAULT_DIGITS_VALUE),
63 | secret,
64 | secret_key,
65 | }
66 | }
67 | }
68 |
69 | pub struct HOTPContext {
70 | alg: HashesAlgorithm,
71 | counter: u64,
72 | resync: u16,
73 | digits: usize,
74 | secret: Vec,
75 | secret_key: MacHashKey,
76 | }
77 |
78 | impl HOTPContext {
79 | /// Create a new HOTP builder
80 | pub fn builder() -> HOTPBuilder {
81 | HOTPBuilder::new()
82 | }
83 |
84 | /// Generate the current HOTP code corresponding to the counter value
85 | pub fn gen(&self) -> String {
86 | self.gen_at(self.counter)
87 | }
88 |
89 | /// Increment the inner counter value
90 | pub fn inc(&mut self) -> &mut Self {
91 | self.counter += 1;
92 | self
93 | }
94 |
95 | /// Check if a code equal the current value at the counter
96 | pub fn validate_current(&self, value: &str) -> bool {
97 | if value.len() != self.digits {
98 | return false;
99 | }
100 |
101 | self.gen().as_str().eq(value)
102 | }
103 |
104 | /// Check if a code is valid, if yes icrements the counter, if not begins the resync procedure.
105 | /// The counter won't be altered if the value is invalidated.
106 | pub fn verify(&mut self, value: &str) -> bool {
107 | if value.len() != self.digits {
108 | return false;
109 | }
110 |
111 | for i in self.counter..(self.counter + self.resync as u64) {
112 | if self.gen_at(i).as_str().eq(value) {
113 | self.counter += i - self.counter + 1;
114 | return true;
115 | }
116 | }
117 |
118 | false
119 | }
120 |
121 | fn gen_at(&self, c: u64) -> String {
122 | let c_b_e = c.to_be_bytes();
123 |
124 | let hs_sig = self
125 | .secret_key
126 | .sign(&c_b_e[..])
127 | .expect("This should not happen since HMAC can take key of any size")
128 | .into_vec();
129 | let s_bits = dt(hs_sig.as_ref());
130 |
131 | let s_num = s_bits % 10_u32.pow(self.digits as u32);
132 |
133 | format!("{:0>6}", s_num)
134 | }
135 | }
136 |
137 | impl OtpAuth for HOTPContext {
138 | fn to_uri(&self, label: Option<&str>, issuer: Option<&str>) -> String {
139 | let mut uri = format!(
140 | "otpauth://hotp/{}?secret={}&algorithm={}&digits={}&counter={}",
141 | label.unwrap_or("slauth"),
142 | base32::encode(base32::Alphabet::Rfc4648 { padding: false }, self.secret.as_slice()),
143 | self.alg,
144 | self.digits,
145 | self.counter
146 | );
147 |
148 | if let Some(iss) = issuer {
149 | uri.push_str("&issuer=");
150 | uri.push_str(iss);
151 | }
152 |
153 | uri
154 | }
155 |
156 | fn from_uri(uri: &str) -> Result
157 | where
158 | Self: Sized,
159 | {
160 | let mut uri_it = uri.split("://");
161 |
162 | uri_it
163 | .next()
164 | .filter(|scheme| scheme.eq(&"otpauth"))
165 | .ok_or_else(|| "Otpauth uri is malformed".to_string())?;
166 |
167 | let type_label_it_opt = uri_it.next().map(|type_label_param| type_label_param.split('/'));
168 |
169 | if let Some(mut type_label_it) = type_label_it_opt {
170 | type_label_it
171 | .next()
172 | .filter(|otp_type| otp_type.eq(&"hotp"))
173 | .ok_or_else(|| "Otpauth uri is malformed, bad type".to_string())?;
174 |
175 | let param_it_opt = type_label_it
176 | .next()
177 | .and_then(|label_param| label_param.split('?').next_back().map(|s| s.split('&')));
178 |
179 | param_it_opt
180 | .ok_or_else(|| "Otpauth uri is malformed, missing parameters".to_string())
181 | .and_then(|param_it| {
182 | let mut secret = Vec::::new();
183 | let mut counter = u64::MAX;
184 | let mut alg = OTP_DEFAULT_ALG_VALUE;
185 | let mut digits = OTP_DEFAULT_DIGITS_VALUE;
186 |
187 | for s_param in param_it {
188 | let mut s_param_it = s_param.split('=');
189 |
190 | match s_param_it.next() {
191 | Some("secret") => {
192 | secret = s_param_it
193 | .next()
194 | .and_then(decode_hex_or_base_32)
195 | .ok_or_else(|| "Otpauth uri is malformed, missing secret value".to_string())?;
196 | continue;
197 | }
198 | Some("algorithm") => {
199 | alg = match s_param_it
200 | .next()
201 | .ok_or_else(|| "Otpauth uri is malformed, missing algorithm value".to_string())?
202 | {
203 | "SHA256" => HashesAlgorithm::SHA256,
204 | "SHA512" => HashesAlgorithm::SHA512,
205 | _ => HashesAlgorithm::SHA1,
206 | };
207 | continue;
208 | }
209 | Some("digits") => {
210 | digits = s_param_it
211 | .next()
212 | .ok_or_else(|| "Otpauth uri is malformed, missing digits value".to_string())?
213 | .parse::()
214 | .map_err(|_| "Otpauth uri is malformed, bad digits value".to_string())?;
215 | continue;
216 | }
217 | Some("counter") => {
218 | counter = s_param_it
219 | .next()
220 | .ok_or_else(|| "Otpauth uri is malformed, missing counter value".to_string())?
221 | .parse::()
222 | .map_err(|_| "Otpauth uri is malformed, bad counter value".to_string())?;
223 | continue;
224 | }
225 | _ => {}
226 | }
227 | }
228 |
229 | if secret.is_empty() || counter == u64::MAX {
230 | return Err("Otpauth uri is malformed".to_string());
231 | }
232 |
233 | let secret_key = alg.to_mac_hash_key(secret.as_slice());
234 |
235 | Ok(HOTPContext {
236 | alg,
237 | counter,
238 | resync: HOTP_DEFAULT_RESYNC_VALUE,
239 | digits,
240 | secret,
241 | secret_key,
242 | })
243 | })
244 | } else {
245 | Err("Otpauth uri is malformed, missing parts".to_string())
246 | }
247 | }
248 | }
249 |
250 | #[cfg(feature = "native-bindings")]
251 | mod native_bindings {
252 | use std::{os::raw::c_char, ptr::null_mut};
253 |
254 | use super::*;
255 | use crate::strings;
256 |
257 | #[no_mangle]
258 | pub unsafe extern "C" fn hotp_from_uri(uri: *const c_char) -> *mut HOTPContext {
259 | let uri_str = strings::c_char_to_string(uri);
260 | let hotp = HOTPContext::from_uri(&uri_str).map(Box::new);
261 | match hotp {
262 | Ok(hotp) => Box::into_raw(hotp),
263 | Err(_) => null_mut(),
264 | }
265 | }
266 |
267 | #[no_mangle]
268 | pub unsafe extern "C" fn hotp_free(hotp: *mut HOTPContext) {
269 | let _ = Box::from_raw(hotp);
270 | }
271 |
272 | #[no_mangle]
273 | pub unsafe extern "C" fn hotp_to_uri(hotp: *mut HOTPContext, label: *const c_char, issuer: *const c_char) -> *mut c_char {
274 | let hotp = &*hotp;
275 | let label = strings::c_char_to_string(label);
276 | let label_opt = if !label.is_empty() { Some(label.as_str()) } else { None };
277 | let issuer = strings::c_char_to_string(issuer);
278 | let issuer_opt = if !issuer.is_empty() { Some(issuer.as_str()) } else { None };
279 | strings::string_to_c_char(hotp.to_uri(label_opt, issuer_opt))
280 | }
281 |
282 | #[no_mangle]
283 | pub unsafe extern "C" fn hotp_gen(hotp: *mut HOTPContext) -> *mut c_char {
284 | let hotp = &*hotp;
285 | strings::string_to_c_char(hotp.gen())
286 | }
287 |
288 | #[no_mangle]
289 | pub unsafe extern "C" fn hotp_inc(hotp: *mut HOTPContext) {
290 | let hotp = &mut *hotp;
291 | hotp.inc();
292 | }
293 |
294 | #[no_mangle]
295 | pub unsafe extern "C" fn hotp_verify(hotp: *mut HOTPContext, code: *const c_char) -> bool {
296 | let hotp = &mut *hotp;
297 | let value = strings::c_char_to_string(code);
298 | hotp.verify(&value)
299 | }
300 |
301 | #[no_mangle]
302 | pub unsafe extern "C" fn hotp_validate_current(hotp: *mut HOTPContext, code: *const c_char) -> bool {
303 | let hotp = &*hotp;
304 | let value = strings::c_char_to_string(code);
305 | hotp.validate_current(&value)
306 | }
307 | }
308 |
309 | #[test]
310 | fn hotp_from_uri() {
311 | const MK_ULTRA: &str = "patate";
312 |
313 | let server = HOTPBuilder::new()
314 | .counter(102)
315 | .re_sync_parameter(3)
316 | .secret(MK_ULTRA.as_bytes())
317 | .build();
318 |
319 | let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
320 |
321 | let client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
322 |
323 | assert!(server.validate_current(client.gen().as_str()));
324 | }
325 |
326 | #[test]
327 | fn hotp_multiple() {
328 | const MK_ULTRA: &str = "patate";
329 |
330 | let mut server = HOTPBuilder::new()
331 | .counter(102)
332 | .re_sync_parameter(3)
333 | .secret(MK_ULTRA.as_bytes())
334 | .build();
335 |
336 | let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
337 |
338 | let mut client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
339 |
340 | assert!(server.verify(client.gen().as_str()));
341 | assert!(server.verify(client.inc().gen().as_str()));
342 | assert!(server.verify(client.inc().gen().as_str()));
343 | assert!(server.verify(client.inc().gen().as_str()));
344 | assert!(server.verify(client.inc().gen().as_str()));
345 | }
346 |
347 | #[test]
348 | fn hotp_multiple_resync() {
349 | const MK_ULTRA: &str = "patate";
350 |
351 | let mut server = HOTPBuilder::new()
352 | .counter(102)
353 | .re_sync_parameter(3)
354 | .secret(MK_ULTRA.as_bytes())
355 | .build();
356 |
357 | let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
358 |
359 | let mut client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
360 |
361 | assert!(server.verify(client.gen().as_str()));
362 | assert!(server.verify(client.inc().gen().as_str()));
363 | assert!(server.verify(client.inc().inc().gen().as_str()));
364 | assert!(server.verify(client.inc().gen().as_str()));
365 | assert!(server.verify(client.inc().inc().inc().gen().as_str()));
366 | }
367 |
--------------------------------------------------------------------------------