//! Task structures to create a new remote secret.
use core::mem;
use const_format::formatcp;
use educe::Educe;
use libthreema_macros::{DebugVariantNames, Name, VariantNames};
use tracing::{debug, info};
use crate::{
common::{
keys::{RemoteSecret, RemoteSecretAuthenticationToken},
task::TaskLoop,
},
https::work_directory,
remote_secret::setup::{
RemoteSecretSetupContext, RemoteSecretSetupError, RemoteSecretSetupInstruction,
RemoteSecretSetupResponse,
},
utils::debug::Name as _,
};
/// 1. Update key storage according to the application data protection scheme to make use of the
/// `remote_secret`, fetchable via `remote_secret_authentication_token`.
/// 2. Purge the `remote_secret` from memory.
/// 3. Initiate the [`crate::remote_secret::monitor::RemoteSecretMonitorProtocol`] from the
/// `remote_secret_authentication_token` and the [`crate::common::keys::RemoteSecretHash`] derived from the
/// `remote_secret`.
pub struct RemoteSecretCreateResult {
/// Established remote secret.
pub remote_secret: RemoteSecret,
/// Assigned remote secret authentication token.
pub remote_secret_authentication_token: RemoteSecretAuthenticationToken,
}
/// Result of polling a [`RemoteSecretCreateTask`].
pub type RemoteSecretCreateLoop = TaskLoop<RemoteSecretSetupInstruction, RemoteSecretCreateResult>;
struct InitState {
remote_secret: RemoteSecret,
}
struct ChallengeState {
remote_secret: RemoteSecret,
response: Option<RemoteSecretSetupResponse>,
}
struct CreateState {
remote_secret: RemoteSecret,
response: Option<RemoteSecretSetupResponse>,
}
#[derive(DebugVariantNames, VariantNames)]
enum State {
Error(RemoteSecretSetupError),
Init(InitState),
Challenge(ChallengeState),
Create(CreateState),
Done,
}
impl State {
fn poll_init(context: &RemoteSecretSetupContext, state: InitState) -> (Self, RemoteSecretCreateLoop) {
debug!("Requesting challenge to create remote secret");
let request = work_directory::create_remote_secret_authentication_request(
&context.client_info,
&context.work_server_url,
&context.work_context,
context.user_identity,
&state.remote_secret,
);
(
Self::Challenge(ChallengeState {
remote_secret: state.remote_secret,
response: None,
}),
RemoteSecretCreateLoop::Instruction(RemoteSecretSetupInstruction { request }),
)
}
fn poll_challenge(
context: &RemoteSecretSetupContext,
state: ChallengeState,
) -> Result<(Self, RemoteSecretCreateLoop), RemoteSecretSetupError> {
// Ensure the caller provided the response.
let Some(response) = state.response else {
return Err(RemoteSecretSetupError::ContractViolation(formatcp!(
"{} result was not provided for '{}' state",
RemoteSecretSetupResponse::NAME,
State::CHALLENGE,
)));
};
// Handle the authentication challenge and provide the final request to create a remote secret.
let authentication =
work_directory::handle_authentication_challenge(&context.client_key, response.result)?;
info!("Creating remote secret");
let instruction = RemoteSecretSetupInstruction {
request: work_directory::create_remote_secret_request(
&context.client_info,
&context.work_server_url,
context.user_identity,
&context.work_context,
authentication,
&state.remote_secret,
),
};
Ok((
Self::Create(CreateState {
remote_secret: state.remote_secret,
response: None,
}),
RemoteSecretCreateLoop::Instruction(instruction),
))
}
fn poll_create(state: CreateState) -> Result<(Self, RemoteSecretCreateLoop), RemoteSecretSetupError> {
// Ensure the caller provided the response.
let Some(response) = state.response else {
return Err(RemoteSecretSetupError::ContractViolation(formatcp!(
"{} result was not provided for '{}' state",
RemoteSecretSetupResponse::NAME,
State::CREATE,
)));
};
// Handle the result.
let remote_secret_authentication_token =
work_directory::handle_create_remote_secret_result(response.result)?;
info!("Remote secret created");
Ok((
Self::Done,
RemoteSecretCreateLoop::Done(RemoteSecretCreateResult {
remote_secret: state.remote_secret,
remote_secret_authentication_token,
}),
))
}
}
/// Task for creating a new remote secret.
#[derive(Name, Educe)]
#[educe(Debug)]
pub struct RemoteSecretCreateTask {
#[educe(Debug(ignore))]
context: RemoteSecretSetupContext,
state: State,
}
impl RemoteSecretCreateTask {
/// Create a new task for creating a new remote secret.
#[must_use]
pub fn new(context: RemoteSecretSetupContext) -> Self {
Self {
context,
state: State::Init(InitState {
remote_secret: RemoteSecret::random(),
}),
}
}
/// Poll to advance the state.
///
/// # Errors
///
/// Returns [`RemoteSecretSetupError`] for all possible reasons.
#[tracing::instrument(skip_all, fields(?self))]
pub fn poll(&mut self) -> Result<RemoteSecretCreateLoop, RemoteSecretSetupError> {
let result = match mem::replace(
&mut self.state,
State::Error(RemoteSecretSetupError::ContractViolation(formatcp!(
"{} in a transitional state",
RemoteSecretCreateTask::NAME
))),
) {
State::Error(error) => Err(error),
State::Init(state) => Ok(State::poll_init(&self.context, state)),
State::Challenge(state) => State::poll_challenge(&self.context, state),
State::Create(state) => State::poll_create(state),
State::Done => Err(RemoteSecretSetupError::ContractViolation(formatcp!(
"{} already done",
RemoteSecretCreateTask::NAME
))),
};
match result {
Ok((state, instruction)) => {
self.state = state;
debug!(state = ?self.state, "Changed state");
Ok(instruction)
},
Err(error) => {
self.state = State::Error(error.clone());
debug!(state = ?self.state, "Changed state to error");
Err(error)
},
}
}
/// Possible results after handling a [`RemoteSecretSetupInstruction`].
///
/// # Errors
///
/// Returns [`RemoteSecretSetupError`] for all possible reasons.
#[tracing::instrument(skip_all, fields(?self))]
pub fn response(&mut self, response: RemoteSecretSetupResponse) -> Result<(), RemoteSecretSetupError> {
match &mut self.state {
State::Challenge(state) => {
let _ = state.response.insert(response);
Ok(())
},
State::Create(state) => {
let _ = state.response.insert(response);
Ok(())
},
_ => Err(RemoteSecretSetupError::ContractViolation(formatcp!(
"Must be in '{}' or '{}' state",
State::CHALLENGE,
State::CREATE,
))),
}
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use serde_json::json;
use super::*;
use crate::{https::HttpsResponse, remote_secret::setup::tests::setup_context};
#[test]
fn init() {
let context = setup_context();
let state = InitState {
remote_secret: RemoteSecret([1_u8; 32]),
};
let (state, instruction) = State::poll_init(&context, state);
let state = assert_matches!(state, State::Challenge(state) => state);
assert_eq!(state.remote_secret.0, [1_u8; 32]);
assert!(state.response.is_none());
assert_matches!(instruction, RemoteSecretCreateLoop::Instruction(_));
}
#[test]
fn challenge_without_response() {
let context = setup_context();
let state = ChallengeState {
remote_secret: RemoteSecret([1_u8; 32]),
response: None,
};
let result = State::poll_challenge(&context, state);
assert_matches!(result, Err(RemoteSecretSetupError::ContractViolation(_)));
}
#[test]
fn challenge_invalid() -> anyhow::Result<()> {
let context = setup_context();
let state = ChallengeState {
remote_secret: RemoteSecret([1_u8; 32]),
response: Some(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"challenge": "bWVvdw==",
}))?,
}),
}),
};
let result = State::poll_challenge(&context, state);
assert_matches!(result, Err(RemoteSecretSetupError::ServerError(_)));
Ok(())
}
#[test]
fn challenge_invalid_public_key() -> anyhow::Result<()> {
let context = setup_context();
let state = ChallengeState {
remote_secret: RemoteSecret([1_u8; 32]),
response: Some(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
// All-zero public key.
"challengePublicKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"challenge": "bWVvdw==",
}))?,
}),
}),
};
let result = State::poll_challenge(&context, state);
assert_matches!(result, Err(RemoteSecretSetupError::ServerError(_)));
Ok(())
}
#[test]
fn challenge_valid() -> anyhow::Result<()> {
let context = setup_context();
let state = ChallengeState {
remote_secret: RemoteSecret([1_u8; 32]),
response: Some(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"challengePublicKey": "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"challenge": "bWVvdw==",
}))?,
}),
}),
};
let (state, instruction) = State::poll_challenge(&context, state)?;
let state = assert_matches!(state, State::Create(state) => state);
assert_eq!(state.remote_secret.0, [1_u8; 32]);
assert!(state.response.is_none());
assert_matches!(instruction, RemoteSecretCreateLoop::Instruction(_));
Ok(())
}
#[test]
fn create_without_response() {
let state = CreateState {
remote_secret: RemoteSecret([1_u8; 32]),
response: None,
};
let result = State::poll_create(state);
assert_matches!(result, Err(RemoteSecretSetupError::ContractViolation(_)));
}
#[test]
fn create_invalid() -> anyhow::Result<()> {
let state = CreateState {
remote_secret: RemoteSecret([1_u8; 32]),
response: Some(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"secretAuthenticationToken": "bWVvdw==",
}))?,
}),
}),
};
let result = State::poll_create(state);
assert_matches!(result, Err(RemoteSecretSetupError::ServerError(_)));
Ok(())
}
#[test]
fn create_valid() -> anyhow::Result<()> {
let state = CreateState {
remote_secret: RemoteSecret([1_u8; 32]),
response: Some(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"secretAuthenticationToken": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=",
}))?,
}),
}),
};
let (state, instruction) = State::poll_create(state)?;
assert_matches!(state, State::Done);
let result = assert_matches!(instruction, RemoteSecretCreateLoop::Done(result) => result);
assert_eq!(result.remote_secret.0, [1_u8; 32]);
assert_eq!(result.remote_secret_authentication_token.0, [2_u8; 32]);
Ok(())
}
#[test]
fn unexpected_response() {
let mut task = RemoteSecretCreateTask::new(setup_context());
let result = task.response(RemoteSecretSetupResponse {
result: Err(crate::https::HttpsError::Timeout("isso".to_owned())),
});
assert_matches!(result, Err(RemoteSecretSetupError::ContractViolation(_)));
}
#[test]
fn unexpected_poll_after_error() {
let mut task = RemoteSecretCreateTask {
context: setup_context(),
state: State::Error(RemoteSecretSetupError::NetworkError(
"meh, Server hatte keine Lust".to_owned(),
)),
};
let result = task.poll();
assert_matches!(result, Err(RemoteSecretSetupError::NetworkError(_)));
}
#[test]
fn unexpected_poll_when_done() {
let mut task = RemoteSecretCreateTask {
context: setup_context(),
state: State::Done,
};
let result = task.poll();
assert_matches!(result, Err(RemoteSecretSetupError::ContractViolation(_)));
}
#[test]
fn complete_task() -> anyhow::Result<()> {
// Init state.
let mut task = RemoteSecretCreateTask::new(setup_context());
let remote_secret = assert_matches!(&task.state, State::Init(state) => state.remote_secret.0);
// Challenge state.
let instruction = task.poll()?;
assert_matches!(task.state, State::Challenge(_));
assert_matches!(instruction, RemoteSecretCreateLoop::Instruction(_));
// Create state.
task.response(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"challengePublicKey": "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"challenge": "bWVvdw==",
}))?,
}),
})?;
let instruction = task.poll()?;
assert_matches!(task.state, State::Create(_));
assert_matches!(instruction, RemoteSecretCreateLoop::Instruction(_));
// Done state.
task.response(RemoteSecretSetupResponse {
result: Ok(HttpsResponse {
status: 200,
body: serde_json::to_vec(&json!({
"secretAuthenticationToken": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=",
}))?,
}),
})?;
let instruction = task.poll()?;
assert_matches!(task.state, State::Done);
let result = assert_matches!(instruction, RemoteSecretCreateLoop::Done(result) => result);
assert_eq!(result.remote_secret.0, remote_secret);
assert_eq!(result.remote_secret_authentication_token.0, [2_u8; 32]);
Ok(())
}
}
¤ Dauer der Verarbeitung: 0.5 Sekunden
¤
*© Formatika GbR, Deutschland
|
|