├── .cargo └── config.toml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── coverage.yml │ ├── release-pr.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── cliff.toml ├── examples ├── 01_signup.rs └── 02_authentication.rs ├── release-plz.toml └── src ├── api ├── host.rs ├── mod.rs └── user.rs ├── big_number.rs ├── dangerous.rs ├── defaults.rs ├── doc_test_mocks.rs ├── error.rs ├── hash ├── mod.rs ├── sha1.rs └── sha512.rs ├── lib.rs ├── primitives.rs ├── protocol_details.rs ├── rfc_5054_appendix_a.rs ├── rfc_5054_appendix_b.rs └── rfc_lingo.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | lint = "clippy --all-targets --all-features -- -D warnings" 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sassman 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: friday 8 | time: "15:00" 9 | timezone: Europe/Berlin 10 | open-pull-requests-limit: 10 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: ["*"] 5 | pull_request: 6 | branches: [main] 7 | workflow_call: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check: 12 | name: check 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: ["macos-latest", "ubuntu-latest", "windows-latest"] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: setup | rust 21 | uses: sassman/.github/.github/actions/rust-toolchain@main 22 | - run: cargo check 23 | 24 | lint: 25 | name: lint 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | os: ["macos-latest", "ubuntu-latest", "windows-latest"] 30 | cargo-cmd: 31 | - fmt --all -- --check 32 | - clippy --all-targets -- -D warnings 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: setup | rust 37 | uses: sassman/.github/.github/actions/rust-toolchain@main 38 | - run: cargo ${{ matrix['cargo-cmd'] }} 39 | 40 | tests: 41 | name: tests 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | os: ["macos-latest", "ubuntu-latest", "windows-latest"] 46 | channel: ["nightly", "stable"] 47 | cargo-test-cmd: 48 | - test 49 | - test --features dangerous --no-default-features --lib 50 | - test --features test-rfc-5054-appendix-b --no-default-features --lib 51 | - test --features hash-sha1 --no-default-features --lib 52 | - test --features wow --no-default-features --lib 53 | - test --features hash-sha512 --no-default-features --lib 54 | max-parallel: 0 55 | runs-on: ${{ matrix.os }} 56 | continue-on-error: ${{ matrix.channel == 'nightly' }} 57 | steps: 58 | - uses: actions/checkout@v4 59 | - name: setup | rust 60 | uses: sassman/.github/.github/actions/rust-toolchain@main 61 | with: 62 | channel: ${{ matrix.channel }} 63 | default: true 64 | profile: minimal 65 | - name: cargo test run 66 | run: cargo ${{ matrix['cargo-cmd'] }} 67 | 68 | audit: 69 | name: security audit 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: setup | rust 74 | uses: sassman/.github/.github/actions/rust-toolchain@main 75 | - uses: taiki-e/install-action@v2 76 | with: 77 | tool: cargo-deny 78 | - name: audit 79 | run: cargo deny check advisories bans sources 80 | continue-on-error: true 81 | 82 | docs: 83 | name: docs 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v4 87 | - name: setup | rust 88 | uses: sassman/.github/.github/actions/rust-toolchain@main 89 | - name: check documentation 90 | env: 91 | RUSTDOCFLAGS: -D warnings 92 | run: cargo doc --no-deps 93 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | types: 7 | - labeled 8 | - opened 9 | - reopened 10 | - synchronize 11 | workflow_dispatch: 12 | 13 | jobs: 14 | coverage: 15 | name: code coverage 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: setup | rust 20 | uses: sassman/.github/.github/actions/rust-toolchain@main 21 | with: 22 | channel: stable 23 | - name: setup | rust 24 | uses: sassman/.github/.github/actions/rust-toolchain@main 25 | with: 26 | channel: nightly 27 | - name: Install cargo-llvm-cov 28 | uses: taiki-e/install-action@cargo-llvm-cov 29 | - name: Generate code coverage 30 | run: | 31 | cargo +nightly llvm-cov --doctests --codecov --output-path codecov-1.json 32 | cargo llvm-cov --features "dangerous,test-rfc-5054-appendix-b" --no-default-features --codecov --output-path codecov-2.json 33 | cargo llvm-cov --features "wow" --no-default-features --codecov --output-path codecov-3.json 34 | cargo llvm-cov --features "hash-sha512" --no-default-features --codecov --output-path codecov-4.json 35 | - name: collect all coverage files into a comma separeted list 36 | run: echo "coverage_files=$(ls -1 codecov*.json | tr '\n' ',')" >> $GITHUB_ENV 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v5 39 | env: 40 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 41 | with: 42 | files: ${{ env.coverage_files }} 43 | verbose: true 44 | fail_ci_if_error: false 45 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | ## See https://release-plz.ieni.dev/docs/github/quickstart 2 | name: Release PR 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | release-plz: 16 | name: Release-plz 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Install Rust toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: Run release-plz pr 27 | uses: MarcoIeni/release-plz-action@v0.5 28 | with: 29 | command: release-pr 30 | config: release-plz.toml 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | ## references: 2 | # cache: https://github.com/actions/cache/blob/main/examples.md#rust---cargo 3 | # audit: https://github.com/actions-rs/audit-check 4 | # "needs": https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idneeds 5 | 6 | name: Release 7 | on: 8 | push: 9 | tags: 10 | - "v[0-9]+.[0-9]+.[0-9]+" 11 | - "v[0-9]+.[0-9]+.[0-9]-alpha.[0-9]+" 12 | - "v[0-9]+.[0-9]+.[0-9]-beta.[0-9]+" 13 | 14 | jobs: 15 | build: 16 | uses: sassman/srp6-rs/.github/workflows/build.yml@main 17 | 18 | publish: 19 | name: post / cargo publish 20 | needs: [build] 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: setup | rust 25 | uses: sassman/.github/.github/actions/rust-toolchain@main 26 | - name: publish 27 | env: 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | run: cargo publish 30 | 31 | release: 32 | name: post / github release 33 | needs: publish 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Get version from tag 38 | id: tag_name 39 | run: echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 40 | shell: bash 41 | - name: Get Changelog Entry 42 | id: changelog_reader 43 | # https://github.com/marketplace/actions/changelog-reader 44 | uses: mindsers/changelog-reader-action@v2 45 | with: 46 | validation_depth: 10 47 | version: ${{ steps.tag_name.outputs.current_version }} 48 | path: ./CHANGELOG.md 49 | - name: Create Release 50 | id: create_release 51 | uses: actions/create-release@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | # This pulls from the "Get Changelog Entry" step above, referencing it's ID to get its outputs object. 56 | # See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 57 | tag_name: ${{ github.ref }} 58 | release_name: Release ${{ steps.changelog_reader.outputs.version }} 59 | body: ${{ steps.changelog_reader.outputs.changes }} 60 | prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} 61 | draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} 62 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.0-beta.1] - 2025-03-31 8 | [1.0.0-beta.1]: https://github.com/sassman/ssd-benchmark-rs/compare/1.0.0-alpha.6...1.0.0-beta.1 9 | 10 | ### Features 11 | 12 | - Appendix b tests for 1024 bit group + sha512 support ([#17](https://github.com/sassman/srp6-rs/pull/17)) 13 | - Consistent serde support + ci updates ([#12](https://github.com/sassman/srp6-rs/pull/12)) 14 | 15 | ### Miscellaneous Tasks 16 | 17 | - Update thiserror requirement from 1.0 to 2.0 ([#15](https://github.com/sassman/srp6-rs/pull/15)) 18 | - Update hex-literal requirement from 0.4 to 1.0 ([#16](https://github.com/sassman/srp6-rs/pull/16)) 19 | - Update hex-literal requirement from 0.3 to 0.4 ([#7](https://github.com/sassman/srp6-rs/pull/7)) 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "srp6" 3 | version = "1.0.0-beta.1" 4 | authors = ["Sven Kanoldt "] 5 | repository = "https://github.com/sassman/srp6-rs" 6 | categories = ["Cryptography"] 7 | keywords = [ 8 | "srp", 9 | "secure remote password", 10 | "authentication", 11 | "key exchange", 12 | "protocols", 13 | ] 14 | edition = "2018" 15 | description = "A safe implementation of the secure remote password authentication and key-exchange protocol - SRP version 6 and 6a" 16 | license = "MIT" 17 | include = ["src", "examples", "LICENSE", "README.md", "CHANGELOG.md"] 18 | 19 | [dependencies] 20 | thiserror = "2.0" 21 | rand = { version = "0.9", default-features = false, features = ["thread_rng"] } 22 | num-bigint = { version = "0.4", features = [ 23 | "rand", 24 | "serde", 25 | ], default-features = false } 26 | num-traits = "0.2" 27 | hex-literal = "1.0" 28 | serde = { version = "1.0", features = ["derive"] } 29 | sha1 = { version = "^0.10", optional = true } 30 | sha2 = { version = "^0.10", optional = true } 31 | 32 | [dev-dependencies] 33 | serde_json = "1.0" 34 | 35 | [features] 36 | default = ["hash-sha512"] 37 | 38 | hash-sha512 = ["dep:sha2"] 39 | hash-sha1 = ["dep:sha1"] 40 | 41 | dangerous = ["hash-sha1"] 42 | wow = ["dangerous"] 43 | 44 | test-rfc-5054-appendix-b = ["hash-sha1"] 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sven Kanoldt 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 | # Secure Remote Password (SRP 6 / 6a) 2 | 3 | [![crates.io](https://img.shields.io/crates/v/srp6.svg)](https://crates.io/crates/srp6) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 5 | [![codecov](https://codecov.io/gh/sassman/srp6-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/sassman/srp6-rs) 6 | 7 | > A safe implementation of the secure remote password authentication and key-exchange protocol (SRP version 6a). 8 | 9 | ## About SRP6 10 | 11 | > The Secure Remote Password protocol performs secure remote authentication of short human-memorizable passwords and resists both passive and active network attacks. Because SRP offers this unique combination of password security, user convenience, and freedom from restrictive licenses, it is the most widely standardized protocol of its type, and as a result is being used by organizations both large and small, commercial and open-source, to secure nearly every type of human-authenticated network traffic on a variety of computing platforms. 12 | 13 | read more at [srp.stanford.edu](http://srp.stanford.edu) and in [RFC2945] that describes in detail the Secure remote password protocol. 14 | 15 | ## Features 16 | 17 | - client and server implementation of SRP 6 / 6a as in [RFC2945] 18 | - key length of 1024 to 8192 bit provided as in [RFC5054] 19 | - sha512 hashing instead of sha1 20 | - free of unsafe code 21 | - no openssl dependencies 22 | - rust native 23 | 24 | ## Feature flags 25 | 26 | - `default` - uses `hash-sha512` and keys >= 2048 bit 27 | - `dangerous` - uses `hash-sha1` and provides keys < 2048 bit, please do not use this in production code. 28 | - `wow` - uses `hash-sha1`, insecure keys and a uppercase of username and password for the hash, please do not use this in production code. This is used in an old World of Warcraft client. 29 | 30 | Those flags are only used for specific test scenarios and should not be used in production code. 31 | - `test-rfc-5054-appendix-b` 32 | 33 | ## Documentation 34 | 35 | To avoid code duplications this README is kept lean, please find examples and code at: 36 | 37 | - [official crate docs](https://docs.rs/srp6) 38 | - [formulas and protocol details](https://docs.rs/srp6/latest/srp6/protocol_details/index.html) 39 | - [usage examples](https://github.com/sassman/srp6-rs/blob/main/examples) 40 | 41 | [RFC2945]: https://datatracker.ietf.org/doc/html/rfc2945 42 | [RFC5054]: https://datatracker.ietf.org/doc/html/rfc5054#appendix-A 43 | 44 | ## License 45 | 46 | - **[MIT License](LICENSE)** 47 | - Copyright 2021 - 2025 © [Sven Kanoldt](https://www.d34dl0ck.me) 48 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog 7 | All notable changes to this project will be documented in this file. 8 | 9 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 10 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 11 | 12 | """ 13 | # template for the changelog body 14 | # https://tera.netlify.app/docs/#introduction 15 | body = """ 16 | {% if version %}\ 17 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 18 | [{{ version | trim_start_matches(pat="v") }}]: https://github.com/sassman/ssd-benchmark-rs/compare/{{ previous.version }}...{{ version }} 19 | {% else %}\ 20 | ## [unreleased] 21 | [Unreleased]: https://github.com/sassman/ssd-benchmark-rs/compare/{{ version }}...HEAD 22 | {% endif %}\ 23 | {% for group, commits in commits | group_by(attribute="group") %} 24 | ### {{ group | upper_first }} 25 | {% for commit in commits %} 26 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 27 | {% endfor %} 28 | {% endfor %}\n 29 | """ 30 | # remove the leading and trailing whitespaces from the template 31 | trim = true 32 | # changelog footer 33 | footer = """ 34 | 35 | """ 36 | 37 | [git] 38 | # parse the commits based on https://www.conventionalcommits.org 39 | conventional_commits = true 40 | # filter out the commits that are not conventional 41 | filter_unconventional = true 42 | # process each line of a commit as an individual commit 43 | split_commits = false 44 | # regex for parsing and grouping commits 45 | commit_parsers = [ 46 | { message = "^feat", group = "Features" }, 47 | { message = "^fix", group = "Bug Fixes" }, 48 | { message = "^doc", group = "Documentation" }, 49 | { message = "^perf", group = "Performance" }, 50 | { message = "^refactor", group = "Refactor" }, 51 | { message = "^style", group = "Styling" }, 52 | { message = "^test", group = "Testing" }, 53 | { message = "^chore\\(release\\): prepare for", skip = true }, 54 | { message = "^chore", group = "Miscellaneous Tasks" }, 55 | { body = ".*security", group = "Security" }, 56 | ] 57 | # protect breaking changes from being skipped due to matching a skipping commit_parser 58 | protect_breaking_commits = false 59 | # filter out the commits that are not matched by commit parsers 60 | filter_commits = true 61 | # glob pattern for matching git tags 62 | tag_pattern = "v[0-9]*" 63 | # regex for skipping tags 64 | skip_tags = "v0.1.0-beta.1" 65 | # regex for ignoring tags 66 | ignore_tags = "" 67 | # sort the tags topologically 68 | topo_order = false 69 | # sort the commits inside sections by oldest/newest order 70 | sort_commits = "newest" -------------------------------------------------------------------------------- /examples/01_signup.rs: -------------------------------------------------------------------------------- 1 | use srp6::prelude::*; 2 | 3 | fn main() { 4 | // this is what a user would enter in a form / terminal 5 | let new_username: UsernameRef = "alice"; 6 | let user_password: ClearTextPasswordRef = "password123"; 7 | 8 | // Reminder: choose always a Srp6_BITS type that is strong like 2048 or 4096 9 | let srp = Srp6_4096::default(); 10 | let (salt_s, verifier_v) = srp.generate_new_user_secrets(new_username, user_password); 11 | 12 | println!("Simulating a server and signup with user {}", new_username); 13 | println!("{}'s secrets are:", new_username); 14 | println!(" - Salt [s]:"); 15 | println!(" - {}", &salt_s.to_string()); 16 | println!(" - Password verifier [v]:"); 17 | println!(" - {}", &verifier_v.to_string()); 18 | println!(); 19 | println!("This is a one time action, normally this data is stored in a user database"); 20 | println!(); 21 | println!("Next authentication process `cargo run --example 02_authenticate`"); 22 | } 23 | -------------------------------------------------------------------------------- /examples/02_authentication.rs: -------------------------------------------------------------------------------- 1 | use srp6::prelude::*; 2 | 3 | const USER_PASSWORD: ClearTextPasswordRef = "password123"; 4 | 5 | // a println -like macro, to nicely format the key 6 | macro_rules! printkeyln { 7 | ($label:expr, $key:expr) => { 8 | println!( 9 | " - {} = \n\n {}\n", 10 | $label, 11 | $key.to_string().replace("\n", "\n ") 12 | ); 13 | }; 14 | } 15 | 16 | fn main() { 17 | // the server looks up the user details by a received username 18 | let user = mocked::lookup_user_details("alice"); 19 | 20 | // the server creates a handshake 21 | let (handshake, proof_verifier) = Srp6_4096::default().start_handshake(&user); 22 | assert_eq!(handshake.B.num_bytes(), Srp6_4096::KEY_LEN); 23 | println!( 24 | "## Simulating a Server and {} is our client.", 25 | user.username 26 | ); 27 | println!("server secrets are:"); 28 | printkeyln!("public key [B]", &proof_verifier.server_keys.0); 29 | printkeyln!("private key [b]", &proof_verifier.server_keys.1); 30 | println!(); 31 | println!("## {}'s secrets", user.username); 32 | printkeyln!("verifier [v]", &user.verifier); 33 | printkeyln!("salt [s]", &user.salt); 34 | println!(); 35 | println!("## {}'s handshake", user.username); 36 | printkeyln!("salt [s]", handshake.s); 37 | printkeyln!("server public key [B]", &handshake.B); 38 | printkeyln!("prime modulus [N]", &handshake.N); 39 | printkeyln!("generator modulus [g]", &handshake.g); 40 | printkeyln!("multiplier [k]", &handshake.k); 41 | println!(); 42 | 43 | // the client provides proof to the server 44 | let (proof, strong_proof_verifier) = handshake 45 | .calculate_proof(user.username.as_str(), USER_PASSWORD) 46 | .unwrap(); 47 | assert_eq!(proof.A.num_bytes(), Srp6_4096::KEY_LEN); 48 | assert_eq!( 49 | proof.M1.num_bytes(), 50 | HASH_LENGTH, 51 | "sha1 or sha-512 hash length expected" 52 | ); 53 | println!("### Next Step: sending this handshake to the client"); 54 | println!(); 55 | println!("## Simulating client {}", user.username); 56 | println!("### {}'s proof", user.username); 57 | printkeyln!("proof [M1]", &proof.M1); 58 | printkeyln!("public key [A]", &proof.A); 59 | println!(); 60 | 61 | // the server verifies this proof 62 | let strong_proof = proof_verifier.verify_proof(&proof); 63 | assert!(strong_proof.is_ok()); 64 | let (strong_proof, session_key_server) = strong_proof.unwrap(); 65 | println!("### Next Step: sending proof to the server"); 66 | println!(); 67 | println!( 68 | "## Simulating a Server and {} is our client.", 69 | user.username 70 | ); 71 | printkeyln!("strong proof [M2]", &strong_proof); 72 | printkeyln!("session key (server) [K]", &session_key_server); 73 | println!(); 74 | println!("[server] 🎉🥳🎊🍾🎈 Proof of the client successfully verified"); 75 | 76 | // the client needs to verify the strong proof 77 | let session_key_client = strong_proof_verifier 78 | .verify_strong_proof(&strong_proof) 79 | .unwrap(); 80 | println!("### Next Step: sending this strong proof to the client"); 81 | println!(); 82 | println!("## Simulating client {}", user.username); 83 | printkeyln!("session key (client) [K]", &session_key_client); 84 | println!(); 85 | println!("[client] 🎉🥳🎊🍾🎈 Proof of the server successfully verified"); 86 | } 87 | 88 | mod mocked { 89 | use super::*; 90 | 91 | /// normally salt and verifier is retrieved rom a user database 92 | pub fn lookup_user_details(username: UsernameRef) -> UserSecrets { 93 | let (salt, verifier) = 94 | Srp6_4096::default().generate_new_user_secrets(username, USER_PASSWORD); 95 | 96 | UserSecrets { 97 | username: username.to_owned(), 98 | salt, 99 | verifier, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | changelog_config = "cliff.toml" 3 | changelog_update = true 4 | git_tag_enable = true 5 | publish = false -------------------------------------------------------------------------------- /src/api/host.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::Result; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// this trait provides a higher level api 6 | pub trait HostAPI { 7 | /// For new users, or if they recover their password 8 | #[allow(non_snake_case)] 9 | fn generate_new_user_secrets( 10 | &self, 11 | I: UsernameRef, 12 | p: ClearTextPasswordRef, 13 | ) -> (Salt, PasswordVerifier); 14 | 15 | /// For new users, or if they recover their password 16 | /// For tests only 17 | #[cfg(test)] 18 | #[allow(non_snake_case)] 19 | fn generate_new_user_secrets_w_salt( 20 | &self, 21 | I: UsernameRef, 22 | p: ClearTextPasswordRef, 23 | s: Salt, 24 | ) -> (Salt, PasswordVerifier); 25 | 26 | /// starts the handshake with the client 27 | fn start_handshake(&self, user: &UserSecrets) -> (Handshake, HandshakeProofVerifier); 28 | } 29 | 30 | /// Contains all variables needed for a successful 31 | /// session key generation provided by the server to the client 32 | #[derive(Debug, Serialize, Deserialize)] 33 | #[allow(non_snake_case)] 34 | pub struct Handshake { 35 | /// the servers public key 36 | pub B: PublicKey, 37 | /// a generator modulo N 38 | pub g: Generator, 39 | /// a big and safe prime number 40 | pub N: PrimeModulus, 41 | /// multiplier parameter 42 | pub k: MultiplierParameter, 43 | /// the users salt 44 | pub s: Salt, 45 | } 46 | 47 | /// This is responsible for verifying a [`HandshakeProof`] that is 48 | /// provided by the client to the server 49 | #[derive(Debug)] 50 | #[allow(non_snake_case)] 51 | pub struct HandshakeProofVerifier { 52 | /// the servers pub and private key 53 | pub server_keys: KeyPair, 54 | /// the users s, v and I 55 | pub user: UserSecrets, 56 | /// a generator modulo N 57 | pub g: Generator, 58 | /// a big and safe prime number 59 | pub N: PrimeModulus, 60 | } 61 | 62 | impl HandshakeProofVerifier { 63 | /// verifies a proof provided by the client 64 | #[allow(non_snake_case)] 65 | pub fn verify_proof( 66 | &self, 67 | proof: &HandshakeProof, 68 | ) -> Result<(StrongProof, StrongSessionKey)> { 69 | let (B, b) = &self.server_keys; 70 | let N = &self.N; 71 | let g = &self.g; 72 | let I = &self.user.username; 73 | let s = &self.user.salt; 74 | let v = &self.user.verifier; 75 | let A = &proof.A; 76 | let M1 = &proof.M1; 77 | 78 | let S = &calculate_session_key_S_for_host::(N, A, B, b, v)?; 79 | let K = calculate_session_key_hash_interleave_K::(S); 80 | let M = &calculate_proof_M::(N, g, I, s, A, B, &K); 81 | 82 | if M != M1 { 83 | return Err(Srp6Error::InvalidProof(M.clone())); 84 | } 85 | let M2 = calculate_strong_proof_M2::(A, M, &K); 86 | 87 | Ok((M2, K)) 88 | } 89 | } 90 | 91 | /// Main interaction point for the server 92 | #[allow(non_snake_case)] 93 | #[derive(Debug, Serialize)] 94 | pub struct Srp6 { 95 | /// A large safe prime (N = 2q+1, where q is prime. All arithmetic is done modulo N. 96 | /// `KEY_LENGTH` needs to match the bytes of [`PrimeModulus`] `N` 97 | pub N: PrimeModulus, 98 | /// A generator modulo N 99 | pub g: Generator, 100 | /// multiplier parameter 101 | pub k: MultiplierParameter, 102 | } 103 | 104 | impl Srp6 { 105 | pub const KEY_LEN: usize = KEY_LENGTH; 106 | pub const SALT_LEN: usize = SALT_LENGTH; 107 | 108 | /// this constructor takes care of calculate the right `k` 109 | #[allow(non_snake_case)] 110 | pub fn new(g: Generator, N: PrimeModulus) -> Result { 111 | if N.num_bytes() != KEY_LENGTH { 112 | return Err(Srp6Error::KeyLengthMismatch { 113 | expected: KEY_LENGTH, 114 | given: N.num_bytes(), 115 | }); 116 | } 117 | let k = calculate_k::(&N, &g); 118 | Ok(Self { N, g, k }) 119 | } 120 | } 121 | 122 | impl HostAPI 123 | for Srp6 124 | { 125 | /// creates a new [`Salt`] `s` and [`PasswordVerifier`] `v` for a new user 126 | #[allow(non_snake_case)] 127 | fn generate_new_user_secrets( 128 | &self, 129 | I: UsernameRef, 130 | p: ClearTextPasswordRef, 131 | ) -> (Salt, PasswordVerifier) { 132 | let s = generate_salt::(); 133 | let x = calculate_private_key_x(I, p, &s); 134 | let v = calculate_password_verifier_v(&self.N, &self.g, &x); 135 | 136 | (s, v) 137 | } 138 | 139 | /// for test purposes only, we allow to inject the salt. 140 | /// In production salt is always random. 141 | #[cfg(test)] 142 | #[allow(non_snake_case)] 143 | fn generate_new_user_secrets_w_salt( 144 | &self, 145 | I: UsernameRef, 146 | p: ClearTextPasswordRef, 147 | s: Salt, 148 | ) -> (Salt, PasswordVerifier) { 149 | let x = calculate_private_key_x(I, p, &s); 150 | let v = calculate_password_verifier_v(&self.N, &self.g, &x); 151 | 152 | (s, v) 153 | } 154 | 155 | /// starts a session handshake for a given user 156 | /// [`Salt`] `s` and [`PasswordVerifier`] `p` are both user specific, 157 | /// initially they are generated by [`HostAPI::generate_new_user_secrets()`] 158 | #[allow(non_snake_case)] 159 | fn start_handshake( 160 | &self, 161 | user: &UserSecrets, 162 | ) -> (Handshake, HandshakeProofVerifier) { 163 | let (s, v) = (&user.salt, &user.verifier); 164 | let b = generate_private_key::(); 165 | 166 | let B = calculate_pubkey_B(&self.N, &self.k, &self.g, v, &b); 167 | 168 | let h = Handshake { 169 | N: self.N.clone(), 170 | g: self.g.clone(), 171 | k: self.k.clone(), 172 | s: s.clone(), 173 | B: B.clone(), 174 | }; 175 | 176 | let pv = HandshakeProofVerifier { 177 | server_keys: (B, b), 178 | user: user.clone(), 179 | g: self.g.clone(), 180 | N: self.N.clone(), 181 | }; 182 | 183 | (h, pv) 184 | } 185 | } 186 | 187 | impl Handshake { 188 | /// client proof calculation 189 | /// User: x = H(s, p) (user enters password) 190 | /// User: S = (B - kg^x) ^ (a + ux) (computes session key) 191 | /// User: K = H(S) 192 | pub fn calculate_proof( 193 | &self, 194 | username: UsernameRef, 195 | password: ClearTextPasswordRef, 196 | ) -> Result<( 197 | HandshakeProof, 198 | StrongProofVerifier, 199 | )> { 200 | use super::user::calculate_proof_M_for_client; 201 | 202 | let credentials = UserCredentials { username, password }; 203 | calculate_proof_M_for_client::(self, &credentials) 204 | } 205 | } 206 | 207 | #[test] 208 | fn should_panic_when_key_length_does_not_fit_to_modulus() { 209 | type Srp = Srp6<10, 10>; 210 | let err = Srp::new( 211 | Generator::from(3), 212 | PrimeModulus::from_hex_str_be("FE27").unwrap(), 213 | ); 214 | assert!(err.is_err()); 215 | assert_eq!( 216 | err.err().unwrap(), 217 | Srp6Error::KeyLengthMismatch { 218 | expected: Srp::KEY_LEN, 219 | given: 2 220 | } 221 | ) 222 | } 223 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod host; 2 | pub(crate) mod user; 3 | -------------------------------------------------------------------------------- /src/api/user.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::host::Handshake; 4 | use crate::primitives::*; 5 | use crate::{error::Srp6Error, Result}; 6 | 7 | /// Contains the client's [`PublicKey`] and their [`Proof`] and is sent to the server 8 | #[derive(Debug, Serialize, Deserialize)] 9 | #[allow(non_snake_case)] 10 | pub struct HandshakeProof { 11 | /// the client public key 12 | pub A: PublicKey, 13 | /// the clients proof 14 | pub M1: Proof, 15 | } 16 | 17 | /// Verifies the [`StrongProof`] provided by the server to the client 18 | #[derive(Debug)] 19 | #[allow(non_snake_case)] 20 | pub struct StrongProofVerifier { 21 | pub A: PublicKey, 22 | pub K: StrongSessionKey, 23 | pub M1: Proof, 24 | } 25 | 26 | impl StrongProofVerifier { 27 | /// verifies a [`StrongProof`] from the server on the client side 28 | #[allow(non_snake_case)] 29 | pub fn verify_strong_proof(&self, M2: &StrongProof) -> Result { 30 | let A = &self.A; 31 | let M = &self.M1; 32 | let K = &self.K; 33 | let my_strong_proof = calculate_strong_proof_M2::(A, M, K); 34 | 35 | if M2 != &my_strong_proof { 36 | Err(Srp6Error::InvalidStrongProof(M2.clone())) 37 | } else { 38 | Ok(K.clone()) 39 | } 40 | } 41 | } 42 | 43 | /// Calculates client [`Proof`] `M1` with a more high level api 44 | #[allow(non_snake_case)] 45 | pub(crate) fn calculate_proof_M_for_client( 46 | handshake: &Handshake, 47 | credentials: &UserCredentials, 48 | ) -> Result<(HandshakeProof, StrongProofVerifier)> { 49 | let username = credentials.username; 50 | let user_password = credentials.password; 51 | let a = generate_private_key::(); 52 | let A = calculate_pubkey_A(&handshake.N, &handshake.g, &a); 53 | let x = calculate_private_key_x(username, user_password, &handshake.s); 54 | let S = calculate_session_key_S_for_client::( 55 | &handshake.N, 56 | &handshake.k, 57 | &handshake.g, 58 | &handshake.B, 59 | &A, 60 | &a, 61 | &x, 62 | )?; 63 | let K = calculate_session_key_hash_interleave_K::(&S); 64 | let M1 = calculate_proof_M::( 65 | &handshake.N, 66 | &handshake.g, 67 | username, 68 | &handshake.s, 69 | &A, 70 | &handshake.B, 71 | &K, 72 | ); 73 | 74 | let strong_proof_verifier = StrongProofVerifier { 75 | A: A.clone(), 76 | K, 77 | M1: M1.clone(), 78 | }; 79 | let proof = HandshakeProof { A, M1 }; 80 | 81 | Ok((proof, strong_proof_verifier)) 82 | } 83 | -------------------------------------------------------------------------------- /src/big_number.rs: -------------------------------------------------------------------------------- 1 | use crate::hash::*; 2 | 3 | use num_bigint::{BigInt, Sign}; 4 | use rand::{rng, Rng}; 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | use std::convert::TryFrom; 7 | use std::fmt::{Debug, Display, Formatter}; 8 | use thiserror::Error; 9 | 10 | /// also exporting the trait here 11 | pub use num_traits::Zero; 12 | pub use std::ops::{Add, Mul, Rem, Sub}; 13 | 14 | impl Serialize for BigNumber { 15 | fn serialize(&self, serializer: S) -> std::result::Result 16 | where 17 | S: Serializer, 18 | { 19 | serializer.serialize_bytes(self.to_vec().as_slice()) 20 | } 21 | } 22 | 23 | impl<'de> Deserialize<'de> for BigNumber { 24 | fn deserialize(deserializer: D) -> std::result::Result 25 | where 26 | D: Deserializer<'de>, 27 | { 28 | let bytes = Vec::::deserialize(deserializer)?; 29 | Ok(Self::from_vec(&bytes)) 30 | } 31 | } 32 | 33 | /// [`BigNumber`] helps to work with big numbers as in openssl used. 34 | #[derive(PartialEq, Clone)] 35 | pub struct BigNumber(BigInt); 36 | 37 | #[derive(Error, Debug)] 38 | pub enum BigNumberError { 39 | #[error("Invalid hex string.")] 40 | InvalidHexStr, 41 | } 42 | 43 | /// new empty unsigned big number 44 | impl Default for BigNumber { 45 | fn default() -> Self { 46 | Self(BigInt::new(Sign::Plus, vec![])) 47 | } 48 | } 49 | 50 | impl BigNumber { 51 | /// new random initialized big number 52 | pub fn new_rand(n_bytes: usize) -> Self { 53 | let rng = rng(); 54 | let bytes: Vec = rng.random_iter().take(n_bytes).collect(); 55 | Self(BigInt::from_bytes_be(Sign::Plus, &bytes)) 56 | } 57 | 58 | /// `raw` is expected to be big endian 59 | pub fn from_bytes_be(raw: &[u8]) -> Self { 60 | Self(BigInt::from_bytes_be(Sign::Plus, raw)) 61 | } 62 | 63 | /// `raw` is expected to be big endian 64 | pub fn from_bytes_le(raw: &[u8]) -> Self { 65 | Self(BigInt::from_bytes_le(Sign::Plus, raw)) 66 | } 67 | 68 | /// from hex string, hex strings are always big endian: 69 | /// High 70 | /// -> Low 71 | /// "123acab" 72 | /// This method strips all non alphanumerical, So block formats are also supported. 73 | pub fn from_hex_str_be(str: &str) -> std::result::Result { 74 | let str = str 75 | .chars() 76 | .filter(|c| c.is_alphanumeric()) 77 | .collect::(); 78 | 79 | let str = if str.len() % 2 != 0 { 80 | format!("{:0>len$}", str, len = (str.len() / 2 + 1) * 2) 81 | } else { 82 | str.to_owned() 83 | }; 84 | 85 | let mut bytes_be = Vec::with_capacity(str.len() / 2); 86 | 87 | for i in (0..str.len()).step_by(2) { 88 | let byte_str = &str[i..i + 2]; 89 | let byte = 90 | u8::from_str_radix(byte_str, 16).map_err(|_| BigNumberError::InvalidHexStr)?; 91 | bytes_be.push(byte); 92 | } 93 | 94 | Ok(Self::from_bytes_be(&bytes_be)) 95 | } 96 | 97 | pub fn modpow(&self, exponent: &Self, modulo: &Self) -> Self { 98 | self.0.modpow(&exponent.0, &modulo.0).into() 99 | } 100 | 101 | pub fn num_bytes(&self) -> usize { 102 | self.0.bits().div_ceil(8) as usize 103 | } 104 | 105 | /// returns the byte vec in little endian byte order 106 | pub fn to_vec(&self) -> Vec { 107 | let (_, x) = self.0.to_bytes_be(); 108 | x 109 | } 110 | 111 | /// the counter part of `::to_vec()` 112 | pub fn from_vec(data: &[u8]) -> Self { 113 | Self::from_bytes_be(data) 114 | } 115 | 116 | /// returns the byte vec in little endian byte order, padded by 0 for `len` bytes 117 | pub fn to_array_pad_zero(&self) -> Vec { 118 | let pad = self.num_bytes().max(N); 119 | 120 | let unpadded = self.to_vec(); 121 | let mut padded = vec![0; pad]; 122 | padded[pad - unpadded.len()..].copy_from_slice(&unpadded); 123 | 124 | padded 125 | } 126 | } 127 | 128 | #[test] 129 | fn test_mod_exp() { 130 | let a = BigNumber::from_hex_str_be("6").unwrap(); 131 | let p = BigNumber::from_hex_str_be("3").unwrap(); 132 | let m = BigNumber::from_hex_str_be("7").unwrap(); 133 | let r = a.modpow(&p, &m); 134 | 135 | assert_eq!(&r, &BigNumber::from(6), "{} is not 6", &r); 136 | assert_eq!( 137 | &a.modpow(&p, &m), 138 | &BigNumber::from(6), 139 | "{}.modExp(3, 7) is not 6", 140 | &r 141 | ); 142 | } 143 | 144 | impl Debug for BigNumber { 145 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 146 | write!(f, "BigNumber(\"{}\")", self) 147 | } 148 | } 149 | 150 | // region from traits 151 | impl From for BigNumber { 152 | fn from(n: u32) -> Self { 153 | Self(BigInt::from(n)) 154 | } 155 | } 156 | 157 | impl From for BigNumber { 158 | fn from(a: BigInt) -> Self { 159 | Self(a) 160 | } 161 | } 162 | 163 | impl From<[u8; N]> for BigNumber { 164 | fn from(k: [u8; N]) -> Self { 165 | Self::from_bytes_be(&k) 166 | } 167 | } 168 | 169 | impl From for BigNumber { 170 | fn from(hasher: HashFunc) -> Self { 171 | hasher.finalize().as_slice().into() 172 | } 173 | } 174 | 175 | impl From<&[u8]> for BigNumber { 176 | fn from(somewhere: &[u8]) -> Self { 177 | Self::from_bytes_be(somewhere) 178 | } 179 | } 180 | 181 | impl From<&BigNumber> for String { 182 | /// Returns a hex string representation of the big number 183 | /// it is formatted in 8 hex chars per block (hexlets) 184 | /// and 7 hexlets per line 185 | fn from(x: &BigNumber) -> Self { 186 | let hex = x.0.to_str_radix(16).to_uppercase(); 187 | 188 | let hexlets: Vec = hex 189 | .chars() 190 | .collect::>() 191 | .chunks(8) 192 | .map(|chunk| chunk.iter().collect::()) 193 | .collect(); 194 | 195 | // Group 7 hexlets per line 196 | hexlets 197 | .chunks(7) 198 | .map(|line| line.join(" ")) 199 | .collect::>() 200 | .join("\n") 201 | } 202 | } 203 | 204 | impl From for String { 205 | fn from(x: BigNumber) -> Self { 206 | (&x).into() 207 | } 208 | } 209 | 210 | impl TryFrom<&str> for BigNumber { 211 | type Error = BigNumberError; 212 | 213 | fn try_from(value: &str) -> std::result::Result { 214 | Self::from_hex_str_be(value) 215 | } 216 | } 217 | 218 | impl TryFrom for BigNumber { 219 | type Error = BigNumberError; 220 | 221 | fn try_from(value: String) -> Result { 222 | Self::from_hex_str_be(value.as_str()) 223 | } 224 | } 225 | 226 | #[test] 227 | fn should_try_from_string() { 228 | use std::convert::TryInto; 229 | 230 | let s = "ab11cd".to_string(); 231 | let x: BigNumber = s.try_into().unwrap(); 232 | assert_eq!(x.to_vec(), &[0xab, 0x11, 0xcd]); 233 | } 234 | 235 | #[test] 236 | fn should_from_bytes() { 237 | let x = BigNumber::from_bytes_be(&[0xab, 0x11, 0xcd]); 238 | assert_eq!(x.to_vec(), &[0xab, 0x11, 0xcd]); 239 | } 240 | 241 | #[test] 242 | fn should_to_vec() { 243 | let x = BigNumber::from_hex_str_be("ab11cd").unwrap(); 244 | assert_eq!(x.to_vec(), &[0xab, 0x11, 0xcd]); 245 | } 246 | 247 | #[test] 248 | fn should_random_initialize() { 249 | let x = BigNumber::new_rand(10); 250 | assert_ne!(x, BigNumber::default()); 251 | } 252 | 253 | #[test] 254 | fn should_pad_0() { 255 | let x = BigNumber::from_bytes_be(&[0x11, 0xcd]); 256 | assert_eq!(x.to_array_pad_zero::<3>(), [0, 0x11, 0xcd]); 257 | 258 | assert_eq!(x.to_array_pad_zero::<1>(), [0x11, 0xcd]); 259 | } 260 | 261 | #[test] 262 | fn should_should_work_with_odd_byte_count() { 263 | assert_eq!(BigNumber::from_hex_str_be("6").unwrap().to_string(), "6"); 264 | } 265 | // endregion 266 | 267 | // region modulo 268 | impl Rem for &BigNumber { 269 | type Output = BigNumber; 270 | 271 | fn rem(self, rhs: &BigNumber) -> Self::Output { 272 | (&self.0).rem(&rhs.0).into() 273 | } 274 | } 275 | #[test] 276 | fn should_modulo_ref() { 277 | let a = &BigNumber::from(10); 278 | assert_eq!(a.rem(&BigNumber::from(4)), BigNumber::from(10 % 4)); 279 | } 280 | impl Rem for BigNumber { 281 | type Output = Self; 282 | 283 | fn rem(self, rhs: Self) -> Self::Output { 284 | (&self).rem(&rhs) 285 | } 286 | } 287 | #[test] 288 | fn should_modulo() { 289 | let exp = BigNumber::from(7 % 6); 290 | assert_eq!(BigNumber::from(7) % BigNumber::from(6), exp); 291 | } 292 | // endregion 293 | 294 | // region mul, add, sub 295 | impl Mul for BigNumber { 296 | type Output = Self; 297 | 298 | fn mul(self, rhs: Self) -> Self::Output { 299 | (self.0 * rhs.0).into() 300 | } 301 | } 302 | 303 | impl Mul for &BigNumber { 304 | type Output = BigNumber; 305 | 306 | fn mul(self, rhs: Self) -> Self::Output { 307 | (&self.0 * &rhs.0).into() 308 | } 309 | } 310 | 311 | #[test] 312 | fn test_big_num_mul() { 313 | let a = BigNumber::from(4); 314 | let b = BigNumber::from(2); 315 | let exp = BigNumber::from(8); 316 | assert_eq!(a * b, exp); 317 | } 318 | 319 | impl Add for BigNumber { 320 | type Output = Self; 321 | 322 | fn add(self, rhs: Self) -> Self::Output { 323 | self.0.add(rhs.0).into() 324 | } 325 | } 326 | impl<'b> Add<&'b BigNumber> for &BigNumber { 327 | type Output = BigNumber; 328 | 329 | fn add(self, rhs: &'b BigNumber) -> Self::Output { 330 | (&self.0).add(&rhs.0).into() 331 | } 332 | } 333 | 334 | impl Sub for BigNumber { 335 | type Output = Self; 336 | 337 | fn sub(self, rhs: Self) -> Self::Output { 338 | self.0.sub(rhs.0).into() 339 | } 340 | } 341 | #[test] 342 | fn should_subtract() { 343 | let (a, b) = (BigNumber::from(6), BigNumber::from(1)); 344 | assert_eq!(a - b, BigNumber::from(5)); 345 | } 346 | 347 | impl<'b> Sub<&'b BigNumber> for &BigNumber { 348 | type Output = BigNumber; 349 | 350 | fn sub(self, rhs: &'b BigNumber) -> Self::Output { 351 | (&self.0).sub(&rhs.0).into() 352 | } 353 | } 354 | #[test] 355 | fn should_subtract_refs() { 356 | let (a, b) = (BigNumber::from(6), BigNumber::from(6)); 357 | assert_eq!(&a - &b, BigNumber::from(0)); 358 | } 359 | // endregion 360 | 361 | impl Display for BigNumber { 362 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 363 | let x: String = self.into(); 364 | write!(f, "{}", x) 365 | } 366 | } 367 | 368 | #[test] 369 | fn test_whitespace_is_ignored() { 370 | assert_eq!( 371 | BigNumber::try_from("A B 1 1 C D",).unwrap(), 372 | BigNumber::from_hex_str_be("AB11CD",).unwrap() 373 | ); 374 | } 375 | 376 | #[test] 377 | fn test_into_string_and_display() { 378 | let expected = "3E9D557B 7899AC2A 8DEC8D00 46FB310A 42A233BD 1DF0244B 574AB946\nA22A4A18"; 379 | let x = BigNumber::from_hex_str_be( 380 | "3E9D557B7899AC2A8DEC8D0046FB310A42A233BD1DF0244B574AB946A22A4A18", 381 | ) 382 | .unwrap(); 383 | let s: String = x.into(); 384 | assert_eq!(s, expected); 385 | assert_eq!( 386 | format!( 387 | "{}", 388 | BigNumber::from_hex_str_be( 389 | "3E9D557B7899AC2A8DEC8D0046FB310A42A233BD1DF0244B574AB946A22A4A18", 390 | ) 391 | .unwrap() 392 | ), 393 | expected 394 | ); 395 | } 396 | 397 | #[test] 398 | fn test_serde_behaviour() { 399 | let v = "3E9D557B7899AC2A8DEC8D0046FB310A42A233BD1DF0244B574AB946A22A4A18"; 400 | let b = BigNumber::from_hex_str_be(v).unwrap(); 401 | 402 | let b_str = serde_json::to_string(&b).unwrap(); 403 | let b2 = serde_json::from_str::(&b_str).unwrap(); 404 | assert_eq!(b, b2); 405 | } 406 | 407 | impl Zero for BigNumber { 408 | fn zero() -> Self { 409 | BigInt::zero().into() 410 | } 411 | 412 | fn is_zero(&self) -> bool { 413 | self.0.is_zero() 414 | } 415 | } 416 | 417 | #[test] 418 | fn test_network_endianess() { 419 | assert_eq!( 420 | hex_literal::hex!("AB 11 CD"), 421 | [0xAB, 0x11, 0xCD], 422 | "we trust the hex_literal crate" 423 | ); 424 | assert_eq!( 425 | BigInt::from_bytes_be(Sign::Plus, &[0xAB, 0x11, 0xCD]), 426 | BigInt::parse_bytes(b"AB11CD", 16).unwrap(), 427 | "BigInt::from_bytes_be is big endian" 428 | ); 429 | let b = BigNumber::try_from("AB 11 CD").unwrap(); 430 | assert_eq!(b.to_array_pad_zero::<3>(), [0xAB, 0x11, 0xCD]); 431 | assert_eq!(b.to_vec(), [0xAB, 0x11, 0xCD]); 432 | assert_eq!(b.to_string(), "AB11CD"); 433 | } 434 | -------------------------------------------------------------------------------- /src/dangerous.rs: -------------------------------------------------------------------------------- 1 | use crate::api::host::Srp6; 2 | use crate::primitives::{Generator, PrimeModulus}; 3 | use crate::rfc_5054_appendix_a::group_1024_bit; 4 | 5 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 256 bit / 32 byte. 6 | pub type Srp6_256 = Srp6<32, 32>; 7 | 8 | impl Default for Srp6_256 { 9 | fn default() -> Self { 10 | Self::new( 11 | Generator::from(7), 12 | PrimeModulus::from_hex_str_be( 13 | "894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 14 | ) 15 | .unwrap(), 16 | ) 17 | .unwrap() 18 | } 19 | } 20 | 21 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 512 bit / 64 byte. 22 | pub type Srp6_512 = Srp6<64, 64>; 23 | 24 | impl Default for Srp6_512 { 25 | fn default() -> Self { 26 | Self::new( 27 | Generator::from(7), 28 | PrimeModulus::from_hex_str_be( 29 | "D58B60A281533E85DA01C6943F8EAF5A14737F8F701788B4611A3A88D5A6A0A0 30 | E3EA3DA917EF8D036BA79706DAC9EB261E469D02B44998B88F3B06EACFF96D7B", 31 | ) 32 | .unwrap(), 33 | ) 34 | .unwrap() 35 | } 36 | } 37 | 38 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 1024 bit / 128 byte. 39 | /// taken from the 1024-bit group at [RFC5054 Appendix A](https://datatracker.ietf.org/doc/html/rfc5054#appendix-A) 40 | pub use group_1024_bit::Srp6_1024; 41 | 42 | impl Default for Srp6_1024 { 43 | fn default() -> Self { 44 | group_1024_bit::values() 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | 52 | #[test] 53 | fn should_ensure_key_length_is_as_expected() { 54 | let srp = Srp6_256::default(); 55 | assert_eq!(srp.N.num_bytes(), 256 / 8); 56 | 57 | let srp = Srp6_512::default(); 58 | assert_eq!(srp.N.num_bytes() as u32, 512 / 8); 59 | 60 | let srp = Srp6_1024::default(); 61 | assert_eq!(srp.N.num_bytes() as u32, 1024 / 8); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/defaults.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! default prime modulus and generator numbers taken from [RFC5054 Appendix A], 3 | //! so they can be treated as vetted and safe. 4 | //! 5 | //! ## Usage: 6 | //! 7 | //! ```rust 8 | //! use srp6::prelude::{Srp6_4096, HostAPI}; 9 | //! 10 | //! let srp = Srp6_4096::default(); 11 | //! let (_salt_s, _verifier_v) = srp.generate_new_user_secrets("Bob", "secret-password"); 12 | //! ``` 13 | //! 14 | //! [RFC5054 Appendix A]: https://datatracker.ietf.org/doc/html/rfc5054#appendix-A 15 | 16 | use crate::rfc_5054_appendix_a::{ 17 | group_2048_bit, group_3072_bit, group_4096_bit, group_6144_bit, group_8192_bit, 18 | }; 19 | 20 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 2048 bit / 256 byte. 21 | pub use group_2048_bit::Srp6_2048; 22 | 23 | impl Default for Srp6_2048 { 24 | fn default() -> Self { 25 | group_2048_bit::values() 26 | } 27 | } 28 | 29 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 3072 bit / 384 byte. 30 | pub use group_3072_bit::Srp6_3072; 31 | 32 | impl Default for Srp6_3072 { 33 | fn default() -> Self { 34 | group_3072_bit::values() 35 | } 36 | } 37 | 38 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 4096 bit / 512 byte. 39 | pub use group_4096_bit::Srp6_4096; 40 | 41 | impl Default for Srp6_4096 { 42 | fn default() -> Self { 43 | group_4096_bit::values() 44 | } 45 | } 46 | 47 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 6144 bit / 768 byte. 48 | pub use group_6144_bit::Srp6_6144; 49 | 50 | impl Default for Srp6_6144 { 51 | fn default() -> Self { 52 | group_6144_bit::values() 53 | } 54 | } 55 | 56 | /// length of [`PrimeModulus`][crate::primitives::PrimeModulus] `N` and [`Salt`][crate::primitives::Salt] `s` is 8192 bit / 1024 byte. 57 | pub use group_8192_bit::Srp6_8192; 58 | 59 | impl Default for Srp6_8192 { 60 | fn default() -> Self { 61 | group_8192_bit::values() 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | 69 | #[test] 70 | fn should_ensure_key_lengths_are_as_expected() { 71 | let srp = Srp6_2048::default(); 72 | assert_eq!(srp.N.num_bytes() as u32, 2048 / 8); 73 | 74 | let srp = Srp6_3072::default(); 75 | assert_eq!(srp.N.num_bytes() as u32, 3072 / 8); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/doc_test_mocks.rs: -------------------------------------------------------------------------------- 1 | //! Mocks for the doc tests. 2 | //! The mock data where produced with the help of example `02_authenticate.rs`. 3 | //! You can run it with `cargo run --example 02_authenticate`. 4 | //! 5 | //! Note: The salt and verifier are generated at random, hence they will be different every time. 6 | use srp6::prelude::*; 7 | 8 | pub fn lookup_user_details(username: UsernameRef) -> UserSecrets { 9 | UserSecrets { 10 | username: username.to_owned(), 11 | salt: Salt::from_hex_str_be( 12 | "2A27E3AF 30443FCA 7A9D8C0A 3D6423A4 3AC28B82 C119E240 E7499F1F 13 | EAA4D9E4 DE18C0BF 8BF689FA E799AA40 FA51437A DB39F206 AB92F62D 14 | 253F3D22 B7D2E500 6DECB7FE 88EAA821 657450ED 7A020EA9 9A947409 15 | 9F504091 1D934DFA D96CCD5C D0FA543E 73892408 E9AD3830 D764ECC1 16 | E1B452C1 569C79E7 D0343C6E EFD88494 6FC75C61 8BCC4C6C 2E2B8A56 17 | 88E5AF31 0B3E171C E50F4211 0C4E523E D25B5C4B 9AD1A876 694A1C7B 18 | 1BC4AB08 6B2A7ADE 81EC1611 5DDD00AB 21E2B0DF 542B2820 C2C9FB59 19 | D14B2792 16FA7A36 1D25A32B 2F0BBC94 0E49F45F 2D5CBF7F 785A7FD4 20 | 3F76009B EB690EFD 6588A570 CF51870E 80ACDEB1 5BEC193C A5B5FC58 21 | 870E5EA1 AE4040FC C7604B03 AF3295A8 5AAB47E6 5857A770 4900F9C7 22 | CA8E2C22 11AD51F2 B69F7732 17DDE937 D766A205 2F53A040 12C9FADA 23 | 50AA98BC 6BF49E17 DCB4B24C 798DDD62 D88F11FE CCA4A19C C219661C 24 | F125BBB0 191A1A24 F2020A25 8BD9DF97 7B7CAB27 04B59908 F568C69B 25 | 845E35F4 BAE68C02 BF810902 19589BEC F12079DA 6FCEE0AC 7EBB223E 26 | B05DDA4C 1766FE34 DB661139 FE894C5D 9D6F0192 2BDECCBE E889F69F 27 | 054A34EA A74D9B7C C9873F2A C3CD8395 D42410AE 3E51409F B7A0B797 28 | 8D93CCDF DA8401C5 3524575B 84B970C2 1EF44BC1 69E6D2CA 95D0C7D3 29 | 1A67B4F9 A88D4A9D F1BCF8C6 F8A5512A F1DBFE6B 9C52FCA1 A00BA79C 30 | 0D60978C 8C313D6A", 31 | ) 32 | .unwrap(), 33 | verifier: PasswordVerifier::from_hex_str_be( 34 | "BF86D308 FDA5D535 D8D99516 B6A6045E D3455ED4 222D0BD5 1C513400 35 | 75278047 9490FC91 D6A53AC4 AFC7FD72 D2534EEB F6D8CB3B C375847A 36 | F1C8D739 46424401 08B24C8A A4F07FE2 9AFA39BA A8CC1625 E6F7CBA7 37 | B9DF924E 033B48E8 FA7A0329 F78F8C10 6DB728E0 49A74AE2 8F2C2EEA 38 | 32A4B124 C499350F C7914C24 13647633 5E9757E3 1892DAA6 0009B2BE 39 | D4DA9043 4C267D60 F302FA22 F7EC376D 93B40003 2EADCD10 74735371 40 | D0949256 E64441DC 0710A20A CD22C22C 99928742 5E901EA3 C45992DB 41 | AFF180B5 1C8B0B77 2C75FC3B 4794CEBB D5BD25E4 6BC90855 3CD4E051 42 | CA0AA7F2 D2298A62 C4B84F2D AB8D90CB 140EE9BC 5388F106 51EA2729 43 | 634BFF03 06101731 BB802E41 89520E1E 8F2129D7 8C8747DC 604969CA 44 | C56E634F 68F38BA6 360B0265 0A48DAC3 42206512 107966DA 436C71DE 45 | F471E9F0 99FBC02F 7258B1B4 4F19BBA5 62115458 83540B8B 88D76544 46 | ABDE2B8E 8326E770 0373A395 20B38CD0 C23C1E1C 42BB988F FCE1C96B 47 | 79D57753 23297AAB 077F5410 525D9428 D3CA4E1B 44AA97A9 667E8171 48 | 80CD8927 C3035DCF 178117FC 472FD5BA 6BA72AD2 1D3DE2E9 C2D9DE8A 49 | 262525E7 0E595581 7AA770F9 D10FD939 E0A0C363 FA579925 ACC9891B 50 | 1BF38BE1 60A4FAD9 C60F7BC4 BCE2C0CF 0555F3C6 9255A63D EC9B7E55 51 | 2DDF03C9 838C3B7B 7C2AB69C FF9390D4 DD9B4409 7FCB9E73 3574B937 52 | 1CB0A045 6B7E0A61", 53 | ) 54 | .unwrap(), 55 | } 56 | } 57 | 58 | pub fn handshake_from_the_server( 59 | username: UsernameRef, 60 | ) -> Handshake<{ Srp6_4096::KEY_LEN }, { Srp6_4096::SALT_LEN }> { 61 | let user = lookup_user_details(username); 62 | let srp = Srp6_4096::default(); 63 | 64 | Handshake { 65 | s: user.salt, 66 | N: srp.N, 67 | g: srp.g, 68 | B: PublicKey::from_hex_str_be( 69 | "305A710C 399CE43C 56D38BEE 8504193E 91242E95 CF16D0C8 4D377C6C 70 | 6A1484EC 70732FFC F469C9D4 612C8F95 1FB478F7 16BE3B18 1E9C1ABE 71 | 8B03B83E 542D3B92 F5C23DDA AEE847BE 7618427B 258882EE E7F99974 72 | BDE2DB8C 5E102129 79212BDC 4A3E49D2 99952839 E02A150E A9B08B24 73 | 6CDBB24B C8DA2CE4 2A742B9D 22FA95E6 A8A5A1EE D0071455 B863ED1B 74 | 68C94F40 88C1B0BA ACF58646 5B045D5B A1F75852 32321E08 D277D91F 75 | FBC24944 98C02249 534BCBB9 40C589D1 EC81F6EE B5F5C1E8 ACFB4B6B 76 | A046B99C 1098AF51 D617DC5A 45B6663E D7AE0FE9 AF2ACD8C E4F87D86 77 | 0F408B1C 42FD3318 6E4D2B52 E081794C 9932992E 74AB89B4 9E67AC8F 78 | AB7A64D1 D5B71813 9AD3F9A5 93C109D9 AC355CCE EE6FE7B2 279DA92F 79 | C6CE1AAF 65C75893 66EDCB34 1AB0ECE1 4C5E811A 2F1677B7 93D59172 80 | 2CFD2CB5 2F495A53 8637AA80 869D49D3 2E52D48F BF354822 6FD4A879 81 | 059887D5 A0AFCFB3 5A44A136 D44994A9 31BEB9CE 977BB29A 67DCB80B 82 | A9E66CEE 6E5B18BC 5A4091C4 B48FA653 3311A05A BEF706BD 03660BD7 83 | 786DC748 19FE16E9 9EC6C7CB 932DFD37 D714766E D81FF3B1 402797EB 84 | A0E7071C B86A9D7D 704625AB CA1AF488 A8B8C720 DD69F418 398BB1D1 85 | 43D477AA 2FC608F8 49346A75 B84627DC 99D7E45F C45F8300 DBD3D1FA 86 | D74D8378 4D881F3A 1FEBBA47 ACFD4785 1CFF7BBC 367410DB D302B6A5 87 | C1BB4F43 FA89B674", 88 | ) 89 | .unwrap(), 90 | k: srp.k, 91 | } 92 | } 93 | 94 | #[allow(non_snake_case)] 95 | pub fn stored_proof_verifier_from_step_2(username: UsernameRef) -> HandshakeProofVerifier { 96 | let user = lookup_user_details(username); 97 | let srp = Srp6_4096::default(); 98 | let B = PublicKey::from_hex_str_be( 99 | "305A710C 399CE43C 56D38BEE 8504193E 91242E95 CF16D0C8 4D377C6C 100 | 6A1484EC 70732FFC F469C9D4 612C8F95 1FB478F7 16BE3B18 1E9C1ABE 101 | 8B03B83E 542D3B92 F5C23DDA AEE847BE 7618427B 258882EE E7F99974 102 | BDE2DB8C 5E102129 79212BDC 4A3E49D2 99952839 E02A150E A9B08B24 103 | 6CDBB24B C8DA2CE4 2A742B9D 22FA95E6 A8A5A1EE D0071455 B863ED1B 104 | 68C94F40 88C1B0BA ACF58646 5B045D5B A1F75852 32321E08 D277D91F 105 | FBC24944 98C02249 534BCBB9 40C589D1 EC81F6EE B5F5C1E8 ACFB4B6B 106 | A046B99C 1098AF51 D617DC5A 45B6663E D7AE0FE9 AF2ACD8C E4F87D86 107 | 0F408B1C 42FD3318 6E4D2B52 E081794C 9932992E 74AB89B4 9E67AC8F 108 | AB7A64D1 D5B71813 9AD3F9A5 93C109D9 AC355CCE EE6FE7B2 279DA92F 109 | C6CE1AAF 65C75893 66EDCB34 1AB0ECE1 4C5E811A 2F1677B7 93D59172 110 | 2CFD2CB5 2F495A53 8637AA80 869D49D3 2E52D48F BF354822 6FD4A879 111 | 059887D5 A0AFCFB3 5A44A136 D44994A9 31BEB9CE 977BB29A 67DCB80B 112 | A9E66CEE 6E5B18BC 5A4091C4 B48FA653 3311A05A BEF706BD 03660BD7 113 | 786DC748 19FE16E9 9EC6C7CB 932DFD37 D714766E D81FF3B1 402797EB 114 | A0E7071C B86A9D7D 704625AB CA1AF488 A8B8C720 DD69F418 398BB1D1 115 | 43D477AA 2FC608F8 49346A75 B84627DC 99D7E45F C45F8300 DBD3D1FA 116 | D74D8378 4D881F3A 1FEBBA47 ACFD4785 1CFF7BBC 367410DB D302B6A5 117 | C1BB4F43 FA89B674", 118 | ) 119 | .unwrap(); 120 | 121 | let b = PrivateKey::from_hex_str_be( 122 | "AF36B579 309478E8 F550F937 5D8EAB43 07FC6E97 5717E872 218748E5 123 | 5DF1380E A04F975A 2F8E612D 81AA461D 2B162E52 48A8204B 7317268C 124 | B2CCD303 8BD03EED DB9A4728 6E99BC02 3EAD64CA D9CC0B51 0EE696E8 125 | 66D300C1 DC8C13C2 20CBFC6C 683C3E86 2BD30E31 E6C97AA3 371DDECD 126 | DBF36414 5A7749A2 E5CDF6ED B90EA14C B60BE165 AC2CA2E5 CDE4D5CE 127 | 8180A13A 7DC3D066 FA00EDE8 F5C4C601 83C664AB 67C026A6 03147B29 128 | 82B6405B D32900D5 C563B034 AFFE3761 B2638F00 411B8827 B6CE88C9 129 | E1F4109D 0DB479B8 642B2A9B FA03024A E8425C0B B6687A11 792DB92C 130 | B5C82920 03F3E45E EE7E52A4 FC499833 F3F35516 C42B97AA B86FA2B3 131 | 823852AA D2E5B24A A6839CEE 8D1C3F0C 82DBA198 9729A7BB 0FBD60D6 132 | DB067881 2E6AF5BE 735312BD C3D9B954 CD5BB490 3BB3DA56 433C32C4 133 | 492FA536 0419FB49 0189CDBC 422D69B8 695D9689 9A41D921 B476E59F 134 | DD7E0288 C524009E 001444DD 95825379 821811B5 3575587C B70D2C76 135 | 27A90570 E1BA3A57 061C0586 C7A504EF 63FA0A75 7C130B6E C29C2513 136 | D5065890 796CFAA3 A58FCE11 BE5D2825 AF27D72C 1617EB49 6A06655A 137 | DE69A6CD B8A8BD93 40E6E4B8 2BE898E2 4CCE4153 10C879B3 782C5A9B 138 | C8134FA4 3C91FE57 8696F6B4 E0D43BE4 78DBA7EE 7EF19C70 A4FE9B23 139 | 5940722E C75C2E32 5F17AE8E D00A773F 8A45CF06 37A05057 8E6BCC43 140 | 428DADDA D9658C5E", 141 | ) 142 | .unwrap(); 143 | 144 | HandshakeProofVerifier { 145 | server_keys: (B, b), 146 | user, 147 | g: srp.g, 148 | N: srp.N, 149 | } 150 | } 151 | 152 | pub fn alice_proof() -> HandshakeProof<{ Srp6_4096::KEY_LEN }, { Srp6_4096::SALT_LEN }> { 153 | HandshakeProof { 154 | A: PublicKey::from_hex_str_be( 155 | "5F1B7060 47346324 30A8A030 18CD56FE 9F837360 52F4D079 D8625A58 156 | 3E490541 4282F002 C3CB2883 765A7FD8 8B5C37D9 7D74554C B0A580FA 157 | 77F834E7 3D89DB9D B7AF622C F2E232B5 D6E4FE96 E7C0754D 6D7B4376 158 | 36250E22 168FBCE8 098ABAD8 21CF0248 3031A312 622B5998 CE2CC179 159 | 1A07DA7A 0E34AFB0 922BA1B6 CB4AE38F 77F08627 077DC11B A495FC56 160 | BC1E43DE A0F3360D 1FEE7299 C4753EF3 68B2D6AC 1EECBB90 42471CB6 161 | 18C22D1C 2EBCEA18 4054E485 4FBDC049 EBA62829 64222744 44A465BB 162 | B797B99A BFAF96B0 653B26CD 65A3E7C7 6E24BB2A E9083546 384B3444 163 | 34A1FC79 3488D4CC A6453B87 F840A42D BFFE37C6 F62CD694 63274D87 164 | EE4807A2 31EA65AA 326A2C3C 4B8EBC90 6255D274 A3B80C6B 126CB078 165 | B4C9CB5E 50E24A4D B2805F5F B1CECB14 8D717910 0B2E24CA 0511565E 166 | 5BC68C7A FA1660C8 6F7C70E1 0BEC8012 393297F9 5882B8A4 16FCDD47 167 | B3B42E65 EA6DFD74 0FCC09D6 FEA8970F 36227A9E 007CA4D2 1C71729D 168 | 7FB1797E 226694A6 99B03F11 2C80EAF8 C0DE37FC 9482C3DE 7104D3AC 169 | 88818D56 AC16A9B0 55B60CE9 CD7834AE 1A555C7D 90F06825 E5005A27 170 | 8CF005A0 CEFB7556 6C53B083 8EF09D2E AC8BAAE5 357EA99B 3D7432E8 171 | 6058852A C8E1B3B1 D20DFD42 3DFA1EB3 E2AFFC14 A9352871 D7F68439 172 | 8C9D1563 37266C6D BD496BC1 BC1978BC 7E9A6E8D 6D0A5CB7 812A7B4D 173 | FDA0DC79 C48FFC12", 174 | ) 175 | .unwrap(), 176 | M1: Proof::from_hex_str_be( 177 | "EA8614A8 EE5C4B79 938C4E50 A3031376 1069895C 3EE1B66E 7E35D679 178 | F9258E74 3C530738 DBB5B3DF 221E73CE 1385B20E 2B85FD36 B90F73D4 179 | AB809D95 E3678C12", 180 | ) 181 | .unwrap(), 182 | } 183 | } 184 | 185 | pub fn strong_proof_verifier_from_step_3() -> StrongProofVerifier<{ Srp6_4096::KEY_LEN }> { 186 | let alice_proof = alice_proof(); 187 | 188 | StrongProofVerifier { 189 | A: alice_proof.A, 190 | K: SessionKey::from_hex_str_be( 191 | "DC5923B7 43FC8512 8AEE0096 D5EB3756 E4FB7EB0 405CEC8A AB4D3AA7 192 | 5F7CDDA8 344D0132 FC87F10D 3495C867 E10898AE CBD6C343 5F2A3221 193 | 3228BF96 72FACBBD DD042649 C4C037DE F5FDB3E3 B55EA3FA 92BE9ED9 194 | 0F8B29A5 7308130F B541BA79 A286223F 70DCA470 EC7E00C3 3F958DDF 195 | 9C4AA12B 66A35E53 85840EBC 6B6BA448", 196 | ) 197 | .unwrap(), 198 | M1: alice_proof.M1, 199 | } 200 | } 201 | 202 | pub fn strong_proof_from_the_server() -> StrongProof { 203 | let proof_verifier = stored_proof_verifier_from_step_2("alice"); 204 | let proof_from_alice = alice_proof(); 205 | 206 | let strong_proof = proof_verifier.verify_proof(&proof_from_alice); 207 | let (strong_proof, _session_key_server) = strong_proof.unwrap(); 208 | 209 | strong_proof 210 | } 211 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::prelude::*; 4 | 5 | #[derive(Error, Debug, PartialEq)] 6 | pub enum Srp6Error { 7 | #[error( 8 | "The provided key length ({given:?} byte) does not match the expected ({expected:?} byte)" 9 | )] 10 | KeyLengthMismatch { given: usize, expected: usize }, 11 | 12 | #[error("The provided proof is invalid")] 13 | InvalidProof(Proof), 14 | 15 | #[error("The provided strong proof is invalid")] 16 | InvalidStrongProof(StrongProof), 17 | 18 | #[error("The provided public key is invalid")] 19 | InvalidPublicKey(PublicKey), 20 | } 21 | -------------------------------------------------------------------------------- /src/hash/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "hash-sha1", feature = "hash-sha512"))] 2 | compile_error!("either feature `hash-sha1` or `hash-sha512` is used, not both"); 3 | 4 | #[cfg(all(not(feature = "hash-sha1"), not(feature = "hash-sha512")))] 5 | compile_error!("either feature `hash-sha1` or `hash-sha512` must be used"); 6 | 7 | #[cfg(feature = "hash-sha1")] 8 | mod sha1; 9 | #[cfg(feature = "hash-sha1")] 10 | pub use sha1::*; 11 | 12 | #[cfg(feature = "hash-sha512")] 13 | mod sha512; 14 | #[cfg(feature = "hash-sha512")] 15 | pub use sha512::*; 16 | 17 | pub type Hash = [u8; HASH_LENGTH]; 18 | pub use HASH_LENGTH; 19 | -------------------------------------------------------------------------------- /src/hash/sha1.rs: -------------------------------------------------------------------------------- 1 | pub use sha1::{digest::Update, Digest}; 2 | 3 | use crate::big_number::BigNumber; 4 | 5 | pub const HASH_LENGTH: usize = 20; 6 | pub type HashFunc = sha1::Sha1; 7 | 8 | /// sha1 hash function 9 | /// Caution: sha1 is cryptographically broken and should not be used for secure applications 10 | pub fn hash_w_pad(a: &BigNumber, b: &BigNumber) -> BigNumber { 11 | BigNumber::from_bytes_be( 12 | HashFunc::new() 13 | .chain(a.to_array_pad_zero::()) 14 | .chain(b.to_array_pad_zero::()) 15 | .finalize() 16 | .as_slice(), 17 | ) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use hex_literal::hex; 23 | use std::convert::TryFrom; 24 | 25 | use super::*; 26 | use crate::prelude::PublicKey; 27 | 28 | #[test] 29 | #[allow(non_snake_case)] 30 | /// u = H(A, B) 31 | fn should_hash_2_big_numbers_with_sha1() { 32 | let A = PublicKey::try_from( 33 | "61D5E490 F6F1B795 47B0704C 436F523D D0E560F0 C64115BB 72557EC4 34 | 4352E890 3211C046 92272D8B 2D1A5358 A2CF1B6E 0BFCF99F 921530EC 35 | 8E393561 79EAE45E 42BA92AE ACED8251 71E1E8B9 AF6D9C03 E1327F44 36 | BE087EF0 6530E69F 66615261 EEF54073 CA11CF58 58F0EDFD FE15EFEA 37 | B349EF5D 76988A36 72FAC47B 0769447B", 38 | ) 39 | .unwrap(); 40 | 41 | let B = PublicKey::try_from( 42 | "BD0C6151 2C692C0C B6D041FA 01BB152D 4916A1E7 7AF46AE1 05393011 43 | BAF38964 DC46A067 0DD125B9 5A981652 236F99D9 B681CBF8 7837EC99 44 | 6C6DA044 53728610 D0C6DDB5 8B318885 D7D82C7F 8DEB75CE 7BD4FBAA 45 | 37089E6F 9C6059F3 88838E7A 00030B33 1EB76840 910440B1 B27AAEAE 46 | EB4012B7 D7665238 A8E3FB00 4B117B58", 47 | ) 48 | .unwrap(); 49 | 50 | // 128 bytes from the 1024 bit N section of appendix A 51 | let u = hash_w_pad::<128>(&A, &B); 52 | let exp_hash = hex!("CE38B959 3487DA98 554ED47D 70A7AE5F 462EF019"); 53 | 54 | assert_eq!(u.to_vec(), exp_hash); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/hash/sha512.rs: -------------------------------------------------------------------------------- 1 | pub use sha2::{digest::Update, Digest}; 2 | 3 | use crate::big_number::BigNumber; 4 | 5 | pub const HASH_LENGTH: usize = 512 / 8; 6 | pub type HashFunc = sha2::Sha512; 7 | 8 | /// sha512 hash function 9 | /// Caution: sha1 is cryptographically broken and should not be used for secure applications 10 | pub fn hash_w_pad(a: &BigNumber, b: &BigNumber) -> BigNumber { 11 | BigNumber::from_bytes_be( 12 | HashFunc::new() 13 | .chain(a.to_array_pad_zero::()) 14 | .chain(b.to_array_pad_zero::()) 15 | .finalize() 16 | .as_slice(), 17 | ) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use std::convert::TryFrom; 23 | 24 | use hex_literal::hex; 25 | 26 | use super::*; 27 | use crate::prelude::PublicKey; 28 | 29 | #[test] 30 | #[allow(non_snake_case)] 31 | /// u = H(A, B) 32 | fn should_hash_2_big_numbers_with_sha512() { 33 | let A = PublicKey::try_from( 34 | "61D5E490 F6F1B795 47B0704C 436F523D D0E560F0 C64115BB 72557EC4 35 | 4352E890 3211C046 92272D8B 2D1A5358 A2CF1B6E 0BFCF99F 921530EC 36 | 8E393561 79EAE45E 42BA92AE ACED8251 71E1E8B9 AF6D9C03 E1327F44 37 | BE087EF0 6530E69F 66615261 EEF54073 CA11CF58 58F0EDFD FE15EFEA 38 | B349EF5D 76988A36 72FAC47B 0769447B", 39 | ) 40 | .unwrap(); 41 | 42 | let B = PublicKey::try_from( 43 | "BD0C6151 2C692C0C B6D041FA 01BB152D 4916A1E7 7AF46AE1 05393011 44 | BAF38964 DC46A067 0DD125B9 5A981652 236F99D9 B681CBF8 7837EC99 45 | 6C6DA044 53728610 D0C6DDB5 8B318885 D7D82C7F 8DEB75CE 7BD4FBAA 46 | 37089E6F 9C6059F3 88838E7A 00030B33 1EB76840 910440B1 B27AAEAE 47 | EB4012B7 D7665238 A8E3FB00 4B117B58", 48 | ) 49 | .unwrap(); 50 | 51 | // 128 bytes from the 1024 bit N section of appendix A 52 | let u = hash_w_pad::<128>(&A, &B); 53 | let exp_hash = hex!( 54 | "3112C8B58EB9326827D201366C00B174AE045313816D62CB110C8178462E20453F47408F5BDA1B1BB23CDE16BD74C8AF07279E0972149FAB3266F6AA713D155C"); 55 | 56 | assert_eq!(u.to_vec(), exp_hash, "u was not calculated correctly: {u}",); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! # An implementation of Secure Remote Password (SRP6) authentication protocol. 3 | //! 4 | //! **NOTE**: Please do only use key length >= 2048 bit in production. 5 | //! You can do so by using [`Srp6_2048`] or [`Srp6_4096`] or related. 6 | //! 7 | //! ## Usage 8 | //! 9 | //! The usage example start on the server side. 10 | //! Client side interaction is marked explicit when needed. 11 | //! 12 | //! ### 1. A new user, welcome Alice 13 | //! 14 | //! ```rust 15 | //! use srp6::prelude::*; 16 | //! 17 | //! // this is happening on the client, 18 | //! // the password is never send to the server at any time 19 | //! let new_username = Username::from("alice"); 20 | //! let user_password = ClearTextPassword::from("password123"); 21 | //! 22 | //! let (salt_s, verifier_v) = Srp6_4096::default() 23 | //! .generate_new_user_secrets( 24 | //! &new_username, 25 | //! &user_password 26 | //! ); 27 | //! 28 | //! assert_eq!(salt_s.num_bytes(), Srp6_4096::KEY_LEN); 29 | //! assert_eq!(verifier_v.num_bytes(), Srp6_4096::KEY_LEN); 30 | //! 31 | //! // The server needs to persist, 32 | //! // `new_username`, `salt_s` and `verifier_v` in a user database / pw file 33 | //! ``` 34 | //! **NOTE:** the password of the user will not be stored! 35 | //! 36 | //! **NOTE2:** the salt and verifier will never be the same, they have a random component to it 37 | //! 38 | //! ### 2. A session [`Handshake`] for Alice 39 | //! 40 | //! On the server side (when alice is already registered) 41 | //! 42 | //! - when a user/client connects they would send their [`Username`] first 43 | //! - with the username the server will lookup their [`Salt`] and [`PasswordVerifier`] from a user database or pw file 44 | //! - the server starts the authentication process with a [`Handshake`] send to the client 45 | //! - the server keeps a [`HandshakeProofVerifier`] for the user in order to verify the proof he will get from the client later on 46 | //! 47 | //! ```rust 48 | //! use srp6::prelude::*; 49 | //! #[path = "doc_test_mocks.rs"] 50 | //! mod mocks; 51 | //! 52 | //! // the username is sent by the client 53 | //! let user = mocks::lookup_user_details("alice"); 54 | //! 55 | //! // the server starts the handshake 56 | //! let srp = Srp6_4096::default(); 57 | //! let (handshake, proof_verifier) = srp.start_handshake(&user); 58 | //! 59 | //! assert_eq!(handshake.s, user.salt); 60 | //! assert_eq!(handshake.N, srp.N); 61 | //! assert_eq!(handshake.g, srp.g); 62 | //! assert_eq!(handshake.B.num_bytes(), Srp6_4096::KEY_LEN); 63 | //! 64 | //! // send `handshake` to the client 65 | //! // keep `proof_verifier` for later in a session or cache 66 | //! ``` 67 | //! 68 | //! ### 3. A [`Proof`] that Alice is Alice 69 | //! 70 | //! - with the handshake, alice needs to create [`Proof`] that she is who she says she is 71 | //! - this [`Proof`] and her [`PublicKey`] will be sent to the server where it is verified 72 | //! 73 | //! ```rust 74 | //! use srp6::prelude::*; 75 | //! #[path = "doc_test_mocks.rs"] 76 | //! mod mocks; 77 | //! 78 | //! // this is entered by the user on the client (none is sent to the server) 79 | //! let username = "alice"; 80 | //! let password = "password123"; 81 | //! 82 | //! // this comes from the server 83 | //! let handshake = mocks::handshake_from_the_server(username); 84 | //! 85 | //! // the final proof calculation 86 | //! let (proof, strong_proof_verifier) = handshake 87 | //! .calculate_proof(username, password) 88 | //! .unwrap(); 89 | //! 90 | //! // send this `proof` to the server 91 | //! // `strong_proof_verifier` is kept for the final verification 92 | //! ``` 93 | //! 94 | //! ### 4. Verify [`Proof`] from Alice 95 | //! 96 | //! - The client sends the proof ([`HandshakeProof`]) to the server 97 | //! - The server calculates their version of the Proof and compoares if they match 98 | //! - On Success both parties have calculated a strong proof ([`StrongProof`] M2) and a session key ([`StrongSessionKey`] K) 99 | //! 100 | //! ```rust 101 | //! use srp6::prelude::*; 102 | //! #[path = "doc_test_mocks.rs"] 103 | //! mod mocks; 104 | //! 105 | //! // this comes from the server 106 | //! let username = "alice"; 107 | //! let proof_verifier = mocks::stored_proof_verifier_from_step_2(username); 108 | //! let proof_from_alice = mocks::alice_proof(); 109 | //! 110 | //! // the server verifies the proof from alice 111 | //! let (strong_proof, session_key_server) = proof_verifier 112 | //! .verify_proof(&proof_from_alice) 113 | //! .expect("proof was invalid"); 114 | //! 115 | //! // `strong_proof` is sent back to alice 116 | //! ``` 117 | //! 118 | //! ### 5. Alice verifies the server 119 | //! 120 | //! - The client receivs the strong proof ([`StrongProof`] K) from the server 121 | //! - Alice calculates their own strong proof and verifies the both match 122 | //! - On Success both parties have verified each other and have a shared strong proof ([`StrongProof`] M2) and a session key ([`StrongSessionKey`] K) 123 | //! 124 | //! ```rust 125 | //! use srp6::prelude::*; 126 | //! #[path = "doc_test_mocks.rs"] 127 | //! mod mocks; 128 | //! 129 | //! // see the previous step.. 130 | //! let strong_proof_verifier = mocks::strong_proof_verifier_from_step_3(); 131 | //! let strong_proof = mocks::strong_proof_from_the_server(); 132 | //! 133 | //! // alice verifies the proof from the server 134 | //! strong_proof_verifier 135 | //! .verify_strong_proof(&strong_proof) 136 | //! .expect("strong proof was invalid"); 137 | //! ``` 138 | //! 139 | //! ## Note on key length 140 | //! 141 | //! this crate provides some default keys [preconfigured and aliased][defaults]. 142 | //! The modulus prime and genrator numbers are taken from [RFC5054](https://datatracker.ietf.org/doc/html/rfc5054). 143 | //! 144 | //! ## Note on hash length 145 | //! 146 | //! The original RFC5054 uses SHA1 as the hash function. This crate uses SHA512 as the default hash function. Because SHA1 is considered weak, it is recommended to use newer versions of the SHA family. The hash length is 64 bytes for SHA512 instead of 20 bytes for SHA1. If you really need to use SHA1, you can use the `dangerous` feature. 147 | //! 148 | //! ## Further details and domain vocabolary 149 | //! - You can find the documentation of SRP6 [variables in a dedicated module][`protocol_details`]. 150 | //! - [RFC2945](https://datatracker.ietf.org/doc/html/rfc2945) that describes in detail the Secure remote password protocol (SRP). 151 | //! - [RFC5054](https://datatracker.ietf.org/doc/html/rfc5054) that describes SRP6 for TLS Authentication 152 | //! - [check out the 2 examples](./examples) that illustrates the srp authentication flow as well 153 | 154 | pub mod defaults; 155 | pub mod hash; 156 | pub mod protocol_details; 157 | pub mod rfc_lingo; 158 | pub mod prelude { 159 | pub use crate::api::host::*; 160 | pub use crate::api::user::*; 161 | pub use crate::big_number::BigNumber; 162 | pub use crate::defaults::*; 163 | pub use crate::error::Srp6Error; 164 | pub use crate::hash::HASH_LENGTH; 165 | pub use crate::primitives::*; 166 | pub use std::convert::TryInto; 167 | } 168 | #[cfg(feature = "dangerous")] 169 | pub mod dangerous; 170 | pub mod rfc_5054_appendix_a; 171 | #[cfg(all(test, feature = "test-rfc-5054-appendix-b"))] 172 | pub mod rfc_5054_appendix_b; 173 | 174 | // #[cfg(any(test, doc, doctest, docsrs, feature = "doc-test-mocks"))] 175 | // pub mod doc_test_mocks; 176 | 177 | mod api; 178 | mod big_number; 179 | mod error; 180 | mod primitives; 181 | 182 | // // TODO: remove this, in favor of the prelude module 183 | // pub use api::host::*; 184 | // pub use api::user::*; 185 | // pub use defaults::*; 186 | // pub use primitives::{ 187 | // ClearTextPassword, Generator, MultiplierParameter, PasswordVerifier, PrimeModulus, PrivateKey, 188 | // Proof, PublicKey, Salt, SessionKey, StrongProof, StrongSessionKey, UserCredentials, 189 | // UserSecrets, Username, UsernameRef, 190 | // }; 191 | // pub use std::convert::TryInto; 192 | 193 | /// encapsulates a [`crate::error::Srp6Error`] 194 | pub type Result = std::result::Result; 195 | 196 | pub use api::host::*; 197 | pub use api::user::*; 198 | pub use defaults::*; 199 | pub use primitives::*; 200 | -------------------------------------------------------------------------------- /src/primitives.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module defines a list of all primitive types and functions 3 | needed to express the meaning of certain variables better. 4 | 5 | For instance in [RFC2945] the big prime number that acts 6 | as the modulus in every mathematical power operation is called `N`. 7 | 8 | In order to increase readability the type of `N` is 9 | an alias to [`BigNumber`] that aims to express the meaning, 10 | so [`PrimeModulus`] is same as `N` which is a [`BigNumber`]. 11 | 12 | This scheme is applied for all variables used in the calculus. 13 | 14 | [RFC2945]: https://datatracker.ietf.org/doc/html/rfc2945 15 | */ 16 | use crate::big_number::{BigNumber, Zero}; 17 | use crate::hash::*; 18 | use crate::{error::Srp6Error, Result}; 19 | 20 | const STRONG_SESSION_KEY_LENGTH: usize = HASH_LENGTH * 2; 21 | 22 | /// Refers to a large safe prime called `N` (`N = 2q+1`, where `q` is prime) 23 | #[doc(alias = "N")] 24 | pub type PrimeModulus = BigNumber; 25 | 26 | /// Refers to the modulus generator `g` 27 | #[doc(alias = "g")] 28 | pub type Generator = BigNumber; 29 | 30 | /// Refers to a User's salt called `s` 31 | #[doc(alias = "s")] 32 | pub type Salt = BigNumber; 33 | 34 | /// Refers to a Public shared key called A (user), B (server) 35 | #[doc(alias("A", "B"))] 36 | pub type PublicKey = BigNumber; 37 | 38 | /// Refers to a private secret random number a (user), b (server) 39 | #[doc(alias("a", "b", "x"))] 40 | pub type PrivateKey = BigNumber; 41 | 42 | /// A pair of [`PublicKey`] B and [`PrivateKey`] b 43 | pub type KeyPair = (PublicKey, PrivateKey); 44 | 45 | /// Password Verifier is the users secret on the server side 46 | #[doc(alias = "v")] 47 | pub type PasswordVerifier = BigNumber; 48 | 49 | /// Refers to a multiplier parameter `k` (k = H(N, g) in SRP-6a, k = 3 for dangerous SRP-6) 50 | #[doc(alias = "k")] 51 | pub type MultiplierParameter = BigNumber; 52 | 53 | /// Refers to the SessionKey `S` 54 | #[doc(alias = "S")] 55 | pub type SessionKey = BigNumber; 56 | /// Refers to the StrongSessionKey `K` 57 | #[doc(alias = "K")] 58 | pub type StrongSessionKey = BigNumber; 59 | 60 | /// Refers to `M` and `M1` Proof of server and client 61 | #[doc(alias("M", "M1"))] 62 | pub type Proof = BigNumber; 63 | /// Refers to `M2` the hash of Proof 64 | #[doc(alias = "M2")] 65 | pub type StrongProof = BigNumber; 66 | 67 | /// Username `I` as [`String`] 68 | #[doc(alias = "I")] 69 | pub type Username = String; 70 | /// Username reference `I` as [`&str`] 71 | pub type UsernameRef<'a> = &'a str; 72 | 73 | /// Clear text password `p` as [`str`] 74 | #[doc(alias("P", "p"))] 75 | pub type ClearTextPassword = String; 76 | pub type ClearTextPasswordRef<'a> = &'a str; 77 | 78 | // u = Hash(A, B) 79 | // #[doc(alias = "u")] 80 | // pub type Hash = BigLNumber; 81 | 82 | /// [`Username`] and [`ClearTextPassword`] used on the client side 83 | #[derive(Debug, Clone)] 84 | pub struct UserCredentials<'a> { 85 | pub username: UsernameRef<'a>, 86 | pub password: ClearTextPasswordRef<'a>, 87 | } 88 | 89 | /// User details composes [`Username`], [`Salt`] and [`PasswordVerifier`] in one struct 90 | #[derive(Debug, Clone)] 91 | pub struct UserSecrets { 92 | pub username: Username, 93 | pub salt: Salt, 94 | pub verifier: PasswordVerifier, 95 | } 96 | 97 | /// host version of a session key for a given user 98 | /// S: is the session key of a user 99 | /// u: is the hash of user and server pub keys 100 | /// 101 | /// u = H(A, B) 102 | /// S = (Av^u) ^ b 103 | #[allow(non_snake_case)] 104 | pub fn calculate_session_key_S_for_host( 105 | N: &PrimeModulus, 106 | A: &PublicKey, 107 | B: &PublicKey, 108 | b: &PrivateKey, 109 | v: &PasswordVerifier, 110 | ) -> Result { 111 | // safeguard A % N == 0 should be checked 112 | if (A % N).is_zero() { 113 | return Err(Srp6Error::InvalidPublicKey(A.clone())); 114 | } 115 | 116 | let u = &calculate_u::(A, B); 117 | let base = &(A * &v.modpow(u, N)); 118 | let S: BigNumber = base.modpow(b, N); 119 | 120 | Ok(S) 121 | } 122 | 123 | /// client version of the session key calculation, depends on 124 | /// - the users [`PrivateKey`] `x` 125 | /// - the users [`PublicKey`] `A` 126 | /// - the servers [`PublicKey`] `B` 127 | /// - formulas found so far: 128 | /// - `S = (B - (k * g^x)) ^ (a + (u * x)) % N` 129 | /// - `S = (B - (k * v)) ^ (a + (u * x)) % N` 130 | #[allow(non_snake_case)] 131 | #[allow(clippy::many_single_char_names)] 132 | pub fn calculate_session_key_S_for_client( 133 | N: &PrimeModulus, 134 | k: &MultiplierParameter, 135 | g: &Generator, 136 | B: &PublicKey, 137 | A: &PublicKey, 138 | a: &PrivateKey, 139 | x: &PrivateKey, 140 | ) -> Result { 141 | // safeguard B % N == 0 142 | if (B % N).is_zero() { 143 | return Err(Srp6Error::InvalidPublicKey(B.clone())); 144 | } 145 | 146 | let u = &calculate_u::(A, B); 147 | let exp: BigNumber = a + &(u * x); 148 | let g_mod_x = &g.modpow(x, N); 149 | let base = B - &(k * g_mod_x); 150 | let S = base.modpow(&exp, N); 151 | 152 | Ok(S) 153 | } 154 | 155 | /// the hash of a session key `S` that is called `K` 156 | /// S: is the session key of a user 157 | /// K: is the hash of S, just not that straight 158 | #[allow(non_snake_case)] 159 | pub fn calculate_session_key_hash_interleave_K( 160 | S: &SessionKey, 161 | ) -> StrongSessionKey { 162 | let S = S.to_array_pad_zero::(); 163 | 164 | // take the even bytes out of S 165 | let n = S.len() / 2; 166 | let mut half = vec![0; n]; 167 | for (i, Si) in S.iter().step_by(2).enumerate() { 168 | half[i] = *Si; 169 | } 170 | // hash the even portion of S 171 | let even_half_of_S_hash = HashFunc::new().chain(&half[..n]).finalize(); 172 | 173 | // take the odd bytes of S 174 | for (i, Si) in S.iter().skip(1).step_by(2).enumerate() { 175 | half[i] = *Si; 176 | } 177 | // hash the odd portion of S 178 | let odd_half_of_S_hash = HashFunc::new().chain(&half[..n]).finalize(); 179 | 180 | let mut vK = [0_u8; STRONG_SESSION_KEY_LENGTH]; 181 | for (i, h_Si) in even_half_of_S_hash 182 | .iter() 183 | .zip(odd_half_of_S_hash.iter()) 184 | .enumerate() 185 | { 186 | vK[i * 2] = *h_Si.0; 187 | vK[i * 2 + 1] = *h_Si.1; 188 | } 189 | 190 | StrongSessionKey::from_bytes_be(&vK) 191 | } 192 | 193 | #[allow(non_snake_case)] 194 | pub fn calculate_proof_M( 195 | N: &PrimeModulus, 196 | g: &Generator, 197 | I: UsernameRef, 198 | s: &Salt, 199 | A: &PublicKey, 200 | B: &PublicKey, 201 | K: &StrongSessionKey, 202 | ) -> Proof { 203 | let xor_hash: Hash = calculate_hash_N_xor_g::(N, g); 204 | let username_hash = HashFunc::new().chain(I.as_bytes()).finalize(); 205 | 206 | let M = Proof::from_bytes_be( 207 | HashFunc::new() 208 | .chain(xor_hash) 209 | .chain(username_hash) 210 | .chain(s.to_array_pad_zero::()) 211 | .chain(A.to_array_pad_zero::()) 212 | .chain(B.to_array_pad_zero::()) 213 | .chain(K.to_array_pad_zero::()) 214 | .finalize() 215 | .as_slice(), 216 | ); 217 | 218 | M 219 | } 220 | 221 | /// todo(verify): check if padding is needed or not 222 | /// formula: `H(A | M | K)` 223 | #[allow(non_snake_case)] 224 | pub fn calculate_strong_proof_M2( 225 | A: &PublicKey, 226 | M: &Proof, 227 | K: &StrongSessionKey, 228 | ) -> StrongProof { 229 | let M2: StrongProof = HashFunc::new() 230 | .chain(A.to_array_pad_zero::()) 231 | .chain(M.to_array_pad_zero::()) 232 | .chain(K.to_array_pad_zero::()) 233 | .into(); 234 | 235 | M2 236 | } 237 | 238 | /// here we hash g and xor it with the hash of N 239 | /// 240 | /// ```plain 241 | /// M = H(H(N) xor H(g), H(I), s, A, B, K) 242 | /// ````````````` 243 | /// // this portion is calculated here 244 | /// ``` 245 | #[allow(non_snake_case)] 246 | fn calculate_hash_N_xor_g(N: &PrimeModulus, g: &Generator) -> Hash { 247 | let mut h = HashFunc::new() 248 | .chain(N.to_array_pad_zero::()) 249 | .finalize(); 250 | let h_g = HashFunc::new().chain(g.to_vec().as_slice()).finalize(); 251 | for (i, v) in h.iter_mut().enumerate() { 252 | *v ^= h_g[i]; 253 | } 254 | 255 | let H_n_g: Hash = h.into(); 256 | 257 | H_n_g 258 | } 259 | 260 | /// here we calculate the `PasswordVerifier` called `v` based on `x` 261 | /// **Note**: something that only needs to be done on user pw change, or user creation 262 | /// `x`: Private key (derived from p and s) 263 | /// `v`: Password verifier 264 | /// `g`: A generator modulo N 265 | /// `N`: A large safe prime (N = 2q+1, where q is prime) 266 | /// formula: `v = g^x % N` 267 | #[allow(non_snake_case)] 268 | pub fn calculate_password_verifier_v( 269 | N: &PrimeModulus, 270 | g: &Generator, 271 | x: &PrivateKey, 272 | ) -> PasswordVerifier { 273 | g.modpow(x, N) 274 | } 275 | 276 | /// [`../rfc_lingo::u`] is the hash of host's [`PublicKey`][`../rfc_lingo::A`] and client's [`PublicKey`][`../rfc_lingo::B`] 277 | /// formula: `H(PAD(A) | PAD(B))` 278 | /// The Padding is based on the key length, e.g. if N is 1024 bit, the padding is 128 bytes 279 | #[allow(non_snake_case)] 280 | pub fn calculate_u(A: &PublicKey, B: &PublicKey) -> BigNumber { 281 | hash_w_pad::(A, B) 282 | } 283 | 284 | /// [`../rfc_lingo::A`] is the [`PublicKey`] of the client 285 | /// formula: `A = g^a % N` 286 | #[allow(non_snake_case)] 287 | pub fn calculate_pubkey_A(N: &PrimeModulus, g: &Generator, a: &PrivateKey) -> PublicKey { 288 | g.modpow(a, N) 289 | } 290 | 291 | /// [`PublicKey`][`../rfc_lingo::B`] is the hosts public key 292 | /// `B = kv + g^b` 293 | #[allow(non_snake_case)] 294 | pub fn calculate_pubkey_B( 295 | N: &PrimeModulus, 296 | k: &MultiplierParameter, 297 | g: &Generator, 298 | v: &PasswordVerifier, 299 | b: &PrivateKey, 300 | ) -> PublicKey { 301 | let g_mod_N = g.modpow(b, N); 302 | // This is B 303 | &((k * v) + g_mod_N) % N 304 | } 305 | 306 | /// `x` is the users private key (only they know) 307 | /// 308 | /// I: Username (is uppercased for WoW) 309 | /// p: Cleartext Password (is uppercased for WoW) 310 | /// s: User's salt 311 | /// x: Private key (derived from p and s) 312 | /// ph = H(I, ':', p) (':' is a string literal) 313 | /// x = H(s, ph) (s is chosen randomly) 314 | #[allow(non_snake_case)] 315 | #[allow(dead_code)] 316 | pub fn calculate_private_key_x(I: UsernameRef, p: ClearTextPasswordRef, s: &Salt) -> PrivateKey { 317 | let ph = calculate_p_hash(I, p); 318 | 319 | PrivateKey::from_bytes_be( 320 | HashFunc::new() 321 | .chain(s.to_vec()) 322 | .chain(ph) 323 | .finalize() 324 | .as_slice(), 325 | ) 326 | } 327 | 328 | /// hashes the user and the password (used for client private key `x`) 329 | #[allow(non_snake_case)] 330 | #[cfg(not(feature = "wow"))] 331 | pub fn calculate_p_hash(I: UsernameRef, p: ClearTextPasswordRef) -> Hash { 332 | HashFunc::new() 333 | .chain(I.as_bytes()) 334 | .chain(":".as_bytes()) 335 | .chain(p.as_bytes()) 336 | .finalize() 337 | .into() 338 | } 339 | 340 | /// hashes the user and the password (used for client private key `x`) 341 | /// WoW flavoured (upper cased user and password) 342 | #[allow(non_snake_case)] 343 | #[cfg(feature = "wow")] 344 | pub fn calculate_p_hash(I: UsernameRef, p: ClearTextPasswordRef) -> Hash { 345 | HashFunc::new() 346 | .chain(I.to_uppercase().as_bytes()) 347 | .chain(":".as_bytes()) 348 | .chain(p.to_uppercase().as_bytes()) 349 | .finalize() 350 | .into() 351 | } 352 | 353 | /// `k = H(N | PAD(g))` (k = 3 for dangerous SRP-6) 354 | #[allow(non_snake_case)] 355 | #[cfg(not(feature = "wow"))] 356 | pub fn calculate_k( 357 | N: &PrimeModulus, 358 | g: &Generator, 359 | ) -> MultiplierParameter { 360 | HashFunc::new() 361 | .chain(N.to_vec().as_slice()) 362 | .chain(g.to_array_pad_zero::()) 363 | .into() 364 | } 365 | 366 | /// `k = H(N | PAD(g))` (k = 3 for dangerous SRP-6) 367 | #[cfg(feature = "wow")] 368 | pub fn calculate_k( 369 | _: &PrimeModulus, 370 | _: &Generator, 371 | ) -> MultiplierParameter { 372 | MultiplierParameter::from(3) 373 | } 374 | 375 | /// [`PrivateKey`] `a` or `b` is in fact just a big (positive) random number 376 | pub fn generate_private_key() -> PrivateKey { 377 | PrivateKey::new_rand(KEY_LENGTH) 378 | } 379 | 380 | /// [`Salt`] `s` is a random number 381 | pub fn generate_salt() -> Salt { 382 | Salt::new_rand(SALT_LENGTH) 383 | } 384 | -------------------------------------------------------------------------------- /src/protocol_details.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A very brief summary of the papers and RFCs of SRP6 and SRP6a 3 | 4 | ## SRP Vocabulary 5 | 6 | ```plain 7 | N A large safe prime (N = 2q+1, where q is prime) 8 | All arithmetic is done modulo N. 9 | g A generator modulo N 10 | k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for dangerous SRP-6) 11 | s User's salt 12 | I Username (the rfc calls it U) 13 | p Cleartext Password 14 | H() One-way hash function 15 | ^ (Modular) Exponentiation 16 | u Random scrambling parameter 17 | a,b Secret ephemeral values 18 | A,B Public ephemeral values 19 | x Private key (derived from p and s) 20 | v Password verifier 21 | S Session key 22 | K Strong session key (SHA1 interleaved) 23 | M Proof (calculated by the server) 24 | M1 Proof provided by the client 25 | ``` 26 | 27 | ## SRP Formulas 28 | 29 | Calculations by the client: 30 | ```plain 31 | I, p = 32 | N, g, s, B = 33 | a = random() 34 | A = g^a % N 35 | u = SHA1(PAD(A) | PAD(B)) 36 | k = SHA1(N | PAD(g)) (k = 3 for dangerous SRP-6) 37 | x = SHA1(s | SHA1(I | ":" | p)) 38 | S = (B - (k * g^x)) ^ (a + (u * x)) % N 39 | K = SHA_Interleave(S) 40 | M = H(H(N) XOR H(g) | H(U) | s | A | B | K) 41 | ``` 42 | 43 | Calculations by the server: 44 | ```plain 45 | N, g, s, v = 46 | v = g^x % N 47 | b = random() 48 | k = SHA1(N | PAD(g)) 49 | B = k*v + g^b % N 50 | A = 51 | u = SHA1(PAD(A) | PAD(B)) 52 | S = (A * v^u) ^ b % N 53 | K = SHA_Interleave(S) 54 | 55 | H(A | M | K) 56 | ``` 57 | 58 | ## Safeguards 59 | 1. The user will abort if he receives one of 60 | - `B mod N == 0` 61 | - `u == 0` 62 | 2. The host will abort if it detects that `A mod N == 0`. 63 | 3. The user must show his proof of `K` first. If the server detects that the user's proof is incorrect, it must abort without showing its own proof of `K`. 64 | 65 | ## References 66 | - [EKE](https://en.wikipedia.org/wiki/Encrypted_key_exchange) 67 | - [papers](http://srp.stanford.edu/doc.html#papers) 68 | - [design](http://srp.stanford.edu/design.html) 69 | - [rfc](https://datatracker.ietf.org/doc/html/rfc2945) 70 | - [vetted N](https://datatracker.ietf.org/doc/html/rfc5054#appendix-A) 71 | - [test vectors](https://datatracker.ietf.org/doc/html/rfc5054#appendix-B) 72 | */ 73 | -------------------------------------------------------------------------------- /src/rfc_5054_appendix_a.rs: -------------------------------------------------------------------------------- 1 | /// from https://datatracker.ietf.org/doc/html/rfc5054#appendix-A 2 | /// 3 | /// > The 1024-, 1536-, and 2048-bit groups are taken from software 4 | /// > developed by Tom Wu and Eugene Jhong for the Stanford SRP 5 | /// > distribution, and subsequently proven to be prime. The larger primes 6 | /// > are taken from [MODP], but generators have been calculated that are 7 | /// > primitive roots of N, unlike the generators in [MODP]. 8 | /// > 9 | /// > [MODP] Kivinen, T. and M. Kojo, "More Modular Exponentiation 10 | /// > (MODP) Diffie-Hellman groups for Internet Key Exchange 11 | /// > (IKE)", RFC 3526, May 2003. 12 | use crate::prelude::*; 13 | 14 | #[allow(non_snake_case)] 15 | pub mod group_1024_bit { 16 | use super::*; 17 | 18 | pub type Srp6_1024 = Srp6<128, 128>; 19 | 20 | pub fn values() -> Srp6_1024 { 21 | Srp6::new(g(), N()).unwrap() 22 | } 23 | 24 | /// 1024-bit group from [RFC5054 Appendix A](https://datatracker.ietf.org/doc/html/rfc5054#appendix-A) 25 | /// The hexadecimal representation value for the prime 26 | pub fn N() -> PrimeModulus { 27 | PrimeModulus::from_hex_str_be( 28 | "EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C 29 | 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 30 | 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 31 | 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A 32 | FD5138FE 8376435B 9FC61D2F C0EB06E3", 33 | ) 34 | .unwrap() 35 | } 36 | 37 | /// The generator for the 1024-bit group 38 | pub fn g() -> Generator { 39 | Generator::from(2) 40 | } 41 | } 42 | 43 | #[allow(non_snake_case)] 44 | pub mod group_1536_bit { 45 | use super::*; 46 | 47 | pub type Srp6_1536 = Srp6<192, 192>; 48 | 49 | pub fn values() -> Srp6_1536 { 50 | Srp6::new(g(), N()).unwrap() 51 | } 52 | 53 | /// 1024-bit group from [RFC5054 Appendix A](https://datatracker.ietf.org/doc/html/rfc5054#appendix-A) 54 | /// The hexadecimal representation value for the prime 55 | pub fn N() -> PrimeModulus { 56 | PrimeModulus::from_hex_str_be( 57 | "EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C 58 | 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 59 | 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 60 | 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A 61 | FD5138FE 8376435B 9FC61D2F C0EB06E3", 62 | ) 63 | .unwrap() 64 | } 65 | 66 | /// The generator for the 1024-bit group 67 | pub fn g() -> Generator { 68 | Generator::from(2) 69 | } 70 | } 71 | 72 | #[allow(non_snake_case)] 73 | pub mod group_2048_bit { 74 | use super::*; 75 | 76 | pub type Srp6_2048 = Srp6<256, 256>; 77 | 78 | pub fn values() -> Srp6_2048 { 79 | Srp6::new(g(), N()).unwrap() 80 | } 81 | 82 | pub fn N() -> PrimeModulus { 83 | PrimeModulus::from_hex_str_be( 84 | "AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 85 | 3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D 86 | CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB 87 | D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74 88 | 7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A 89 | 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D 90 | 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73 91 | 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6 92 | 94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F 93 | 9E4AFF73", 94 | ) 95 | .unwrap() 96 | } 97 | 98 | pub fn g() -> Generator { 99 | Generator::from(2) 100 | } 101 | } 102 | 103 | #[allow(non_snake_case)] 104 | pub mod group_3072_bit { 105 | pub use super::*; 106 | 107 | pub type Srp6_3072 = Srp6<384, 384>; 108 | 109 | pub fn values() -> Srp6_3072 { 110 | Srp6::new(g(), N()).unwrap() 111 | } 112 | 113 | pub fn N() -> PrimeModulus { 114 | PrimeModulus::from_hex_str_be( 115 | "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 116 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 117 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 118 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 119 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 120 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 121 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 122 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 123 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 124 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 125 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 126 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 127 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 128 | E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF", 129 | ) 130 | .unwrap() 131 | } 132 | 133 | pub fn g() -> Generator { 134 | Generator::from(5) 135 | } 136 | } 137 | 138 | #[allow(non_snake_case)] 139 | pub mod group_4096_bit { 140 | pub use super::*; 141 | 142 | pub type Srp6_4096 = Srp6<512, 512>; 143 | 144 | pub fn values() -> Srp6_4096 { 145 | Srp6::new(g(), N()).unwrap() 146 | } 147 | 148 | pub fn N() -> PrimeModulus { 149 | PrimeModulus::from_hex_str_be( 150 | "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 151 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 152 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 153 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 154 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 155 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 156 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 157 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 158 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 159 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 160 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 161 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 162 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 163 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 164 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 165 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 166 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 167 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 168 | FFFFFFFF FFFFFFFF", 169 | ) 170 | .unwrap() 171 | } 172 | 173 | pub fn g() -> Generator { 174 | Generator::from(5) 175 | } 176 | } 177 | 178 | #[allow(non_snake_case)] 179 | pub mod group_6144_bit { 180 | pub use super::*; 181 | 182 | pub type Srp6_6144 = Srp6<768, 768>; 183 | 184 | pub fn values() -> Srp6_6144 { 185 | Srp6::new(g(), N()).unwrap() 186 | } 187 | 188 | pub fn N() -> PrimeModulus { 189 | PrimeModulus::from_hex_str_be( 190 | "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 191 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 192 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 193 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 194 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 195 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 196 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 197 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 198 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 199 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 200 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 201 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 202 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 203 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 204 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 205 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 206 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 207 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 208 | 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 209 | AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 210 | DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 211 | 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 212 | F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F 213 | BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA 214 | CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B 215 | B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 216 | 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E 217 | 6DCC4024 FFFFFFFF FFFFFFFF", 218 | ) 219 | .unwrap() 220 | } 221 | 222 | pub fn g() -> Generator { 223 | Generator::from(5) 224 | } 225 | } 226 | 227 | #[allow(non_snake_case)] 228 | pub mod group_8192_bit { 229 | pub use super::*; 230 | 231 | pub type Srp6_8192 = Srp6<1024, 1024>; 232 | 233 | pub fn values() -> Srp6_8192 { 234 | Srp6::new(g(), N()).unwrap() 235 | } 236 | 237 | pub fn N() -> PrimeModulus { 238 | PrimeModulus::from_hex_str_be( 239 | "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 240 | 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 241 | 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 242 | A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 243 | 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 244 | FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 245 | 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 246 | 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 247 | 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 248 | 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D 249 | B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 250 | 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C 251 | BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC 252 | E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 253 | 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB 254 | 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 255 | 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 256 | D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 257 | 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 258 | AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 259 | DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 260 | 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 261 | F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F 262 | BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA 263 | CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B 264 | B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 265 | 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E 266 | 6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA 267 | 3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 268 | 5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9 269 | 22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886 270 | 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6 271 | 6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5 272 | 0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268 273 | 359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6 274 | FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71 275 | 60C980DD 98EDD3DF FFFFFFFF FFFFFFFF", 276 | ) 277 | .unwrap() 278 | } 279 | 280 | pub fn g() -> Generator { 281 | Generator::from(19) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/rfc_5054_appendix_b.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::prelude::*; 4 | use crate::rfc_5054_appendix_a::group_1024_bit; 5 | 6 | /// This test is based on the test vectors from [RFC5054 Appendix B](https://datatracker.ietf.org/doc/html/rfc5054#appendix-B) 7 | #[allow(non_snake_case)] 8 | #[test] 9 | fn test_appendix_b_srp_test_vectors() { 10 | /// this is all based on sha1, that is why the length is 32 11 | const N_BYTE_LEN: usize = 1024 / 8; 12 | 13 | let I = Username::from("alice"); 14 | let P = ClearTextPassword::from("password123"); 15 | let s = Salt::from_hex_str_be("BEB25379 D1A8581E B5A72767 3A2441EE").unwrap(); 16 | const SALT_LENGTH: usize = 16; 17 | assert_eq!(SALT_LENGTH, s.num_bytes(), "Salt length is not correct"); 18 | 19 | let N = group_1024_bit::N(); 20 | let g = group_1024_bit::g(); 21 | let srp = group_1024_bit::values(); 22 | 23 | let k = MultiplierParameter::from_hex_str_be("7556AA04 5AEF2CDD 07ABAF0F 665C3E81 8913186F") 24 | .unwrap(); 25 | let x = PrivateKey::from_hex_str_be("94B7555A ABE9127C C58CCF49 93DB6CF8 4D16C124").unwrap(); 26 | let v = PasswordVerifier::from_hex_str_be( 27 | "7E273DE8 696FFC4F 4E337D05 B4B375BE B0DDE156 9E8FA00A 9886D812 28 | 9BADA1F1 822223CA 1A605B53 0E379BA4 729FDC59 F105B478 7E5186F5 29 | C671085A 1447B52A 48CF1970 B4FB6F84 00BBF4CE BFBB1681 52E08AB5 30 | EA53D15C 1AFF87B2 B9DA6E04 E058AD51 CC72BFC9 033B564E 26480D78 31 | E955A5E2 9E7AB245 DB2BE315 E2099AFB", 32 | ) 33 | .unwrap(); 34 | 35 | assert_eq!( 36 | calculate_private_key_x(&I, &P, &s), 37 | x, 38 | "Private key x was not calculated correctly" 39 | ); 40 | 41 | assert_eq!( 42 | calculate_password_verifier_v(&N, &g, &x), 43 | v, 44 | "Password verifiert v was not calculated correctly" 45 | ); 46 | 47 | let a = PrivateKey::try_from( 48 | "60975527 035CF2AD 1989806F 0407210B C81EDC04 E2762A56 AFD529DD 49 | DA2D4393", 50 | ) 51 | .unwrap(); 52 | 53 | let A = PublicKey::try_from( 54 | "61D5E490 F6F1B795 47B0704C 436F523D D0E560F0 C64115BB 72557EC4 55 | 4352E890 3211C046 92272D8B 2D1A5358 A2CF1B6E 0BFCF99F 921530EC 56 | 8E393561 79EAE45E 42BA92AE ACED8251 71E1E8B9 AF6D9C03 E1327F44 57 | BE087EF0 6530E69F 66615261 EEF54073 CA11CF58 58F0EDFD FE15EFEA 58 | B349EF5D 76988A36 72FAC47B 0769447B", 59 | ) 60 | .unwrap(); 61 | 62 | assert_eq!( 63 | calculate_pubkey_A(&N, &g, &a), 64 | A, 65 | "Public key A was not calculated correctly" 66 | ); 67 | 68 | let b = PrivateKey::try_from( 69 | "E487CB59 D31AC550 471E81F0 0F6928E0 1DDA08E9 74A004F4 9E61F5D1 70 | 05284D20", 71 | ) 72 | .unwrap(); 73 | 74 | let B = PublicKey::try_from( 75 | "BD0C6151 2C692C0C B6D041FA 01BB152D 4916A1E7 7AF46AE1 05393011 76 | BAF38964 DC46A067 0DD125B9 5A981652 236F99D9 B681CBF8 7837EC99 77 | 6C6DA044 53728610 D0C6DDB5 8B318885 D7D82C7F 8DEB75CE 7BD4FBAA 78 | 37089E6F 9C6059F3 88838E7A 00030B33 1EB76840 910440B1 B27AAEAE 79 | EB4012B7 D7665238 A8E3FB00 4B117B58", 80 | ) 81 | .unwrap(); 82 | 83 | assert_eq!( 84 | calculate_pubkey_B(&N, &k, &g, &v, &b), 85 | B, 86 | "Public key B was not calculated correctly" 87 | ); 88 | 89 | let u = hex_literal::hex!("CE38B959 3487DA98 554ED47D 70A7AE5F 462EF019"); 90 | 91 | assert_eq!( 92 | calculate_u::(&A, &B).to_vec(), 93 | u, 94 | "u was not calculated correctly" 95 | ); 96 | 97 | let premaster_secret = BigNumber::try_from( 98 | "B0DC82BA BCF30674 AE450C02 87745E79 90A3381F 63B387AA F271A10D 99 | 233861E3 59B48220 F7C4693C 9AE12B0A 6F67809F 0876E2D0 13800D6C 100 | 41BB59B6 D5979B5C 00A172B4 A2A5903A 0BDCAF8A 709585EB 2AFAFA8F 101 | 3499B200 210DCC1F 10EB3394 3CD67FC8 8A2F39A4 BE5BEC4E C0A3212D 102 | C346D7E4 74B29EDE 8A469FFE CA686E5A", 103 | ) 104 | .unwrap(); 105 | 106 | let session_key_host = 107 | calculate_session_key_S_for_host::(&N, &A, &B, &b, &v).unwrap(); 108 | assert_eq!( 109 | session_key_host, premaster_secret, 110 | "Session key host is not correct" 111 | ); 112 | 113 | let session_key_client = 114 | calculate_session_key_S_for_client::(&N, &k, &g, &B, &A, &a, &x).unwrap(); 115 | assert_eq!( 116 | session_key_client, premaster_secret, 117 | "Session key client is not correct" 118 | ); 119 | 120 | let (proof, strong_proof_verifier) = calculate_proof_M_for_client::( 121 | &Handshake:: { B, g, N, k, s }, 122 | &UserCredentials { 123 | username: &I, 124 | password: &P, 125 | }, 126 | ) 127 | .unwrap(); 128 | } 129 | -------------------------------------------------------------------------------- /src/rfc_lingo.rs: -------------------------------------------------------------------------------- 1 | /// This module exposes the protocol types in the lingo of the RFC. 2 | /// For example 3 | /// - `Username` is referred to as `I` 4 | /// - `ClearTextPassword` is referred to as `P` 5 | /// - `Salt` is referred to as `s` 6 | use crate::primitives::*; 7 | 8 | pub type N = PrimeModulus; 9 | 10 | #[allow(non_camel_case_types)] 11 | pub type g = Generator; 12 | 13 | pub type A = PublicKey; 14 | 15 | pub type B = PublicKey; 16 | 17 | #[allow(non_camel_case_types)] 18 | pub type a = PrivateKey; 19 | 20 | #[allow(non_camel_case_types)] 21 | pub type b = PrivateKey; 22 | 23 | #[allow(non_camel_case_types)] 24 | pub type x = PrivateKey; 25 | 26 | #[allow(non_camel_case_types)] 27 | pub type v = PasswordVerifier; 28 | 29 | #[allow(non_camel_case_types)] 30 | pub type k = MultiplierParameter; 31 | 32 | pub type S = SessionKey; 33 | pub type K = StrongSessionKey; 34 | 35 | pub type M1 = Proof; 36 | pub type M = Proof; 37 | 38 | pub type M2 = StrongProof; 39 | 40 | pub type I = Username; 41 | pub type P = ClearTextPassword; 42 | --------------------------------------------------------------------------------