Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

Commit

Permalink
feat(proxy): Use password from user to unseal key store (#1153)
Browse files Browse the repository at this point in the history
feat(proxy): use password from user to unseal key store
  • Loading branch information
Thomas Scholtes committed Nov 5, 2020
1 parent cfa046d commit 0f3290e
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 250 deletions.
2 changes: 1 addition & 1 deletion cypress/integration/lock.spec.js
Expand Up @@ -14,7 +14,7 @@ context("lock screen", () => {
cy.pick("unlock-button").should("exist");
cy.pick("passphrase-input").type("wrong-pw");
cy.pick("unlock-button").click();
cy.contains(/Could not unlock the session: Passphrase incorrect/).should(
cy.contains(/Could not unlock the session: incorrect passphrase/).should(
"exist"
);
cy.pick("passphrase-input").should("have.value", "wrong-pw");
Expand Down
75 changes: 0 additions & 75 deletions cypress/integration/onboarding.spec.js
Expand Up @@ -69,81 +69,6 @@ context("onboarding", () => {
cy.pick("entity-name").contains(validUser.handle);
});

it("is possible to create a second identity", () => {
// Intro screen.
cy.pick("get-started-button").click();

// Enter name screen.
cy.pick("handle-input").type(validUser.handle);
cy.pick("next-button").click();

// Enter passphrase screen.
cy.pick("passphrase-input").type(validUser.passphrase);
cy.pick("repeat-passphrase-input").type(validUser.passphrase);
cy.pick("set-passphrase-button").click();

// Success screen.
cy.pick("urn").contains(radIdRegex).should("exist");

// Land on profile screen.
cy.pick("go-to-profile-button").click();
cy.pick("entity-name").contains(validUser.handle);

// Clear session to restart onboarding.
cy.pick("sidebar", "settings").click();
cy.pick("clear-session-button").click();
cy.contains("A free and open-source way to host").should("exist");

cy.pick("get-started-button").click();
cy.pick("handle-input").type("cloudhead");
cy.pick("next-button").click();
cy.pick("passphrase-input").type("1234");
cy.pick("repeat-passphrase-input").type("1234");
cy.pick("set-passphrase-button").click();
cy.pick("urn").contains(radIdRegex).should("exist");
cy.pick("go-to-profile-button").click();
cy.pick("entity-name").contains("cloudhead");
});

it("is not possible to create the same identity again", () => {
// Intro screen.
cy.pick("get-started-button").click();

// Enter name screen.
cy.pick("handle-input").type(validUser.handle);
cy.pick("next-button").click();

// Enter passphrase screen.
cy.pick("passphrase-input").type(validUser.passphrase);
cy.pick("repeat-passphrase-input").type(validUser.passphrase);
cy.pick("set-passphrase-button").click();

// Success screen.
cy.pick("urn").contains(radIdRegex).should("exist");
cy.pick("go-to-profile-button").click();

cy.log("reset session");
// Clear session to restart onboarding.
cy.pick("sidebar", "settings").click();
cy.pick("clear-session-button").click();
cy.contains("A free and open-source way to host").should("exist");

// When creating the same identity again without resetting all data, it
// should show an error and return to the name entry screen.
cy.pick("get-started-button").click();

cy.pick("handle-input").type(validUser.handle);
cy.pick("next-button").click();

cy.pick("passphrase-input").type(validUser.passphrase);
cy.pick("repeat-passphrase-input").type(validUser.passphrase);
cy.pick("set-passphrase-button").click();
cy.contains(
/Could not create identity: the identity 'rad:git:[\w]{3}…[\w]{3}' already exists/
).should("exist");
cy.pick("handle-input").should("exist");
});

context("when clicking the back button on the passphrase screen", () => {
it("sends the user back to the previous screen", () => {
cy.pick("get-started-button").click();
Expand Down
12 changes: 1 addition & 11 deletions cypress/support/commands.js
@@ -1,16 +1,6 @@
Cypress.Commands.add("resetProxyState", async () => {
console.log("Reset Proxy state");
await fetchOk("http://localhost:8080/v1/control/reset");
await fetchOk("http://localhost:8080/v1/keystore/unseal", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
passphrase: "radicle-upstream",
}),
});
});

Cypress.Commands.add("sealKeystore", async () => {
Expand Down Expand Up @@ -46,7 +36,7 @@ Cypress.Commands.add(
);

Cypress.Commands.add("onboardUser", async (handle = "secretariat") => {
await fetchOk("http://localhost:8080/v1/keystore/unseal", {
await fetchOk("http://localhost:8080/v1/keystore", {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
54 changes: 51 additions & 3 deletions proxy/api/src/context.rs
Expand Up @@ -9,7 +9,7 @@ use coco::PeerControl;
use crate::service;

#[cfg(test)]
use coco::{keystore, signer, RunConfig};
use coco::{signer, RunConfig};

/// Container to pass down dependencies into HTTP filter chains.
#[derive(Clone)]
Expand Down Expand Up @@ -53,6 +53,50 @@ impl Context {
Self::Unsealed(unsealed) => &mut unsealed.service_handle,
}
}

/// Unseal the key store and restart the coco service with the obtained key.
///
/// # Errors
///
/// * Errors if the passphrase is wrong.
/// * Errors if backend fails to retrieve the data.
/// * Errors if there is no key in the storage yet.
pub async fn unseal_keystore(
&mut self,
passphrase: coco::keystore::SecUtf8,
) -> Result<(), crate::error::Error> {
let keystore = self.keystore();
let key = tokio::task::spawn_blocking(move || keystore.get(passphrase))
.await
.expect("Task to unseal key was aborted")?;
self.service_handle().set_secret_key(key);
Ok(())
}

/// Create a key and store it encrypted with the given passphrase. Then restart the coco
/// service to use the new key.
///
/// # Errors
///
/// Errors when the storage backend fails to persist the key or a key already exists.
pub async fn create_key(
&mut self,
passphrase: coco::keystore::SecUtf8,
) -> Result<(), crate::error::Error> {
let keystore = self.keystore();
let key = tokio::task::spawn_blocking(move || keystore.create_key(passphrase))
.await
.expect("Task to create key was aborted")?;
self.service_handle().set_secret_key(key);
Ok(())
}

fn keystore(&self) -> Arc<dyn coco::keystore::Keystore + Sync + Send> {
match self {
Self::Sealed(sealed) => sealed.keystore.clone(),
Self::Unsealed(unsealed) => unsealed.keystore.clone(),
}
}
}

impl From<Unsealed> for Context {
Expand Down Expand Up @@ -82,6 +126,8 @@ pub struct Unsealed {
pub service_handle: service::Handle,
/// Cookie set on unsealing the key store.
pub auth_token: Arc<RwLock<Option<String>>>,
/// Reference to the key store.
pub keystore: Arc<dyn coco::keystore::Keystore + Send + Sync>,
}

/// Context for HTTP request if the coco peer APIs have not been initialized yet.
Expand All @@ -95,6 +141,8 @@ pub struct Sealed {
pub service_handle: service::Handle,
/// Cookie set on unsealing the key store.
pub auth_token: Arc<RwLock<Option<String>>>,
/// Reference to the key store.
pub keystore: Arc<dyn coco::keystore::Keystore + Send + Sync>,
}

impl Unsealed {
Expand All @@ -109,8 +157,7 @@ impl Unsealed {
pub async fn tmp(tmp_dir: &tempfile::TempDir) -> Result<Self, crate::error::Error> {
let store = kv::Store::new(kv::Config::new(tmp_dir.path().join("store")))?;

let pw = keystore::SecUtf8::from("radicle-upstream");
let key = keystore::Keystorage::memory(pw)?.get();
let key = coco::keys::SecretKey::new();
let signer = signer::BoxedSigner::from(signer::SomeSigner { signer: key });

let (peer_control, state) = {
Expand All @@ -132,6 +179,7 @@ impl Unsealed {
test: false,
service_handle: service::Handle::dummy(),
auth_token: Arc::new(RwLock::new(None)),
keystore: Arc::new(coco::keystore::memory()),
})
}
}
21 changes: 21 additions & 0 deletions proxy/api/src/http/error.rs
Expand Up @@ -181,6 +181,27 @@ pub async fn recover(err: Rejection) -> Result<impl Reply, Infallible> {
)
},
},
error::Error::Keystore(keystore_err) => {
if keystore_err.is_invalid_passphrase() {
(
StatusCode::FORBIDDEN,
"INCORRECT_PASSPHRASE",
"incorrect passphrase".to_string(),
)
} else if keystore_err.is_key_exists() {
(
StatusCode::CONFLICT,
"KEY_EXISTS",
"A key already exists".to_string(),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_SERVER_ERROR",
err.to_string(),
)
}
},
error::Error::KeystoreSealed
| error::Error::WrongPassphrase
| error::Error::InvalidAuthCookie => {
Expand Down
30 changes: 16 additions & 14 deletions proxy/api/src/http/keystore.rs
Expand Up @@ -31,28 +31,22 @@ fn create_filter(
warp::post()
.and(path::end())
.and(http::with_context(ctx))
.and(warp::body::json())
.and_then(handler::create)
}

/// Keystore handlers for conversion between core domain and HTTP request fulfilment.
mod handler {
use warp::{http::StatusCode, reply, Rejection, Reply};

use crate::{context, error};
use crate::context;

/// Unseal the keystore.
pub async fn unseal(
mut ctx: context::Context,
input: super::UnsealInput,
) -> Result<impl Reply, Rejection> {
// TODO(merle): Replace with correct password check
if input.passphrase.unsecure() != "radicle-upstream" {
return Err(Rejection::from(error::Error::WrongPassphrase));
}
// TODO Load the real key from disk. The service manager ignores the key for now and uses a
// hardcoded one.
let key = coco::keys::SecretKey::new();
ctx.service_handle().set_secret_key(key);
ctx.unseal_keystore(input.passphrase).await?;

let auth_token_lock = ctx.auth_token();
let mut auth_token = auth_token_lock.write().await;
Expand All @@ -67,11 +61,11 @@ mod handler {
}

/// Initialize the keystore with a new key.
pub async fn create(mut ctx: context::Context) -> Result<impl Reply, Rejection> {
// TODO Load the real key from disk. The service manager ignores the key for now and uses a
// hardcoded one.
let key = coco::keys::SecretKey::new();
ctx.service_handle().set_secret_key(key);
pub async fn create(
mut ctx: context::Context,
input: super::CreateInput,
) -> Result<impl Reply, Rejection> {
ctx.create_key(input.passphrase).await?;

let auth_token_lock = ctx.auth_token();
let mut auth_token = auth_token_lock.write().await;
Expand All @@ -94,6 +88,14 @@ pub struct UnsealInput {
passphrase: coco::keystore::SecUtf8,
}

/// Bundled input data for `create` request.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateInput {
/// Passphrase to encrypt the keystore with.
passphrase: coco::keystore::SecUtf8,
}

/// Generates a random auth token.
fn gen_token() -> String {
let randoms = rand::thread_rng().gen::<[u8; 32]>();
Expand Down
1 change: 1 addition & 0 deletions proxy/api/src/lib.rs
Expand Up @@ -21,6 +21,7 @@
clippy::multiple_crate_versions,
clippy::or_fun_call,
clippy::shadow_reuse,
clippy::clippy::option_if_let_else,
clippy::similar_names
)]

Expand Down

0 comments on commit 0f3290e

Please sign in to comment.