From 88696763cc320a909f72c81c95e2024e1ceb142e Mon Sep 17 00:00:00 2001 From: Kevin Lewi Date: Fri, 5 Jun 2020 09:35:14 -0700 Subject: [PATCH] Initial commit --- .github/workflows/main.yml | 34 ++ .gitignore | 5 + CHANGELOG.md | 5 + CODE_OF_CONDUCT.md | 76 +++ CONTRIBUTING.md | 30 ++ Cargo.lock | 479 +++++++++++++++++++ Cargo.toml | 30 ++ LICENSE | 21 + README.md | 51 ++ deny.toml | 189 ++++++++ src/errors.rs | 123 +++++ src/group.rs | 160 +++++++ src/key_exchange.rs | 406 ++++++++++++++++ src/keypair.rs | 283 +++++++++++ src/lib.rs | 336 +++++++++++++ src/opaque.rs | 918 ++++++++++++++++++++++++++++++++++++ src/oprf.rs | 133 ++++++ src/rkr_encryption.rs | 202 ++++++++ src/tests/mock_rng.rs | 63 +++ src/tests/mod.rs | 8 + src/tests/opaque_ke_test.rs | 577 ++++++++++++++++++++++ src/tests/serialization.rs | 124 +++++ 22 files changed, 4253 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deny.toml create mode 100644 src/errors.rs create mode 100644 src/group.rs create mode 100644 src/key_exchange.rs create mode 100644 src/keypair.rs create mode 100644 src/lib.rs create mode 100644 src/opaque.rs create mode 100644 src/oprf.rs create mode 100644 src/rkr_encryption.rs create mode 100644 src/tests/mock_rng.rs create mode 100644 src/tests/mod.rs create mode 100644 src/tests/opaque_ke_test.rs create mode 100644 src/tests/serialization.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..04508a3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Rust CI +on: [push] +jobs: + combo: + name: test + Clippy + rustfmt + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c53126 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vs +.vscode/ +src/.DS_Store +/target +**/*.rs.bk diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..60f4647 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 (June 5, 2020) + +* Initial release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f049d4c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cf89f14 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing to this library +We want to make contributing to this project as easy and transparent as +possible. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe +disclosure of security bugs. In those cases, please go through the process +outlined on that page and do not file a public issue. + +## License +By contributing to Ristretto255.js, you agree that your contributions will be +licensed under the LICENSE file in the root directory of this source tree. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f74806e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,479 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aead" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aes" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aes-gcm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ghash 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aes-soft" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aesni" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-cipher-trait" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chacha20" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chacha20poly1305" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chacha20 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "poly1305 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curve25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ghash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "polyval 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "hkdf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hmac" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "opaque-ke" +version = "0.1.0" +dependencies = [ + "aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "aes-gcm 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chacha20poly1305 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "poly1305" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "universal-hash 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "polyval" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "universal-hash 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "stream-cipher" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "subtle" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thiserror" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thiserror-impl 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "universal-hash" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "x25519-dalek" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "zeroize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum aead 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4cf01b9b56e767bb57b94ebf91a58b338002963785cdd7013e21c0d4679471e4" +"checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" +"checksum aes-gcm 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "834a6bda386024dbb7c8fc51322856c10ffe69559f972261c868485f5759c638" +"checksum aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" +"checksum aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +"checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" +"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chacha20 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bea8b86bdf2f2b18a0f28fbfed740ee395e6ba1785b4b7123c021172eaab8ef9" +"checksum chacha20poly1305 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "48901293601228db2131606f741db33561f7576b5d19c99cd66222380a7dc863" +"checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +"checksum curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" +"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +"checksum ghash 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f0930ed19a7184089ea46d2fedead2f6dc2b674c5db4276b7da336c7cd83252" +"checksum hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +"checksum hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" +"checksum hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +"checksum poly1305 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5829f50f48e9ddb79f3f7c3097029d0caee30f8286accb241416df603b080b8" +"checksum polyval 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ec3341498978de3bfd12d1b22f1af1de22818f5473a11e8a6ef997989e3a212" +"checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +"checksum serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)" = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" +"checksum serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +"checksum sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" +"checksum stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" +"checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +"checksum subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" +"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" +"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +"checksum thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ee14bf8e6767ab4c687c9e8bc003879e042a96fd67a3ba5934eadb6536bef4db" +"checksum thiserror-impl 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a7b51e1fbc44b5a0840be594fbc0f960be09050f2617e61e6aa43bef97cd3ef4" +"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum universal-hash 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df0c900f2f9b4116803415878ff48b63da9edb268668e08cf9292d7503114a01" +"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..700c711 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "opaque-ke" +version = "0.1.0" +repository = "https://github.com/novifinancial/opaque-ke" +keywords = ["cryptography", "crypto", "opaque", "passwords", "authentication"] +description = "An implementation of the OPAQUE password-authenticated key exchange protocol" +authors = ["Kevin Lewi ", "François Garillot "] +license = "MIT" +edition = "2018" +readme = "README.md" + +[dependencies] +aead = "0.2.0" +curve25519-dalek = "2.0.0" +generic-array = "0.12.3" +hkdf = "0.8.0" +hmac = "0.7.1" +rand_core = "0.5.1" +sha2 = "0.8" +thiserror = "1" +x25519-dalek = "0.6.0" +zeroize = "1.1" + +[dev-dependencies] +aes-gcm = "0.5.0" +base64 = "0.11.0" +chacha20poly1305 = "0.4.1" +hex = "0.4.2" +lazy_static = "1.4.0" +serde_json = "1.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b96dcb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6b8d9f --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +## The OPAQUE key exchange protocol + +[OPAQUE](https://eprint.iacr.org/2018/163.pdf) is an asymmetric password-authenticated key exchange protocol. It allows a client to authenticate to a server using a password, without ever having to expose the plaintext password to the server. + +This implementation is based on the [Internet Draft for OPAQUE](https://github.com/cfrg/draft-irtf-cfrg-opaque). + +Background +---------- + +Asymmetric Password Authenticated Key Exchange (aPAKE) protocols are designed to provide password authentication and mutually authenticated key exchange without relying on PKI (except during user/password registration) and without disclosing passwords to servers or other entities other than the client machine. + +OPAQUE is a PKI-free aPAKE that is secure against pre-computation attacks and capable of using a secret salt. + +Documentation +------------- + +The API can be found [here](https://docs.rs/opaque-ke/) along with an example for usage. + +Installation +------------ + +Add the following line to the dependencies of your `Cargo.toml`: + +``` +opaque-ke = "0.1.0" +``` + +Resources +--------- + +- [OPAQUE academic publication](https://eprint.iacr.org/2018/163.pdf), including formal definitions and a proof of security +- [draft-krawczyk-cfrg-opaque-05](https://tools.ietf.org/html/draft-krawczyk-cfrg-opaque-05), containing a specification for the OPAQUE protocol +- ["Let's talk about PAKE"](https://blog.cryptographyengineering.com/2018/10/19/lets-talk-about-pake/), an introductory blog post written by Matthew Green that covers OPAQUE + +Contributors +------------ + +The authors of this code are Kevin Lewi +([@kevinlewi](https://github.com/kevinlewi)) and François Garillot ([@huitseeker](https://github.com/huitseeker)). +To learn more about contributing to this project, [see this document](./CONTRIBUTING.md). + +#### Acknowledgments + +Special thanks go to Hugo Krawczyk for helping to clarify discrepancies and making suggestions for improving +this implementation. + + +License +------- + +This project is [MIT licensed](./LICENSE). diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..af1e513 --- /dev/null +++ b/deny.toml @@ -0,0 +1,189 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url of the advisory database to use +db-url = "https://github.com/rustsec/advisory-db" +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + #"MIT", + #"Apache-2.0", + #"Apache-2.0 WITH LLVM-exception", +] +# List of explictly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "allow" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# THe optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..636955b --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,123 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! A list of error types which are produced during an execution of the protocol + +use thiserror::Error; + +/// Represents an error in the manipulation of internal cryptographic data +#[derive(Debug, Error)] +pub enum InternalPakeError { + #[error("Invalid length for {name}: expected {len}, but is actually {actual_len}.")] + SizeError { + name: &'static str, + len: usize, + actual_len: usize, + }, + #[error("Could not decompress point.")] + PointError, + #[error("Key belongs to a small subgroup!")] + SubGroupError, + #[error("hashing to a key failed")] + HashingFailure, + #[error("Computing HKDF failed while deriving subkeys")] + HkdfError, + #[error("Computing HMAC failed while supplying a secret key")] + HmacError, +} + +/// Represents an error in password checking +#[derive(Debug, Error)] +pub enum PakeError { + /// This error results from an internal error during PRF construction + /// + #[error("Internal error during PRF verification: {0}")] + CryptoError(InternalPakeError), + /// This error occurs when the symmetric encryption fails + #[error("Symmetric encryption failed.")] + EncryptionError, + /// This error occurs when the symmetric decryption fails + #[error("Symmetric decryption failed.")] + DecryptionError, + /// This error occurs when the symmetric decryption's hmac check fails + #[error("HMAC check in symmetric decryption failed.")] + DecryptionHmacError, + /// This error occurs when the server object that is being called finish() on is malformed + #[error("Incomplete set of keys passed into finish() function")] + IncompleteKeysError, + #[error("The provided server public key doesn't match the encrypted one")] + IncompatibleServerStaticPublicKeyError, + #[error("Error in key exchange protocol when attempting to validate MACs")] + KeyExchangeMacValidationError, + #[error("Error in validating credentials")] + InvalidLoginError, +} + +// This is meant to express future(ly) non-trivial ways of converting the +// internal error into a PakeError +impl From for PakeError { + fn from(e: InternalPakeError) -> PakeError { + PakeError::CryptoError(e) + } +} + +/// Represents an error in protocol handling +#[derive(Debug, Error)] +pub enum ProtocolError { + /// This error results from an error during password verification + /// + #[error("Internal error during password verification: {0}")] + VerificationError(PakeError), + /// This error occurs when the server answer cannot be handled + #[error("Server response cannot be handled.")] + ServerError, + /// This error occurs when the client request cannot be handled + #[error("Client request cannot be handled.")] + ClientError, +} + +// This is meant to express future(ly) non-trivial ways of converting the +// Pake error into a ProtocolError +impl From for ProtocolError { + fn from(e: PakeError) -> ProtocolError { + ProtocolError::VerificationError(e) + } +} + +// This is meant to express future(ly) non-trivial ways of converting the +// internal error into a ProtocolError +impl From for ProtocolError { + fn from(e: InternalPakeError) -> ProtocolError { + ProtocolError::VerificationError(e.into()) + } +} + +// See https://github.com/rust-lang/rust/issues/64715 and remove this when +// merged, and https://github.com/dtolnay/thiserror/issues/62 for why this +// comes up in our doc tests. +impl From<::std::convert::Infallible> for ProtocolError { + fn from(_: ::std::convert::Infallible) -> Self { + unreachable!() + } +} + +pub(crate) mod utils { + use super::*; + + pub fn check_slice_size<'a>( + slice: &'a [u8], + expected_len: usize, + arg_name: &'static str, + ) -> Result<&'a [u8], InternalPakeError> { + if slice.len() != expected_len { + return Err(InternalPakeError::SizeError { + name: arg_name, + len: expected_len, + actual_len: slice.len(), + }); + } + Ok(slice) + } +} diff --git a/src/group.rs b/src/group.rs new file mode 100644 index 0000000..0666853 --- /dev/null +++ b/src/group.rs @@ -0,0 +1,160 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::errors::InternalPakeError; + +use curve25519_dalek::{ + edwards::{CompressedEdwardsY, EdwardsPoint}, + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, +}; +use generic_array::{ + typenum::{U32, U64}, + ArrayLength, GenericArray, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use std::ops::Mul; +use zeroize::Zeroize; + +/// A prime-order subgroup of a base field (EC, prime-order field ...). This +/// subgroup is noted additively — as in the draft RFC — in this trait. +pub trait Group: Sized + for<'a> Mul<&'a ::Scalar, Output = Self> { + /// The type of base field scalars + type Scalar: Zeroize; + /// The byte length necessary to represent scalars + type ScalarLen: ArrayLength; + /// Return a scalat from its fixed-length bytes representation + fn from_scalar_slice( + scalar_bits: &GenericArray, + ) -> Result; + /// picks a scalar at random + fn random_scalar(rng: &mut R) -> Self::Scalar; + /// Serializes a scalar to bytes + fn scalar_as_bytes(scalar: &Self::Scalar) -> &GenericArray; + /// The multiplicative inverse of this scalar + fn scalar_invert(scalar: &Self::Scalar) -> Self::Scalar; + + /// The byte length necessary to represent group elements + type ElemLen: ArrayLength; + /// Return an element from its fixed-length bytes representation + fn from_element_slice( + element_bits: &GenericArray, + ) -> Result; + /// Serializes the `self` group element + fn to_bytes(&self) -> GenericArray; + + /// Hashes points presumed to be uniformly random to the curve. The + /// impl is allowed to perform additional hashes if it needs to, but this + /// may not be necessary as this function is going to be called with the + /// output of a kdf. + type UniformBytesLen: ArrayLength; + fn hash_to_curve(uniform_bytes: &GenericArray) -> Self; +} + +/// The implementation of such a subgroup for Ristretto +impl Group for RistrettoPoint { + type Scalar = Scalar; + type ScalarLen = U32; + fn from_scalar_slice( + scalar_bits: &GenericArray, + ) -> Result { + let mut bits = [0u8; 32]; + bits.copy_from_slice(scalar_bits); + Ok(Scalar::from_bytes_mod_order(bits)) + } + fn random_scalar(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + fn scalar_as_bytes(scalar: &Self::Scalar) -> &GenericArray { + GenericArray::from_slice(scalar.as_bytes()) + } + fn scalar_invert(scalar: &Self::Scalar) -> Self::Scalar { + scalar.invert() + } + + // The byte length necessary to represent group elements + type ElemLen = U32; + fn from_element_slice( + element_bits: &GenericArray, + ) -> Result { + CompressedRistretto::from_slice(element_bits) + .decompress() + .ok_or_else(|| InternalPakeError::PointError) + } + // serialization of a group element + fn to_bytes(&self) -> GenericArray { + let c = self.compress(); + *GenericArray::from_slice(c.as_bytes()) + } + + type UniformBytesLen = U64; + fn hash_to_curve(uniform_bytes: &GenericArray) -> Self { + let mut bits = [0u8; 64]; + bits.copy_from_slice(uniform_bytes); + // This could really be a from_uniform_bytes! + RistrettoPoint::hash_from_bytes::(&bits) + } +} + +/// The implementation of such a subgroup for points on the large Curve25519-subgroup +impl Group for EdwardsPoint { + type Scalar = Scalar; + type ScalarLen = U32; + fn from_scalar_slice( + scalar_bits: &GenericArray, + ) -> Result { + let mut bits = [0u8; 32]; + bits.copy_from_slice(scalar_bits); + Ok(Scalar::from_bytes_mod_order(bits)) + } + fn random_scalar(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + fn scalar_as_bytes(scalar: &Self::Scalar) -> &GenericArray { + GenericArray::from_slice(scalar.as_bytes()) + } + fn scalar_invert(scalar: &Self::Scalar) -> Self::Scalar { + scalar.invert() + } + + // The byte length necessary to represent group elements + type ElemLen = U32; + fn from_element_slice( + element_bits: &GenericArray, + ) -> Result { + CompressedEdwardsY::from_slice(element_bits) + .decompress() + .ok_or_else(|| InternalPakeError::PointError) + } + // serialization of a group element + fn to_bytes(&self) -> GenericArray { + let c = self.compress(); + *GenericArray::from_slice(c.as_bytes()) + } + + type UniformBytesLen = U64; + fn hash_to_curve(uniform_bytes: &GenericArray) -> Self { + let mut result = [0u8; 32]; + let mut counter = 0; + let mut wrapped_point: Option = None; + + while wrapped_point.is_none() { + result.copy_from_slice( + &Sha256::new() + .chain(&uniform_bytes[..32]) + .chain(&[counter]) + .result()[..32], + ); + wrapped_point = CompressedEdwardsY::from_slice(&result).decompress(); + counter += 1; + } + + wrapped_point + .expect("guarded by loop exit condition") + .mul_by_cofactor() + } +} diff --git a/src/key_exchange.rs b/src/key_exchange.rs new file mode 100644 index 0000000..9a035a1 --- /dev/null +++ b/src/key_exchange.rs @@ -0,0 +1,406 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::{ + errors::{utils::check_slice_size, InternalPakeError, PakeError, ProtocolError}, + keypair::{Key, KeyPair, SizedBytes}, +}; +use generic_array::GenericArray; +use hkdf::Hkdf; +use hmac::{Hmac, Mac}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; +use std::convert::TryFrom; + +/// This module is a somewhat minimalistic implementation of a key Exchange +/// protocol based on 3DH. It assumes a pre-exchange has allowed client and +/// server to learn each other's static public key. +/// +/// This private module may undergo significant changes in the near term. + +const KEY_LEN: usize = 32; +pub(crate) const NONCE_LEN: usize = 32; +pub(crate) const KE1_STATE_LEN: usize = KEY_LEN + KEY_LEN + NONCE_LEN; +pub(crate) const KE2_MESSAGE_LEN: usize = NONCE_LEN + 2 * KEY_LEN; + +static STR_3DH: &[u8] = b"3DH keys"; + +pub(crate) struct KE1State { + client_e_sk: Key, + client_nonce: Vec, + hashed_l1: Vec, +} + +pub(crate) struct KE1Message { + pub(crate) client_nonce: Vec, + pub(crate) client_e_pk: Key, +} + +impl TryFrom<&[u8]> for KE1State { + type Error = ProtocolError; + + fn try_from(bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(bytes, KE1_STATE_LEN, "ke1_state")?; + + Ok(Self { + client_e_sk: Key::from_bytes(&checked_bytes[..KEY_LEN])?, + client_nonce: checked_bytes[KEY_LEN..KEY_LEN + NONCE_LEN].to_vec(), + hashed_l1: checked_bytes[KEY_LEN + NONCE_LEN..].to_vec(), + }) + } +} + +impl KE1State { + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + &self.client_e_sk.to_arr(), + &self.client_nonce[..], + &self.hashed_l1[..], + ] + .concat(); + output + } +} + +impl KE1Message { + pub fn to_bytes(&self) -> Vec { + [&self.client_nonce[..], &self.client_e_pk.to_arr()].concat() + } +} + +impl TryFrom<&[u8]> for KE1Message { + type Error = ProtocolError; + + fn try_from(ke1_message_bytes: &[u8]) -> Result { + let checked_bytes = + check_slice_size(ke1_message_bytes, NONCE_LEN + KEY_LEN, "ke1_message")?; + + Ok(Self { + client_nonce: checked_bytes[..NONCE_LEN].to_vec(), + client_e_pk: Key::from_bytes(&checked_bytes[NONCE_LEN..])?, + }) + } +} + +pub(crate) fn generate_ke1>( + l1_component: Vec, + rng: &mut R, +) -> Result<(KE1State, KE1Message), ProtocolError> { + let client_e_kp = KeyFormat::generate_random(rng)?; + let mut client_nonce = [0u8; NONCE_LEN]; + rng.fill_bytes(&mut client_nonce); + + let ke1_message = KE1Message { + client_nonce: client_nonce.to_vec(), + client_e_pk: client_e_kp.public().clone(), + }; + + let l1_data: Vec = [&l1_component[..], &ke1_message.to_bytes()].concat(); + let mut hasher = Sha256::new(); + hasher.input(&l1_data); + let hashed_l1 = hasher.result(); + + Ok(( + KE1State { + client_e_sk: client_e_kp.private().clone(), + client_nonce: client_nonce.to_vec(), + hashed_l1: hashed_l1.to_vec(), + }, + ke1_message, + )) +} + +pub(crate) struct KE2State { + km3: Vec, + hashed_transcript: Vec, + shared_secret: Vec, +} + +pub(crate) struct KE2Message { + server_nonce: Vec, + server_e_pk: Key, + mac: Vec, +} + +impl KE2State { + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + &self.km3[..], + &self.hashed_transcript[..], + &self.shared_secret[..], + ] + .concat(); + output + } +} + +impl TryFrom<&[u8]> for KE2State { + type Error = ProtocolError; + + fn try_from(ke1_message_bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(ke1_message_bytes, 3 * KEY_LEN, "ke2_state")?; + + Ok(Self { + km3: checked_bytes[..KEY_LEN].to_vec(), + hashed_transcript: checked_bytes[KEY_LEN..2 * KEY_LEN].to_vec(), + shared_secret: checked_bytes[2 * KEY_LEN..].to_vec(), + }) + } +} + +impl KE2Message { + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + &self.server_nonce[..], + &self.server_e_pk.to_arr(), + &self.mac[..], + ] + .concat(); + output + } +} + +impl TryFrom<&[u8]> for KE2Message { + type Error = ProtocolError; + + fn try_from(ke1_message_bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(ke1_message_bytes, KE2_MESSAGE_LEN, "ke2_message")?; + + Ok(Self { + server_nonce: checked_bytes[..NONCE_LEN].to_vec(), + server_e_pk: Key::from_bytes(&checked_bytes[NONCE_LEN..NONCE_LEN + KEY_LEN])?, + mac: checked_bytes[NONCE_LEN + KEY_LEN..].to_vec(), + }) + } +} + +// The triple of public and private components used in the 3DH computation +struct TripleDHComponents { + pk1: Key, + sk1: Key, + pk2: Key, + sk2: Key, + pk3: Key, + sk3: Key, +} + +// Consists of a shared secret, followed by two mac keys +type TripleDHDerivationResult = ( + GenericArray::OutputSize>, + GenericArray::OutputSize>, + GenericArray::OutputSize>, +); + +// Internal function which takes the public and private components of the client and server keypairs, along +// with some auxiliary metadata, to produce the shared secret and two MAC keys +fn derive_3dh_keys>( + dh: TripleDHComponents, + client_nonce: &[u8], + server_nonce: &[u8], + client_s_pk: KeyFormat::Repr, + server_s_pk: KeyFormat::Repr, +) -> Result { + let ikm: Vec = [ + &KeyFormat::diffie_hellman(dh.pk1, dh.sk1)[..], + &KeyFormat::diffie_hellman(dh.pk2, dh.sk2)[..], + &KeyFormat::diffie_hellman(dh.pk3, dh.sk3)[..], + ] + .concat(); + + let info: Vec = [ + STR_3DH, + &client_nonce, + &server_nonce, + &client_s_pk.to_arr(), + &server_s_pk.to_arr(), + ] + .concat(); + + const OUTPUT_SIZE: usize = 32; + let mut okm = [0u8; 3 * OUTPUT_SIZE]; + let h = Hkdf::::new(None, &ikm); + h.expand(&info, &mut okm) + .map_err(|_| InternalPakeError::HkdfError)?; + Ok(( + *GenericArray::from_slice(&okm[..OUTPUT_SIZE]), + *GenericArray::from_slice(&okm[OUTPUT_SIZE..2 * OUTPUT_SIZE]), + *GenericArray::from_slice(&okm[2 * OUTPUT_SIZE..]), + )) +} + +pub(crate) fn generate_ke2>( + rng: &mut R, + l1_bytes: Vec, + l2_bytes: Vec, + client_e_pk: KeyFormat::Repr, + client_s_pk: KeyFormat::Repr, + server_s_sk: KeyFormat::Repr, + client_nonce: Vec, +) -> Result<(KE2State, KE2Message), ProtocolError> { + let server_e_kp = KeyFormat::generate_random(rng)?; + let mut server_nonce = [0u8; NONCE_LEN]; + rng.fill_bytes(&mut server_nonce); + + let (shared_secret, km2, km3) = derive_3dh_keys::( + TripleDHComponents { + pk1: client_e_pk.clone(), + sk1: server_e_kp.private().clone(), + pk2: client_e_pk, + sk2: server_s_sk.clone(), + pk3: client_s_pk.clone(), + sk3: server_e_kp.private().clone(), + }, + &client_nonce, + &server_nonce, + client_s_pk, + KeyFormat::public_from_private(&server_s_sk), + )?; + + let mut hasher = Sha256::new(); + hasher.input(&l1_bytes); + let hashed_l1 = hasher.result(); + + let transcript2: Vec = [ + &hashed_l1[..], + &l2_bytes[..], + &server_nonce[..], + &server_e_kp.public().to_arr(), + ] + .concat(); + + let mut hasher2 = Sha256::new(); + hasher2.input(&transcript2); + let hashed_transcript = hasher2.result(); + + let mut mac = Hmac::::new_varkey(&km2).map_err(|_| InternalPakeError::HmacError)?; + mac.input(&hashed_transcript); + + Ok(( + KE2State { + km3: km3.to_vec(), + hashed_transcript: hashed_transcript.to_vec(), + shared_secret: shared_secret.to_vec(), + }, + KE2Message { + server_nonce: server_nonce.to_vec(), + server_e_pk: server_e_kp.public().clone(), + mac: mac.result().code().to_vec(), + }, + )) +} + +pub(crate) struct KE3State { + pub(crate) shared_secret: Vec, +} + +pub(crate) struct KE3Message { + mac: Vec, +} + +impl TryFrom<&[u8]> for KE3State { + type Error = ProtocolError; + + fn try_from(bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(bytes, KEY_LEN, "ke3_state")?; + + Ok(Self { + shared_secret: checked_bytes.to_vec(), + }) + } +} + +impl KE3Message { + pub fn to_bytes(&self) -> Vec { + self.mac.clone() + } +} + +impl TryFrom<&[u8]> for KE3Message { + type Error = ProtocolError; + + fn try_from(bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(bytes, KEY_LEN, "ke3_message")?; + + Ok(Self { + mac: checked_bytes.to_vec(), + }) + } +} + +pub(crate) fn generate_ke3>( + l2_component: Vec, + ke2_message: KE2Message, + ke1_state: &KE1State, + server_s_pk: KeyFormat::Repr, + client_s_sk: KeyFormat::Repr, +) -> Result<(KE3State, KE3Message), ProtocolError> { + let (shared_secret, km2, km3) = derive_3dh_keys::( + TripleDHComponents { + pk1: ke2_message.server_e_pk.clone(), + sk1: ke1_state.client_e_sk.clone(), + pk2: server_s_pk.clone(), + sk2: ke1_state.client_e_sk.clone(), + pk3: ke2_message.server_e_pk.clone(), + sk3: client_s_sk.clone(), + }, + &ke1_state.client_nonce, + &ke2_message.server_nonce, + KeyFormat::public_from_private(&client_s_sk), + server_s_pk, + )?; + + let transcript: Vec = [ + &ke1_state.hashed_l1[..], + &l2_component[..], + &ke2_message.server_nonce[..], + &ke2_message.server_e_pk[..], + ] + .concat(); + + let mut hasher = Sha256::new(); + hasher.input(&transcript); + let hashed_transcript = hasher.result(); + + let mut server_mac = + Hmac::::new_varkey(&km2).map_err(|_| InternalPakeError::HmacError)?; + server_mac.input(&hashed_transcript); + + if ke2_message.mac != server_mac.result().code().to_vec() { + return Err(ProtocolError::VerificationError( + PakeError::KeyExchangeMacValidationError, + )); + } + + let mut client_mac = + Hmac::::new_varkey(&km3).map_err(|_| InternalPakeError::HmacError)?; + client_mac.input(&hashed_transcript); + + Ok(( + KE3State { + shared_secret: shared_secret.to_vec(), + }, + KE3Message { + mac: client_mac.result().code().to_vec(), + }, + )) +} + +// Outputs a shared secret +pub(crate) fn finish_ke( + ke3_message: KE3Message, + ke2_state: &KE2State, +) -> Result, ProtocolError> { + let mut client_mac = + Hmac::::new_varkey(&ke2_state.km3).map_err(|_| InternalPakeError::HmacError)?; + client_mac.input(&ke2_state.hashed_transcript); + + if ke3_message.mac != client_mac.result().code().to_vec() { + return Err(ProtocolError::VerificationError( + PakeError::KeyExchangeMacValidationError, + )); + } + + Ok(ke2_state.shared_secret.to_vec()) +} diff --git a/src/keypair.rs b/src/keypair.rs new file mode 100644 index 0000000..825a2e4 --- /dev/null +++ b/src/keypair.rs @@ -0,0 +1,283 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Contains the keypair types that must be supplied for the OPAQUE API + +use crate::errors::{utils::check_slice_size, InternalPakeError}; +use generic_array::{ + sequence::Concat, + typenum::{Sum, Unsigned, U32}, + ArrayLength, GenericArray, +}; +use rand_core::{CryptoRng, RngCore}; +use x25519_dalek::{PublicKey, StaticSecret}; + +use std::convert::TryFrom; + +use std::ops::{Add, Deref}; + +/// A trait for sized key material that can be represented within a fixed byte +/// array size, used to represent our DH key types +pub trait SizedBytes: Sized + PartialEq { + /// The typed representation of the byte length + type Len: ArrayLength; + + /// Converts this sized key material to a `GenericArray` of the same + /// size. One can convert this to a `&[u8]` with `GenericArray::as_slice()` + /// but the size information is then lost from the type. + fn to_arr(&self) -> GenericArray; + + /// How to parse such sized material from a byte slice. + fn from_bytes(key_bytes: &[u8]) -> Result; +} + +/// A Keypair trait with public-private verification +pub trait KeyPair: Sized { + /// The single key representation must have a specific byte size itself + type Repr: SizedBytes + Clone; + + /// The public key component + fn public(&self) -> &Self::Repr; + + /// The private key component + fn private(&self) -> &Self::Repr; + + /// A constructor that receives public and private key independently as + /// bytes + fn new(public: Self::Repr, private: Self::Repr) -> Result; + + /// Generating a random key pair given a cryptographic rng + fn generate_random(rng: &mut R) -> Result; + + /// Obtaining a public key from secret bytes. At all times, we should have + /// &public_from_private(self.private()) == self.public() + fn public_from_private(secret: &Self::Repr) -> Self::Repr; + + /// Check whether a public key is valid. This is meant to be applied on + /// material provided through the network which fits the key + /// representation (i.e. can be mapped to a curve point), but presents + /// some risk - e.g. small subgroup check + fn check_public_key(key: Self::Repr) -> Result; + + /// Computes the diffie hellman function on a public key and private key + fn diffie_hellman(pk: Self::Repr, sk: Self::Repr) -> Vec; +} + +/// This is a blanket implementation of SizedBytes for any instance of KeyPair +/// with any length of keys. This encodes that we serialize the public key +/// first, followed by the private key in binary formats (and expect it in this +/// order upon decoding). +impl SizedBytes for KP +where + T: SizedBytes + Clone, + KP: KeyPair + PartialEq, + T::Len: Add, + Sum: ArrayLength, +{ + type Len = Sum; + + fn to_arr(&self) -> GenericArray { + let private = self.private().to_arr(); + let public = self.public().to_arr(); + public.concat(private) + } + + fn from_bytes(key_bytes: &[u8]) -> Result { + let checked_bytes = + check_slice_size(key_bytes, ::to_usize(), "key_bytes")?; + let single_key_len = <::Len as Unsigned>::to_usize(); + let public = ::from_bytes(&checked_bytes[..single_key_len])?; + let private = ::from_bytes(&checked_bytes[single_key_len..])?; + KP::new(public, private) + } +} + +/// A minimalist key type built around [u8;32] +#[derive(PartialEq, Eq, Clone)] +#[repr(transparent)] +pub struct Key(Vec); + +impl Deref for Key { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for Key { + type Error = InternalPakeError; + + fn try_from(key_bytes: Vec) -> Result { + Key::from_bytes(&key_bytes[..]) + } +} + +impl SizedBytes for Key { + type Len = U32; + + fn to_arr(&self) -> GenericArray { + GenericArray::clone_from_slice(&self.0[..]) + } + + fn from_bytes(key_bytes: &[u8]) -> Result { + let checked_bytes = + check_slice_size(key_bytes, ::to_usize(), "key_bytes")?; + Ok(Key(checked_bytes.to_vec())) + } +} + +/// A representation of an X25519 keypair according to RFC7748 +#[derive(PartialEq)] +pub struct X25519KeyPair { + pk: Key, + sk: Key, +} + +impl X25519KeyPair { + fn gen(rng: &mut R) -> (Vec, Vec) { + let sk = StaticSecret::new(rng); + let pk = PublicKey::from(&sk); + (pk.as_bytes().to_vec(), sk.to_bytes().to_vec()) + } +} + +impl KeyPair for X25519KeyPair { + type Repr = Key; + + fn public(&self) -> &Self::Repr { + &self.pk + } + + fn private(&self) -> &Self::Repr { + &self.sk + } + + fn new(public: Self::Repr, private: Self::Repr) -> Result { + Ok(X25519KeyPair { + pk: public, + sk: private, + }) + } + + fn generate_random(rng: &mut R) -> Result { + let (public, private) = X25519KeyPair::gen(rng); + Ok(X25519KeyPair { + pk: Key(public), + sk: Key(private), + }) + } + + fn public_from_private(secret: &Self::Repr) -> Self::Repr { + let mut secret_data = [0u8; 32]; + secret_data.copy_from_slice(&secret.0[..]); + let base_data = ::x25519_dalek::X25519_BASEPOINT_BYTES; + Key(::x25519_dalek::x25519(secret_data, base_data).to_vec()) + } + + fn check_public_key(key: Self::Repr) -> Result { + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(&key); + let point = ::curve25519_dalek::montgomery::MontgomeryPoint(key_bytes) + .to_edwards(1) + .ok_or(InternalPakeError::PointError)?; + if !point.is_torsion_free() { + Err(InternalPakeError::SubGroupError) + } else { + Ok(key) + } + } + + fn diffie_hellman(pk: Self::Repr, sk: Self::Repr) -> Vec { + let mut pk_data = [0; 32]; + pk_data.copy_from_slice(&pk.0[..]); + let mut sk_data = [0; 32]; + sk_data.copy_from_slice(&sk.0[..]); + ::x25519_dalek::x25519(sk_data, pk_data).to_vec() + } +} + +/// A custom, minimalistic Key pair struct built on Key, aimed at reproducing the behavior of libsignal's keypairs +#[derive(PartialEq)] +pub struct SignalKeyPair { + pk: Key, + sk: Key, +} + +impl SignalKeyPair { + fn clamp_scalar(mut scalar: [u8; 32]) -> ::curve25519_dalek::scalar::Scalar { + scalar[0] &= 248; + scalar[31] &= 127; + scalar[31] |= 64; + + ::curve25519_dalek::scalar::Scalar::from_bits(scalar) + } + + fn gen(rng: &mut R) -> (Vec, Vec) { + let mut bits = [0u8; 32]; + rng.fill_bytes(&mut bits); + + // It's proper to sanitize the scalar here, and reproduces x25519::StaticSecret::new + let sk = SignalKeyPair::clamp_scalar(bits); + let pk = ::curve25519_dalek::constants::X25519_BASEPOINT * sk; + + (pk.as_bytes().to_vec(), sk.as_bytes().to_vec()) + } +} + +impl KeyPair for SignalKeyPair { + type Repr = Key; + + fn public(&self) -> &Self::Repr { + &self.pk + } + + fn private(&self) -> &Self::Repr { + &self.sk + } + + fn new(public: Self::Repr, private: Self::Repr) -> Result { + Ok(SignalKeyPair { + pk: public, + sk: private, + }) + } + + fn generate_random(rng: &mut R) -> Result { + let (public, private) = SignalKeyPair::gen(rng); + Ok(SignalKeyPair { + pk: Key(public), + sk: Key(private), + }) + } + + fn public_from_private(secret: &Self::Repr) -> Self::Repr { + let mut secret_data = [0u8; 32]; + secret_data.copy_from_slice(&secret.0[..]); + let base_data = ::x25519_dalek::X25519_BASEPOINT_BYTES; + Key(::x25519_dalek::x25519(secret_data, base_data).to_vec()) + } + + fn check_public_key(key: Self::Repr) -> Result { + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(&key); + let point = ::curve25519_dalek::montgomery::MontgomeryPoint(key_bytes) + .to_edwards(1) + .ok_or(InternalPakeError::PointError)?; + if !point.is_torsion_free() { + Err(InternalPakeError::SubGroupError) + } else { + Ok(key) + } + } + + fn diffie_hellman(pk: Self::Repr, sk: Self::Repr) -> Vec { + let mut pk_data = [0; 32]; + pk_data.copy_from_slice(&pk.0[..]); + let mut sk_data = [0; 32]; + sk_data.copy_from_slice(&sk.0[..]); + ::x25519_dalek::x25519(sk_data, pk_data).to_vec() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..148d1fd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,336 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! An implementation of the OPAQUE asymmetric password authentication key exchange protocol +//! +//! # Overview +//! +//! OPAQUE is a protocol between a client and a server. They must first agree on a collection of primitives +//! to be kept consistent throughout protocol execution. These include: +//! * an authenticated encryption scheme, +//! * a finite cyclic group along with a point representation, and +//! * a keypair type. +//! +//! We will use the following choices in this example: +//! ``` +//! use chacha20poly1305::ChaCha20Poly1305; +//! use curve25519_dalek::ristretto::RistrettoPoint; +//! use opaque_ke::keypair::X25519KeyPair; +//! ``` +//! +//! This implementation is in sync with [draft-krawczyk-cfrg-opaque-05](https://tools.ietf.org/html/draft-krawczyk-cfrg-opaque-05), +//! with a concrete instantiation of the authenticated key exchange protocol using 3DH. In the future, we plan to +//! add support for other KE protocols as well. +//! +//! +//! ## Setup +//! To setup the protocol, the server begins by generating a static keypair: +//! ``` +//! # use opaque_ke::keypair::{KeyPair, X25519KeyPair, SizedBytes}; +//! # use opaque_ke::errors::ProtocolError; +//! use rand_core::{OsRng, RngCore}; +//! let mut rng = OsRng; +//! let server_kp = X25519KeyPair::generate_random(&mut rng)?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! The server must persist this keypair for the registration and login steps, where the public component will be +//! used by the client during both registration and login, and the private component will be used by the server during login. +//! +//! ## Registration +//! The registration protocol between the client and server consists of four steps along with three messages, denoted +//! as `r1`, `r2`, and `r3`. Before registration begins, it is expected that the server's static public key, `server_kp.public()`, +//! has been transmitted to the client in an offline step. A successful execution of the registration protocol results in the +//! server producing a password file corresponding to the tuple combination of (password, pepper, server public key) provided by +//! the client. This password file is typically stored server-side, and retrieved upon future login attempts made by the client. +//! +//! In the first step (client registration start), the client chooses a registration password and an optional "pepper", and +//! runs `ClientRegistration::start` to produce a message `r1`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! use rand_core::{OsRng, RngCore}; +//! let mut client_rng = OsRng; +//! let (r1, client_state) = ClientRegistration::::start( +//! b"password", +//! Some(b"pepper"), +//! &mut client_rng, +//! )?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! `r1` is sent to the server, and `client_state` must be persisted on the client for the final step of client +//! registration. +//! +//! In the second step (server registration start), the server takes as input the `r1` message from the client and runs +//! `ServerRegistration::start` to produce `r2`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! let mut server_rng = OsRng; +//! let (r2, server_state) = +//! ServerRegistration::::start( +//! r1, +//! &mut server_rng, +//! )?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! `r2` is returned to the client, and `server_state` must be persisted on the server for the final step of server +//! registration. +//! +//! In the third step (client registration finish), the client takes as input the `r2` message from the server, along +//! with the server's static public key `server_kp.public()`, and uses `client_state` from the first step to run +//! `finish` and produce a message `r3` along with the key derivation key `kd_key_registration`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # let mut server_rng = OsRng; +//! # let (r2, server_state) = +//! # ServerRegistration::::start( +//! # r1, +//! # &mut server_rng, +//! # )?; +//! # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; +//! let (r3, kd_key_registration) = +//! client_state.finish::<_, X25519KeyPair>(r2, server_kp.public(), &mut client_rng)?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! `r3` is sent to the server, and the client can optionally use `kd_key_registration` for applications that choose to +//! process user information beyond the OPAQUE functionality (e.g., additional secrets or credentials). +//! +//! In the fourth step of registration, the server takes as input the `r3` message from the client and uses +//! `server_state` from the second step to run `finish` and produce `password_file`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # let mut server_rng = OsRng; +//! # let (r2, server_state) = +//! # ServerRegistration::::start( +//! # r1, +//! # &mut server_rng, +//! # )?; +//! # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; +//! # let (r3, kd_key_registration) = +//! # client_state.finish::<_, X25519KeyPair>(r2, server_kp.public(), &mut client_rng)?; +//! let password_file = server_state.finish(r3)?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! At this point, the client can be considered as successfully registered, and the server can store +//! `password_file.to_bytes()` for use during the login protocol. +//! +//! +//! ## Login +//! The login protocol between a client and server also consists of four steps along with three messages, denoted as +//! `l1`, `l2`, and `l3`. The server is expected to have access to the a password file corresponding to an output +//! of the registration phase. The login protocol will execute successfully only if the same tuple combination of +//! (password, pepper, server public key) is presented as was used in the registration phase that produced the +//! password file that the server is testing against. +//! +//! In the first step (client login start), the client chooses a registration password and an optional "pepper", and runs +//! `ClientLogin::start` to produce a message `l1`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration, ClientLogin, ServerLogin, LoginThirdMessage}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! let mut client_rng = OsRng; +//! let (l1, client_state) = ClientLogin::::start( +//! b"password", +//! Some(b"pepper"), +//! &mut client_rng, +//! )?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! `l1` is sent to the server, and `client_state` must be persisted on the client for the final step of client login. +//! +//! In the second step (server login start), the server takes as input the `l1` message from the client, the server's +//! private key `server_kp.private()`, along with a serialized version of the password file, `password_file_bytes`, and +//! runs `ServerLogin::start` to produce `l2`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration, ClientLogin, ServerLogin, LoginThirdMessage}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # let mut server_rng = OsRng; +//! # let (r2, server_state) = +//! # ServerRegistration::::start( +//! # r1, +//! # &mut server_rng, +//! # )?; +//! # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; +//! # let (r3, kd_key_registration) = +//! # client_state.finish::<_, X25519KeyPair>(r2, server_kp.public(), &mut client_rng)?; +//! # let password_file_bytes = server_state.finish(r3)?.to_bytes(); +//! # let (l1, client_state) = ClientLogin::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! use std::convert::TryFrom; +//! let password_file = +//! ServerRegistration::::try_from( +//! &password_file_bytes[..], +//! )?; +//! let mut server_rng = OsRng; +//! let (l2, server_state) = +//! ServerLogin::start(password_file, &server_kp.private(), l1, &mut server_rng)?; +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! `l2` is returned to the client, and `server_state` must be persisted on the server for the final step of server login. +//! +//! In the third step (client login finish), the client takes as input the `l2` message from the server, along with the +//! server's static public key `server_kp.public()`, and uses `client_state` from the first step to run `finish` and produce +//! a message `l3`, the shared secret `client_shared_secret`, and the key derivation key `kd_key_login`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration, ClientLogin, ServerLogin, LoginThirdMessage}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # let mut server_rng = OsRng; +//! # let (r2, server_state) = +//! # ServerRegistration::::start( +//! # r1, +//! # &mut server_rng, +//! # )?; +//! # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; +//! # let (r3, kd_key_registration) = +//! # client_state.finish::<_, X25519KeyPair>(r2, server_kp.public(), &mut client_rng)?; +//! # let password_file_bytes = server_state.finish(r3)?.to_bytes(); +//! # let (l1, client_state) = ClientLogin::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # use std::convert::TryFrom; +//! # let password_file = +//! # ServerRegistration::::try_from( +//! # &password_file_bytes[..], +//! # )?; +//! # let (l2, server_state) = +//! # ServerLogin::start(password_file, &server_kp.private(), l1, &mut server_rng)?; +//! let (l3, client_shared_secret, kd_key_login) = client_state.finish( +//! l2, +//! &server_kp.public(), +//! &mut client_rng, +//! )?; +//! assert_eq!(kd_key_registration, kd_key_login); +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! Note that if the client supplies a tuple (password, pepper, server public key) that does not match the tuple +//! used to create the password file, then at this point the `finish` algorithm outputs the error `InvalidLoginError`. +//! +//! If `finish` completes successfully, then `l3` is sent to the server, and (similarly to registration) the client +//! can use `kd_key_login` for applications that can take advantage of the fact that this key is identical to +//! `kd_key_registration`. +//! +//! In the fourth step of login, the server takes as input the `l3` message from the client and uses `server_state` from +//! the second step to run `finish`: +//! ``` +//! # use opaque_ke::{opaque::{ClientRegistration, ServerRegistration, ClientLogin, ServerLogin, LoginThirdMessage}, keypair::{KeyPair, X25519KeyPair, SizedBytes}}; +//! # use opaque_ke::errors::ProtocolError; +//! # use curve25519_dalek::ristretto::RistrettoPoint; +//! # use chacha20poly1305::ChaCha20Poly1305; +//! # use rand_core::{OsRng, RngCore}; +//! # let mut client_rng = OsRng; +//! # let (r1, client_state) = ClientRegistration::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # let mut server_rng = OsRng; +//! # let (r2, server_state) = +//! # ServerRegistration::::start( +//! # r1, +//! # &mut server_rng, +//! # )?; +//! # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; +//! # let (r3, kd_key) = +//! # client_state.finish::<_, X25519KeyPair>(r2, server_kp.public(), &mut client_rng)?; +//! # let password_file_bytes = server_state.finish(r3)?.to_bytes(); +//! # let (l1, client_state) = ClientLogin::::start( +//! # b"password", +//! # Some(b"pepper"), +//! # &mut client_rng, +//! # )?; +//! # use std::convert::TryFrom; +//! # let password_file = +//! # ServerRegistration::::try_from( +//! # &password_file_bytes[..], +//! # )?; +//! # let (l2, server_state) = +//! # ServerLogin::start(password_file, &server_kp.private(), l1, &mut server_rng)?; +//! # let (l3, client_shared_secret, kd_key) = client_state.finish( +//! # l2, +//! # &server_kp.public(), +//! # &mut client_rng, +//! # )?; +//! let server_shared_secret = server_state.finish(l3)?; +//! assert_eq!(client_shared_secret, server_shared_secret); +//! # Ok::<(), ProtocolError>(()) +//! ``` +//! If the protocol completes successfully, then the server obtains a `server_shared_secret` which is guaranteed to +//! match `client_shared_secret`. Otherwise, on failure, the `finish` algorithm outputs the error `InvalidLoginError`. +//! + +// Error types +pub mod errors; +// High-level API +pub mod opaque; + +// Your choice of RKR encryption +mod rkr_encryption; +// Your choice of KE +mod key_exchange; +pub mod keypair; +// Low-level API contains OPRF stuff +mod oprf; +// Technical module for your choice of cyclic subgroup to +// do the oprf on +mod group; + +#[cfg(test)] +mod tests; diff --git a/src/opaque.rs b/src/opaque.rs new file mode 100644 index 0000000..20f698c --- /dev/null +++ b/src/opaque.rs @@ -0,0 +1,918 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +//! Provides the main OPAQUE API + +use crate::{ + errors::{utils::check_slice_size, InternalPakeError, PakeError, ProtocolError}, + group::Group, + key_exchange::{ + finish_ke, generate_ke1, generate_ke2, generate_ke3, KE1Message, KE1State, KE2Message, + KE2State, KE3Message, KE1_STATE_LEN, KE2_MESSAGE_LEN, + }, + keypair::{Key, KeyPair, SizedBytes}, + oprf, + oprf::OprfClientBytes, + rkr_encryption::{RKRCipher, RKRCiphertext}, +}; +use generic_array::{ + typenum::{Unsigned, U32, U64}, + GenericArray, +}; +use hkdf::Hkdf; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; +use std::{convert::TryFrom, marker::PhantomData}; +use zeroize::Zeroize; + +// Constant string used as salt for HKDF computation +const STR_ENVU: &[u8] = b"EnvU"; + +/// The length of the "key-derivation key" output by the client registration +/// and login finish steps +pub const DERIVED_KEY_LEN: usize = 32; + +// Messages +// ========= + +/// The message sent by the client to the server, to initiate registration +pub struct RegisterFirstMessage { + /// blinded password information + alpha: Grp, +} + +impl TryFrom<&[u8]> for RegisterFirstMessage { + type Error = ProtocolError; + fn try_from(first_message_bytes: &[u8]) -> Result { + // Check that the message is actually containing an element of the + // correct subgroup + let arr = GenericArray::from_slice(first_message_bytes); + let alpha = Grp::from_element_slice(arr)?; + Ok(Self { alpha }) + } +} + +impl RegisterFirstMessage { + pub fn to_bytes(&self) -> GenericArray { + self.alpha.to_bytes() + } +} + +/// The answer sent by the server to the user, upon reception of the +/// registration attempt +pub struct RegisterSecondMessage { + /// The server's oprf output + beta: Grp, +} + +impl TryFrom<&[u8]> for RegisterSecondMessage +where + Grp: Group, +{ + type Error = ProtocolError; + + fn try_from(second_message_bytes: &[u8]) -> Result { + let checked_slice = check_slice_size( + second_message_bytes, + Grp::ElemLen::to_usize(), + "second_message_bytes", + )?; + // Check that the message is actually containing an element of the + // correct subgroup + let arr = GenericArray::from_slice(&checked_slice); + let beta = Grp::from_element_slice(arr)?; + Ok(Self { beta }) + } +} + +impl RegisterSecondMessage +where + Grp: Group, +{ + pub fn to_bytes(&self) -> Vec { + self.beta.to_bytes().to_vec() + } +} + +/// The final message from the client, containing encrypted cryptographic +/// identifiers +pub struct RegisterThirdMessage { + /// The "envelope" generated by the user, containing encrypted + /// cryptographic identifiers + envelope: RKRCiphertext, + /// The user's public key + client_s_pk: KeyFormat::Repr, +} + +impl RegisterThirdMessage +where + Aead: aead::Aead + aead::NewAead, + KeyFormat: KeyPair, +{ + pub fn to_bytes(&self) -> Vec { + let mut res = Vec::new(); + res.extend(self.envelope.to_bytes()); + res.extend(self.client_s_pk.to_arr()); + res + } +} + +impl TryFrom<&[u8]> for RegisterThirdMessage +where + Aead: aead::Aead + aead::NewAead, + KeyFormat: KeyPair, +{ + type Error = ProtocolError; + + fn try_from(third_message_bytes: &[u8]) -> Result { + let rkr_size = RKRCiphertext::::rkr_with_nonce_size(); + let key_len = ::Len::to_usize(); + let checked_bytes = + check_slice_size(third_message_bytes, rkr_size + key_len, "third_message")?; + let unchecked_client_s_pk = KeyFormat::Repr::from_bytes(&checked_bytes[rkr_size..])?; + let client_s_pk = KeyFormat::check_public_key(unchecked_client_s_pk)?; + + Ok(Self { + envelope: RKRCiphertext::from_bytes(&checked_bytes[..rkr_size])?, + client_s_pk, + }) + } +} + +/// The message sent by the user to the server, to initiate registration +pub struct LoginFirstMessage { + /// blinded password information + alpha: Grp, + ke1_message: KE1Message, +} + +impl TryFrom<&[u8]> for LoginFirstMessage { + type Error = ProtocolError; + fn try_from(first_message_bytes: &[u8]) -> Result { + // Check that the message is actually containing an element of the + // correct subgroup + let elem_len = Grp::ElemLen::to_usize(); + let arr = GenericArray::from_slice(&first_message_bytes[..elem_len]); + let alpha = Grp::from_element_slice(arr)?; + + let ke1_message = KE1Message::try_from(&first_message_bytes[elem_len..])?; + Ok(Self { alpha, ke1_message }) + } +} + +impl LoginFirstMessage { + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + self.alpha.to_bytes().as_slice(), + &self.ke1_message.to_bytes(), + ] + .concat(); + output + } +} + +/// The answer sent by the server to the user, upon reception of the +/// login attempt. +pub struct LoginSecondMessage { + /// the server's oprf output + beta: Grp, + /// the user's encrypted information, + envelope: RKRCiphertext, + ke2_message: KE2Message, +} + +impl LoginSecondMessage +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, +{ + pub fn to_bytes(&self) -> Vec { + [ + &self.beta.to_bytes()[..], + &self.envelope.to_bytes()[..], + &self.ke2_message.to_bytes()[..], + ] + .concat() + } +} + +impl TryFrom<&[u8]> for LoginSecondMessage +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, +{ + type Error = ProtocolError; + fn try_from(second_message_bytes: &[u8]) -> Result { + let cipher_len = RKRCiphertext::::rkr_with_nonce_size(); + let elem_len = Grp::ElemLen::to_usize(); + let checked_slice = check_slice_size( + second_message_bytes, + elem_len + cipher_len + KE2_MESSAGE_LEN, + "login_second_message_bytes", + )?; + + // Check that the message is actually containing an element of the + // correct subgroup + let beta_bytes = &checked_slice[..elem_len]; + let arr = GenericArray::from_slice(beta_bytes); + let beta = Grp::from_element_slice(arr)?; + + let envelope = + RKRCiphertext::::from_bytes(&checked_slice[elem_len..elem_len + cipher_len])?; + let ke2_message = KE2Message::try_from(&checked_slice[elem_len + cipher_len..])?; + + Ok(Self { + beta, + envelope, + ke2_message, + }) + } +} + +/// The answer sent by the client to the server, upon reception of the +/// encrypted envelope +pub struct LoginThirdMessage { + ke3_message: KE3Message, +} + +impl TryFrom<&[u8]> for LoginThirdMessage { + type Error = ProtocolError; + + fn try_from(bytes: &[u8]) -> Result { + let ke3_message = KE3Message::try_from(&bytes[..])?; + Ok(Self { ke3_message }) + } +} + +impl LoginThirdMessage { + pub fn to_bytes(&self) -> Vec { + self.ke3_message.to_bytes() + } +} + +// Registration +// ============ + +/// The state elements the client holds to register itself +pub struct ClientRegistration { + /// A choice of symmetric encryption for the envelope + _aead: PhantomData, + /// a blinding factor + pub(crate) blinding_factor: Grp::Scalar, + /// the client's password + password: Vec, +} + +impl + aead::Aead, Grp: Group> TryFrom<&[u8]> + for ClientRegistration +{ + type Error = ProtocolError; + fn try_from(bytes: &[u8]) -> Result { + // Check that the message is actually containing an element of the + // correct subgroup + let scalar_len = Grp::ScalarLen::to_usize(); + let blinding_factor_bytes = GenericArray::from_slice(&bytes[..scalar_len]); + let blinding_factor = Grp::from_scalar_slice(blinding_factor_bytes)?; + let password = bytes[scalar_len..].to_vec(); + Ok(Self { + _aead: PhantomData, + blinding_factor, + password, + }) + } +} + +impl ClientRegistration +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, +{ + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + Grp::scalar_as_bytes(&self.blinding_factor).as_slice(), + &self.password, + ] + .concat(); + output + } +} + +impl ClientRegistration +where + Grp: Group, +{ + /// Returns an initial "blinded" request to send to the server, as well as a ClientRegistration + /// + /// # Arguments + /// * `password` - A user password + /// + /// # Example + /// + /// ``` + /// use opaque_ke::opaque::ClientRegistration; + /// # use opaque_ke::errors::ProtocolError; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// use rand_core::{OsRng, RngCore}; + /// let mut rng = OsRng; + /// let (register_m1, registration_state) = ClientRegistration::::start(b"hunter2", None, &mut rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn start( + password: &[u8], + pepper: Option<&[u8]>, + blinding_factor_rng: &mut R, + ) -> Result<(RegisterFirstMessage, Self), ProtocolError> { + let OprfClientBytes { + alpha, + blinding_factor, + } = oprf::generate_oprf1::(&password, pepper, blinding_factor_rng)?; + + Ok(( + RegisterFirstMessage:: { alpha }, + Self { + _aead: PhantomData, + blinding_factor, + password: password.to_vec(), + }, + )) + } +} + +type ClientRegistrationFinishResult = ( + RegisterThirdMessage, + GenericArray::OutputSize>, +); + +impl ClientRegistration +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, +{ + /// "Unblinds" the server's answer and returns a final message containing + /// cryptographic identifiers, to be sent to the server on setup finalization + /// + /// # Arguments + /// * `message` - the server's answer to the initial registration attempt + /// + /// # Example + /// + /// ``` + /// use opaque_ke::{opaque::{ClientRegistration, ServerRegistration}, keypair::{X25519KeyPair, SizedBytes}}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::KeyPair; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// let mut server_rng = OsRng; + /// let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; + /// let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// let (register_m2, server_state) = + /// ServerRegistration::::start(register_m1, &mut server_rng)?; + /// let mut client_rng = OsRng; + /// let register_m3 = client_state.finish::<_, X25519KeyPair>(register_m2, server_kp.public(), &mut client_rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn finish( + self, + r2: RegisterSecondMessage, + server_s_pk: &KeyFormat::Repr, + rng: &mut R, + ) -> Result, ProtocolError> { + let client_static_keypair = KeyFormat::generate_random(rng)?; + + let password_derived_key = + get_password_derived_key::(self.password.clone(), r2.beta, &self.blinding_factor)?; + let h = Hkdf::::new(None, &password_derived_key); + let mut okm = [0u8; 3 * DERIVED_KEY_LEN]; + h.expand(STR_ENVU, &mut okm) + .map_err(|_| InternalPakeError::HkdfError)?; + let encryption_key = &okm[..DERIVED_KEY_LEN]; + let hmac_key = &okm[DERIVED_KEY_LEN..2 * DERIVED_KEY_LEN]; + let kd_key = &okm[2 * DERIVED_KEY_LEN..]; + + let envelope = RKRCiphertext::::encrypt( + &encryption_key, + &hmac_key, + &client_static_keypair.private().to_arr(), + &server_s_pk.to_arr(), + rng, + )?; + + Ok(( + RegisterThirdMessage { + envelope, + client_s_pk: client_static_keypair.public().clone(), + }, + *GenericArray::from_slice(&kd_key), + )) + } +} + +// This can't be derived because of the use of a phantom parameter +impl Zeroize for ClientRegistration { + fn zeroize(&mut self) { + self.password.zeroize(); + self.blinding_factor.zeroize(); + } +} + +impl Drop for ClientRegistration { + fn drop(&mut self) { + self.zeroize(); + } +} + +// This can't be derived because of the use of a phantom parameter +impl Zeroize for ClientLogin { + fn zeroize(&mut self) { + self.password.zeroize(); + self.blinding_factor.zeroize(); + } +} + +impl Drop for ClientLogin { + fn drop(&mut self) { + self.zeroize(); + } +} + +/// The state elements the server holds to record a registration +pub struct ServerRegistration { + envelope: Option>, + client_s_pk: Option, + pub(crate) oprf_key: Grp::Scalar, +} + +impl TryFrom<&[u8]> for ServerRegistration +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, + KeyFormat: KeyPair + PartialEq, + ::Len: std::ops::Add<::Len>, + generic_array::typenum::Sum< + ::Len, + ::Len, + >: generic_array::ArrayLength, +{ + type Error = ProtocolError; + fn try_from(server_registration_bytes: &[u8]) -> Result { + let key_len = ::Len::to_usize(); + let scalar_len = Grp::ScalarLen::to_usize(); + let rkr_size = RKRCiphertext::::rkr_with_nonce_size(); + + if server_registration_bytes.len() == scalar_len { + return Ok(Self { + oprf_key: Grp::from_scalar_slice(GenericArray::from_slice( + server_registration_bytes, + ))?, + client_s_pk: None, + envelope: None, + }); + } + + let checked_bytes = check_slice_size( + server_registration_bytes, + rkr_size + key_len + scalar_len, + "server_registration_bytes", + )?; + let oprf_key_bytes = GenericArray::from_slice(&checked_bytes[..scalar_len]); + let oprf_key = Grp::from_scalar_slice(oprf_key_bytes)?; + let unchecked_client_s_pk = + KeyFormat::Repr::from_bytes(&checked_bytes[scalar_len..scalar_len + key_len])?; + let client_s_pk = KeyFormat::check_public_key(unchecked_client_s_pk)?; + Ok(Self { + envelope: Some(RKRCiphertext::from_bytes( + &checked_bytes[checked_bytes.len() - rkr_size..], + )?), + client_s_pk: Some(client_s_pk), + oprf_key, + }) + } +} + +impl ServerRegistration +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, + KeyFormat: KeyPair + PartialEq, + ::Len: std::ops::Add<::Len>, + generic_array::typenum::Sum< + ::Len, + ::Len, + >: generic_array::ArrayLength, +{ + pub fn to_bytes(&self) -> Vec { + let mut output: Vec = Grp::scalar_as_bytes(&self.oprf_key).to_vec(); + match &self.client_s_pk { + Some(v) => output.extend_from_slice(&v.to_arr()), + None => {} + }; + match &self.envelope { + Some(v) => output.extend_from_slice(&v.to_bytes()), + None => {} + }; + output + } + + /// From the client's "blinded" password, returns a response to be + /// sent back to the client, as well as a ServerRegistration + /// + /// # Arguments + /// * `message` - the initial registration message + /// + /// # Example + /// + /// ``` + /// use opaque_ke::{opaque::*, keypair::{X25519KeyPair, SizedBytes}}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::KeyPair; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// let mut server_rng = OsRng; + /// let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// let (register_m2, server_state) = + /// ServerRegistration::::start(register_m1, &mut server_rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn start( + message: RegisterFirstMessage, + rng: &mut R, + ) -> Result<(RegisterSecondMessage, Self), ProtocolError> { + // RFC: generate oprf_key (salt) and v_u = g^oprf_key + let oprf_key = Grp::random_scalar(rng); + + // Compute beta = alpha^oprf_key + let beta = oprf::generate_oprf2::(message.alpha, &oprf_key)?; + + Ok(( + RegisterSecondMessage { beta }, + Self { + envelope: None, + client_s_pk: None, + oprf_key, + }, + )) + } + + /// From the client's cryptographic identifiers, fully populates and + /// returns a ServerRegistration + /// + /// # Arguments + /// * `message` - the final client message + /// + /// # Example + /// + /// ``` + /// use opaque_ke::{opaque::*, keypair::{X25519KeyPair, SizedBytes}}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::KeyPair; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// let mut server_rng = OsRng; + /// let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; + /// let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// let (register_m2, server_state) = + /// ServerRegistration::::start(register_m1, &mut server_rng)?; + /// let mut client_rng = OsRng; + /// let (register_m3, _opaque_key) = client_state.finish(register_m2, server_kp.public(), &mut client_rng)?; + /// let client_record = server_state.finish(register_m3)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn finish( + self, + message: RegisterThirdMessage, + ) -> Result { + Ok(Self { + envelope: Some(message.envelope), + client_s_pk: Some(message.client_s_pk), + oprf_key: self.oprf_key, + }) + } +} + +// Login +// ===== + +/// The state elements the client holds to perform a login +pub struct ClientLogin { + /// A choice of symmetric encryption for the envelope + _aead: PhantomData, + /// A choice of the keypair type + _key_format: PhantomData, + /// A blinding factor, which is used to mask (and unmask) secret + /// information before transmission + blinding_factor: Grp::Scalar, + /// The user's password + password: Vec, + ke1_state: KE1State, +} + +impl + aead::Aead, Grp: Group, KeyFormat: KeyPair> TryFrom<&[u8]> + for ClientLogin +{ + type Error = ProtocolError; + fn try_from(bytes: &[u8]) -> Result { + let scalar_len = Grp::ScalarLen::to_usize(); + let blinding_factor_bytes = GenericArray::from_slice(&bytes[..scalar_len]); + let blinding_factor = Grp::from_scalar_slice(blinding_factor_bytes)?; + let ke1_state = KE1State::try_from(&bytes[scalar_len..scalar_len + KE1_STATE_LEN])?; + let password = bytes[scalar_len + KE1_STATE_LEN..].to_vec(); + Ok(Self { + _aead: PhantomData, + _key_format: PhantomData, + blinding_factor, + password, + ke1_state, + }) + } +} + +impl ClientLogin +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, + KeyFormat: KeyPair, +{ + pub fn to_bytes(&self) -> Vec { + let output: Vec = [ + Grp::scalar_as_bytes(&self.blinding_factor).as_slice(), + &self.ke1_state.to_bytes(), + &self.password, + ] + .concat(); + output + } +} + +type ClientLoginFinishResult = ( + LoginThirdMessage, + Vec, + GenericArray::OutputSize>, +); + +impl ClientLogin +where + Aead: aead::NewAead + aead::Aead, + Grp: Group, + KeyFormat: KeyPair, +{ + /// Returns an initial "blinded" password request to send to the server, as well as a ClientLogin + /// + /// # Arguments + /// * `password` - A user password + /// + /// # Example + /// + /// ``` + /// use opaque_ke::opaque::ClientLogin; + /// # use opaque_ke::errors::ProtocolError; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// use opaque_ke::keypair::X25519KeyPair; + /// use rand_core::{OsRng, RngCore}; + /// let mut client_rng = OsRng; + /// let (login_m1, client_login_state) = ClientLogin::::start(b"hunter2", None, &mut client_rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn start( + password: &[u8], + pepper: Option<&[u8]>, + rng: &mut R, + ) -> Result<(LoginFirstMessage, Self), ProtocolError> { + let OprfClientBytes { + alpha, + blinding_factor, + } = oprf::generate_oprf1::(&password, pepper, rng)?; + + let (ke1_state, ke1_message) = + generate_ke1::<_, KeyFormat>(alpha.to_bytes().to_vec(), rng)?; + + let l1 = LoginFirstMessage { alpha, ke1_message }; + + Ok(( + l1, + Self { + _aead: PhantomData, + _key_format: PhantomData, + blinding_factor, + password: password.to_vec(), + ke1_state, + }, + )) + } + + /// "Unblinds" the server's answer and returns the decrypted assets from + /// the server + /// + /// # Arguments + /// * `message` - the server's answer to the initial login attempt + /// + /// # Example + /// + /// ``` + /// use opaque_ke::opaque::{ClientLogin, ServerLogin}; + /// # use opaque_ke::opaque::{ClientRegistration, ServerRegistration}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::{X25519KeyPair, KeyPair}; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// # let mut server_rng = OsRng; + /// # let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// # let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; + /// # let (register_m2, server_state) = ServerRegistration::::start(register_m1, &mut server_rng)?; + /// # let (register_m3, _opaque_key) = client_state.finish(register_m2, server_kp.public(), &mut client_rng)?; + /// # let p_file = server_state.finish(register_m3)?; + /// let (login_m1, client_login_state) = ClientLogin::::start(b"hunter2", None, &mut client_rng)?; + /// let (login_m2, server_login_state) = ServerLogin::start(p_file, &server_kp.private(), login_m1, &mut server_rng)?; + /// let (login_m3, client_transport, _opaque_key) = client_login_state.finish(login_m2, &server_kp.public(), &mut client_rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn finish( + self, + l2: LoginSecondMessage, + server_s_pk: &KeyFormat::Repr, + _client_e_sk_rng: &mut R, + ) -> Result { + let l2_bytes: Vec = [l2.beta.to_bytes().as_slice(), &l2.envelope.to_bytes()].concat(); + + let password_derived_key = + get_password_derived_key::(self.password.clone(), l2.beta, &self.blinding_factor)?; + let h = Hkdf::::new(None, &password_derived_key); + let mut okm = [0u8; 3 * DERIVED_KEY_LEN]; + h.expand(STR_ENVU, &mut okm) + .map_err(|_| InternalPakeError::HkdfError)?; + let encryption_key = &okm[..DERIVED_KEY_LEN]; + let hmac_key = &okm[DERIVED_KEY_LEN..2 * DERIVED_KEY_LEN]; + let kd_key = &okm[2 * DERIVED_KEY_LEN..]; + + let client_s_sk = Key::from_bytes( + &l2.envelope + .decrypt(&encryption_key, &hmac_key, &server_s_pk.to_arr()) + .map_err(|e| match e { + PakeError::DecryptionHmacError => PakeError::InvalidLoginError, + err => err, + })?, + )?; + + let (ke3_state, ke3_message) = generate_ke3::( + l2_bytes, + l2.ke2_message, + &self.ke1_state, + server_s_pk.clone(), + client_s_sk, + )?; + + Ok(( + LoginThirdMessage { ke3_message }, + ke3_state.shared_secret, + *GenericArray::from_slice(&kd_key), + )) + } +} + +/// The state elements the server holds to record a login +pub struct ServerLogin { + ke2_state: KE2State, +} + +impl TryFrom<&[u8]> for ServerLogin { + type Error = ProtocolError; + fn try_from(bytes: &[u8]) -> Result { + Ok(Self { + ke2_state: KE2State::try_from(&bytes[..])?, + }) + } +} + +impl ServerLogin { + pub fn to_bytes(&self) -> Vec { + self.ke2_state.to_bytes() + } + + /// From the client's "blinded"" password, returns a challenge to be + /// sent back to the client, as well as a ServerLogin + /// + /// # Arguments + /// * `message` - the initial registration message + /// + /// # Example + /// + /// ``` + /// use opaque_ke::opaque::{ClientLogin, ServerLogin}; + /// # use opaque_ke::opaque::{ClientRegistration, ServerRegistration}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::{KeyPair, X25519KeyPair}; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// let mut server_rng = OsRng; + /// let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; + /// # let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// # let (register_m2, server_state) = + /// ServerRegistration::::start(register_m1, &mut server_rng)?; + /// # let (register_m3, _opaque_key) = client_state.finish(register_m2, server_kp.public(), &mut client_rng)?; + /// # let p_file = server_state.finish(register_m3)?; + /// let (login_m1, client_login_state) = ClientLogin::::start(b"hunter2", None, &mut client_rng)?; + /// let (login_m2, server_login_state) = ServerLogin::start(p_file, &server_kp.private(), login_m1, &mut server_rng)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn start< + R: RngCore + CryptoRng, + Aead: aead::NewAead + aead::Aead, + Grp: Group, + KeyFormat: KeyPair, + >( + password_file: ServerRegistration, + server_s_sk: &Key, + l1: LoginFirstMessage, + rng: &mut R, + ) -> Result<(LoginSecondMessage, Self), ProtocolError> { + let l1_bytes = &l1.to_bytes(); + let beta = oprf::generate_oprf2(l1.alpha, &password_file.oprf_key)?; + + let client_s_pk = password_file + .client_s_pk + .ok_or(PakeError::EncryptionError)?; + let envelope = password_file.envelope.ok_or(PakeError::EncryptionError)?; + + let l2_component: Vec = [beta.to_bytes().as_slice(), &envelope.to_bytes()].concat(); + + let (ke2_state, ke2_message) = generate_ke2::<_, KeyFormat>( + rng, + l1_bytes.to_vec(), + l2_component, + l1.ke1_message.client_e_pk, + client_s_pk, + server_s_sk.clone(), + l1.ke1_message.client_nonce.to_vec(), + )?; + + let l2 = LoginSecondMessage { + beta, + envelope, + ke2_message, + }; + + Ok((l2, Self { ke2_state })) + } + + /// From the client's second & final message, check the client's + /// authentication & produce a message transport + /// + /// # Arguments + /// * `message` - the client's second login message + /// + /// # Example + /// + /// ``` + /// use opaque_ke::opaque::{ClientLogin, ServerLogin}; + /// # use opaque_ke::opaque::{ClientRegistration, ServerRegistration}; + /// # use opaque_ke::errors::ProtocolError; + /// # use opaque_ke::keypair::{KeyPair, X25519KeyPair}; + /// use rand_core::{OsRng, RngCore}; + /// use chacha20poly1305::ChaCha20Poly1305; + /// use curve25519_dalek::ristretto::RistrettoPoint; + /// let mut client_rng = OsRng; + /// let mut server_rng = OsRng; + /// let server_kp = X25519KeyPair::generate_random(&mut server_rng)?; + /// # let (register_m1, client_state) = ClientRegistration::::start(b"hunter2", None, &mut client_rng)?; + /// # let (register_m2, server_state) = + /// ServerRegistration::::start(register_m1, &mut server_rng)?; + /// # let (register_m3, _opaque_key) = client_state.finish(register_m2, server_kp.public(), &mut client_rng)?; + /// # let p_file = server_state.finish(register_m3)?; + /// let (login_m1, client_login_state) = ClientLogin::::start(b"hunter2", None, &mut client_rng)?; + /// let (login_m2, server_login_state) = ServerLogin::start(p_file, &server_kp.private(), login_m1, &mut server_rng)?; + /// let (login_m3, client_transport, _opaque_key) = client_login_state.finish(login_m2, &server_kp.public(), &mut client_rng)?; + /// let mut server_transport = server_login_state.finish(login_m3)?; + /// # Ok::<(), ProtocolError>(()) + /// ``` + pub fn finish(&self, message: LoginThirdMessage) -> Result, ProtocolError> { + finish_ke(message.ke3_message, &self.ke2_state).map_err(|e| match e { + ProtocolError::VerificationError(PakeError::KeyExchangeMacValidationError) => { + ProtocolError::VerificationError(PakeError::InvalidLoginError) + } + err => err, + }) + } +} + +// Helper functions + +fn get_password_derived_key( + password: Vec, + beta: G, + blinding_factor: &G::Scalar, +) -> Result::OutputSize>, PakeError> { + Ok(oprf::generate_oprf3::(&password, beta, blinding_factor)?) +} diff --git a/src/oprf.rs b/src/oprf.rs new file mode 100644 index 0000000..3137cc3 --- /dev/null +++ b/src/oprf.rs @@ -0,0 +1,133 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::{errors::InternalPakeError, group::Group}; +use generic_array::{typenum::U64, GenericArray}; +use hkdf::Hkdf; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +// Low-level API +// ============= +// This file contains an implementation of an oblivious pseudorandom function (OPRF), as well as password hashing and encryption functions. + +pub(crate) struct OprfClientBytes { + pub(crate) alpha: Grp, + pub(crate) blinding_factor: Grp::Scalar, +} + +/// Computes the first step for the multiplicative blinding version of DH-OPRF. This +/// message is sent from the client (who holds the input) to the server (who holds the OPRF key). +/// The client can also pass in an optional "pepper" string to be mixed in with the input through +/// an HKDF computation. +pub(crate) fn generate_oprf1>( + input: &[u8], + pepper: Option<&[u8]>, + blinding_factor_rng: &mut R, +) -> Result, InternalPakeError> { + let (hashed_input, _) = Hkdf::::extract(pepper, &input); + let curve_input: Vec = [hashed_input.as_slice(), &[0u8; 32]].concat(); + let blinding_factor = G::random_scalar(blinding_factor_rng); + let alpha = G::hash_to_curve(GenericArray::from_slice(&curve_input)) * &blinding_factor; + Ok(OprfClientBytes { + alpha, + blinding_factor, + }) +} + +/// Computes the second step for the multiplicative blinding version of DH-OPRF. This +/// message is sent from the server (who holds the OPRF key) to the client. +pub(crate) fn generate_oprf2( + point: G, + oprf_key: &G::Scalar, +) -> Result { + Ok(point * oprf_key) +} + +/// Computes the third step for the multiplicative blinding version of DH-OPRF, in which +/// the client unblinds the server's message. +pub(crate) fn generate_oprf3( + input: &[u8], + point: G, + blinding_factor: &G::Scalar, +) -> Result::OutputSize>, InternalPakeError> { + let unblinded = point * &G::scalar_invert(&blinding_factor); + let ikm: Vec = [&unblinded.to_bytes(), input].concat(); + let (prk, _) = Hkdf::::extract(None, &ikm); + Ok(prk) +} + +// Tests +// ===== + +#[cfg(test)] +mod tests { + use super::*; + use crate::group::Group; + use curve25519_dalek::ristretto::RistrettoPoint; + use generic_array::{arr, arr_impl, GenericArray}; + use hkdf::Hkdf; + use rand_core::OsRng; + + fn prf( + input: &[u8], + oprf_key: &[u8; 32], + ) -> GenericArray::ElemLen> { + let (hashed_input, _) = Hkdf::::extract(None, &input); + let curve_input: Vec = [hashed_input.as_slice(), &[0u8; 32]].concat(); + let point = RistrettoPoint::hash_to_curve(GenericArray::from_slice(&curve_input)); + let scalar = + RistrettoPoint::from_scalar_slice(GenericArray::from_slice(&oprf_key[..])).unwrap(); + let res = point * scalar; + let ikm: Vec = [res.to_bytes().as_slice(), &input].concat(); + + let (prk, _) = Hkdf::::extract(None, &ikm); + prk + } + + #[test] + fn oprf_retrieval() -> Result<(), InternalPakeError> { + let input = b"hunter2"; + let mut rng = OsRng; + let OprfClientBytes { + alpha, + blinding_factor, + } = generate_oprf1::<_, RistrettoPoint>(&input[..], None, &mut rng)?; + let salt_bytes = arr![ + u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]; + let salt = RistrettoPoint::from_scalar_slice(&salt_bytes)?; + let beta = generate_oprf2::(alpha, &salt)?; + let res = generate_oprf3::(input, beta, &blinding_factor)?; + let res2 = prf(&input[..], &salt.as_bytes()); + assert_eq!(res, res2); + Ok(()) + } + + #[test] + fn oprf_inversion_unsalted() { + let mut rng = OsRng; + let mut input = vec![0u8; 64]; + rng.fill_bytes(&mut input); + let OprfClientBytes { + alpha, + blinding_factor, + } = generate_oprf1::<_, RistrettoPoint>(&input, None, &mut rng).unwrap(); + let res = generate_oprf3::(&input, alpha, &blinding_factor).unwrap(); + + let (hashed_input, _) = Hkdf::::extract(None, &input); + let mut curve_input: Vec = Vec::new(); + curve_input.extend_from_slice(&hashed_input); + curve_input.extend_from_slice(&[0u8; 32]); + let point = RistrettoPoint::hash_from_bytes::(&curve_input); + let mut ikm: Vec = Vec::new(); + ikm.extend_from_slice(&point.to_bytes()); + ikm.extend_from_slice(&input); + let (prk, _) = Hkdf::::extract(None, &ikm); + + assert_eq!(res, prk); + } +} diff --git a/src/rkr_encryption.rs b/src/rkr_encryption.rs new file mode 100644 index 0000000..1b17d85 --- /dev/null +++ b/src/rkr_encryption.rs @@ -0,0 +1,202 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::errors::{utils::check_slice_size, InternalPakeError, PakeError}; +use aead::{Aead, NewAead}; +use generic_array::{typenum::Unsigned, GenericArray}; +use hmac::{Hmac, Mac}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +/// This trait encapsulates an encryption scheme that satisfies random-key robustness (RKR), which is implemented +/// through encrypt-then-HMAC -- see Section 3.1.1 of +/// https://www.ietf.org/id/draft-krawczyk-cfrg-opaque-03.txt +/// We require an Aead implementation with a 32-bit key size, since we +/// will derive the symmetric key from pw using Sha256 +pub trait RKRCipher: Sized { + /// The requirement of KeySize = U32 is so that we can use a 32-bit hash + /// for key derivation form the user's password + type AEAD: NewAead::OutputSize> + Aead; + + // Required members + fn new( + aead_output: Vec, + hmac: &GenericArray::OutputSize>, + nonce: &GenericArray::NonceSize>, + ) -> Self; + + fn aead_output(&self) -> &Vec; + fn hmac(&self) -> &GenericArray::OutputSize>; + fn nonce(&self) -> &GenericArray::NonceSize>; + + fn to_bytes(&self) -> Vec; + + // Provided members for enc / dec + fn key_len() -> usize { + ::KeySize::to_usize() + } + + fn nonce_size() -> usize { + ::NonceSize::to_usize() + } + + fn hmac_size() -> usize { + ::OutputSize::to_usize() + } + + /// This estimates the size of the ciphertext once we encode —very specifically— + /// the payload we have planned for the protocol's env_u + fn ciphertest_size() -> usize { + Self::key_len() + ::TagSize::to_usize() + Self::hmac_size() + } + + fn rkr_with_nonce_size() -> usize { + Self::ciphertest_size() + Self::nonce_size() + } + + /// The format of the output ciphertext here is: + /// encryption_output | tag | hmac | nonce + /// variable length | AEAD_TAG_SIZE bytes | HMAC_SIZE bytes | NONCE_SIZE bytes + fn from_bytes(bytes: &[u8]) -> Result { + let checked_bytes = check_slice_size(&bytes[..], Self::rkr_with_nonce_size(), "bytes")?; + let nonce_start = bytes.len() - Self::nonce_size(); + let hmac_start = nonce_start - Self::hmac_size(); + + Ok(::new( + bytes[..hmac_start].to_vec(), + GenericArray::from_slice(&checked_bytes[hmac_start..nonce_start]), + GenericArray::from_slice(&checked_bytes[nonce_start..]), + )) + } + + /// Encrypt with AEAD. Note that this encryption scheme needs to satisfy "random-key robustness" (RKR). + fn encrypt( + encryption_key: &[u8], + hmac_key: &[u8], + plaintext: &[u8], + aad: &[u8], + rng: &mut R, + ) -> Result { + let mut nonce = vec![0u8; Self::nonce_size()]; + rng.fill_bytes(&mut nonce); + let gen_nonce = GenericArray::from_slice(&nonce[..]); + + let ciphertext = ::new(*GenericArray::from_slice(&encryption_key)) + .encrypt( + GenericArray::from_slice(&nonce), + aead::Payload { + msg: &plaintext, + aad: &aad, + }, + ) + .map_err(|_| PakeError::EncryptionError)?; + + let mut mac = + Hmac::::new_varkey(&hmac_key).map_err(|_| InternalPakeError::HmacError)?; + mac.input(&ciphertext); + + Ok(::new( + ciphertext, + &mac.result().code(), + gen_nonce, + )) + } + + fn decrypt( + &self, + encryption_key: &[u8], + hmac_key: &[u8], + aad: &[u8], + ) -> Result, PakeError> { + let mut mac = + Hmac::::new_varkey(&hmac_key).map_err(|_| InternalPakeError::HmacError)?; + mac.input(self.aead_output()); + if mac.verify(self.hmac()).is_err() { + return Err(PakeError::DecryptionHmacError); + } + + Aead::decrypt( + &::new(*GenericArray::from_slice(&encryption_key)), + self.nonce(), + aead::Payload { + msg: self.aead_output(), + aad: &aad, + }, + ) + .map_err(|_| PakeError::DecryptionError) + } +} + +/// This struct is a straightforward instantiation of the trait separating the +/// three components in Vecs +pub struct RKRCiphertext { + aead_choice: std::marker::PhantomData, + aead_output: Vec, + hmac: Vec, + nonce: Vec, +} + +impl::OutputSize> + Aead> RKRCipher for RKRCiphertext { + type AEAD = T; + + fn new( + aead_output: Vec, + hmac: &GenericArray::OutputSize>, + nonce: &GenericArray::NonceSize>, + ) -> Self { + Self { + aead_choice: std::marker::PhantomData, + aead_output, + hmac: hmac.to_vec(), + nonce: nonce.to_vec(), + } + } + + fn aead_output(&self) -> &Vec { + &self.aead_output + } + + fn to_bytes(&self) -> Vec { + [&self.aead_output[..], &self.hmac[..], &self.nonce[..]].concat() + } + + fn hmac(&self) -> &GenericArray::OutputSize> { + GenericArray::from_slice(&self.hmac[..]) + } + + fn nonce(&self) -> &GenericArray::NonceSize> { + GenericArray::from_slice(&self.nonce[..]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chacha20poly1305::ChaCha20Poly1305; + use rand_core::OsRng; + + #[test] + fn encrypt_and_decrypt() { + let mut rng = OsRng; + let mut encryption_key = [0u8; 32]; + rng.fill_bytes(&mut encryption_key); + let mut hmac_key = [0u8; 32]; + rng.fill_bytes(&mut hmac_key); + + let mut msg = [0u8; 100]; + rng.fill_bytes(&mut msg); + + let ciphertext = RKRCiphertext::::encrypt( + &encryption_key, + &hmac_key, + &msg, + b"", + &mut rng, + ) + .unwrap(); + let decrypted = ciphertext.decrypt(&encryption_key, &hmac_key, b"").unwrap(); + assert_eq!(&msg.to_vec(), &decrypted); + } +} diff --git a/src/tests/mock_rng.rs b/src/tests/mock_rng.rs new file mode 100644 index 0000000..9af7e64 --- /dev/null +++ b/src/tests/mock_rng.rs @@ -0,0 +1,63 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use rand_core::{CryptoRng, Error, RngCore}; +use std::cmp::min; + +/// A simple implementation of `RngCore` for testing purposes. +/// +/// This generates a cyclic sequence (i.e. cycles over an initial buffer) +/// +/// +#[derive(Debug, Clone)] +pub struct CycleRng { + v: Vec, +} + +impl CycleRng { + /// Create a `CycleRng`, yielding a sequence starting with + /// `initial` and looping thereafter + pub fn new(initial: Vec) -> Self { + CycleRng { v: initial } + } +} + +fn rotate_left(data: &mut [T], steps: usize) { + if data.is_empty() { + return; + } + let steps = steps % data.len(); + + data[..steps].reverse(); + data[steps..].reverse(); + data.reverse(); +} + +impl RngCore for CycleRng { + fn next_u32(&mut self) -> u32 { + unimplemented!() + } + + #[inline] + fn next_u64(&mut self) -> u64 { + unimplemented!() + } + + #[inline] + fn fill_bytes(&mut self, dest: &mut [u8]) { + let len = min(self.v.len(), dest.len()); + (&mut dest[..len]).copy_from_slice(&self.v[..len]); + rotate_left(&mut self.v, len); + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +// This is meant for testing only +impl CryptoRng for CycleRng {} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..09ac24f --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +pub mod mock_rng; +mod opaque_ke_test; +mod serialization; diff --git a/src/tests/opaque_ke_test.rs b/src/tests/opaque_ke_test.rs new file mode 100644 index 0000000..d01434d --- /dev/null +++ b/src/tests/opaque_ke_test.rs @@ -0,0 +1,577 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::{ + errors::*, + group::Group, + key_exchange::NONCE_LEN, + keypair::{Key, KeyPair, SignalKeyPair}, + opaque::*, + tests::mock_rng::CycleRng, +}; +use aes_gcm::Aes256Gcm; +use curve25519_dalek::edwards::EdwardsPoint; +use rand_core::{OsRng, RngCore}; +use serde_json::Value; +use std::convert::TryFrom; + +// Tests +// ===== + +pub struct TestVectorParameters { + pub client_s_pk: Vec, + pub client_s_sk: Vec, + pub client_e_pk: Vec, + pub client_e_sk: Vec, + pub server_s_pk: Vec, + pub server_s_sk: Vec, + pub server_e_pk: Vec, + pub server_e_sk: Vec, + pub password: Vec, + pub blinding_factor_raw: Vec, + pub blinding_factor: Vec, + pub pepper: Vec, + pub oprf_key: Vec, + pub envelope_nonce: Vec, + pub client_nonce: Vec, + pub server_nonce: Vec, + pub r1: Vec, + pub r2: Vec, + pub r3: Vec, + pub l1: Vec, + pub l2: Vec, + pub l3: Vec, + client_registration_state: Vec, + server_registration_state: Vec, + client_login_state: Vec, + server_login_state: Vec, + pub password_file: Vec, + pub opaque_key: Vec, + pub shared_secret: Vec, +} + +static TEST_VECTOR: &str = r#" +{ + "client_s_pk": "f7b150789db3322c8c7b8c4a10ce42baa5ee846de83eaf04c17ffbd0d9e5cd60", + "client_s_sk": "601ed276a42ec5795b3471f1a64e312f192e17ff252ce6053c8ecaf210138273", + "client_e_pk": "57260d4e231035f0f3e1fb836fe5d9ddb498c956cacb5fab1d6b287e1422376c", + "client_e_sk": "e89d0fa4e387a9bd7c26466704ec30e62f58892bf3dfd1fd25133be52f34ea68", + "server_s_pk": "a2b4e12d0621ebfb2631e00f5c872ab749e1a33915f16fb11203658b2189cc5e", + "server_s_sk": "90b6ca2ea8a37306060c7cd0998d4cdae59e972af7760312f7cf77099e78f940", + "server_e_pk": "64ce4a453eb8c27b1d81f6acdc01d36d3ae6cea506432e9509917b195ad90073", + "server_e_sk": "883148cc1ba70acb1eb909d99e09493b5d4b3fe6b12c75e2f5aeea6c5d4b267f", + "password": "70617373776f7264", + "blinding_factor_raw": "b85e0df2ad0495771edf09a04b1073045e6472e2f86a41e9bab3143ebfb8eb08a3462503eb3750bf006dc82c93b37e07cdf3768018c22b431cf5146a9caeda1c", + "blinding_factor": "fac0ed1c38bc8945a91dc4d944af22c466cbffc24fc3d97b8a91798d1ec8b60f", + "pepper": "706570706572", + "oprf_key": "d5cedff72509af4702a985bb31af8dbe88d72c4eee13a09e3f52a76766fa6f0b", + "envelope_nonce": "c87e44792a9dfd8858db676e", + "client_nonce": "1f023acc6155a06166ee7e5b7ef0360277ed5da3a46adcd4a0a5bce938a67a23", + "server_nonce": "d448cb1f58c38605fc29069ac688ec9c667c99d0316b38cd1b2609c1bc14aa90", + "r1": "e46efe7d673805b6135a5293ecab13082b322c45f029595efa4b8d1d53ccd897", + "r2": "a2a3df89cf85976c4aa5add752736419f728805722571a9646983587ce4c55fb", + "r3": "374c49768e4399d4cd46e8b3bc2050e2f6737e3a2f8aee6fddc82e117f340f79a7f10c84445657c6bb4940bd02bc08ca0f107618d810ec94639e8ae43af48ab66f1f75e8bbc169eed0035e347310978bc87e44792a9dfd8858db676ef7b150789db3322c8c7b8c4a10ce42baa5ee846de83eaf04c17ffbd0d9e5cd60", + "l1": "e46efe7d673805b6135a5293ecab13082b322c45f029595efa4b8d1d53ccd8971f023acc6155a06166ee7e5b7ef0360277ed5da3a46adcd4a0a5bce938a67a2357260d4e231035f0f3e1fb836fe5d9ddb498c956cacb5fab1d6b287e1422376c", + "l2": "a2a3df89cf85976c4aa5add752736419f728805722571a9646983587ce4c55fb374c49768e4399d4cd46e8b3bc2050e2f6737e3a2f8aee6fddc82e117f340f79a7f10c84445657c6bb4940bd02bc08ca0f107618d810ec94639e8ae43af48ab66f1f75e8bbc169eed0035e347310978bc87e44792a9dfd8858db676e883148cc1ba70acb1eb909d99e09493b5d4b3fe6b12c75e2f5aeea6c5d4b267f64ce4a453eb8c27b1d81f6acdc01d36d3ae6cea506432e9509917b195ad90073d81a1104fbd599ef56228bdbe9bf7be4a38ae907a8717ca0883b9d69b2efc529", + "l3": "a01332643e8aa7113f6f160205a9b3bd0705f3b33d8e4ea8eab9eae6685a6adb", + "client_registration_state": "fac0ed1c38bc8945a91dc4d944af22c466cbffc24fc3d97b8a91798d1ec8b60f70617373776f7264", + "client_login_state": "fac0ed1c38bc8945a91dc4d944af22c466cbffc24fc3d97b8a91798d1ec8b60fe89d0fa4e387a9bd7c26466704ec30e62f58892bf3dfd1fd25133be52f34ea681f023acc6155a06166ee7e5b7ef0360277ed5da3a46adcd4a0a5bce938a67a23dd1a7c2b4e9f9be94bd36f3b6c7f23aa9f1e6b3fda9030412a918d1288b4af1970617373776f7264", + "server_registration_state": "d5cedff72509af4702a985bb31af8dbe88d72c4eee13a09e3f52a76766fa6f0b", + "server_login_state": "809f95143f8f7fc1d0b42f578a83f714f58cfd96d9499aacee730ad296b37b19c18c903396e85da607d02542d4d07456e5357ff2e2eade3aaa42e532d4e9364f66317ab0460307e33d6151e99c7406f2fa1d309f507b46e43f732924d1dc8d0d", + "password_file": "d5cedff72509af4702a985bb31af8dbe88d72c4eee13a09e3f52a76766fa6f0bf7b150789db3322c8c7b8c4a10ce42baa5ee846de83eaf04c17ffbd0d9e5cd60374c49768e4399d4cd46e8b3bc2050e2f6737e3a2f8aee6fddc82e117f340f79a7f10c84445657c6bb4940bd02bc08ca0f107618d810ec94639e8ae43af48ab66f1f75e8bbc169eed0035e347310978bc87e44792a9dfd8858db676e", + "opaque_key": "682f2868a3e1460fed5a16767bd8778c33b4aecac6607270f848aa61c95a1a68", + "shared_secret": "66317ab0460307e33d6151e99c7406f2fa1d309f507b46e43f732924d1dc8d0d" +} +"#; + +fn decode(values: &Value, key: &str) -> Option> { + values[key] + .as_str() + .and_then(|s| hex::decode(&s.to_string()).ok()) +} + +fn populate_test_vectors(values: &Value) -> TestVectorParameters { + TestVectorParameters { + client_s_pk: decode(&values, "client_s_pk").unwrap(), + client_s_sk: decode(&values, "client_s_sk").unwrap(), + client_e_pk: decode(&values, "client_e_pk").unwrap(), + client_e_sk: decode(&values, "client_e_sk").unwrap(), + server_s_pk: decode(&values, "server_s_pk").unwrap(), + server_s_sk: decode(&values, "server_s_sk").unwrap(), + server_e_pk: decode(&values, "server_e_pk").unwrap(), + server_e_sk: decode(&values, "server_e_sk").unwrap(), + password: decode(&values, "password").unwrap(), + blinding_factor_raw: decode(&values, "blinding_factor_raw").unwrap(), + blinding_factor: decode(&values, "blinding_factor").unwrap(), + pepper: decode(&values, "pepper").unwrap(), + oprf_key: decode(&values, "oprf_key").unwrap(), + envelope_nonce: decode(&values, "envelope_nonce").unwrap(), + client_nonce: decode(&values, "client_nonce").unwrap(), + server_nonce: decode(&values, "server_nonce").unwrap(), + r1: decode(&values, "r1").unwrap(), + r2: decode(&values, "r2").unwrap(), + r3: decode(&values, "r3").unwrap(), + l1: decode(&values, "l1").unwrap(), + l2: decode(&values, "l2").unwrap(), + l3: decode(&values, "l3").unwrap(), + client_registration_state: decode(&values, "client_registration_state").unwrap(), + client_login_state: decode(&values, "client_login_state").unwrap(), + server_registration_state: decode(&values, "server_registration_state").unwrap(), + server_login_state: decode(&values, "server_login_state").unwrap(), + password_file: decode(&values, "password_file").unwrap(), + opaque_key: decode(&values, "opaque_key").unwrap(), + shared_secret: decode(&values, "shared_secret").unwrap(), + } +} + +fn stringify_test_vectors(p: &TestVectorParameters) -> String { + let mut s = String::new(); + s.push_str("{\n"); + s.push_str(format!("\"client_s_pk\": \"{}\",\n", hex::encode(&p.client_s_pk)).as_str()); + s.push_str(format!("\"client_s_sk\": \"{}\",\n", hex::encode(&p.client_s_sk)).as_str()); + s.push_str(format!("\"client_e_pk\": \"{}\",\n", hex::encode(&p.client_e_pk)).as_str()); + s.push_str(format!("\"client_e_sk\": \"{}\",\n", hex::encode(&p.client_e_sk)).as_str()); + s.push_str(format!("\"server_s_pk\": \"{}\",\n", hex::encode(&p.server_s_pk)).as_str()); + s.push_str(format!("\"server_s_sk\": \"{}\",\n", hex::encode(&p.server_s_sk)).as_str()); + s.push_str(format!("\"server_e_pk\": \"{}\",\n", hex::encode(&p.server_e_pk)).as_str()); + s.push_str(format!("\"server_e_sk\": \"{}\",\n", hex::encode(&p.server_e_sk)).as_str()); + s.push_str(format!("\"password\": \"{}\",\n", hex::encode(&p.password)).as_str()); + s.push_str( + format!( + "\"blinding_factor_raw\": \"{}\",\n", + hex::encode(&p.blinding_factor_raw) + ) + .as_str(), + ); + s.push_str( + format!( + "\"blinding_factor\": \"{}\",\n", + hex::encode(&p.blinding_factor) + ) + .as_str(), + ); + s.push_str(format!("\"pepper\": \"{}\",\n", hex::encode(&p.pepper)).as_str()); + s.push_str(format!("\"oprf_key\": \"{}\",\n", hex::encode(&p.oprf_key)).as_str()); + s.push_str( + format!( + "\"envelope_nonce\": \"{}\",\n", + hex::encode(&p.envelope_nonce) + ) + .as_str(), + ); + s.push_str(format!("\"client_nonce\": \"{}\",\n", hex::encode(&p.client_nonce)).as_str()); + s.push_str(format!("\"server_nonce\": \"{}\",\n", hex::encode(&p.server_nonce)).as_str()); + s.push_str(format!("\"r1\": \"{}\",\n", hex::encode(&p.r1)).as_str()); + s.push_str(format!("\"r2\": \"{}\",\n", hex::encode(&p.r2)).as_str()); + s.push_str(format!("\"r3\": \"{}\",\n", hex::encode(&p.r3)).as_str()); + s.push_str(format!("\"l1\": \"{}\",\n", hex::encode(&p.l1)).as_str()); + s.push_str(format!("\"l2\": \"{}\",\n", hex::encode(&p.l2)).as_str()); + s.push_str(format!("\"l3\": \"{}\",\n", hex::encode(&p.l3)).as_str()); + s.push_str( + format!( + "\"client_registration_state\": \"{}\",\n", + hex::encode(&p.client_registration_state) + ) + .as_str(), + ); + s.push_str( + format!( + "\"client_login_state\": \"{}\",\n", + hex::encode(&p.client_login_state) + ) + .as_str(), + ); + s.push_str( + format!( + "\"server_registration_state\": \"{}\",\n", + hex::encode(&p.server_registration_state) + ) + .as_str(), + ); + s.push_str( + format!( + "\"server_login_state\": \"{}\",\n", + hex::encode(&p.server_login_state) + ) + .as_str(), + ); + s.push_str( + format!( + "\"password_file\": \"{}\",\n", + hex::encode(&p.password_file) + ) + .as_str(), + ); + s.push_str(format!("\"opaque_key\": \"{}\",\n", hex::encode(&p.opaque_key)).as_str()); + s.push_str(format!("\"shared_secret\": \"{}\"\n", hex::encode(&p.shared_secret)).as_str()); + s.push_str("}\n"); + s +} + +fn generate_parameters() -> TestVectorParameters { + let mut rng = OsRng; + + // Inputs + let server_s_kp = SignalKeyPair::generate_random(&mut rng).unwrap(); + let server_e_kp = SignalKeyPair::generate_random(&mut rng).unwrap(); + let client_s_kp = SignalKeyPair::generate_random(&mut rng).unwrap(); + let client_e_kp = SignalKeyPair::generate_random(&mut rng).unwrap(); + let password = b"password"; + let pepper = b"pepper"; + let mut blinding_factor_raw = [0u8; 64]; + rng.fill_bytes(&mut blinding_factor_raw); + let mut oprf_key_raw = [0u8; 32]; + rng.fill_bytes(&mut oprf_key_raw); + let mut envelope_nonce = [0u8; 12]; + rng.fill_bytes(&mut envelope_nonce); + let mut client_nonce = [0u8; NONCE_LEN]; + rng.fill_bytes(&mut client_nonce); + let mut server_nonce = [0u8; NONCE_LEN]; + rng.fill_bytes(&mut server_nonce); + + let mut blinding_factor_registration_rng = CycleRng::new(blinding_factor_raw.to_vec()); + let (r1, client_registration) = ClientRegistration::::start( + password, + Some(pepper), + &mut blinding_factor_registration_rng, + ) + .unwrap(); + let r1_bytes = r1.to_bytes().to_vec(); + let blinding_factor_bytes = client_registration.blinding_factor.to_bytes(); + let client_registration_state = client_registration.to_bytes().to_vec(); + + let mut oprf_key_rng = CycleRng::new(oprf_key_raw.to_vec()); + let (r2, server_registration) = + ServerRegistration::::start(r1, &mut oprf_key_rng) + .unwrap(); + let r2_bytes = r2.to_bytes().to_vec(); + let oprf_key = server_registration.oprf_key; + let oprf_key_bytes = EdwardsPoint::scalar_as_bytes(&oprf_key); + let server_registration_state = server_registration.to_bytes().to_vec(); + + let mut client_s_sk_and_nonce: Vec = Vec::new(); + client_s_sk_and_nonce.extend_from_slice(&client_s_kp.private()); + client_s_sk_and_nonce.extend_from_slice(&envelope_nonce); + + let mut finish_registration_rng = CycleRng::new(client_s_sk_and_nonce); + let (r3, opaque_key_registration) = client_registration + .finish::<_, SignalKeyPair>(r2, server_s_kp.public(), &mut finish_registration_rng) + .unwrap(); + let r3_bytes = r3.to_bytes().to_vec(); + + let password_file = server_registration.finish(r3).unwrap(); + let password_file_bytes = password_file.to_bytes(); + + let mut client_login_start: Vec = Vec::new(); + client_login_start.extend_from_slice(&blinding_factor_raw); + client_login_start.extend_from_slice(&client_e_kp.private()); + client_login_start.extend_from_slice(&client_nonce); + + let mut client_login_start_rng = CycleRng::new(client_login_start); + let (l1, client_login) = ClientLogin::::start( + password, + Some(pepper), + &mut client_login_start_rng, + ) + .unwrap(); + let l1_bytes = l1.to_bytes().to_vec(); + let client_login_state = client_login.to_bytes().to_vec(); + + let mut server_e_sk_rng = CycleRng::new(server_e_kp.private().to_vec()); + let (l2, server_login) = ServerLogin::start( + password_file, + server_s_kp.private(), + l1, + &mut server_e_sk_rng, + ) + .unwrap(); + let l2_bytes = l2.to_bytes().to_vec(); + let server_login_state = server_login.to_bytes().to_vec(); + + let mut client_e_sk_rng = CycleRng::new(client_e_kp.private().to_vec()); + let (l3, client_shared_secret, _opaque_key_login) = client_login + .finish(l2, server_s_kp.public(), &mut client_e_sk_rng) + .unwrap(); + let l3_bytes = l3.to_bytes().to_vec(); + + TestVectorParameters { + client_s_pk: client_s_kp.public().to_vec(), + client_s_sk: client_s_kp.private().to_vec(), + client_e_pk: client_e_kp.public().to_vec(), + client_e_sk: client_e_kp.private().to_vec(), + server_s_pk: server_s_kp.public().to_vec(), + server_s_sk: server_s_kp.private().to_vec(), + server_e_pk: server_e_kp.public().to_vec(), + server_e_sk: server_e_kp.private().to_vec(), + password: password.to_vec(), + blinding_factor_raw: blinding_factor_raw.to_vec(), + blinding_factor: blinding_factor_bytes.to_vec(), + pepper: pepper.to_vec(), + oprf_key: oprf_key_bytes.to_vec(), + envelope_nonce: envelope_nonce.to_vec(), + client_nonce: client_nonce.to_vec(), + server_nonce: server_nonce.to_vec(), + r1: r1_bytes, + r2: r2_bytes, + r3: r3_bytes, + l1: l1_bytes, + l2: l2_bytes, + l3: l3_bytes, + password_file: password_file_bytes, + client_registration_state, + server_registration_state, + client_login_state, + server_login_state, + shared_secret: client_shared_secret, + opaque_key: opaque_key_registration.to_vec(), + } +} + +#[test] +fn generate_test_vectors() { + let parameters = generate_parameters(); + println!("{}", stringify_test_vectors(¶meters)); +} + +#[test] +fn test_r1() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + let mut blinding_factor_rng = CycleRng::new(parameters.blinding_factor_raw); + let (r1, client_registration) = ClientRegistration::::start( + ¶meters.password, + Some(¶meters.pepper), + &mut blinding_factor_rng, + ) + .unwrap(); + assert_eq!(hex::encode(¶meters.r1), hex::encode(r1.to_bytes())); + assert_eq!( + hex::encode(¶meters.client_registration_state), + hex::encode(client_registration.to_bytes()) + ); + Ok(()) +} + +#[test] +fn test_r2() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + let mut oprf_key_rng = CycleRng::new(parameters.oprf_key); + let (r2, server_registration) = + ServerRegistration::::start( + RegisterFirstMessage::try_from(¶meters.r1[..]).unwrap(), + &mut oprf_key_rng, + ) + .unwrap(); + assert_eq!(hex::encode(parameters.r2), hex::encode(r2.to_bytes())); + assert_eq!( + hex::encode(¶meters.server_registration_state), + hex::encode(server_registration.to_bytes()) + ); + Ok(()) +} + +#[test] +fn test_r3() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let client_s_sk_and_nonce: Vec = + [parameters.client_s_sk, parameters.envelope_nonce].concat(); + let mut finish_registration_rng = CycleRng::new(client_s_sk_and_nonce); + let (r3, opaque_key_registration) = ClientRegistration::::try_from( + ¶meters.client_registration_state[..], + ) + .unwrap() + .finish::( + RegisterSecondMessage::try_from(¶meters.r2[..]).unwrap(), + &Key::try_from(parameters.server_s_pk).unwrap(), + &mut finish_registration_rng, + ) + .unwrap(); + + assert_eq!(hex::encode(parameters.r3), hex::encode(r3.to_bytes())); + assert_eq!( + hex::encode(parameters.opaque_key), + hex::encode(opaque_key_registration.to_vec()) + ); + + Ok(()) +} + +#[test] +fn test_password_file() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let server_registration = + ServerRegistration::::try_from( + ¶meters.server_registration_state[..], + ) + .unwrap(); + let password_file = server_registration + .finish(RegisterThirdMessage::try_from(¶meters.r3[..]).unwrap()) + .unwrap(); + + assert_eq!( + hex::encode(parameters.password_file), + hex::encode(password_file.to_bytes()) + ); + Ok(()) +} + +#[test] +fn test_l1() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let client_login_start = [ + parameters.blinding_factor_raw, + parameters.client_e_sk, + parameters.client_nonce, + ] + .concat(); + let mut client_login_start_rng = CycleRng::new(client_login_start); + let (l1, client_login) = ClientLogin::::start( + ¶meters.password, + Some(¶meters.pepper), + &mut client_login_start_rng, + ) + .unwrap(); + assert_eq!(hex::encode(¶meters.l1), hex::encode(l1.to_bytes())); + assert_eq!( + hex::encode(¶meters.client_login_state), + hex::encode(client_login.to_bytes()) + ); + Ok(()) +} + +#[test] +fn test_l2() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let mut server_e_sk_rng = CycleRng::new(parameters.server_e_sk); + let (l2, server_login) = ServerLogin::start::<_, Aes256Gcm, _, SignalKeyPair>( + ServerRegistration::try_from(¶meters.password_file[..]).unwrap(), + &Key::try_from(parameters.server_s_sk).unwrap(), + LoginFirstMessage::::try_from(¶meters.l1[..]).unwrap(), + &mut server_e_sk_rng, + ) + .unwrap(); + + assert_eq!(hex::encode(¶meters.l2), hex::encode(l2.to_bytes())); + assert_eq!( + hex::encode(¶meters.server_login_state), + hex::encode(server_login.to_bytes()) + ); + Ok(()) +} + +#[test] +fn test_l3() -> Result<(), PakeError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let mut client_e_sk_rng = CycleRng::new(parameters.client_e_sk.to_vec()); + let (l3, shared_secret, opaque_key_login) = + ClientLogin::::try_from( + ¶meters.client_login_state[..], + ) + .unwrap() + .finish( + LoginSecondMessage::::try_from(¶meters.l2[..]).unwrap(), + &Key::try_from(parameters.server_s_pk)?, + &mut client_e_sk_rng, + ) + .unwrap(); + + assert_eq!( + hex::encode(¶meters.shared_secret), + hex::encode(&shared_secret) + ); + assert_eq!(hex::encode(¶meters.l3), hex::encode(l3.to_bytes())); + assert_eq!( + hex::encode(¶meters.opaque_key), + hex::encode(opaque_key_login) + ); + + Ok(()) +} + +#[test] +fn test_server_login_finish() -> Result<(), ProtocolError> { + let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap()); + + let shared_secret = ServerLogin::try_from(¶meters.server_login_state[..]) + .unwrap() + .finish(LoginThirdMessage::try_from(¶meters.l3[..])?) + .unwrap(); + + assert_eq!( + hex::encode(parameters.shared_secret), + hex::encode(shared_secret) + ); + + Ok(()) +} + +fn test_complete_flow( + registration_password: &[u8], + login_password: &[u8], +) -> Result<(), ProtocolError> { + let mut client_rng = OsRng; + let mut server_rng = OsRng; + let server_kp = SignalKeyPair::generate_random(&mut server_rng)?; + let (register_m1, client_state) = ClientRegistration::::start( + registration_password, + None, + &mut client_rng, + )?; + let (register_m2, server_state) = + ServerRegistration::::start( + register_m1, + &mut server_rng, + )?; + let (register_m3, registration_opaque_key) = + client_state.finish(register_m2, server_kp.public(), &mut client_rng)?; + let p_file = server_state.finish(register_m3)?; + let (login_m1, client_login_state) = + ClientLogin::::start( + login_password, + None, + &mut client_rng, + )?; + let (login_m2, server_login_state) = + ServerLogin::start(p_file, &server_kp.private(), login_m1, &mut server_rng)?; + + let client_login_result = + client_login_state.finish(login_m2, &server_kp.public(), &mut client_rng); + + if hex::encode(registration_password) == hex::encode(login_password) { + let (login_m3, client_shared_secret, login_opaque_key) = client_login_result?; + let server_shared_secret = server_login_state.finish(login_m3)?; + + assert_eq!( + hex::encode(server_shared_secret), + hex::encode(client_shared_secret) + ); + assert_eq!( + hex::encode(registration_opaque_key), + hex::encode(login_opaque_key) + ); + } else { + let res = match client_login_result { + Err(ProtocolError::VerificationError(PakeError::InvalidLoginError)) => true, + _ => false, + }; + assert!(res); + } + + Ok(()) +} + +#[test] +fn test_complete_flow_success() -> Result<(), ProtocolError> { + test_complete_flow(b"good password", b"good password") +} + +#[test] +fn test_complete_flow_fail() -> Result<(), ProtocolError> { + test_complete_flow(b"good password", b"bad password") +} diff --git a/src/tests/serialization.rs b/src/tests/serialization.rs new file mode 100644 index 0000000..cf1766c --- /dev/null +++ b/src/tests/serialization.rs @@ -0,0 +1,124 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::{ + group::Group, + keypair::{KeyPair, SignalKeyPair, SizedBytes}, + opaque::*, + rkr_encryption::{RKRCipher as _, RKRCiphertext}, +}; + +use curve25519_dalek::ristretto::RistrettoPoint; + +use chacha20poly1305::ChaCha20Poly1305; +use rand_core::{OsRng, RngCore}; +use std::convert::TryFrom; + +fn random_ristretto_point() -> RistrettoPoint { + let mut rng = OsRng; + let mut bits = [0u8; 64]; + rng.fill_bytes(&mut bits); + RistrettoPoint::hash_from_bytes::(&bits) +} + +#[test] +fn client_registration_roundtrip() { + let pw = b"hunter2"; + let mut rng = OsRng; + let sc = ::random_scalar(&mut rng); + // serialization order: scalar, password + let mut bytes: Vec = vec![]; + bytes.extend_from_slice(sc.as_bytes()); + bytes.extend_from_slice(pw); + let reg = ClientRegistration::::try_from(&bytes[..]).unwrap(); + let reg_bytes = reg.to_bytes(); + assert_eq!(reg_bytes, bytes); +} + +#[test] +fn server_registration_roundtrip() { + // If we don't have envelope and client_pk, the server registration just + // contains the prf key + let mut rng = OsRng; + let sc = ::random_scalar(&mut rng); + let mut oprf_bytes: Vec = vec![]; + oprf_bytes.extend_from_slice(sc.as_bytes()); + let reg = ServerRegistration::::try_from( + &oprf_bytes[..], + ) + .unwrap(); + let reg_bytes = reg.to_bytes(); + assert_eq!(reg_bytes, oprf_bytes); + // If we do have envelope and client pk, the server registration contains + // the whole kit + let rkr_size = RKRCiphertext::::rkr_with_nonce_size(); + let mut mock_rkr_bytes = vec![0u8; rkr_size]; + rng.fill_bytes(&mut mock_rkr_bytes); + println!("{}", mock_rkr_bytes.len()); + let mock_client_kp = SignalKeyPair::generate_random(&mut rng).unwrap(); + // serialization order: scalar, public key, envelope + let mut bytes = Vec::::new(); + bytes.extend_from_slice(sc.as_bytes()); + bytes.extend_from_slice(&mock_client_kp.public().to_arr()); + bytes.extend_from_slice(&mock_rkr_bytes); + let reg = + ServerRegistration::::try_from(&bytes[..]) + .unwrap(); + let reg_bytes = reg.to_bytes(); + assert_eq!(reg_bytes, bytes); +} + +#[test] +fn register_first_message_roundtrip() { + let pt = random_ristretto_point(); + let pt_bytes = pt.to_bytes(); + let r1 = RegisterFirstMessage::::try_from(pt_bytes.as_slice()).unwrap(); + let r1_bytes = r1.to_bytes(); + assert_eq!(pt_bytes, r1_bytes); +} + +#[test] +fn register_second_message_roundtrip() { + let pt = random_ristretto_point(); + let pt_bytes = pt.to_bytes(); + + let message = pt_bytes.to_vec(); + let r2 = RegisterSecondMessage::::try_from(&message[..]).unwrap(); + let r2_bytes = r2.to_bytes(); + assert_eq!(message, r2_bytes); +} + +#[test] +fn register_third_message_roundtrip() { + let mut rng = OsRng; + let skp = SignalKeyPair::generate_random(&mut rng).unwrap(); + let pubkey_bytes = skp.public().to_arr(); + + let mut encryption_key = [0u8; 32]; + rng.fill_bytes(&mut encryption_key); + let mut hmac_key = [0u8; 32]; + rng.fill_bytes(&mut hmac_key); + + let mut msg = [0u8; 32]; + rng.fill_bytes(&mut msg); + + let ciphertext = RKRCiphertext::::encrypt( + &encryption_key, + &hmac_key, + &msg, + &pubkey_bytes, + &mut rng, + ) + .unwrap(); + + let mut message = Vec::new(); + message.extend_from_slice(&ciphertext.to_bytes()); + message.extend_from_slice(&pubkey_bytes); + + let r3 = + RegisterThirdMessage::::try_from(&message[..]).unwrap(); + let r3_bytes = r3.to_bytes(); + assert_eq!(message, r3_bytes); +}