Rust CI / cargo audit (pull_request) Has been cancelled
Rust CI / cargo fmt (pull_request) Failing after 3s
Rust CI / cargo clippy (pull_request) Successful in 1m27s
Rust CI / test (stable / no backend / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / no backend / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / no backend / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / no backend / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / no backend / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features curve25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features curve25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features curve25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features curve25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features curve25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features curve25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ecdsa / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features ecdsa / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ecdsa / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features ecdsa / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ecdsa / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features ecdsa / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ed25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features ed25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ed25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features ed25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ed25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features ed25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255 / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255 / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255 / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255 / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,curve25519,ecdsa,ed25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,curve25519,ecdsa,ed25519 / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,curve25519,ecdsa,ed25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,curve25519,ecdsa,ed25519 / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,curve25519,ecdsa,ed25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,curve25519,ecdsa,ed25519 / --features serde) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,kem / no frontend) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,kem / no frontend) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,kem / --features argon2) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,kem / --features argon2) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / --features ristretto255,kem / --features serde) (pull_request) Has been cancelled
Rust CI / test (stable / --features ristretto255,kem / --features serde) (pull_request) Has been cancelled
Rust CI / test simple_login example (pull_request) Has been cancelled
Rust CI / test digital_locker example (pull_request) Has been cancelled
Rust CI / cargo bench compilation () (pull_request) Has been cancelled
Rust CI / cargo bench compilation (--features ristretto255) (pull_request) Has been cancelled
Rust CI / cargo bench compilation (--features ristretto255,kem) (pull_request) Has been cancelled
Rust CI / test (1.90.0 / no backend / no frontend) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / no backend) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / no backend) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / curve25519) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / curve25519) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / ecdsa) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / ecdsa) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / ed25519) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / ed25519) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / ristretto255) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / ristretto255) (pull_request) Has been cancelled
Rust CI / no-std (thumbv6m-none-eabi / ristretto255,curve25519,ecdsa,ed25519) (pull_request) Has been cancelled
Rust CI / no-std (wasm32-unknown-unknown / ristretto255,curve25519,ecdsa,ed25519) (pull_request) Has been cancelled
- Added VexaHub copyright alongside original Meta copyright - Replaced verbose license blocks with SPDX-License-Identifier - Minor code improvements
342 lines
12 KiB
Rust
342 lines
12 KiB
Rust
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
// Copyright (c) VexaHub and contributors.
|
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
|
|
//! 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 std::process::exit;
|
|
|
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
|
use opaque_vx::ciphersuite::CipherSuite;
|
|
use opaque_vx::rand::Rng;
|
|
use opaque_vx::rand::rngs::SysRng;
|
|
use opaque_vx::{
|
|
ClientLogin, ClientLoginFinishParameters, ClientRegistration,
|
|
ClientRegistrationFinishParameters, CredentialFinalization, CredentialRequest,
|
|
CredentialResponse, RegistrationRequest, RegistrationResponse, RegistrationUpload, ServerLogin,
|
|
ServerLoginParameters, ServerRegistration, ServerSetup,
|
|
};
|
|
use rand_core::UnwrapErr;
|
|
use rustyline::Editor;
|
|
use rustyline::error::ReadlineError;
|
|
use rustyline::history::DefaultHistory;
|
|
|
|
// The ciphersuite trait allows to specify the underlying primitives that will
|
|
// be used in the OPAQUE protocol
|
|
#[allow(dead_code)]
|
|
struct DefaultCipherSuite;
|
|
|
|
#[cfg(feature = "ristretto255")]
|
|
impl CipherSuite for DefaultCipherSuite {
|
|
type OprfCs = opaque_vx::Ristretto255;
|
|
type KeyExchange = opaque_vx::TripleDh<opaque_vx::Ristretto255, sha2::Sha512>;
|
|
type Ksf = opaque_vx::ksf::Identity;
|
|
}
|
|
|
|
#[cfg(not(feature = "ristretto255"))]
|
|
impl CipherSuite for DefaultCipherSuite {
|
|
type OprfCs = p256::NistP256;
|
|
type KeyExchange = opaque_vx::TripleDh<p256::NistP256, sha2::Sha256>;
|
|
type Ksf = opaque_vx::ksf::Identity;
|
|
}
|
|
|
|
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::try_from(&key[..32]).unwrap());
|
|
|
|
let mut rng = UnwrapErr(SysRng);
|
|
let mut nonce_bytes = [0u8; 12];
|
|
rng.fill_bytes(&mut nonce_bytes);
|
|
let nonce = Nonce::try_from(&nonce_bytes[..]).unwrap();
|
|
|
|
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::try_from(&key[..32]).unwrap());
|
|
cipher
|
|
.decrypt(
|
|
&Nonce::try_from(&ciphertext[..12]).unwrap(),
|
|
ciphertext[12..].as_ref(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
// Password-based registration and encryption of client secret message between a
|
|
// client and server
|
|
fn register_locker(
|
|
server_setup: &ServerSetup<DefaultCipherSuite>,
|
|
locker_id: usize,
|
|
password: String,
|
|
secret_message: String,
|
|
) -> Locker {
|
|
let mut client_rng = UnwrapErr(SysRng);
|
|
let client_registration_start_result =
|
|
ClientRegistration::<DefaultCipherSuite>::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 server_registration_start_result = ServerRegistration::<DefaultCipherSuite>::start(
|
|
server_setup,
|
|
RegistrationRequest::deserialize(®istration_request_bytes).unwrap(),
|
|
&locker_id.to_be_bytes(),
|
|
)
|
|
.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,
|
|
password.as_bytes(),
|
|
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 = ServerRegistration::finish(
|
|
RegistrationUpload::<DefaultCipherSuite>::deserialize(&message_bytes).unwrap(),
|
|
);
|
|
|
|
Locker {
|
|
contents: ciphertext,
|
|
password_file: password_file.serialize().to_vec(),
|
|
}
|
|
}
|
|
|
|
// Open the contents of a locker with a password between a client and server
|
|
fn open_locker(
|
|
server_setup: &ServerSetup<DefaultCipherSuite>,
|
|
locker_id: usize,
|
|
password: String,
|
|
locker: &Locker,
|
|
) -> Result<String, String> {
|
|
let mut client_rng = UnwrapErr(SysRng);
|
|
let client_login_start_result =
|
|
ClientLogin::<DefaultCipherSuite>::start(&mut client_rng, password.as_bytes()).unwrap();
|
|
let credential_request_bytes = client_login_start_result.message.serialize();
|
|
|
|
// Client sends credential_request_bytes to server
|
|
|
|
let password_file =
|
|
ServerRegistration::<DefaultCipherSuite>::deserialize(&locker.password_file).unwrap();
|
|
let mut server_rng = UnwrapErr(SysRng);
|
|
let server_login_start_result = ServerLogin::start(
|
|
&mut server_rng,
|
|
server_setup,
|
|
Some(password_file),
|
|
CredentialRequest::deserialize(&credential_request_bytes).unwrap(),
|
|
&locker_id.to_be_bytes(),
|
|
ServerLoginParameters::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(
|
|
&mut client_rng,
|
|
password.as_bytes(),
|
|
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();
|
|
let credential_finalization_bytes = client_login_finish_result.message.serialize();
|
|
|
|
// Client sends credential_finalization_bytes to server
|
|
|
|
let server_login_finish_result = server_login_start_result
|
|
.state
|
|
.finish(
|
|
CredentialFinalization::deserialize(&credential_finalization_bytes).unwrap(),
|
|
ServerLoginParameters::default(),
|
|
)
|
|
.unwrap();
|
|
|
|
// Server sends locker contents, encrypted under the session key, to the client
|
|
let encrypted_locker_contents =
|
|
encrypt(&server_login_finish_result.session_key, &locker.contents);
|
|
|
|
// Client decrypts contents of locker, first under the session key, and then
|
|
let plaintext = decrypt(
|
|
&client_login_finish_result.export_key,
|
|
&decrypt(
|
|
&client_login_finish_result.session_key,
|
|
&encrypted_locker_contents,
|
|
),
|
|
);
|
|
String::from_utf8(plaintext).map_err(|_| String::from("UTF8 error"))
|
|
}
|
|
|
|
fn main() {
|
|
let mut rng = UnwrapErr(SysRng);
|
|
let server_setup = ServerSetup::<DefaultCipherSuite>::new(&mut rng);
|
|
|
|
let mut rl = Editor::<(), _>::new().unwrap();
|
|
let mut registered_lockers: Vec<Locker> = 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,
|
|
);
|
|
let locker_id = registered_lockers.len();
|
|
registered_lockers.push(register_locker(
|
|
&server_setup,
|
|
locker_id,
|
|
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!("Error: Could not find locker number");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if locker_index >= registered_lockers.len() {
|
|
println!("Error: Could not find locker number");
|
|
continue;
|
|
}
|
|
|
|
match open_locker(
|
|
&server_setup,
|
|
locker_index,
|
|
password,
|
|
®istered_lockers[locker_index],
|
|
) {
|
|
Ok(contents) => {
|
|
println!("\n\nSuccess! Contents: {contents}\n\n");
|
|
}
|
|
Err(err) => {
|
|
println!("\n\nError encountered, could not open locker: {err}\n\n");
|
|
}
|
|
}
|
|
}
|
|
_ => exit(0),
|
|
}
|
|
}
|
|
Err(err) => {
|
|
handle_error(err);
|
|
exit(0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
fn display_lockers(lockers: &[Locker]) {
|
|
let mut locker_numbers = vec![];
|
|
for (i, _) in lockers.iter().enumerate() {
|
|
locker_numbers.push(i);
|
|
}
|
|
|
|
println!("\nCurrently registered locker numbers: {locker_numbers:?}\n");
|
|
}
|
|
|
|
// 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<(), DefaultHistory>,
|
|
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)
|
|
}
|
|
}
|
|
}
|