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

@@ -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

View File

@@ -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({

View File

@@ -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 {

View File

@@ -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()
},
)
}

View File

@@ -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())))
} }

View File

@@ -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',
} }

View File

@@ -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",

View File

@@ -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",

View File

@@ -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()

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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']