diff --git a/appmgr/src/bin/embassy-init.rs b/appmgr/src/bin/embassy-init.rs index 0a2fc7c75..628431e9f 100644 --- a/appmgr/src/bin/embassy-init.rs +++ b/appmgr/src/bin/embassy-init.rs @@ -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(()) diff --git a/appmgr/src/context/rpc.rs b/appmgr/src/context/rpc.rs index 096343264..81e6b1474 100644 --- a/appmgr/src/context/rpc.rs +++ b/appmgr/src/context/rpc.rs @@ -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 { + pub async fn db(&self, secret_store: &SqlitePool, product_key: &str) -> Result { + 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( &::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() diff --git a/appmgr/src/context/setup.rs b/appmgr/src/context/setup.rs index eb5abd348..25231e114 100644 --- a/appmgr/src/context/setup.rs +++ b/appmgr/src/context/setup.rs @@ -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(&::default()).await? { + let pkey = self.product_key().await?; + let sid = derive_id(&*pkey); + let hostname = derive_hostname(&sid); db.put( &::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?, ), diff --git a/appmgr/src/error.rs b/appmgr/src/error.rs index 710e6703e..6410333c3 100644 --- a/appmgr/src/error.rs +++ b/appmgr/src/error.rs @@ -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", } } } diff --git a/appmgr/src/hostname.rs b/appmgr/src/hostname.rs index 9e9fb0a86..cc441ca37 100644 --- a/appmgr/src/hostname.rs +++ b/appmgr/src/hostname.rs @@ -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 { - 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 { } #[instrument] -pub async fn get_id() -> Result { - 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 { + 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 diff --git a/appmgr/src/init.rs b/appmgr/src/init.rs index 56eea0923..7d0066156 100644 --- a/appmgr/src/init.rs +++ b/appmgr/src/init.rs @@ -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(); diff --git a/appmgr/src/setup.rs b/appmgr/src/setup.rs index 5f9e9f2f6..012bfba1f 100644 --- a/appmgr/src/setup.rs +++ b/appmgr/src/setup.rs @@ -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(secrets: &mut Ex) -> Result @@ -92,14 +92,31 @@ pub async fn attach( #[arg] guid: Arc, ) -> Result { 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 + 'a + Send + Sync, P1: AsRef + '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 = - 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::::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 = + 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::::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) { diff --git a/setup-wizard/src/app/pages/recover/recover.page.ts b/setup-wizard/src/app/pages/recover/recover.page.ts index c41264a54..659ba94a0 100644 --- a/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/setup-wizard/src/app/pages/recover/recover.page.ts @@ -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() + } } }