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
|
||||
|
||||
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)
|
||||
npm --prefix frontend run build:diagnostic-ui
|
||||
npm --prefix frontend run build:dui
|
||||
|
||||
frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.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)
|
||||
.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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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 encrypted = match serde_json::to_string(&self.encrypted) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not deserialize");
|
||||
tracing::debug!("{:?}", e);
|
||||
|
||||
return 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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()),
|
||||
|_| 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),
|
||||
.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()
|
||||
);
|
||||
}
|
||||
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::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())))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ module.exports = {
|
||||
'*.ts': 'tslint --fix',
|
||||
'projects/ui/**/*.ts': () => 'npm run check:ui',
|
||||
'projects/shared/**/*.ts': () => 'npm run check:shared',
|
||||
'projects/diagnostic-ui/**/*.ts': () => 'npm run check:diagnostic-ui',
|
||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup-wizard',
|
||||
'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui',
|
||||
'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",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@types/aes-js": "^3.1.1",
|
||||
"aes-js": "^3.1.2",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
@@ -3570,11 +3568,6 @@
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||
"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": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||
@@ -4077,11 +4070,6 @@
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "1.19.2",
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
"homepage": "https://start9.com/",
|
||||
"scripts": {
|
||||
"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:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:dui": "tsc --project projects/diagnostic-ui/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",
|
||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
||||
"build:diagnostic-ui": "ng run diagnostic-ui:build",
|
||||
"build:setup-wizard": "ng run setup-wizard:build",
|
||||
"build:dui": "ng run diagnostic-ui:build",
|
||||
"build:setup": "ng run setup-wizard: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",
|
||||
"start:diagnostic": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0",
|
||||
"build:all": "npm run build:deps && npm run build:dui && npm run build:setup && npm run build:ui",
|
||||
"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: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",
|
||||
@@ -35,8 +35,6 @@
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@types/aes-js": "^3.1.1",
|
||||
"aes-js": "^3.1.2",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
|
||||
@@ -27,7 +27,7 @@ export class CifsModal {
|
||||
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
@@ -44,7 +44,12 @@ export class CifsModal {
|
||||
await loader.present()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '@ionic/angular'
|
||||
import { PasswordPage } from 'src/app/modals/password/password.page'
|
||||
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 SwiperCore, { Swiper } from 'swiper'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
@@ -26,8 +25,7 @@ export class HomePage {
|
||||
error = false
|
||||
|
||||
constructor(
|
||||
private readonly unencrypted: ApiService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
private readonly api: ApiService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
@@ -38,8 +36,8 @@ export class HomePage {
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.encrypted.secret = await this.unencrypted.getSecret()
|
||||
const disks = await this.unencrypted.getDrives()
|
||||
await this.api.getPubKey()
|
||||
const disks = await this.api.getDrives()
|
||||
this.guid = disks.find(d => !!d.guid)?.guid
|
||||
} catch (e: any) {
|
||||
this.error = true
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import * as jose from 'node-jose'
|
||||
export abstract class ApiService {
|
||||
// unencrypted
|
||||
pubkey?: jose.JWK.Key
|
||||
|
||||
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 set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
|
||||
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
|
||||
|
||||
// encrypted
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
||||
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
|
||||
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||
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 = {
|
||||
@@ -19,14 +33,14 @@ export type GetStatusRes = {
|
||||
|
||||
export type ImportDriveReq = {
|
||||
guid: string
|
||||
'embassy-password': string
|
||||
'embassy-password': Encrypted
|
||||
}
|
||||
|
||||
export type SetupEmbassyReq = {
|
||||
'embassy-logicalname': string
|
||||
'embassy-password': string
|
||||
'embassy-password': Encrypted
|
||||
'recovery-source': CifsRecoverySource | DiskRecoverySource | null
|
||||
'recovery-password': string | null
|
||||
'recovery-password': Encrypted | null
|
||||
}
|
||||
|
||||
export type SetupEmbassyRes = {
|
||||
@@ -72,7 +86,7 @@ export type CifsRecoverySource = {
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
password: Encrypted | null
|
||||
}
|
||||
|
||||
export type DiskInfo = {
|
||||
|
||||
@@ -18,19 +18,15 @@ import {
|
||||
SetupEmbassyReq,
|
||||
SetupEmbassyRes,
|
||||
} from './api.service'
|
||||
import { RPCEncryptedService } from '../rpc-encrypted.service'
|
||||
import * as jose from 'node-jose'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(
|
||||
private readonly unencrypted: HttpService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
// ** UNENCRYPTED **
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor(private readonly http: HttpService) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
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
|
||||
* getSecret, and all the information is never in the clear, and only public
|
||||
* We want to update the pubkey, which means that we will call in clearnet the
|
||||
* 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
|
||||
* this wil all public/private key, which means that there is no information loss
|
||||
* through the network.
|
||||
*/
|
||||
async getSecret() {
|
||||
const keystore = jose.JWK.createKeyStore()
|
||||
const key = await keystore.generate('EC', 'P-256')
|
||||
const response: string = await this.rpcRequest({
|
||||
method: 'setup.get-secret',
|
||||
params: { pubkey: key.toJSON() },
|
||||
async getPubKey() {
|
||||
const response: jose.JWK.Key = await this.rpcRequest({
|
||||
method: 'setup.get-pubkey',
|
||||
params: {},
|
||||
})
|
||||
|
||||
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
|
||||
const decoded = new TextDecoder().decode(decrypted.plaintext)
|
||||
|
||||
return decoded
|
||||
this.pubkey = response
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
@@ -81,18 +72,16 @@ export class LiveApiService implements ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs(source: CifsRecoverySource) {
|
||||
source.path = source.path.replace('/\\/g', '/')
|
||||
return this.encrypted.rpcRequest<EmbassyOSRecoveryInfo>({
|
||||
return this.rpcRequest<EmbassyOSRecoveryInfo>({
|
||||
method: 'setup.cifs.verify',
|
||||
params: source,
|
||||
})
|
||||
}
|
||||
|
||||
async importDrive(params: ImportDriveReq) {
|
||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
||||
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.attach',
|
||||
params,
|
||||
})
|
||||
@@ -110,7 +99,7 @@ export class LiveApiService implements ApiService {
|
||||
].path.replace('/\\/g', '/')
|
||||
}
|
||||
|
||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
||||
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo,
|
||||
})
|
||||
@@ -122,7 +111,7 @@ export class LiveApiService implements ApiService {
|
||||
}
|
||||
|
||||
async setupComplete() {
|
||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
||||
const res = await this.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.complete',
|
||||
params: {},
|
||||
})
|
||||
@@ -134,7 +123,7 @@ export class LiveApiService implements ApiService {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@ import {
|
||||
ImportDriveReq,
|
||||
SetupEmbassyReq,
|
||||
} from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
|
||||
let tries = 0
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService implements ApiService {
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
export class MockApiService extends ApiService {
|
||||
async getStatus() {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
@@ -22,17 +21,21 @@ export class MockApiService implements ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async getSecret() {
|
||||
async getPubKey() {
|
||||
await pauseFor(1000)
|
||||
|
||||
const ascii = 'thisisasecret'
|
||||
const keystore = jose.JWK.createKeyStore()
|
||||
|
||||
const arr1 = []
|
||||
for (let n = 0, l = ascii.length; n < l; n++) {
|
||||
var hex = Number(ascii.charCodeAt(n)).toString(16)
|
||||
arr1.push(hex)
|
||||
}
|
||||
return arr1.join('')
|
||||
// randomly generated
|
||||
// this.pubkey = await keystore.generate('EC', 'P-256')
|
||||
|
||||
// generated from backend
|
||||
this.pubkey = await jose.JWK.asKey({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4',
|
||||
y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI',
|
||||
})
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
@@ -76,8 +79,6 @@ export class MockApiService implements ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs(params: CifsRecoverySource) {
|
||||
await pauseFor(1000)
|
||||
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 = ''
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly api: ApiService,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
) {}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class StateService {
|
||||
|
||||
let progress
|
||||
try {
|
||||
progress = await this.apiService.getRecoveryStatus()
|
||||
progress = await this.api.getRecoveryStatus()
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present({
|
||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||
@@ -67,9 +67,9 @@ export class StateService {
|
||||
}
|
||||
|
||||
async importDrive(guid: string, password: string): Promise<void> {
|
||||
const ret = await this.apiService.importDrive({
|
||||
const ret = await this.api.importDrive({
|
||||
guid,
|
||||
'embassy-password': password,
|
||||
'embassy-password': await this.api.encrypt(password),
|
||||
})
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
@@ -80,11 +80,13 @@ export class StateService {
|
||||
storageLogicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const ret = await this.apiService.setupEmbassy({
|
||||
const ret = await this.api.setupEmbassy({
|
||||
'embassy-logicalname': storageLogicalname,
|
||||
'embassy-password': password,
|
||||
'embassy-password': await this.api.encrypt(password),
|
||||
'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.lanAddress = ret['lan-address']
|
||||
@@ -92,7 +94,7 @@ export class StateService {
|
||||
}
|
||||
|
||||
async completeEmbassy(): Promise<void> {
|
||||
const ret = await this.apiService.setupComplete()
|
||||
const ret = await this.api.setupComplete()
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
this.cert = ret['root-ca']
|
||||
|
||||
Reference in New Issue
Block a user