mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
4
Makefile
4
Makefile
@@ -79,10 +79,10 @@ frontend/dist/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
|||||||
npm --prefix frontend run build:ui
|
npm --prefix frontend run build:ui
|
||||||
|
|
||||||
frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||||
npm --prefix frontend run build:setup-wizard
|
npm --prefix frontend run build:setup
|
||||||
|
|
||||||
frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||||
npm --prefix frontend run build:diagnostic-ui
|
npm --prefix frontend run build:dui
|
||||||
|
|
||||||
frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.json
|
frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.json
|
||||||
jq '.useMocks = false' frontend/config-sample.json > frontend/config.json
|
jq '.useMocks = false' frontend/config-sample.json > frontend/config.json
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
|||||||
.invoke(embassy::ErrorKind::Nginx)
|
.invoke(embassy::ErrorKind::Nginx)
|
||||||
.await?;
|
.await?;
|
||||||
let ctx = SetupContext::init(cfg_path).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
|
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
|
||||||
CHIME.play().await?;
|
CHIME.play().await?;
|
||||||
rpc_server!({
|
rpc_server!({
|
||||||
@@ -57,7 +56,6 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
|||||||
status: status_fn,
|
status: status_fn,
|
||||||
middleware: [
|
middleware: [
|
||||||
cors,
|
cors,
|
||||||
encrypt,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.with_graceful_shutdown({
|
.with_graceful_shutdown({
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ use std::ops::Deref;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use josekit::jwk::Jwk;
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use patch_db::PatchDb;
|
use patch_db::PatchDb;
|
||||||
use rand::distributions::Alphanumeric;
|
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::Context;
|
use rpc_toolkit::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -70,13 +69,19 @@ pub struct SetupContextSeed {
|
|||||||
pub datadir: PathBuf,
|
pub datadir: PathBuf,
|
||||||
/// Used to encrypt for hidding from snoopers for setups create password
|
/// Used to encrypt for hidding from snoopers for setups create password
|
||||||
/// Set via path
|
/// Set via path
|
||||||
pub current_secret: RwLock<Option<String>>,
|
pub current_secret: Arc<Jwk>,
|
||||||
pub selected_v2_drive: RwLock<Option<PathBuf>>,
|
pub selected_v2_drive: RwLock<Option<PathBuf>>,
|
||||||
pub cached_product_key: RwLock<Option<Arc<String>>>,
|
pub cached_product_key: RwLock<Option<Arc<String>>>,
|
||||||
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
|
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
|
||||||
pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
|
pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<Jwk> for SetupContextSeed {
|
||||||
|
fn as_ref(&self) -> &Jwk {
|
||||||
|
&self.current_secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SetupContext(Arc<SetupContextSeed>);
|
pub struct SetupContext(Arc<SetupContextSeed>);
|
||||||
impl SetupContext {
|
impl SetupContext {
|
||||||
@@ -90,7 +95,16 @@ impl SetupContext {
|
|||||||
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||||
shutdown,
|
shutdown,
|
||||||
datadir,
|
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),
|
selected_v2_drive: RwLock::new(None),
|
||||||
cached_product_key: RwLock::new(None),
|
cached_product_key: RwLock::new(None),
|
||||||
recovery_status: RwLock::new(None),
|
recovery_status: RwLock::new(None),
|
||||||
@@ -131,18 +145,6 @@ impl SetupContext {
|
|||||||
}
|
}
|
||||||
Ok(secret_store)
|
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 {
|
impl Context for SetupContext {
|
||||||
|
|||||||
@@ -2,23 +2,13 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
|
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
|
||||||
use aes::Aes256Ctr;
|
use aes::Aes256Ctr;
|
||||||
use color_eyre::eyre::eyre;
|
use futures::Stream;
|
||||||
use futures::future::BoxFuture;
|
|
||||||
use futures::{FutureExt, Stream};
|
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use http::{HeaderMap, HeaderValue};
|
use josekit::jwk::Jwk;
|
||||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
use rpc_toolkit::hyper::{self, Body};
|
||||||
use rpc_toolkit::hyper::{self, Body, Request, Response, StatusCode};
|
use serde::{Deserialize, Serialize};
|
||||||
use rpc_toolkit::rpc_server_helpers::{
|
|
||||||
to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
|
|
||||||
};
|
|
||||||
use rpc_toolkit::yajrc::RpcMethod;
|
|
||||||
use rpc_toolkit::Metadata;
|
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
use tracing::instrument;
|
||||||
use crate::context::SetupContext;
|
|
||||||
use crate::util::Apply;
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey<Aes256Ctr> {
|
pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey<Aes256Ctr> {
|
||||||
let mut aeskey = CipherKey::<Aes256Ctr>::default();
|
let mut aeskey = CipherKey::<Aes256Ctr>::default();
|
||||||
@@ -56,219 +46,73 @@ pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct DecryptStream {
|
pub struct EncryptedWire {
|
||||||
key: Arc<String>,
|
encrypted: serde_json::Value,
|
||||||
#[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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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]
|
let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
|
||||||
pub struct EncryptStream {
|
.decrypter_from_jwk(current_secret)
|
||||||
#[pin]
|
{
|
||||||
body: Body,
|
Ok(a) => a,
|
||||||
aes: Aes256Ctr,
|
Err(e) => {
|
||||||
prefix: Option<[u8; 32]>,
|
tracing::warn!("Could not setup awk");
|
||||||
}
|
tracing::debug!("{:?}", e);
|
||||||
impl EncryptStream {
|
return None;
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
let encrypted = match serde_json::to_string(&self.encrypted) {
|
||||||
*req.body_mut() =
|
Ok(a) => a,
|
||||||
Body::wrap_stream(DecryptStream::new(Arc::new(key.clone()), body));
|
Err(e) => {
|
||||||
Some(key)
|
tracing::warn!("Could not deserialize");
|
||||||
} else {
|
tracing::debug!("{:?}", e);
|
||||||
None
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let res: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
|
let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) {
|
||||||
async move {
|
Ok(a) => a,
|
||||||
if !encrypted
|
Err(e) => {
|
||||||
&& metadata
|
tracing::warn!("Could not decrypt");
|
||||||
.get(rpc_req.method.as_str(), "authenticated")
|
tracing::debug!("{:?}", e);
|
||||||
.unwrap_or(true)
|
return None;
|
||||||
{
|
}
|
||||||
let (res_parts, _) = Response::new(()).into_parts();
|
};
|
||||||
Ok(Err(to_response(
|
match String::from_utf8(decoded) {
|
||||||
&req.headers,
|
Ok(a) => Some(a),
|
||||||
res_parts,
|
Err(e) => {
|
||||||
Err(Error::new(
|
tracing::warn!("Could not decrypt into utf8");
|
||||||
eyre!("Must be encrypted"),
|
tracing::debug!("{:?}", e);
|
||||||
crate::ErrorKind::Authorization,
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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"
|
||||||
|
}"#,
|
||||||
)
|
)
|
||||||
.into()),
|
.unwrap();
|
||||||
|_| StatusCode::OK,
|
let encrypted: EncryptedWire = serde_json::from_str(r#"{
|
||||||
)?))
|
"encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" }
|
||||||
} else {
|
}"#).unwrap();
|
||||||
let res: DynMiddlewareStage3 = Box::new(move |_, _| {
|
assert_eq!(
|
||||||
async move {
|
"testing12345",
|
||||||
let res: DynMiddlewareStage4 = Box::new(move |res| {
|
&encrypted.decrypt(Arc::new(private_key)).unwrap()
|
||||||
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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use crate::hostname::{get_hostname, Hostname};
|
|||||||
use crate::id::Id;
|
use crate::id::Id;
|
||||||
use crate::init::init;
|
use crate::init::init;
|
||||||
use crate::install::PKG_PUBLIC_DIR;
|
use crate::install::PKG_PUBLIC_DIR;
|
||||||
|
use crate::middleware::encrypt::EncryptedWire;
|
||||||
use crate::net::ssl::SslManager;
|
use crate::net::ssl::SslManager;
|
||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
use crate::sound::BEETHOVEN;
|
use crate::sound::BEETHOVEN;
|
||||||
@@ -63,7 +64,7 @@ where
|
|||||||
Ok(password)
|
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> {
|
pub fn setup() -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -95,8 +96,20 @@ pub async fn list_disks() -> Result<Vec<DiskInfo>, Error> {
|
|||||||
pub async fn attach(
|
pub async fn attach(
|
||||||
#[context] ctx: SetupContext,
|
#[context] ctx: SetupContext,
|
||||||
#[arg] guid: Arc<String>,
|
#[arg] guid: Arc<String>,
|
||||||
#[arg(rename = "embassy-password")] password: Option<String>,
|
#[arg(rename = "embassy-password")] password: Option<EncryptedWire>,
|
||||||
) -> Result<SetupResult, Error> {
|
) -> 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(
|
let requires_reboot = crate::disk::main::import(
|
||||||
&*guid,
|
&*guid,
|
||||||
&ctx.datadir,
|
&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
|
/// 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
|
/// 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.
|
/// since it is fine to share the public, and encrypt against the public.
|
||||||
#[command(rename = "get-secret", rpc_only, metadata(authenticated = false))]
|
#[command(rename = "get-pubkey", rpc_only, metadata(authenticated = false))]
|
||||||
pub async fn get_secret(
|
pub async fn get_pubkey(#[context] ctx: SetupContext) -> Result<Jwk, RpcError> {
|
||||||
#[context] ctx: SetupContext,
|
let secret = ctx.current_secret.clone();
|
||||||
#[arg] pubkey: Jwk,
|
let pub_key = secret.to_public_key()?;
|
||||||
) -> Result<String, RpcError> {
|
Ok(pub_key)
|
||||||
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(subcommands(verify_cifs))]
|
#[command(subcommands(verify_cifs))]
|
||||||
@@ -228,11 +229,13 @@ pub fn cifs() -> Result<(), Error> {
|
|||||||
|
|
||||||
#[command(rename = "verify", rpc_only)]
|
#[command(rename = "verify", rpc_only)]
|
||||||
pub async fn verify_cifs(
|
pub async fn verify_cifs(
|
||||||
|
#[context] ctx: SetupContext,
|
||||||
#[arg] hostname: String,
|
#[arg] hostname: String,
|
||||||
#[arg] path: PathBuf,
|
#[arg] path: PathBuf,
|
||||||
#[arg] username: String,
|
#[arg] username: String,
|
||||||
#[arg] password: Option<String>,
|
#[arg] password: Option<EncryptedWire>,
|
||||||
) -> Result<EmbassyOsRecoveryInfo, Error> {
|
) -> Result<EmbassyOsRecoveryInfo, Error> {
|
||||||
|
let password: Option<String> = password.map(|x| x.decrypt(&*ctx)).flatten();
|
||||||
let guard = TmpMountGuard::mount(
|
let guard = TmpMountGuard::mount(
|
||||||
&Cifs {
|
&Cifs {
|
||||||
hostname,
|
hostname,
|
||||||
@@ -252,10 +255,31 @@ pub async fn verify_cifs(
|
|||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
#[context] ctx: SetupContext,
|
#[context] ctx: SetupContext,
|
||||||
#[arg(rename = "embassy-logicalname")] embassy_logicalname: PathBuf,
|
#[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-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> {
|
) -> 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 {
|
if let Some(v2_drive) = &*ctx.selected_v2_drive.read().await {
|
||||||
recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone())))
|
recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ module.exports = {
|
|||||||
'*.ts': 'tslint --fix',
|
'*.ts': 'tslint --fix',
|
||||||
'projects/ui/**/*.ts': () => 'npm run check:ui',
|
'projects/ui/**/*.ts': () => 'npm run check:ui',
|
||||||
'projects/shared/**/*.ts': () => 'npm run check:shared',
|
'projects/shared/**/*.ts': () => 'npm run check:shared',
|
||||||
'projects/diagnostic-ui/**/*.ts': () => 'npm run check:diagnostic-ui',
|
'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui',
|
||||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup-wizard',
|
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
||||||
}
|
}
|
||||||
|
|||||||
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@@ -21,8 +21,6 @@
|
|||||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||||
"@start9labs/argon2": "^0.1.0",
|
"@start9labs/argon2": "^0.1.0",
|
||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"@types/aes-js": "^3.1.1",
|
|
||||||
"aes-js": "^3.1.2",
|
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
@@ -3570,11 +3568,6 @@
|
|||||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/aes-js": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.2",
|
"version": "1.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||||
@@ -4077,11 +4070,6 @@
|
|||||||
"node": ">=8.9.0"
|
"node": ">=8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aes-js": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="
|
|
||||||
},
|
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -17168,11 +17156,6 @@
|
|||||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/aes-js": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw=="
|
|
||||||
},
|
|
||||||
"@types/body-parser": {
|
"@types/body-parser": {
|
||||||
"version": "1.19.2",
|
"version": "1.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||||
@@ -17657,11 +17640,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aes-js": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="
|
|
||||||
},
|
|
||||||
"agent-base": {
|
"agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"check": "npm run check:shared && npm run check:ui && npm run check:setup-wizard && npm run check:diagnostic-ui",
|
"check": "npm run check:shared && npm run check:ui && npm run check:setup && npm run check:dui",
|
||||||
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck",
|
"check:dui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
||||||
"build:diagnostic-ui": "ng run diagnostic-ui:build",
|
"build:dui": "ng run diagnostic-ui:build",
|
||||||
"build:setup-wizard": "ng run setup-wizard:build",
|
"build:setup": "ng run setup-wizard:build",
|
||||||
"build:ui": "ng run ui:build",
|
"build:ui": "ng run ui:build",
|
||||||
"build:all": "npm run build:deps && npm run build:diagnostic-ui && npm run build:setup-wizard && npm run build:ui",
|
"build:all": "npm run build:deps && npm run build:dui && npm run build:setup && npm run build:ui",
|
||||||
"start:diagnostic": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0",
|
"start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0",
|
||||||
"start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0",
|
"start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0",
|
||||||
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0",
|
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0",
|
||||||
"start:ui:proxy": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0 -- --proxy-config proxy.conf.json",
|
"start:ui:proxy": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0 -- --proxy-config proxy.conf.json",
|
||||||
@@ -35,8 +35,6 @@
|
|||||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||||
"@start9labs/argon2": "^0.1.0",
|
"@start9labs/argon2": "^0.1.0",
|
||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"@types/aes-js": "^3.1.1",
|
|
||||||
"aes-js": "^3.1.2",
|
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class CifsModal {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly modalController: ModalController,
|
private readonly modalController: ModalController,
|
||||||
private readonly apiService: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
) {}
|
) {}
|
||||||
@@ -44,7 +44,12 @@ export class CifsModal {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const embassyOS = await this.apiService.verifyCifs(this.cifs)
|
const embassyOS = await this.api.verifyCifs({
|
||||||
|
...this.cifs,
|
||||||
|
password: this.cifs.password
|
||||||
|
? await this.api.encrypt(this.cifs.password)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
|
||||||
await loader.dismiss()
|
await loader.dismiss()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { PasswordPage } from 'src/app/modals/password/password.page'
|
import { PasswordPage } from 'src/app/modals/password/password.page'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
|
||||||
import { StateService } from 'src/app/services/state.service'
|
import { StateService } from 'src/app/services/state.service'
|
||||||
import SwiperCore, { Swiper } from 'swiper'
|
import SwiperCore, { Swiper } from 'swiper'
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
@@ -26,8 +25,7 @@ export class HomePage {
|
|||||||
error = false
|
error = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly unencrypted: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly encrypted: RPCEncryptedService,
|
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
@@ -38,8 +36,8 @@ export class HomePage {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
this.encrypted.secret = await this.unencrypted.getSecret()
|
await this.api.getPubKey()
|
||||||
const disks = await this.unencrypted.getDrives()
|
const disks = await this.api.getDrives()
|
||||||
this.guid = disks.find(d => !!d.guid)?.guid
|
this.guid = disks.find(d => !!d.guid)?.guid
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.error = true
|
this.error = true
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
|
import * as jose from 'node-jose'
|
||||||
export abstract class ApiService {
|
export abstract class ApiService {
|
||||||
// unencrypted
|
pubkey?: jose.JWK.Key
|
||||||
|
|
||||||
abstract getStatus(): Promise<GetStatusRes> // setup.status
|
abstract getStatus(): Promise<GetStatusRes> // setup.status
|
||||||
abstract getSecret(): Promise<string> // setup.get-secret
|
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||||
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
|
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
|
||||||
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
|
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
|
||||||
|
|
||||||
// encrypted
|
|
||||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
||||||
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
|
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
|
||||||
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||||
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
|
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
|
||||||
|
|
||||||
|
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||||
|
if (!this.pubkey) throw new Error('No pubkey found!')
|
||||||
|
const encrypted = await jose.JWE.createEncrypt(this.pubkey!)
|
||||||
|
.update(toEncrypt)
|
||||||
|
.final()
|
||||||
|
return {
|
||||||
|
encrypted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Encrypted = {
|
||||||
|
encrypted: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetStatusRes = {
|
export type GetStatusRes = {
|
||||||
@@ -19,14 +33,14 @@ export type GetStatusRes = {
|
|||||||
|
|
||||||
export type ImportDriveReq = {
|
export type ImportDriveReq = {
|
||||||
guid: string
|
guid: string
|
||||||
'embassy-password': string
|
'embassy-password': Encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetupEmbassyReq = {
|
export type SetupEmbassyReq = {
|
||||||
'embassy-logicalname': string
|
'embassy-logicalname': string
|
||||||
'embassy-password': string
|
'embassy-password': Encrypted
|
||||||
'recovery-source': CifsRecoverySource | DiskRecoverySource | null
|
'recovery-source': CifsRecoverySource | DiskRecoverySource | null
|
||||||
'recovery-password': string | null
|
'recovery-password': Encrypted | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetupEmbassyRes = {
|
export type SetupEmbassyRes = {
|
||||||
@@ -72,7 +86,7 @@ export type CifsRecoverySource = {
|
|||||||
hostname: string
|
hostname: string
|
||||||
path: string
|
path: string
|
||||||
username: string
|
username: string
|
||||||
password: string | null
|
password: Encrypted | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiskInfo = {
|
export type DiskInfo = {
|
||||||
|
|||||||
@@ -18,19 +18,15 @@ import {
|
|||||||
SetupEmbassyReq,
|
SetupEmbassyReq,
|
||||||
SetupEmbassyRes,
|
SetupEmbassyRes,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
import { RPCEncryptedService } from '../rpc-encrypted.service'
|
|
||||||
import * as jose from 'node-jose'
|
import * as jose from 'node-jose'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class LiveApiService implements ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
constructor(
|
constructor(private readonly http: HttpService) {
|
||||||
private readonly unencrypted: HttpService,
|
super()
|
||||||
private readonly encrypted: RPCEncryptedService,
|
}
|
||||||
) {}
|
|
||||||
|
|
||||||
// ** UNENCRYPTED **
|
|
||||||
|
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
return this.rpcRequest<GetStatusRes>({
|
return this.rpcRequest<GetStatusRes>({
|
||||||
@@ -40,24 +36,19 @@ export class LiveApiService implements ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We want to update the secret, which means that we will call in clearnet the
|
* We want to update the pubkey, which means that we will call in clearnet the
|
||||||
* getSecret, and all the information is never in the clear, and only public
|
* getPubKey, and all the information is never in the clear, and only public
|
||||||
* information is sent across the network. We don't want to expose that we do
|
* information is sent across the network. We don't want to expose that we do
|
||||||
* this wil all public/private key, which means that there is no information loss
|
* this wil all public/private key, which means that there is no information loss
|
||||||
* through the network.
|
* through the network.
|
||||||
*/
|
*/
|
||||||
async getSecret() {
|
async getPubKey() {
|
||||||
const keystore = jose.JWK.createKeyStore()
|
const response: jose.JWK.Key = await this.rpcRequest({
|
||||||
const key = await keystore.generate('EC', 'P-256')
|
method: 'setup.get-pubkey',
|
||||||
const response: string = await this.rpcRequest({
|
params: {},
|
||||||
method: 'setup.get-secret',
|
|
||||||
params: { pubkey: key.toJSON() },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
|
this.pubkey = response
|
||||||
const decoded = new TextDecoder().decode(decrypted.plaintext)
|
|
||||||
|
|
||||||
return decoded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDrives() {
|
async getDrives() {
|
||||||
@@ -81,18 +72,16 @@ export class LiveApiService implements ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ** ENCRYPTED **
|
|
||||||
|
|
||||||
async verifyCifs(source: CifsRecoverySource) {
|
async verifyCifs(source: CifsRecoverySource) {
|
||||||
source.path = source.path.replace('/\\/g', '/')
|
source.path = source.path.replace('/\\/g', '/')
|
||||||
return this.encrypted.rpcRequest<EmbassyOSRecoveryInfo>({
|
return this.rpcRequest<EmbassyOSRecoveryInfo>({
|
||||||
method: 'setup.cifs.verify',
|
method: 'setup.cifs.verify',
|
||||||
params: source,
|
params: source,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async importDrive(params: ImportDriveReq) {
|
async importDrive(params: ImportDriveReq) {
|
||||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||||
method: 'setup.attach',
|
method: 'setup.attach',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -110,7 +99,7 @@ export class LiveApiService implements ApiService {
|
|||||||
].path.replace('/\\/g', '/')
|
].path.replace('/\\/g', '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||||
method: 'setup.execute',
|
method: 'setup.execute',
|
||||||
params: setupInfo,
|
params: setupInfo,
|
||||||
})
|
})
|
||||||
@@ -122,7 +111,7 @@ export class LiveApiService implements ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setupComplete() {
|
async setupComplete() {
|
||||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||||
method: 'setup.complete',
|
method: 'setup.complete',
|
||||||
params: {},
|
params: {},
|
||||||
})
|
})
|
||||||
@@ -134,7 +123,7 @@ export class LiveApiService implements ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||||
const res = await this.unencrypted.rpcRequest<T>(opts)
|
const res = await this.http.rpcRequest<T>(opts)
|
||||||
|
|
||||||
const rpcRes = res.body
|
const rpcRes = res.body
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ import {
|
|||||||
ImportDriveReq,
|
ImportDriveReq,
|
||||||
SetupEmbassyReq,
|
SetupEmbassyReq,
|
||||||
} from './api.service'
|
} from './api.service'
|
||||||
|
import * as jose from 'node-jose'
|
||||||
|
|
||||||
let tries = 0
|
let tries = 0
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class MockApiService implements ApiService {
|
export class MockApiService extends ApiService {
|
||||||
// ** UNENCRYPTED **
|
|
||||||
|
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return {
|
return {
|
||||||
@@ -22,17 +21,21 @@ export class MockApiService implements ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSecret() {
|
async getPubKey() {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
const ascii = 'thisisasecret'
|
const keystore = jose.JWK.createKeyStore()
|
||||||
|
|
||||||
const arr1 = []
|
// randomly generated
|
||||||
for (let n = 0, l = ascii.length; n < l; n++) {
|
// this.pubkey = await keystore.generate('EC', 'P-256')
|
||||||
var hex = Number(ascii.charCodeAt(n)).toString(16)
|
|
||||||
arr1.push(hex)
|
// generated from backend
|
||||||
}
|
this.pubkey = await jose.JWK.asKey({
|
||||||
return arr1.join('')
|
kty: 'EC',
|
||||||
|
crv: 'P-256',
|
||||||
|
x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4',
|
||||||
|
y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDrives() {
|
async getDrives() {
|
||||||
@@ -76,8 +79,6 @@ export class MockApiService implements ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ** ENCRYPTED **
|
|
||||||
|
|
||||||
async verifyCifs(params: CifsRecoverySource) {
|
async verifyCifs(params: CifsRecoverySource) {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import * as aesjs from 'aes-js'
|
|
||||||
import * as pbkdf2 from 'pbkdf2'
|
|
||||||
import {
|
|
||||||
HttpError,
|
|
||||||
RpcError,
|
|
||||||
HttpService,
|
|
||||||
RPCOptions,
|
|
||||||
Method,
|
|
||||||
RPCResponse,
|
|
||||||
isRpcError,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class RPCEncryptedService {
|
|
||||||
secret?: string
|
|
||||||
|
|
||||||
constructor(private readonly http: HttpService) {}
|
|
||||||
|
|
||||||
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
|
|
||||||
const encryptedBody = await AES_CTR.encryptPbkdf2(
|
|
||||||
this.secret || '',
|
|
||||||
encodeUtf8(JSON.stringify(opts)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const res: RPCResponse<T> = await this.http
|
|
||||||
.httpRequest<ArrayBuffer>({
|
|
||||||
method: Method.POST,
|
|
||||||
url: this.http.relativeUrl,
|
|
||||||
body: encryptedBody.buffer,
|
|
||||||
responseType: 'arrayBuffer',
|
|
||||||
headers: {
|
|
||||||
'Content-Encoding': 'aesctr256',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
|
|
||||||
.then(res => JSON.parse(res))
|
|
||||||
.catch(e => {
|
|
||||||
if (!e.status && !e.statusText) {
|
|
||||||
throw new NetworkError()
|
|
||||||
} else {
|
|
||||||
throw new HttpError(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (isRpcError(res)) throw new RpcError(res.error)
|
|
||||||
return res.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NetworkError {
|
|
||||||
readonly code = null
|
|
||||||
readonly message =
|
|
||||||
'Network Error. Please try refreshing the page or clearing your browser cache'
|
|
||||||
readonly details = null
|
|
||||||
}
|
|
||||||
|
|
||||||
type AES_CTR = {
|
|
||||||
encryptPbkdf2: (
|
|
||||||
secretKey: string,
|
|
||||||
messageBuffer: Uint8Array,
|
|
||||||
) => Promise<Uint8Array>
|
|
||||||
decryptPbkdf2: (secretKey: string, arr: ArrayBuffer) => Promise<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
const AES_CTR: AES_CTR = {
|
|
||||||
encryptPbkdf2: async (secretKey: string, messageBuffer: Uint8Array) => {
|
|
||||||
const salt = window.crypto.getRandomValues(new Uint8Array(16))
|
|
||||||
const counter = window.crypto.getRandomValues(new Uint8Array(16))
|
|
||||||
|
|
||||||
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
|
|
||||||
|
|
||||||
const aesCtr = new aesjs.ModeOfOperation.ctr(
|
|
||||||
key,
|
|
||||||
new aesjs.Counter(counter),
|
|
||||||
)
|
|
||||||
const encryptedBytes = aesCtr.encrypt(messageBuffer)
|
|
||||||
return new Uint8Array([...counter, ...salt, ...encryptedBytes])
|
|
||||||
},
|
|
||||||
decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => {
|
|
||||||
const buff = new Uint8Array(arr)
|
|
||||||
const counter = buff.slice(0, 16)
|
|
||||||
const salt = buff.slice(16, 32)
|
|
||||||
|
|
||||||
const cipher = buff.slice(32)
|
|
||||||
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
|
|
||||||
|
|
||||||
const aesCtr = new aesjs.ModeOfOperation.ctr(
|
|
||||||
key,
|
|
||||||
new aesjs.Counter(counter),
|
|
||||||
)
|
|
||||||
const decryptedBytes = aesCtr.decrypt(cipher)
|
|
||||||
|
|
||||||
return aesjs.utils.utf8.fromBytes(decryptedBytes)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeUtf8(str: string): Uint8Array {
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
return encoder.encode(str)
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ export class StateService {
|
|||||||
cert = ''
|
cert = ''
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly apiService: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly errorToastService: ErrorToastService,
|
private readonly errorToastService: ErrorToastService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export class StateService {
|
|||||||
|
|
||||||
let progress
|
let progress
|
||||||
try {
|
try {
|
||||||
progress = await this.apiService.getRecoveryStatus()
|
progress = await this.api.getRecoveryStatus()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorToastService.present({
|
this.errorToastService.present({
|
||||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||||
@@ -67,9 +67,9 @@ export class StateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async importDrive(guid: string, password: string): Promise<void> {
|
async importDrive(guid: string, password: string): Promise<void> {
|
||||||
const ret = await this.apiService.importDrive({
|
const ret = await this.api.importDrive({
|
||||||
guid,
|
guid,
|
||||||
'embassy-password': password,
|
'embassy-password': await this.api.encrypt(password),
|
||||||
})
|
})
|
||||||
this.torAddress = ret['tor-address']
|
this.torAddress = ret['tor-address']
|
||||||
this.lanAddress = ret['lan-address']
|
this.lanAddress = ret['lan-address']
|
||||||
@@ -80,11 +80,13 @@ export class StateService {
|
|||||||
storageLogicalname: string,
|
storageLogicalname: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const ret = await this.apiService.setupEmbassy({
|
const ret = await this.api.setupEmbassy({
|
||||||
'embassy-logicalname': storageLogicalname,
|
'embassy-logicalname': storageLogicalname,
|
||||||
'embassy-password': password,
|
'embassy-password': await this.api.encrypt(password),
|
||||||
'recovery-source': this.recoverySource || null,
|
'recovery-source': this.recoverySource || null,
|
||||||
'recovery-password': this.recoveryPassword || null,
|
'recovery-password': this.recoveryPassword
|
||||||
|
? await this.api.encrypt(this.recoveryPassword)
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
this.torAddress = ret['tor-address']
|
this.torAddress = ret['tor-address']
|
||||||
this.lanAddress = ret['lan-address']
|
this.lanAddress = ret['lan-address']
|
||||||
@@ -92,7 +94,7 @@ export class StateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async completeEmbassy(): Promise<void> {
|
async completeEmbassy(): Promise<void> {
|
||||||
const ret = await this.apiService.setupComplete()
|
const ret = await this.api.setupComplete()
|
||||||
this.torAddress = ret['tor-address']
|
this.torAddress = ret['tor-address']
|
||||||
this.lanAddress = ret['lan-address']
|
this.lanAddress = ret['lan-address']
|
||||||
this.cert = ret['root-ca']
|
this.cert = ret['root-ca']
|
||||||
|
|||||||
Reference in New Issue
Block a user