Fix/encryption (#1811)

* change encryption to use pubkey and only encrypt specific fields

* adjust script names for convenience

* remove unused fn

* fix build script name

* augment mocks

* remove log

* fix prod build

* feat: backend keys

* fix: Using the correct name with the public key

* chore: Fix the type for the encrypted

* chore: Add some tracing

* remove aes-js from package lock file

Co-authored-by: BluJ <mogulslayer@gmail.com>
This commit is contained in:
Lucy C
2022-09-21 14:03:05 -06:00
committed by GitHub
parent f8ea2ebf62
commit 28f9fa35e5
15 changed files with 213 additions and 463 deletions

View File

@@ -48,7 +48,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
.invoke(embassy::ErrorKind::Nginx)
.await?;
let ctx = SetupContext::init(cfg_path).await?;
let encrypt = embassy::middleware::encrypt::encrypt(ctx.clone());
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
CHIME.play().await?;
rpc_server!({
@@ -57,7 +56,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
status: status_fn,
middleware: [
cors,
encrypt,
]
})
.with_graceful_shutdown({

View File

@@ -3,10 +3,9 @@ use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use josekit::jwk::Jwk;
use patch_db::json_ptr::JsonPointer;
use patch_db::PatchDb;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::Context;
use serde::{Deserialize, Serialize};
@@ -70,13 +69,19 @@ pub struct SetupContextSeed {
pub datadir: PathBuf,
/// Used to encrypt for hidding from snoopers for setups create password
/// Set via path
pub current_secret: RwLock<Option<String>>,
pub current_secret: Arc<Jwk>,
pub selected_v2_drive: RwLock<Option<PathBuf>>,
pub cached_product_key: RwLock<Option<Arc<String>>>,
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
}
impl AsRef<Jwk> for SetupContextSeed {
fn as_ref(&self) -> &Jwk {
&self.current_secret
}
}
#[derive(Clone)]
pub struct SetupContext(Arc<SetupContextSeed>);
impl SetupContext {
@@ -90,7 +95,16 @@ impl SetupContext {
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
shutdown,
datadir,
current_secret: RwLock::new(None),
current_secret: Arc::new(
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
tracing::debug!("{:?}", e);
tracing::error!("Couldn't generate ec key");
Error::new(
color_eyre::eyre::eyre!("Couldn't generate ec key"),
crate::ErrorKind::Unknown,
)
})?,
),
selected_v2_drive: RwLock::new(None),
cached_product_key: RwLock::new(None),
recovery_status: RwLock::new(None),
@@ -131,18 +145,6 @@ impl SetupContext {
}
Ok(secret_store)
}
/// So we assume that there will only be one client that will ask for a secret,
/// And during that time do we upsert to a new key
pub async fn update_secret(&self) -> Result<String, Error> {
let new_secret: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
*self.current_secret.write().await = Some(new_secret.clone());
Ok(new_secret)
}
}
impl Context for SetupContext {

View File

@@ -2,23 +2,13 @@ use std::sync::Arc;
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
use aes::Aes256Ctr;
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::{FutureExt, Stream};
use futures::Stream;
use hmac::Hmac;
use http::{HeaderMap, HeaderValue};
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{self, Body, Request, Response, StatusCode};
use rpc_toolkit::rpc_server_helpers::{
to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
};
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use josekit::jwk::Jwk;
use rpc_toolkit::hyper::{self, Body};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::context::SetupContext;
use crate::util::Apply;
use crate::Error;
use tracing::instrument;
pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey<Aes256Ctr> {
let mut aeskey = CipherKey::<Aes256Ctr>::default();
@@ -56,219 +46,73 @@ pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
res
}
#[pin_project::pin_project]
pub struct DecryptStream {
key: Arc<String>,
#[pin]
body: Body,
ctr: Vec<u8>,
salt: Vec<u8>,
aes: Option<Aes256Ctr>,
}
impl DecryptStream {
pub fn new(key: Arc<String>, body: Body) -> Self {
DecryptStream {
key,
body,
ctr: Vec::new(),
salt: Vec::new(),
aes: None,
}
}
}
impl Stream for DecryptStream {
type Item = hyper::Result<hyper::body::Bytes>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.project();
match this.body.poll_next(cx) {
std::task::Poll::Pending => std::task::Poll::Pending,
std::task::Poll::Ready(Some(Ok(bytes))) => std::task::Poll::Ready(Some(Ok({
let mut buf = &*bytes;
if let Some(aes) = this.aes.as_mut() {
let mut res = buf.to_vec();
aes.apply_keystream(&mut res);
res.into()
} else {
if this.ctr.len() < 16 && !buf.is_empty() {
let to_read = std::cmp::min(16 - this.ctr.len(), buf.len());
this.ctr.extend_from_slice(&buf[0..to_read]);
buf = &buf[to_read..];
}
if this.salt.len() < 16 && !buf.is_empty() {
let to_read = std::cmp::min(16 - this.salt.len(), buf.len());
this.salt.extend_from_slice(&buf[0..to_read]);
buf = &buf[to_read..];
}
if this.ctr.len() == 16 && this.salt.len() == 16 {
let aeskey = pbkdf2(this.key.as_bytes(), &this.salt);
let ctr = Nonce::<Aes256Ctr>::from_slice(this.ctr);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = buf.to_vec();
aes.apply_keystream(&mut res);
*this.aes = Some(aes);
res.into()
} else {
hyper::body::Bytes::new()
}
}
}))),
std::task::Poll::Ready(a) => std::task::Poll::Ready(a),
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EncryptedWire {
encrypted: serde_json::Value,
}
impl EncryptedWire {
#[instrument(skip(current_secret))]
pub fn decrypt(self, current_secret: impl AsRef<Jwk>) -> Option<String> {
let current_secret = current_secret.as_ref();
#[pin_project::pin_project]
pub struct EncryptStream {
#[pin]
body: Body,
aes: Aes256Ctr,
prefix: Option<[u8; 32]>,
}
impl EncryptStream {
pub fn new(key: &str, body: Body) -> Self {
let prefix: [u8; 32] = rand::random();
let aeskey = pbkdf2(key.as_bytes(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let aes = Aes256Ctr::new(&aeskey, ctr);
EncryptStream {
body,
aes,
prefix: Some(prefix),
}
}
}
impl Stream for EncryptStream {
type Item = hyper::Result<hyper::body::Bytes>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.project();
if let Some(prefix) = this.prefix.take() {
std::task::Poll::Ready(Some(Ok(prefix.to_vec().into())))
} else {
match this.body.poll_next(cx) {
std::task::Poll::Pending => std::task::Poll::Pending,
std::task::Poll::Ready(Some(Ok(bytes))) => std::task::Poll::Ready(Some(Ok({
let mut res = bytes.to_vec();
this.aes.apply_keystream(&mut res);
res.into()
}))),
std::task::Poll::Ready(a) => std::task::Poll::Ready(a),
let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
.decrypter_from_jwk(current_secret)
{
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not setup awk");
tracing::debug!("{:?}", e);
return None;
}
};
let encrypted = match serde_json::to_string(&self.encrypted) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not deserialize");
tracing::debug!("{:?}", e);
return None;
}
};
let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not decrypt");
tracing::debug!("{:?}", e);
return None;
}
};
match String::from_utf8(decoded) {
Ok(a) => Some(a),
Err(e) => {
tracing::warn!("Could not decrypt into utf8");
tracing::debug!("{:?}", e);
return None;
}
}
}
}
fn encrypted(headers: &HeaderMap) -> bool {
headers
.get("Content-Encoding")
.and_then(|h| {
h.to_str()
.ok()?
.split(',')
.any(|s| s == "aesctr256")
.apply(Some)
})
.unwrap_or_default()
}
pub fn encrypt<M: Metadata>(ctx: SetupContext) -> DynMiddleware<M> {
Box::new(
move |req: &mut Request<Body>,
metadata: M|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
let keysource = ctx.clone();
async move {
let encrypted = encrypted(req.headers());
let current_secret: Option<String> = keysource.current_secret.read().await.clone();
let key = if encrypted {
let key = match current_secret {
Some(s) => s,
None => {
let (res_parts, _) = Response::new(()).into_parts();
return Ok(Err(to_response(
req.headers(),
res_parts,
Err(Error::new(
eyre!("No Secret has been set"),
crate::ErrorKind::RateLimited,
)
.into()),
|_| StatusCode::OK,
)?));
}
};
let body = std::mem::take(req.body_mut());
*req.body_mut() =
Body::wrap_stream(DecryptStream::new(Arc::new(key.clone()), body));
Some(key)
} else {
None
};
let res: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
async move {
if !encrypted
&& metadata
.get(rpc_req.method.as_str(), "authenticated")
.unwrap_or(true)
{
let (res_parts, _) = Response::new(()).into_parts();
Ok(Err(to_response(
&req.headers,
res_parts,
Err(Error::new(
eyre!("Must be encrypted"),
crate::ErrorKind::Authorization,
)
.into()),
|_| StatusCode::OK,
)?))
} else {
let res: DynMiddlewareStage3 = Box::new(move |_, _| {
async move {
let res: DynMiddlewareStage4 = Box::new(move |res| {
async move {
if let Some(key) = key {
res.headers_mut().insert(
"Content-Encoding",
HeaderValue::from_static("aesctr256"),
);
if let Some(len_header) =
res.headers_mut().get_mut("Content-Length")
{
if let Some(len) = len_header
.to_str()
.ok()
.and_then(|l| l.parse::<u64>().ok())
{
*len_header = HeaderValue::from(len + 32);
}
}
let body = std::mem::take(res.body_mut());
*res.body_mut() = Body::wrap_stream(
EncryptStream::new(key.as_ref(), body),
);
}
Ok(())
}
.boxed()
});
Ok(Ok(res))
}
.boxed()
});
Ok(Ok(res))
}
}
.boxed()
});
Ok(Ok(res))
}
.boxed()
},
/// We created this test by first making the private key, then restoring from this private key for recreatability.
/// After this the frontend then encoded an password, then we are testing that the output that we got (hand coded)
/// will be the shape we want.
#[test]
fn test_gen_awk() {
let private_key: Jwk = serde_json::from_str(
r#"{
"kty": "EC",
"crv": "P-256",
"d": "3P-MxbUJtEhdGGpBCRFXkUneGgdyz_DGZWfIAGSCHOU",
"x": "yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4",
"y": "8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI"
}"#,
)
.unwrap();
let encrypted: EncryptedWire = serde_json::from_str(r#"{
"encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" }
}"#).unwrap();
assert_eq!(
"testing12345",
&encrypted.decrypt(Arc::new(private_key)).unwrap()
);
}

View File

@@ -42,6 +42,7 @@ use crate::hostname::{get_hostname, Hostname};
use crate::id::Id;
use crate::init::init;
use crate::install::PKG_PUBLIC_DIR;
use crate::middleware::encrypt::EncryptedWire;
use crate::net::ssl::SslManager;
use crate::s9pk::manifest::PackageId;
use crate::sound::BEETHOVEN;
@@ -63,7 +64,7 @@ where
Ok(password)
}
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_secret))]
#[command(subcommands(status, disk, attach, execute, recovery, cifs, complete, get_pubkey))]
pub fn setup() -> Result<(), Error> {
Ok(())
}
@@ -95,8 +96,20 @@ pub async fn list_disks() -> Result<Vec<DiskInfo>, Error> {
pub async fn attach(
#[context] ctx: SetupContext,
#[arg] guid: Arc<String>,
#[arg(rename = "embassy-password")] password: Option<String>,
#[arg(rename = "embassy-password")] password: Option<EncryptedWire>,
) -> Result<SetupResult, Error> {
let password: Option<String> = match password {
Some(a) => match a.decrypt(&*ctx) {
a @ Some(_) => a,
None => {
return Err(Error::new(
color_eyre::eyre::eyre!("Couldn't decode password"),
crate::ErrorKind::Unknown,
));
}
},
None => None,
};
let requires_reboot = crate::disk::main::import(
&*guid,
&ctx.datadir,
@@ -202,23 +215,11 @@ pub async fn recovery_status(
/// This way the frontend can send a secret, like the password for the setup/ recovory
/// without knowing the password over clearnet. We use the public key shared across the network
/// since it is fine to share the public, and encrypt against the public.
#[command(rename = "get-secret", rpc_only, metadata(authenticated = false))]
pub async fn get_secret(
#[context] ctx: SetupContext,
#[arg] pubkey: Jwk,
) -> Result<String, RpcError> {
let secret = ctx.update_secret().await?;
let mut header = josekit::jwe::JweHeader::new();
header.set_algorithm("ECDH-ES");
header.set_content_encryption("A256GCM");
let encrypter = josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
.encrypter_from_jwk(&pubkey)
.unwrap();
Ok(josekit::jwe::serialize_compact(secret.as_bytes(), &header, &encrypter).unwrap())
// Need to encrypt from the public key sent
// then encode via hex
#[command(rename = "get-pubkey", rpc_only, metadata(authenticated = false))]
pub async fn get_pubkey(#[context] ctx: SetupContext) -> Result<Jwk, RpcError> {
let secret = ctx.current_secret.clone();
let pub_key = secret.to_public_key()?;
Ok(pub_key)
}
#[command(subcommands(verify_cifs))]
@@ -228,11 +229,13 @@ pub fn cifs() -> Result<(), Error> {
#[command(rename = "verify", rpc_only)]
pub async fn verify_cifs(
#[context] ctx: SetupContext,
#[arg] hostname: String,
#[arg] path: PathBuf,
#[arg] username: String,
#[arg] password: Option<String>,
#[arg] password: Option<EncryptedWire>,
) -> Result<EmbassyOsRecoveryInfo, Error> {
let password: Option<String> = password.map(|x| x.decrypt(&*ctx)).flatten();
let guard = TmpMountGuard::mount(
&Cifs {
hostname,
@@ -252,10 +255,31 @@ pub async fn verify_cifs(
pub async fn execute(
#[context] ctx: SetupContext,
#[arg(rename = "embassy-logicalname")] embassy_logicalname: PathBuf,
#[arg(rename = "embassy-password")] embassy_password: String,
#[arg(rename = "embassy-password")] embassy_password: EncryptedWire,
#[arg(rename = "recovery-source")] mut recovery_source: Option<BackupTargetFS>,
#[arg(rename = "recovery-password")] recovery_password: Option<String>,
#[arg(rename = "recovery-password")] recovery_password: Option<EncryptedWire>,
) -> Result<SetupResult, Error> {
let embassy_password = match embassy_password.decrypt(&*ctx) {
Some(a) => a,
None => {
return Err(Error::new(
color_eyre::eyre::eyre!("Couldn't decode embassy_password"),
crate::ErrorKind::Unknown,
))
}
};
let recovery_password: Option<String> = match recovery_password {
Some(a) => match a.decrypt(&*ctx) {
Some(a) => Some(a),
None => {
return Err(Error::new(
color_eyre::eyre::eyre!("Couldn't decode recovery_password"),
crate::ErrorKind::Unknown,
))
}
},
None => None,
};
if let Some(v2_drive) = &*ctx.selected_v2_drive.read().await {
recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone())))
}