From 511d371c9acbb2a8a01bcd55951f3c35607d59ba Mon Sep 17 00:00:00 2001 From: Kevin Lewi Date: Thu, 21 Jan 2021 20:18:57 -0800 Subject: [PATCH] Adding digital locker example (#117) --- .github/workflows/main.yml | 20 +++ Cargo.lock | 61 +++++++- Cargo.toml | 1 + examples/digital_locker.rs | 308 +++++++++++++++++++++++++++++++++++++ examples/simple_login.rs | 78 +++++----- scripts/digital_locker.exp | 84 ++++++++++ src/lib.rs | 5 +- 7 files changed, 517 insertions(+), 40 deletions(-) create mode 100644 examples/digital_locker.rs create mode 100644 scripts/digital_locker.exp diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9bc2047..1d1ff4f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,6 +91,26 @@ jobs: - name: Run expect (which then runs cargo run) run: expect -f scripts/simple_login.exp + digital-locker-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: test digital_locker command-line example + steps: + - name: install expect + run: sudo apt-get install expect + - name: Checkout sources + uses: actions/checkout@v2 + - name: install rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt, clippy + - name: Run expect (which then runs cargo run) + run: expect -f scripts/digital_locker.exp + benches: name: cargo bench compilation runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index fb821ce..8c30e27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + [[package]] name = "anyhow" version = "1.0.35" @@ -133,6 +142,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed8738f14471a99f0e316c327e68fc82a3611cc2895fcb604b89eedaf8f39d95" +dependencies = [ + "cipher", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fc18e6d90c40164bf6c317476f2a98f04661e310e79830366b7e914c58a8e" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "cipher" version = "0.2.5" @@ -165,6 +197,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + [[package]] name = "criterion" version = "0.3.3" @@ -570,6 +608,7 @@ version = "0.3.0-pre.1" dependencies = [ "anyhow", "base64", + "chacha20poly1305", "criterion", "curve25519-dalek", "digest", @@ -616,6 +655,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "poly1305" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7456bc1ad2d4cf82b3a016be4c2ac48daf11bf990c1603ebd447fe6f30fca8" +dependencies = [ + "cpuid-bool 0.2.0", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.8" @@ -952,7 +1001,7 @@ checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpuid-bool", + "cpuid-bool 0.1.2", "digest", "opaque-debug", ] @@ -1063,6 +1112,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "utf8parse" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index d3c6300..c80ccfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ zeroize = "1.1.1" [dev-dependencies] anyhow = "1.0.35" base64 = "0.13.0" +chacha20poly1305 = "0.7.1" criterion = "0.3.3" hex = "0.4.2" lazy_static = "1.4.0" diff --git a/examples/digital_locker.rs b/examples/digital_locker.rs new file mode 100644 index 0000000..7897de4 --- /dev/null +++ b/examples/digital_locker.rs @@ -0,0 +1,308 @@ +// 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. + +//! Demonstrates an implementation of a server-side secured digital locker using +//! the client's OPAQUE export key, over a command-line interface +//! +//! A client can password-protect a secret message to be stored in a digital locker, +//! controlled by the server. The locker's contents are only revealed to the holder +//! of the password when attempting to open the locker. +//! +//! The client-server interactions are executed in a three-step protocol +//! within the account_registration (for password registration) and +//! account_login (for password login) functions. These steps +//! must be performed in the specific sequence outlined in each of these +//! functions. +//! +//! The CipherSuite trait allows the application to configure the +//! primitives used by OPAQUE, but must be kept consistent across the steps +//! of the protocol. +//! +//! In a more realistic client-server interaction, the client must send +//! messages over "the wire" to the server. These bytes are serialized +//! and explicitly annotated in the below functions. + +use chacha20poly1305::aead::{Aead, NewAead}; +use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use rand_core::{OsRng, RngCore}; +use rustyline::error::ReadlineError; +use rustyline::Editor; +use std::convert::TryFrom; +use std::process::exit; + +use opaque_ke::{ + ciphersuite::CipherSuite, keypair::KeyPair, ClientLogin, ClientLoginFinishParameters, + ClientLoginStartParameters, ClientRegistration, ClientRegistrationFinishParameters, + CredentialRequest, CredentialResponse, RegistrationRequest, RegistrationResponse, + RegistrationUpload, ServerLogin, ServerLoginStartParameters, ServerRegistration, +}; + +// The ciphersuite trait allows to specify the underlying primitives +// that will be used in the OPAQUE protocol +#[allow(dead_code)] +struct Default; +impl CipherSuite for Default { + type Group = curve25519_dalek::ristretto::RistrettoPoint; + type KeyFormat = opaque_ke::keypair::X25519KeyPair; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDH; + type Hash = sha2::Sha256; + type SlowHash = opaque_ke::slow_hash::NoOpHash; +} + +struct Locker { + contents: Vec, + password_file: Vec, +} + +// Given a key and plaintext, produce an AEAD ciphertext along with a nonce +fn encrypt(key: &[u8], plaintext: &[u8]) -> Vec { + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); + + let mut rng = OsRng; + let mut nonce_bytes = [0u8; 12]; + rng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap(); + [nonce_bytes.to_vec(), ciphertext].concat() +} + +// Decrypt using a key and a ciphertext (nonce included) to recover the original plaintext +fn decrypt(key: &[u8], ciphertext: &[u8]) -> Vec { + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); + cipher + .decrypt( + Nonce::from_slice(&ciphertext[..12]), + ciphertext[12..].as_ref(), + ) + .unwrap() +} + +// Password-based registration and encryption of client secret message between a client and server +fn register_locker( + server_kp: &opaque_ke::keypair::X25519KeyPair, + password: String, + secret_message: String, +) -> Locker { + let mut client_rng = OsRng; + let client_registration_start_result = + ClientRegistration::::start(&mut client_rng, password.as_bytes()).unwrap(); + let registration_request_bytes = client_registration_start_result.message.serialize(); + + // Client sends registration_request_bytes to server + + let mut server_rng = OsRng; + let server_registration_start_result = ServerRegistration::::start( + &mut server_rng, + RegistrationRequest::deserialize(®istration_request_bytes[..]).unwrap(), + server_kp.public(), + ) + .unwrap(); + let registration_response_bytes = server_registration_start_result.message.serialize(); + + // Server sends registration_response_bytes to client + + let client_finish_registration_result = client_registration_start_result + .state + .finish( + &mut client_rng, + RegistrationResponse::deserialize(®istration_response_bytes[..]).unwrap(), + ClientRegistrationFinishParameters::default(), + ) + .unwrap(); + let message_bytes = client_finish_registration_result.message.serialize(); + + // Client encrypts secret message using export key + let ciphertext = encrypt( + &client_finish_registration_result.export_key, + secret_message.as_bytes(), + ); + + // Client sends message_bytes to server + + let password_file = server_registration_start_result + .state + .finish(RegistrationUpload::deserialize(&message_bytes[..]).unwrap()) + .unwrap(); + + Locker { + contents: ciphertext, + password_file: password_file.to_bytes(), + } +} + +// Open the contents of a locker with a password between a client and server +fn open_locker( + server_kp: &opaque_ke::keypair::X25519KeyPair, + password: String, + locker: &Locker, +) -> Result { + let mut client_rng = OsRng; + let client_login_start_result = ClientLogin::::start( + &mut client_rng, + password.as_bytes(), + ClientLoginStartParameters::default(), + ) + .unwrap(); + let credential_request_bytes = client_login_start_result.message.serialize(); + + // Client sends credential_request_bytes to server + + let password_file = ServerRegistration::::try_from(&locker.password_file[..]).unwrap(); + let mut server_rng = OsRng; + let server_login_start_result = ServerLogin::start( + &mut server_rng, + password_file, + &server_kp.private(), + CredentialRequest::deserialize(&credential_request_bytes[..]).unwrap(), + ServerLoginStartParameters::default(), + ) + .unwrap(); + let credential_response_bytes = server_login_start_result.message.serialize(); + + // Server sends credential_response_bytes to client + + let result = client_login_start_result.state.finish( + CredentialResponse::deserialize(&credential_response_bytes[..]).unwrap(), + ClientLoginFinishParameters::default(), + ); + + if result.is_err() { + // Client-detected login failure + return Err(String::from("Incorrect password, please try again.")); + } + let client_login_finish_result = result.unwrap(); + + // Decrypt contents of locker + let plaintext = decrypt(&client_login_finish_result.export_key, &locker.contents); + String::from_utf8(plaintext).map_err(|_| String::from("UTF8 error")) +} + +fn main() { + let mut rng = OsRng; + let server_kp = Default::generate_random_keypair(&mut rng).unwrap(); + + let mut rl = Editor::<()>::new(); + let mut registered_lockers: Vec = vec![]; + loop { + display_lockers(®istered_lockers); + + println!("Enter an option (1 or 2):"); + println!("1) Register a locker"); + println!("2) Open a locker\n"); + let readline = rl.readline("> "); + match readline { + Ok(line) => { + if line != "1" && line != "2" { + println!("Error: Invalid option (either specify 1 or 2)"); + continue; + } + match line.as_ref() { + "1" => { + let (password, secret_message) = get_two_strings( + "Choose a password", + "Set a secret message", + &mut rl, + None, + ); + registered_lockers.push(register_locker( + &server_kp, + password, + secret_message, + )); + continue; + } + "2" => { + let (locker, password) = get_two_strings( + "Choose a locker number", + "Enter the password", + &mut rl, + None, + ); + let locker_index: usize = match locker.parse() { + Ok(index) => index, + Err(_) => { + println!("Could not find locker number"); + continue; + } + }; + + if locker_index >= registered_lockers.len() { + println!("Could not find locker number"); + continue; + } + + match open_locker(&server_kp, password, ®istered_lockers[locker_index]) { + Ok(contents) => { + println!("\n\nSuccess! Contents: {}\n\n", contents); + } + Err(err) => { + println!( + "\n\nError encountered, could not open locker: {}\n\n", + err + ); + } + } + } + _ => exit(0), + } + } + Err(err) => { + handle_error(err); + exit(0) + } + } + } +} + +// Helper functions + +fn display_lockers(lockers: &Vec) { + let mut locker_numbers = vec![]; + for (i, _) in lockers.iter().enumerate() { + locker_numbers.push(i); + } + + println!( + "\nCurrently registered locker numbers: {:?}\n", + locker_numbers + ); +} + +// Handle readline errors +fn handle_error(err: ReadlineError) { + match err { + ReadlineError::Interrupted => { + println!("CTRL-C"); + } + ReadlineError::Eof => { + println!("CTRL-D"); + } + err => { + println!("Error: {:?}", err); + } + } +} + +// A function run on the client which extracts two strings from the CLI +fn get_two_strings( + s1: &str, + s2: &str, + rl: &mut Editor<()>, + string1: Option, +) -> (String, String) { + let query = if string1.is_none() { s1 } else { s2 }; + let readline = rl.readline(&format!("{}: ", query)); + match readline { + Ok(line) => match string1 { + Some(x) => (x, line), + None => get_two_strings(s1, s2, rl, Some(line)), + }, + Err(err) => { + handle_error(err); + exit(0) + } + } +} diff --git a/examples/simple_login.rs b/examples/simple_login.rs index 9d24a6b..ce1357c 100644 --- a/examples/simple_login.rs +++ b/examples/simple_login.rs @@ -142,34 +142,6 @@ fn account_login( client_login_finish_result.shared_secret == server_login_finish_result.shared_secret } -// A function run on the client which extracts a username and password from the CLI -fn get_username_and_password(rl: &mut Editor<()>, username: Option) -> (String, String) { - let query = if username.is_none() { - "Username: " - } else { - "Password: " - }; - let readline = rl.readline(query); - match readline { - Ok(line) => match username { - Some(x) => (x, line), - None => get_username_and_password(rl, Some(line)), - }, - Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); - exit(0) - } - Err(ReadlineError::Eof) => { - println!("CTRL-D"); - exit(0) - } - Err(err) => { - println!("Error: {:?}", err); - exit(0) - } - } -} - fn main() { let mut rng = OsRng; let server_kp = Default::generate_random_keypair(&mut rng).unwrap(); @@ -192,7 +164,7 @@ fn main() { println!("Error: Invalid option (either specify 1 or 2)"); continue; } - let (username, password) = get_username_and_password(&mut rl, None); + let (username, password) = get_two_strings("Username", "Password", &mut rl, None); match line.as_ref() { "1" => { registered_users @@ -217,18 +189,48 @@ fn main() { _ => exit(0), } } - Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); - exit(0) - } - Err(ReadlineError::Eof) => { - println!("CTRL-D"); - exit(0) - } Err(err) => { - println!("Error: {:?}", err); + handle_error(err); exit(0) } } } } + +// Helper functions + +// Handle readline errors +fn handle_error(err: ReadlineError) { + match err { + ReadlineError::Interrupted => { + println!("CTRL-C"); + } + ReadlineError::Eof => { + println!("CTRL-D"); + } + err => { + println!("Error: {:?}", err); + } + } +} + +// A function run on the client which extracts two strings from the CLI +fn get_two_strings( + s1: &str, + s2: &str, + rl: &mut Editor<()>, + string1: Option, +) -> (String, String) { + let query = if string1.is_none() { s1 } else { s2 }; + let readline = rl.readline(&format!("{}: ", query)); + match readline { + Ok(line) => match string1 { + Some(x) => (x, line), + None => get_two_strings(s1, s2, rl, Some(line)), + }, + Err(err) => { + handle_error(err); + exit(0) + } + } +} diff --git a/scripts/digital_locker.exp b/scripts/digital_locker.exp new file mode 100644 index 0000000..c36c8ac --- /dev/null +++ b/scripts/digital_locker.exp @@ -0,0 +1,84 @@ +#!/bin/expect -f +# 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. + +set timeout 1 +spawn cargo run --example digital_locker +match_max 100000 +sleep 1 +expect "* +\r +Currently registered locker numbers: \\\[\\\]\r +\r +Enter an option (1 or 2):\r +1) Register a locker\r +2) Open a locker\r +\r +" +sleep .1 +send -- "1\r" +expect "Choose a password: \r" +send -- "foo\r" +expect "Set a secret message: \r" +send -- "bar\r" +expect "* +\r +Currently registered locker numbers: \\\[0\\\]\r +\r +Enter an option (1 or 2):\r +1) Register a locker\r +2) Open a locker\r +\r +" +sleep .1 +send -- "2\r" +expect "Choose a locker number: \r" +send -- "1\r" +expect "Password: \r" +send -- "foo\r" +expect "*Error: Could not find locker number\r +\r +Currently registered locker numbers: \\\[0\\\]\r +\r +Enter an option (1 or 2):\r +1) Register a locker\r +2) Open a locker\r +\r +" +sleep .1 +send -- "2\r" +expect "Choose a locker number: \r" +send -- "0\r" +expect "Password: \r" +send -- "baz\r" +expect "*Error encountered, could not open locker: Incorrect password, please try again.\r +\r +Currently registered locker numbers: \\\[0\\\]\r +\r +Enter an option (1 or 2):\r +1) Register a locker\r +2) Open a locker\r +\r +" +sleep .1 +send -- "2\r" +expect "Choose a locker number: \r" +send -- "0\r" +expect "Password: \r" +send -- "foo\r" +expect "* +\r +*Success! Contents: bar\r +\r +Currently registered locker numbers: \\\[0\\\]\r +\r +Enter an option (1 or 2):\r +1) Register a locker\r +2) Open a locker\r +\r +" +sleep .1 +send -- "" +expect eof diff --git a/src/lib.rs b/src/lib.rs index 3ea14a3..28ea40f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,8 @@ //! type SlowHash = opaque_ke::slow_hash::NoOpHash; //! } //! ``` +//! See [examples/simple_login.rs](https://github.com/novifinancial/opaque-ke/blob/master/examples/simple_login.rs) +//! for a working example of a simple password-based login using OPAQUE. //! //! Note that our choice of slow hashing function in this example, `NoOpHash`, is selected only to ensure //! that the tests execute quickly. A real application should use an actual slow hashing function, such as `scrypt`, @@ -509,7 +511,8 @@ //! know the password the client uses during registration and login can recover this secret, as it is never exposed to the server. As a result, the export key //! can be used (separately from the OPAQUE protocol) to provide confidentiality and integrity to other data which only the client should be able to process. //! For instance, if the server is expected to maintain any client-side secrets which require a password to access, then this export key can be used to encrypt -//! these secrets so that they remain hidden from the server. +//! these secrets so that they remain hidden from the server (see [examples/digital_locker.rs](https://github.com/novifinancial/opaque-ke/blob/master/examples/digital_locker.rs) +//! for a working example). //! //! You can access the export key from the `export_key` field of [ClientRegistrationFinishResult] and [ClientLoginFinishResult]. //!