├── .cargo └── config.toml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── Slauth.podspec ├── build.gradle ├── deny.toml ├── examples └── web-server.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── fuzz_messages.rs │ └── fuzz_webauthn_messages.rs ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── rustfmt.toml ├── settings.gradle ├── slauth.h ├── src ├── base64.rs ├── lib.rs ├── oath │ ├── hotp.rs │ ├── mod.rs │ └── totp.rs ├── u2f │ ├── client │ │ ├── mod.rs │ │ └── token.rs │ ├── error.rs │ ├── mod.rs │ ├── proto │ │ ├── constants.rs │ │ ├── hid.rs │ │ ├── mod.rs │ │ ├── raw_message.rs │ │ └── web_message.rs │ └── server │ │ └── mod.rs ├── wasm.rs └── webauthn │ ├── authenticator │ ├── mod.rs │ ├── native.rs │ └── responses.rs │ ├── error.rs │ ├── mod.rs │ ├── proto │ ├── constants.rs │ ├── mod.rs │ ├── raw_message.rs │ ├── tpm.rs │ └── web_message.rs │ └── server │ └── mod.rs └── wrappers ├── android ├── build.gradle ├── build.sh ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── net │ │ └── devolutions │ │ └── slauth │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── net │ │ │ └── devolutions │ │ │ └── slauth │ │ │ ├── AttestationFlags.java │ │ │ ├── Hotp.java │ │ │ ├── InvalidRequestTypeException.java │ │ │ ├── InvalidResponseTypeException.java │ │ │ ├── InvalidSigningKeyException.java │ │ │ ├── JNA.java │ │ │ ├── RustObject.java │ │ │ ├── SigningKey.java │ │ │ ├── Totp.java │ │ │ ├── WebAuthnCreationResponse.java │ │ │ ├── WebAuthnRequestResponse.java │ │ │ ├── WebRequest.java │ │ │ └── WebResponse.java │ ├── jniLibs │ │ ├── arm64-v8a │ │ │ └── libslauth.so │ │ └── x86_64 │ │ │ └── libslauth.so │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── net │ └── devolutions │ └── slauth │ └── ExampleUnitTest.java ├── swift ├── build.sh └── classes │ ├── Hotp.swift │ ├── RustObject.swift │ ├── Totp.swift │ ├── U2f.swift │ ├── WebAuthnCreationResponse.swift │ └── WebAuthnRequestResponse.swift └── wasm ├── build-web.sh ├── build.sh └── example └── otp-example ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── services │ │ └── otp.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.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 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # File auto-generated and managed by Devops 2 | /.github/ @devolutions/devops 3 | /.github/dependabot.yml @devolutions/security-managers 4 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 (cocoapods) 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 | 43 | steps: 44 | - name: Checkout repo 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup wasm 48 | run: | 49 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 50 | wasm-pack --version 51 | 52 | - name: Build 53 | run: sh build.sh 54 | working-directory: wrappers/wasm 55 | 56 | - name: Upload artifact 57 | uses: actions/upload-artifact@v4.3.6 58 | with: 59 | name: wasm 60 | path: dist/bundler 61 | 62 | - name: Configure NPM 63 | run: npm config set "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" 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 | 79 | steps: 80 | - name: Checkout repo 81 | uses: actions/checkout@v4 82 | 83 | - name: Setup wasm 84 | run: | 85 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 86 | wasm-pack --version 87 | 88 | - name: Build 89 | run: bash build-web.sh 90 | working-directory: wrappers/wasm 91 | 92 | - name: Upload artifact 93 | uses: actions/upload-artifact@v4.3.6 94 | with: 95 | name: wasm-web 96 | path: dist/web 97 | 98 | - name: Configure NPM 99 | run: npm config set "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" 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 | 169 | steps: 170 | - uses: actions/checkout@v4 171 | 172 | - name: Publish 173 | run: cargo publish 174 | env: 175 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 176 | 177 | build-swift: 178 | environment: cocoapods-publish 179 | if: ${{ inputs.swift }} 180 | runs-on: macos-latest 181 | 182 | steps: 183 | - name: Checkout repo 184 | uses: actions/checkout@v4 185 | 186 | - name: Setup rust 187 | run: | 188 | rustup target add aarch64-apple-ios 189 | rustup target add x86_64-apple-ios 190 | cargo install cargo-lipo 191 | 192 | - name: Build iOS libraries 193 | run: sh wrappers/swift/build.sh 194 | 195 | - name: Setup CocoaPods Trunk Token 196 | run: | 197 | echo -e "machine trunk.cocoapods.org\n login bot@devolutions.net\n password ${{ secrets.COCOAPODS_TRUNK_TOKEN }}" > ~/.netrc 198 | chmod 0600 ~/.netrc 199 | 200 | - name: Setup version 201 | id: version 202 | run: | 203 | VERSION=$(grep -E "^\s*s\.version\s*=\s*['\"][0-9]+\.[0-9]+\.[0-9]+['\"]" Slauth.podspec | awk -F"[\"\']" '{print $2}') 204 | echo "version=$VERSION" >> $GITHUB_OUTPUT 205 | 206 | - name: Push to a new branch 207 | run: | 208 | git checkout --orphan release/cocoapods-v${{ steps.version.outputs.version }} 209 | git rm -rf . 210 | git checkout master -- LICENSE wrappers/swift/classes slauth.h Slauth.podspec 211 | 212 | git add LICENSE wrappers/swift/classes/** slauth.h Slauth.podspec 213 | find target/universal/release -name "*.a" -exec git add {} \; 214 | find target/x86_64-apple-ios/release -name "*.a" -exec git add {} \; 215 | find target/aarch64-apple-ios/release -name "*.a" -exec git add {} \; 216 | 217 | git commit -m "Set up CocoaPods release branch" 218 | git push origin release/cocoapods-v${{ steps.version.outputs.version }} 219 | 220 | git tag '${{ steps.version.outputs.version }}' 221 | git push --tags 222 | env: 223 | GITHUB_TOKEN: ${{ github.token }} 224 | 225 | - name: Publish to CocoaPods 226 | run: pod trunk push Slauth.podspec --skip-import-validation --use-libraries --allow-warnings 227 | env: 228 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 229 | 230 | - name: Delete branch 231 | run: | 232 | git fetch 233 | git switch master 234 | git branch -D release/cocoapods-v${{ steps.version.outputs.version }} 235 | git push origin --delete release/cocoapods-v${{ steps.version.outputs.version }} 236 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Test Package 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Build 16 | run: cargo build --verbose 17 | 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slauth" 3 | version = "0.7.15" 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.16", optional = true } 73 | ed25519-dalek = { version = "2.1.1", features = [ 74 | "rand_core", 75 | "pkcs8", 76 | ], optional = true } 77 | p256 = { version = "0.13.2", optional = true } 78 | indexmap = { version = "2.9.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.100" } 85 | js-sys = "0.3.77" 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.50" 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.16" 108 | rand = "0.9" 109 | bytes = "1.10" 110 | 111 | #[package.metadata.wasm-pack.profile.release] 112 | #wasm-opt = false 113 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slauth 2 | [![doc](https://docs.rs/slauth/badge.svg)](https://docs.rs/slauth/) 3 | [![crate](https://img.shields.io/crates/v/slauth.svg)](https://crates.io/crates/slauth) 4 | [![issue](https://img.shields.io/github/issues/devolutions/slauth.svg)](https://github.com/devolutions/slauth/issues) 5 | ![downloads](https://img.shields.io/crates/d/slauth.svg) 6 | [![license](https://img.shields.io/crates/l/slauth.svg)](https://github.com/devolutions/slauth/blob/master/LICENSE) 7 | [![dependency status](https://deps.rs/repo/github/devolutions/slauth/status.svg)](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 -------------------------------------------------------------------------------- /Slauth.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Slauth' 3 | s.version = '0.7.15' 4 | s.summary = 'A Swift wrapper around Slauth Rust crate' 5 | s.description = <<-DESC 6 | TODO: Add long description of the pod here. 7 | DESC 8 | 9 | s.homepage = 'https://github.com/Devolutions/Slauth.git' 10 | s.license = { :type => 'MIT', :file => './LICENSE' } 11 | s.author = { 'Devolutions' => 'lfauvel@devolutions.net' } 12 | s.source = { :git => 'https://github.com/Devolutions/Slauth.git', :tag => s.version.to_s } 13 | 14 | s.swift_version = '5.0' 15 | s.ios.deployment_target = '16.0' 16 | 17 | s.source_files = 'wrappers/swift/classes/**/*', 'slauth.h' 18 | s.vendored_libraries = 'target/universal/release/*.a', 'target/x86_64-apple-ios/release/*.a', 'target/aarch64-apple-ios/release/*.a' 19 | end 20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.3' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | apply plugin: 'maven-publish' 17 | repositories { 18 | google() 19 | jcenter() 20 | 21 | } 22 | } 23 | 24 | Properties properties = new Properties() 25 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 26 | 27 | project('slauth') { 28 | ext { 29 | libraryVersion = '0.7.15' 30 | } 31 | publishing { 32 | repositories { 33 | maven { 34 | name = "cloudsmith" 35 | url = "https://maven.cloudsmith.io/devolutions/maven-public/" 36 | credentials { 37 | username = System.getenv('CLOUDSMITH_USERNAME') 38 | password = System.getenv('CLOUDSMITH_API_KEY') 39 | } 40 | } 41 | } 42 | publications { 43 | aar(MavenPublication) { 44 | groupId = 'devolutions' 45 | artifactId = project.getName() 46 | version = project.ext.libraryVersion 47 | // Tell maven to prepare the generated "*.aar" file for publishing 48 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 49 | 50 | pom { 51 | withXml { 52 | def dependenciesNode = asNode().appendNode("dependencies") 53 | def dependencyNode = dependenciesNode.appendNode("dependency") 54 | 55 | dependencyNode.appendNode("groupId", "net.java.dev.jna") 56 | dependencyNode.appendNode("artifactId", "jna") 57 | dependencyNode.appendNode("version", "5.16.0") 58 | dependencyNode.appendNode("scope", "runtime") 59 | dependencyNode.appendNode("type", "aar") 60 | 61 | def exclusionsNode = dependencyNode.appendNode("exclusions") 62 | def exclusionNode = exclusionsNode.appendNode("exclusion") 63 | 64 | exclusionNode.appendNode("groupId", "*") 65 | exclusionNode.appendNode("artifactId", "*") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | task clean(type: Delete) { 74 | delete rootProject.buildDir 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" "$@" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'slauth' 2 | project(':slauth').projectDir = new File(settingsDir, 'wrappers/android') -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/oath/mod.rs: -------------------------------------------------------------------------------- 1 | use hmac::{ 2 | digest::{generic_array::GenericArray, FixedOutputReset, InvalidLength, OutputSizeUser}, 3 | Mac, SimpleHmac, 4 | }; 5 | use sha1::Sha1; 6 | use sha2::{Sha256, Sha512}; 7 | use std::fmt::Display; 8 | 9 | pub mod hotp; 10 | pub mod totp; 11 | 12 | pub const OTP_DEFAULT_DIGITS_VALUE: usize = 6; 13 | pub const OTP_DEFAULT_ALG_VALUE: HashesAlgorithm = HashesAlgorithm::SHA1; 14 | 15 | #[derive(Clone)] 16 | pub enum HashesAlgorithm { 17 | SHA1, 18 | SHA256, 19 | SHA512, 20 | } 21 | 22 | #[derive(Clone)] 23 | pub(crate) struct MacHashKey { 24 | secret: Vec, 25 | alg: HashesAlgorithm, 26 | } 27 | 28 | impl MacHashKey { 29 | pub(crate) fn sign(&self, data: &[u8]) -> Result { 30 | match self.alg { 31 | HashesAlgorithm::SHA1 => { 32 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?; 33 | context.update(data); 34 | Ok(HmacShaResult::RSHA1(context.finalize_fixed_reset())) 35 | } 36 | HashesAlgorithm::SHA256 => { 37 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?; 38 | context.update(data); 39 | Ok(HmacShaResult::RSHA256(context.finalize_fixed_reset())) 40 | } 41 | HashesAlgorithm::SHA512 => { 42 | let mut context = SimpleHmac::::new_from_slice(&self.secret)?; 43 | context.update(data); 44 | Ok(HmacShaResult::RSHA512(context.finalize_fixed_reset())) 45 | } 46 | } 47 | } 48 | } 49 | 50 | pub(crate) enum HmacShaResult { 51 | RSHA1(GenericArray::OutputSize>), 52 | RSHA256(GenericArray::OutputSize>), 53 | RSHA512(GenericArray::OutputSize>), 54 | } 55 | 56 | impl HmacShaResult { 57 | pub(crate) fn into_vec(self) -> Vec { 58 | match self { 59 | HmacShaResult::RSHA1(res) => res.to_vec(), 60 | HmacShaResult::RSHA256(res) => res.to_vec(), 61 | HmacShaResult::RSHA512(res) => res.to_vec(), 62 | } 63 | } 64 | } 65 | 66 | impl HashesAlgorithm { 67 | pub(crate) fn to_mac_hash_key(&self, key: &[u8]) -> MacHashKey { 68 | MacHashKey { 69 | secret: key.to_vec(), 70 | alg: self.clone(), 71 | } 72 | } 73 | } 74 | 75 | impl Display for HashesAlgorithm { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | let str = match self { 78 | HashesAlgorithm::SHA1 => "SHA1".to_string(), 79 | HashesAlgorithm::SHA256 => "SHA256".to_string(), 80 | HashesAlgorithm::SHA512 => "SHA512".to_string(), 81 | }; 82 | write!(f, "{}", str) 83 | } 84 | } 85 | 86 | pub trait OtpAuth { 87 | fn to_uri(&self, label: Option<&str>, issuer: Option<&str>) -> String; 88 | fn from_uri(uri: &str) -> Result 89 | where 90 | Self: Sized; 91 | } 92 | 93 | #[inline] 94 | pub(crate) fn dt(hmac_res: &[u8]) -> u32 { 95 | let offset_val = (hmac_res[hmac_res.len() - 1] & 0x0F) as usize; 96 | let h = &hmac_res[offset_val..offset_val + 4]; 97 | 98 | ((h[0] as u32 & 0x7f) << 24) | ((h[1] as u32 & 0xff) << 16) | ((h[2] as u32 & 0xff) << 8) | (h[3] as u32 & 0xff) 99 | } 100 | 101 | #[inline] 102 | pub(crate) fn decode_hex_or_base_32(encoded: &str) -> Option> { 103 | // Try base32 first then is it does not follows RFC4648, try HEX 104 | base32::decode(base32::Alphabet::Rfc4648 { padding: false }, encoded).or_else(|| hex::decode(encoded).ok()) 105 | } 106 | 107 | #[cfg(target_arch = "wasm32")] 108 | pub fn get_time() -> u64 { 109 | let dt = js_sys::Date::new_0(); 110 | let ut: f64 = dt.get_time(); 111 | if ut < 0.0 { 112 | 0 113 | } else { 114 | (ut.floor() as u64) / 1000 115 | } 116 | } 117 | 118 | #[cfg(not(target_arch = "wasm32"))] 119 | pub fn get_time() -> u64 { 120 | time::OffsetDateTime::now_utc().unix_timestamp() as u64 121 | } 122 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/webauthn/proto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod raw_message; 3 | pub mod tpm; 4 | pub mod web_message; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.15" 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 | -------------------------------------------------------------------------------- /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 PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH 11 | CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release --features "android" 12 | CC=x86_64-linux-android21-clang cargo build --target x86_64-linux-android --release --features "android" 13 | 14 | cp target/aarch64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so 15 | cp target/x86_64-linux-android/release/libslauth.so wrappers/android/src/main/jniLibs/x86_64/libslauth.so -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /wrappers/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/android/src/main/jniLibs/arm64-v8a/libslauth.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/wrappers/android/src/main/jniLibs/arm64-v8a/libslauth.so -------------------------------------------------------------------------------- /wrappers/android/src/main/jniLibs/x86_64/libslauth.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/wrappers/android/src/main/jniLibs/x86_64/libslauth.so -------------------------------------------------------------------------------- /wrappers/android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Slauth 3 | 4 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /wrappers/swift/build.sh: -------------------------------------------------------------------------------- 1 | cargo lipo --release 2 | mv target/universal/release/libslauth.a target/universal/release/libslauth_universal.a 3 | mv target/x86_64-apple-ios/release/libslauth.a target/x86_64-apple-ios/release/libslauth_x86.a 4 | mv target/aarch64-apple-ios/release/libslauth.a target/aarch64-apple-ios/release/libslauth_arm64.a -------------------------------------------------------------------------------- /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 | public class Hotp: NSObject, RustObject { 12 | var raw: OpaquePointer 13 | 14 | required init(raw: OpaquePointer) { 15 | self.raw = raw 16 | } 17 | 18 | func intoRaw() -> OpaquePointer { 19 | return self.raw 20 | } 21 | 22 | public convenience init(uri: String) throws { 23 | let r = hotp_from_uri(uri) 24 | if r == nil { 25 | throw Err(message: "InvalidUri") 26 | } else { 27 | self.init(raw: r!) 28 | } 29 | } 30 | 31 | deinit { 32 | hotp_free(raw) 33 | } 34 | 35 | public func to_uri(label: String, issuer: String) -> String { 36 | let uri = hotp_to_uri(raw, label, issuer) 37 | let s = String(cString: uri!) 38 | free(uri) 39 | return s 40 | } 41 | 42 | public func inc() { 43 | hotp_inc(raw) 44 | } 45 | 46 | public func gen() -> String { 47 | let code = hotp_gen(raw) 48 | let s_code = String(cString: code!) 49 | free(code) 50 | return s_code 51 | } 52 | 53 | public func verify(code: String) -> Bool { 54 | return hotp_verify(raw, code) 55 | } 56 | 57 | public func validate_current(code: String) -> Bool { 58 | return hotp_validate_current(raw, code) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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/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 | public class Totp: NSObject, RustObject { 12 | var raw: OpaquePointer 13 | 14 | required init(raw: OpaquePointer) { 15 | self.raw = raw 16 | } 17 | 18 | func intoRaw() -> OpaquePointer { 19 | return self.raw 20 | } 21 | 22 | public convenience init(uri: String) throws { 23 | let r = totp_from_uri(uri) 24 | if r == nil { 25 | throw Err(message: "InvalidUri") 26 | } else { 27 | self.init(raw: r!) 28 | } 29 | } 30 | 31 | deinit { 32 | totp_free(raw) 33 | } 34 | 35 | public func to_uri(label: String, issuer: String) -> String { 36 | let uri = totp_to_uri(raw, label, issuer) 37 | let s = String(cString: uri!) 38 | free(uri) 39 | return s 40 | } 41 | 42 | public func gen() -> String { 43 | let code = totp_gen(raw) 44 | let s = String(cString: code!) 45 | free(code) 46 | return s 47 | } 48 | 49 | public func gen_with(elapsed: UInt) -> String { 50 | let code = totp_gen_with(raw, elapsed) 51 | let s = String(cString: code!) 52 | free(code) 53 | return s 54 | } 55 | 56 | public func verify(code: String) -> Bool { 57 | return totp_verify(raw, code) 58 | } 59 | 60 | public func validate_current(code: String) -> Bool { 61 | return totp_validate_current(raw, code) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 | public class WebRequest : RustObject { 11 | var raw: OpaquePointer 12 | 13 | required init(raw: OpaquePointer) { 14 | self.raw = raw 15 | } 16 | 17 | public convenience init?(json: String) { 18 | let pointer = web_request_from_json(json) 19 | if pointer == nil { 20 | return nil 21 | } 22 | self.init(raw: pointer!) 23 | } 24 | 25 | public func intoRaw() -> OpaquePointer { 26 | return self.raw 27 | } 28 | 29 | deinit { 30 | if raw != nil { 31 | web_request_free(raw) 32 | } 33 | } 34 | 35 | public func isRegister() -> Bool { 36 | return web_request_is_register(raw) 37 | } 38 | 39 | public func isSign() -> Bool { 40 | return web_request_is_sign(raw) 41 | } 42 | 43 | public func getOrigin() -> Optional { 44 | let cOrigin = web_request_origin(raw) 45 | if cOrigin == nil { 46 | return .none 47 | } 48 | 49 | let origin = String(cString: cOrigin!) 50 | free(cOrigin) 51 | return .some(origin) 52 | } 53 | 54 | public func getTimeout() -> UInt64 { 55 | return web_request_timeout(raw) 56 | } 57 | 58 | public func getKeyHandle(origin: String) -> Optional { 59 | if self.isSign() { 60 | let cKeyHandle = web_request_key_handle(raw, origin) 61 | if cKeyHandle == nil { 62 | return .none 63 | } 64 | 65 | let keyHandle = String(cString: cKeyHandle!) 66 | free(cKeyHandle) 67 | return .some(keyHandle) 68 | } else { 69 | return .none 70 | } 71 | } 72 | 73 | public func register(origin: String, attestationCert: [UInt8], attestationKey: [UInt8]) -> WebResponse { 74 | return WebResponse(raw: web_request_register(raw, origin, attestationCert, UInt64(attestationCert.count), attestationKey, UInt64(attestationKey.count))) 75 | } 76 | 77 | public func sign(origin: String, signingKey: SigningKey, counter: UInt32, userPresence: Bool) -> WebResponse { 78 | return WebResponse(raw: web_request_sign(raw, signingKey.intoRaw(), origin, UInt(counter), userPresence)) 79 | } 80 | 81 | } 82 | 83 | public class SigningKey: RustObject { 84 | var raw: OpaquePointer 85 | 86 | public required init(raw: OpaquePointer) { 87 | self.raw = raw 88 | } 89 | 90 | public convenience init?(string: String) { 91 | let pointer = signing_key_from_string(string) 92 | 93 | if pointer == nil { 94 | return nil 95 | } 96 | 97 | self.init(raw: pointer!) 98 | } 99 | 100 | func intoRaw() -> OpaquePointer { 101 | return self.raw 102 | } 103 | 104 | deinit { 105 | signing_key_free(raw) 106 | } 107 | 108 | public func getKeyHandle() -> String { 109 | let cString = signing_key_get_key_handle(raw) 110 | let keyHandle = String(cString: cString!) 111 | free(cString) 112 | return keyHandle 113 | } 114 | 115 | public func toString() -> String { 116 | let csString = signing_key_to_string(raw) 117 | let sign = String(cString: csString!) 118 | free(csString) 119 | return sign 120 | } 121 | } 122 | 123 | public class WebResponse: RustObject { 124 | var raw: OpaquePointer 125 | 126 | public required init(raw: OpaquePointer) { 127 | self.raw = raw 128 | } 129 | 130 | func intoRaw() -> OpaquePointer { 131 | return self.raw 132 | } 133 | 134 | deinit { 135 | client_web_response_free(raw) 136 | } 137 | 138 | public func getSigningKey() -> Optional { 139 | let rawKey = client_web_response_signing_key(raw) 140 | if rawKey == nil { 141 | return .none 142 | } 143 | 144 | return .some(SigningKey(raw: rawKey!)) 145 | } 146 | 147 | public func toJson() -> String { 148 | let cJsonString = client_web_response_to_json(raw) 149 | let json = String(cString: cJsonString!) 150 | free(cJsonString) 151 | return json 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /wrappers/swift/classes/WebAuthnCreationResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AuthenticationServices 3 | 4 | @available(iOS 15.0, *) 5 | public class WebAuthnCreationResponse: NSObject { 6 | 7 | var raw: OpaquePointer 8 | var aaguid: String 9 | 10 | required init(raw: OpaquePointer, aaguid: String) { 11 | self.raw = raw 12 | self.aaguid = aaguid 13 | } 14 | 15 | func intoRaw() -> OpaquePointer { 16 | return self.raw 17 | } 18 | 19 | public func getPrivateKey() -> String { 20 | let cString = get_private_key_from_response(self.raw) 21 | let privateKey = String(cString: cString!) 22 | free(cString) 23 | return privateKey 24 | } 25 | 26 | public func getAttestationObject() -> Data { 27 | let buffer = get_attestation_object_from_response(self.raw) 28 | return Data(bytes: buffer.data, count: Int(buffer.len)) 29 | } 30 | 31 | public convenience init(aaguid: String, credentialId: Data, rpId: String, attestationFlags: UInt8, cose_algorithm_identifiers: [ASCOSEAlgorithmIdentifier]) throws { 32 | let credentialPointer = UnsafeMutablePointer.allocate(capacity: credentialId.count) 33 | credentialId.copyBytes(to: credentialPointer, count: credentialId.count) 34 | 35 | let cose_algorithm_identifiers_pointer = UnsafeMutablePointer.allocate(capacity: cose_algorithm_identifiers.count) 36 | for i in 0...(cose_algorithm_identifiers.count - 1) { 37 | cose_algorithm_identifiers_pointer[i] = Int32(cose_algorithm_identifiers[i].rawValue) 38 | } 39 | 40 | let r = generate_credential_creation_response(aaguid, credentialPointer, UInt(credentialId.count), rpId, attestationFlags.bigEndian, cose_algorithm_identifiers_pointer, UInt(cose_algorithm_identifiers.count)) 41 | if r == nil { 42 | throw Err(message: "Invalid parameters") 43 | } else { 44 | self.init(raw: r!, aaguid: aaguid) 45 | } 46 | credentialPointer.deallocate() 47 | cose_algorithm_identifiers_pointer.deallocate() 48 | } 49 | 50 | deinit { 51 | response_free(raw) 52 | } 53 | } 54 | 55 | public enum AttestationFlags: UInt8 { 56 | case userPresent = 1 57 | //Reserved for future = 2 58 | case userVerified = 4 59 | case backupEligible = 8 60 | case backedUp = 16 61 | //Reserved for future = 32 62 | case attestedCredentialDataIncluded = 64 63 | case extensionDataIncluded = 128 64 | } 65 | -------------------------------------------------------------------------------- /wrappers/swift/classes/WebAuthnRequestResponse.swift: -------------------------------------------------------------------------------- 1 | @available(iOS 15.0, *) 2 | public class WebAuthnRequestResponse: NSObject, RustObject { 3 | 4 | var raw: OpaquePointer 5 | 6 | required init(raw: OpaquePointer) { 7 | self.raw = raw 8 | } 9 | 10 | func intoRaw() -> OpaquePointer { 11 | return self.raw 12 | } 13 | 14 | public func getAuthData() -> Data { 15 | let buffer = get_auth_data_from_response(self.raw) 16 | return Data(bytes: buffer.data, count: Int(buffer.len)) 17 | } 18 | 19 | public func getSignature() -> Data { 20 | let buffer = get_signature_from_response(self.raw) 21 | return Data(bytes: buffer.data, count: Int(buffer.len)) 22 | } 23 | 24 | public func isSuccess() -> Bool { 25 | return is_success(self.raw) 26 | } 27 | 28 | public func getErrorMessage() -> String { 29 | let cString = get_error_message(self.raw) 30 | let errorMessage = String(cString: cString!) 31 | free(cString) 32 | return errorMessage 33 | } 34 | 35 | public convenience init( 36 | rpId: String, attestationFlags: UInt8, clientDataHash: Data, privateKey: String 37 | ) throws { 38 | let clientDataHashPointer = UnsafeMutablePointer.allocate( 39 | capacity: clientDataHash.count) 40 | clientDataHash.copyBytes(to: clientDataHashPointer, count: clientDataHash.count) 41 | 42 | let r = generate_credential_request_response( 43 | rpId, privateKey, attestationFlags.bigEndian, clientDataHashPointer, 44 | UInt(clientDataHash.count)) 45 | if r == nil { 46 | throw Err(message: "Invalid parameters") 47 | } else { 48 | self.init(raw: r!) 49 | } 50 | clientDataHashPointer.deallocate() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/.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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/wrappers/wasm/example/otp-example/src/app/app.component.css -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/wrappers/wasm/example/otp-example/src/assets/.gitkeep -------------------------------------------------------------------------------- /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/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/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devolutions/slauth/2034a5d906c9917898b979d84d121243fd4706f8/wrappers/wasm/example/otp-example/src/favicon.ico -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } --------------------------------------------------------------------------------