Integration/02x migration fixes (#1105)

* migrate root ca from 0.2.x

* migrate tor key, fix threading

* fixes lan address divergence across setup scenarios

* verifies that data drive matches the OS key on attachment

* fix 0.2.x recovery flow

* fixes issues with reading the product_key.txt file when it doesn't exist (NO_KEY=1)

* migrate tor key before initializing patchdb

* move patchdb lock guard acquisition into deferred async block

* pass product_key by parameter

* works, fucking finally

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Keagan McClelland
2022-01-20 10:20:27 -07:00
committed by Aiden McClelland
parent 9d13c48640
commit 666f1bc0eb
8 changed files with 332 additions and 178 deletions

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use embassy::context::rpc::RpcContextConfig;
use embassy::context::{DiagnosticContext, SetupContext};
use embassy::disk::main::DEFAULT_PASSWORD;
use embassy::hostname::get_product_key;
use embassy::middleware::cors::cors;
use embassy::middleware::diagnostic::diagnostic;
use embassy::middleware::encrypt::encrypt;
@@ -80,7 +81,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
)
.await?;
tracing::info!("Loaded Disk");
embassy::init::init(&cfg).await?;
embassy::init::init(&cfg, &get_product_key().await?).await?;
}
Ok(())

View File

@@ -23,7 +23,7 @@ use tracing::instrument;
use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
use crate::db::model::{Database, InstalledPackageDataEntry, PackageDataEntry};
use crate::hostname::{get_hostname, get_id};
use crate::hostname::{derive_hostname, derive_id, get_hostname, get_id, get_product_key};
use crate::install::cleanup::{cleanup_failed, uninstall};
use crate::manager::ManagerMap;
use crate::middleware::auth::HashSessionToken;
@@ -72,7 +72,9 @@ impl RpcContextConfig {
.as_deref()
.unwrap_or_else(|| Path::new("/embassy-data"))
}
pub async fn db(&self, secret_store: &SqlitePool) -> Result<PatchDb, Error> {
pub async fn db(&self, secret_store: &SqlitePool, product_key: &str) -> Result<PatchDb, Error> {
let sid = derive_id(product_key);
let hostname = derive_hostname(&sid);
let db_path = self.datadir().join("main").join("embassy.db");
let db = PatchDb::open(&db_path)
.await
@@ -81,8 +83,8 @@ impl RpcContextConfig {
db.put(
&<JsonPointer>::default(),
&Database::init(
get_id().await?,
&get_hostname().await?,
sid,
&hostname,
&os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
),
@@ -159,7 +161,7 @@ impl RpcContext {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let secret_store = base.secret_store().await?;
tracing::info!("Opened Sqlite DB");
let db = base.db(&secret_store).await?;
let db = base.db(&secret_store, &get_product_key().await?).await?;
tracing::info!("Opened PatchDB");
let share = crate::db::DatabaseModel::new()
.server_info()

View File

@@ -18,7 +18,7 @@ use tracing::instrument;
use url::Host;
use crate::db::model::Database;
use crate::hostname::{get_hostname, get_id, get_product_key};
use crate::hostname::{derive_hostname, derive_id, get_hostname, get_id, get_product_key};
use crate::net::tor::os_key;
use crate::setup::{password_hash, RecoveryStatus};
use crate::util::io::from_toml_async_reader;
@@ -91,11 +91,14 @@ impl SetupContext {
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
if !db.exists(&<JsonPointer>::default()).await? {
let pkey = self.product_key().await?;
let sid = derive_id(&*pkey);
let hostname = derive_hostname(&sid);
db.put(
&<JsonPointer>::default(),
&Database::init(
get_id().await?,
&get_hostname().await?,
sid,
&hostname,
&os_key(&mut secret_store.acquire().await?).await?,
password_hash(&mut secret_store.acquire().await?).await?,
),

View File

@@ -62,6 +62,7 @@ pub enum ErrorKind {
MultipleErrors = 54,
Incoherent = 55,
InvalidBackupTargetId = 56,
ProductKeyMismatch = 57,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
@@ -123,6 +124,7 @@ impl ErrorKind {
MultipleErrors => "Multiple Errors",
Incoherent => "Incoherent",
InvalidBackupTargetId => "Invalid Backup Target ID",
ProductKeyMismatch => "Incompatible Product Keys",
}
}
}

View File

@@ -1,4 +1,6 @@
use digest::Digest;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tracing::instrument;
@@ -9,7 +11,11 @@ pub const PRODUCT_KEY_PATH: &'static str = "/embassy-os/product_key.txt";
#[instrument]
pub async fn get_hostname() -> Result<String, Error> {
Ok(format!("embassy-{}", get_id().await?))
Ok(derive_hostname(&get_id().await?))
}
pub fn derive_hostname(id: &str) -> String {
format!("embassy-{}", id)
}
#[instrument]
@@ -40,12 +46,23 @@ pub async fn get_product_key() -> Result<String, Error> {
}
#[instrument]
pub async fn get_id() -> Result<String, Error> {
let key = get_product_key().await?;
pub async fn set_product_key(key: &str) -> Result<(), Error> {
let mut pkey_file = File::create(PRODUCT_KEY_PATH).await?;
pkey_file.write_all(key.as_bytes()).await?;
Ok(())
}
pub fn derive_id(key: &str) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(key.as_bytes());
let res = hasher.finalize();
Ok(hex::encode(&res[0..4]))
hex::encode(&res[0..4])
}
#[instrument]
pub async fn get_id() -> Result<String, Error> {
let key = get_product_key().await?;
Ok(derive_id(&key))
}
// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "embassy-"$1}' | xargs hostnamectl set-hostname && systemctl restart avahi-daemon

View File

@@ -6,7 +6,7 @@ use crate::install::PKG_DOCKER_DIR;
use crate::util::Invoke;
use crate::Error;
pub async fn init(cfg: &RpcContextConfig) -> Result<(), Error> {
pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error> {
let secret_store = cfg.secret_store().await?;
let log_dir = cfg.datadir().join("main").join("logs");
if tokio::fs::metadata(&log_dir).await.is_err() {
@@ -59,7 +59,7 @@ pub async fn init(cfg: &RpcContextConfig) -> Result<(), Error> {
crate::ssh::sync_keys_from_db(&secret_store, "/root/.ssh/authorized_keys").await?;
tracing::info!("Synced SSH Keys");
let db = cfg.db(&secret_store).await?;
let db = cfg.db(&secret_store, product_key).await?;
let mut handle = db.handle();

View File

@@ -28,8 +28,8 @@ use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::guard::TmpMountGuard;
use crate::disk::util::{pvscan, recovery_info, DiskInfo, DiskListResponse, EmbassyOsRecoveryInfo};
use crate::hostname::PRODUCT_KEY_PATH;
use crate::disk::util::{pvscan, recovery_info, DiskListResponse, EmbassyOsRecoveryInfo};
use crate::hostname::{get_product_key, PRODUCT_KEY_PATH};
use crate::id::Id;
use crate::init::init;
use crate::install::PKG_PUBLIC_DIR;
@@ -39,7 +39,7 @@ use crate::sound::BEETHOVEN;
use crate::util::io::{dir_size, from_yaml_async_reader};
use crate::util::Version;
use crate::volume::{data_dir, VolumeId};
use crate::{ensure_code, Error, ResultExt};
use crate::{ensure_code, Error, ErrorKind, ResultExt};
#[instrument(skip(secrets))]
pub async fn password_hash<Ex>(secrets: &mut Ex) -> Result<String, Error>
@@ -92,14 +92,31 @@ pub async fn attach(
#[arg] guid: Arc<String>,
) -> Result<SetupResult, Error> {
crate::disk::main::import(&*guid, &ctx.datadir, DEFAULT_PASSWORD).await?;
init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?).await?;
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&get_product_key().await?,
)
.await?;
let product_id_path = Path::new("/embassy-data/main/product_id.txt");
if tokio::fs::metadata(product_id_path).await.is_ok() {
let pid = tokio::fs::read_to_string(product_id_path).await?;
if pid != crate::hostname::derive_id(&*ctx.product_key().await?) {
return Err(Error::new(
eyre!("The EmbassyOS product key does not match the supplied drive"),
ErrorKind::ProductKeyMismatch,
));
}
}
*ctx.disk_guid.write().await = Some(guid.clone());
let secrets = ctx.secret_store().await?;
let tor_key = crate::net::tor::os_key(&mut secrets.acquire().await?).await?;
let (_, root_ca) = SslManager::init(secrets).await?.export_root_ca().await?;
Ok(SetupResult {
tor_address: format!("http://{}", tor_key.public().get_onion_address()),
lan_address: format!("https://embassy-{}.local", crate::hostname::get_id().await?),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&*ctx.product_key().await?)
),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
})
}
@@ -186,7 +203,7 @@ pub async fn execute(
recovery_source = Some(BackupTargetFS::Disk(BlockDev::new(v2_drive.clone())))
}
match execute_inner(
ctx,
ctx.clone(),
embassy_logicalname,
embassy_password,
recovery_source,
@@ -198,7 +215,10 @@ pub async fn execute(
tracing::info!("Setup Successful! Tor Address: {}", tor_addr);
Ok(SetupResult {
tor_address: format!("http://{}", tor_addr),
lan_address: format!("https://embassy-{}.local", crate::hostname::get_id().await?),
lan_address: format!(
"https://embassy-{}.local",
crate::hostname::derive_id(&ctx.product_key().await?)
),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
})
}
@@ -222,12 +242,33 @@ pub async fn complete(#[context] ctx: SetupContext) -> Result<(), Error> {
));
};
if tokio::fs::metadata(PRODUCT_KEY_PATH).await.is_err() {
let mut pkey_file = File::create(PRODUCT_KEY_PATH).await?;
pkey_file
.write_all(ctx.product_key().await?.as_bytes())
.await?;
pkey_file.sync_all().await?;
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
} else {
let key_on_disk = crate::hostname::get_product_key().await?;
let key_in_cache = ctx.product_key().await?;
if *key_in_cache != key_on_disk {
crate::hostname::set_product_key(&*ctx.product_key().await?).await?;
}
}
tokio::fs::write(
Path::new("/embassy-data/main/product_id.txt"),
crate::hostname::derive_id(&*ctx.product_key().await?),
)
.await?;
let secrets = ctx.secret_store().await?;
let mut db = ctx.db(&secrets).await?.handle();
let hostname = crate::hostname::get_hostname().await?;
let si = crate::db::DatabaseModel::new().server_info();
si.clone()
.id()
.put(&mut db, &crate::hostname::get_id().await?)
.await?;
si.lan_address()
.put(
&mut db,
&format!("https://{}.local", &hostname).parse().unwrap(),
)
.await?;
let mut guid_file = File::create("/embassy-os/disk.guid").await?;
guid_file.write_all(guid.as_bytes()).await?;
guid_file.sync_all().await?;
@@ -269,7 +310,11 @@ pub async fn execute_inner(
recovery_password,
)
.await?;
init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?).await?;
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&ctx.product_key().await?,
)
.await?;
tokio::spawn(async move {
if let Err(e) = recover_fut
.and_then(|_| async {
@@ -290,7 +335,11 @@ pub async fn execute_inner(
(tor_addr, root_ca)
} else {
let res = fresh_setup(&ctx, &embassy_password).await?;
init(&RpcContextConfig::load(ctx.config_path.as_ref()).await?).await?;
init(
&RpcContextConfig::load(ctx.config_path.as_ref()).await?,
&ctx.product_key().await?,
)
.await?;
*ctx.disk_guid.write().await = Some(guid);
res
};
@@ -342,12 +391,7 @@ async fn recover(
.map(|i| i.version.clone())
.unwrap_or_else(|| emver::Version::new(0, 2, 0, 0).into());
let res = if recovery_version.major() == 0 && recovery_version.minor() == 2 {
let (tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?;
(
tor_addr,
root_ca,
recover_v2(ctx.clone(), recovery_source).boxed(),
)
recover_v2(ctx.clone(), &embassy_password, recovery_source).await?
} else if recovery_version.major() == 0 && recovery_version.minor() == 3 {
recover_full_embassy(
ctx.clone(),
@@ -466,137 +510,207 @@ fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + Send
}
#[instrument(skip(ctx))]
async fn recover_v2(ctx: SetupContext, recovery_source: TmpMountGuard) -> Result<(), Error> {
async fn recover_v2(
ctx: SetupContext,
embassy_password: &str,
recovery_source: TmpMountGuard,
) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> {
let secret_store = ctx.secret_store().await?;
let db = ctx.db(&secret_store).await?;
let mut handle = db.handle();
let apps_yaml_path = recovery_source
// migrate the root CA
let root_ca_key_path = recovery_source
.as_ref()
.join("root")
.join("appmgr")
.join("apps.yaml");
#[derive(Deserialize)]
struct LegacyAppInfo {
title: String,
version: Version,
}
let packages: BTreeMap<PackageId, LegacyAppInfo> =
from_yaml_async_reader(File::open(&apps_yaml_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
apps_yaml_path.display().to_string(),
)
})?)
.await?;
.join("agent")
.join("ca")
.join("private")
.join("embassy-root-ca.key.pem");
let root_ca_cert_path = recovery_source
.as_ref()
.join("root")
.join("agent")
.join("ca")
.join("certs")
.join("embassy-root-ca.cert.pem");
let (root_ca_key_bytes, root_ca_cert_bytes) = tokio::try_join!(
tokio::fs::read(root_ca_key_path),
tokio::fs::read(root_ca_cert_path)
)?;
let root_ca_key = openssl::pkey::PKey::private_key_from_pem(&root_ca_key_bytes)?;
let root_ca_cert = openssl::x509::X509::from_pem(&root_ca_cert_bytes)?;
crate::net::ssl::SslManager::import_root_ca(
secret_store.clone(),
root_ca_key,
root_ca_cert.clone(),
)
.await?;
let volume_path = recovery_source.as_ref().join("root/volumes");
let mut total_bytes = 0;
for (pkg_id, _) in &packages {
let volume_src_path = volume_path.join(&pkg_id);
total_bytes += dir_size(&volume_src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
volume_src_path.display().to_string(),
)
})?;
}
*ctx.recovery_status.write().await = Some(Ok(RecoveryStatus {
bytes_transferred: 0,
total_bytes,
complete: false,
}));
let bytes_transferred = AtomicU64::new(0);
let volume_id = VolumeId::Custom(Id::try_from("main".to_owned())?);
for (pkg_id, info) in packages {
let (src_id, dst_id) = rename_pkg_id(pkg_id);
let volume_src_path = volume_path.join(&src_id);
let volume_dst_path = data_dir(&ctx.datadir, &dst_id, &volume_id);
tokio::select!(
res = dir_copy(
&volume_src_path,
&volume_dst_path,
&bytes_transferred
) => res?,
_ = async {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
*ctx.recovery_status.write().await = Some(Ok(RecoveryStatus {
bytes_transferred: bytes_transferred.load(Ordering::Relaxed),
total_bytes,
complete: false
}));
}
} => (),
);
let tor_src_path = recovery_source
// migrate the tor address
let tor_key_path = recovery_source
.as_ref()
.join("var")
.join("lib")
.join("tor")
.join("agent")
.join("hs_ed25519_secret_key");
let tor_key_bytes = tokio::fs::read(tor_key_path).await?;
let mut tor_key_array_tmp = [0u8; 64];
tor_key_array_tmp.clone_from_slice(&tor_key_bytes[32..]);
let tor_key: TorSecretKeyV3 = tor_key_array_tmp.into();
let key_vec = tor_key.as_bytes().to_vec();
let password = argon2::hash_encoded(
embassy_password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::default(),
)
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
let sqlite_pool = ctx.secret_store().await?;
sqlx::query!(
"REPLACE INTO account (id, password, tor_key) VALUES (?, ?, ?)",
0,
password,
key_vec
)
.execute(&mut sqlite_pool.acquire().await?)
.await?;
// rest of migration as future
let fut = async move {
let db = ctx.db(&secret_store).await?;
let mut handle = db.handle();
let apps_yaml_path = recovery_source
.as_ref()
.join("var/lib/tor")
.join(format!("app-{}", src_id))
.join("hs_ed25519_secret_key");
let key_vec = tokio::fs::read(&tor_src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
tor_src_path.display().to_string(),
)
})?;
ensure_code!(
key_vec.len() == 96,
crate::ErrorKind::Tor,
"{} not 96 bytes",
tor_src_path.display()
);
let key_vec = key_vec[32..].to_vec();
sqlx::query!(
"REPLACE INTO tor (package, interface, key) VALUES (?, 'main', ?)",
*dst_id,
key_vec,
)
.execute(&mut secret_store.acquire().await?)
.await?;
let icon_leaf = AsRef::<Path>::as_ref(&dst_id)
.join(info.version.as_str())
.join("icon.png");
let icon_src_path = recovery_source
.as_ref()
.join("root/agent/icons")
.join(format!("{}.png", src_id));
let icon_dst_path = ctx.datadir.join(PKG_PUBLIC_DIR).join(&icon_leaf);
if let Some(parent) = icon_dst_path.parent() {
tokio::fs::create_dir_all(&parent)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?;
.join("root")
.join("appmgr")
.join("apps.yaml");
#[derive(Deserialize)]
struct LegacyAppInfo {
title: String,
version: Version,
}
tokio::fs::copy(&icon_src_path, &icon_dst_path)
.await
.with_ctx(|_| {
let packages: BTreeMap<PackageId, LegacyAppInfo> =
from_yaml_async_reader(File::open(&apps_yaml_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!(
"cp {} -> {}",
icon_src_path.display(),
icon_dst_path.display()
),
apps_yaml_path.display().to_string(),
)
})?)
.await?;
let volume_path = recovery_source.as_ref().join("root/volumes");
let mut total_bytes = 0;
for (pkg_id, _) in &packages {
let volume_src_path = volume_path.join(&pkg_id);
total_bytes += dir_size(&volume_src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
volume_src_path.display().to_string(),
)
})?;
let icon_url = Path::new("/public/package-data").join(&icon_leaf);
crate::db::DatabaseModel::new()
.recovered_packages()
.idx_model(&dst_id)
.put(
&mut handle,
&RecoveredPackageInfo {
title: info.title,
icon: icon_url.display().to_string(),
version: info.version,
},
}
*ctx.recovery_status.write().await = Some(Ok(RecoveryStatus {
bytes_transferred: 0,
total_bytes,
complete: false,
}));
let bytes_transferred = AtomicU64::new(0);
let volume_id = VolumeId::Custom(Id::try_from("main".to_owned())?);
for (pkg_id, info) in packages {
let (src_id, dst_id) = rename_pkg_id(pkg_id);
let volume_src_path = volume_path.join(&src_id);
let volume_dst_path = data_dir(&ctx.datadir, &dst_id, &volume_id);
tokio::select!(
res = dir_copy(
&volume_src_path,
&volume_dst_path,
&bytes_transferred
) => res?,
_ = async {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
*ctx.recovery_status.write().await = Some(Ok(RecoveryStatus {
bytes_transferred: bytes_transferred.load(Ordering::Relaxed),
total_bytes,
complete: false
}));
}
} => (),
);
let tor_src_path = recovery_source
.as_ref()
.join("var/lib/tor")
.join(format!("app-{}", src_id))
.join("hs_ed25519_secret_key");
let key_vec = tokio::fs::read(&tor_src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
tor_src_path.display().to_string(),
)
})?;
ensure_code!(
key_vec.len() == 96,
crate::ErrorKind::Tor,
"{} not 96 bytes",
tor_src_path.display()
);
let key_vec = key_vec[32..].to_vec();
sqlx::query!(
"REPLACE INTO tor (package, interface, key) VALUES (?, 'main', ?)",
*dst_id,
key_vec,
)
.execute(&mut secret_store.acquire().await?)
.await?;
}
let icon_leaf = AsRef::<Path>::as_ref(&dst_id)
.join(info.version.as_str())
.join("icon.png");
let icon_src_path = recovery_source
.as_ref()
.join("root/agent/icons")
.join(format!("{}.png", src_id));
let icon_dst_path = ctx.datadir.join(PKG_PUBLIC_DIR).join(&icon_leaf);
if let Some(parent) = icon_dst_path.parent() {
tokio::fs::create_dir_all(&parent)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?;
}
tokio::fs::copy(&icon_src_path, &icon_dst_path)
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!(
"cp {} -> {}",
icon_src_path.display(),
icon_dst_path.display()
),
)
})?;
let icon_url = Path::new("/public/package-data").join(&icon_leaf);
crate::db::DatabaseModel::new()
.recovered_packages()
.idx_model(&dst_id)
.put(
&mut handle,
&RecoveredPackageInfo {
title: info.title,
icon: icon_url.display().to_string(),
version: info.version,
},
)
.await?;
}
secret_store.close().await;
recovery_source.unmount().await?;
Ok(())
secret_store.close().await;
recovery_source.unmount().await?;
Ok(())
};
Ok((
tor_key.public().get_onion_address(),
root_ca_cert,
fut.boxed(),
))
}
fn rename_pkg_id(src_pkg_id: PackageId) -> (PackageId, PackageId) {

View File

@@ -131,36 +131,51 @@ export class RecoverPage {
}
async select (target: DiskBackupTarget) {
if (target['embassy-os'].version.startsWith('0.2')) {
return this.selectRecoverySource(target.logicalname)
}
const is02x = target['embassy-os'].version.startsWith('0.2')
if (this.stateService.hasProductKey) {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.selectRecoverySource(target.logicalname, res.data.password)
}
})
await modal.present()
if (is02x) {
this.selectRecoverySource(target.logicalname)
} else {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.selectRecoverySource(target.logicalname, res.data.password)
}
})
await modal.present()
}
// if no product key, it means they are an upgrade kit user
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.productKey) {
this.selectRecoverySource(target.logicalname)
}
})
await modal.present()
if (!is02x) {
const alert = await this.alertCtrl.create({
header: 'Error',
message: 'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.productKey) {
this.selectRecoverySource(target.logicalname)
}
})
await modal.present()
}
}
}