Adding digital locker example (#117)

This commit is contained in:
Kevin Lewi
2021-01-21 20:18:57 -08:00
committed by GitHub
parent f73fc55254
commit 511d371c9a
7 changed files with 517 additions and 40 deletions
+20
View File
@@ -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
Generated
+60 -1
View File
@@ -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"
+1
View File
@@ -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"
+308
View File
@@ -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<u8>,
password_file: Vec<u8>,
}
// Given a key and plaintext, produce an AEAD ciphertext along with a nonce
fn encrypt(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
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<u8> {
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::<Default>::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::<Default>::start(
&mut server_rng,
RegistrationRequest::deserialize(&registration_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(&registration_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<String, String> {
let mut client_rng = OsRng;
let client_login_start_result = ClientLogin::<Default>::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::<Default>::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<Locker> = vec![];
loop {
display_lockers(&registered_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, &registered_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<Locker>) {
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, 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)
}
}
}
+40 -38
View File
@@ -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, 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, 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)
}
}
}
+84
View File
@@ -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
+4 -1
View File
@@ -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].
//!