Skip to content

Commit 16cdbef

Browse files
cli: Allow migrating keyrings from v0 to v1
1 parent fe2168f commit 16cdbef

5 files changed

Lines changed: 143 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ version.workspace = true
1616
anstyle = "1.0"
1717
clap.workspace = true
1818
hex = "0.4"
19+
kwallet-parser = { version = "0.6.0-alpha", path = "../kwallet/parser", optional = true }
1920
oo7 = { workspace = true, features = ["tokio"] }
2021
rpassword.workspace = true
2122
serde.workspace = true
@@ -25,10 +26,11 @@ tokio = { workspace = true, features = ["macros", "rt"] }
2526
zbus.workspace = true
2627

2728
[features]
28-
default = ["native_crypto"]
29+
default = ["native_crypto", "kwallet_migration"]
2930
native_crypto = [
3031
"oo7/native_crypto"
3132
]
3233
openssl_crypto = [
3334
"oo7/openssl_crypto"
3435
]
36+
kwallet_migration = ["dep:kwallet-parser"]

cli/src/main.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,44 @@ enum Commands {
347347

348348
#[command(name = "repair", about = "Repair the keyring")]
349349
Repair,
350+
351+
#[command(
352+
name = "migrate",
353+
about = "Migrate a legacy keyring to the current format",
354+
after_help = format!("{H_STYLE}Examples:{H_STYLE:#}\n {} migrate ~/.local/share/keyrings/login.keyring\n {0} migrate --format kwallet ~/.local/share/kwalletd/kdewallet.kwl", BINARY_NAME)
355+
)]
356+
Migrate {
357+
#[arg(help = "Path to the legacy keyring file")]
358+
file: PathBuf,
359+
#[arg(short, long, default_value_t, help = "Format of the legacy keyring")]
360+
format: MigrateFormat,
361+
},
362+
}
363+
364+
#[derive(Clone, Default, clap::ValueEnum, fmt::Debug)]
365+
enum MigrateFormat {
366+
#[default]
367+
V0,
368+
#[cfg(feature = "kwallet_migration")]
369+
Kwallet,
370+
}
371+
372+
impl fmt::Display for MigrateFormat {
373+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374+
match self {
375+
Self::V0 => write!(f, "v0"),
376+
#[cfg(feature = "kwallet_migration")]
377+
Self::Kwallet => write!(f, "kwallet"),
378+
}
379+
}
350380
}
351381

352382
impl Commands {
353383
async fn execute(self, args: Arguments) -> Result<(), Error> {
384+
if matches!(self, Commands::Migrate { .. }) {
385+
return self.migrate(args.secret).await;
386+
}
387+
354388
let service = Service::new().await?;
355389
if args.app_id.is_some() && args.keyring.is_some() {
356390
return Err(Error::new(
@@ -434,6 +468,7 @@ impl Commands {
434468
};
435469

436470
let output = match self {
471+
Commands::Migrate { .. } => unreachable!(),
437472
Commands::Delete { attributes } => {
438473
match keyring {
439474
Keyring::Collection(collection) => {
@@ -645,6 +680,90 @@ impl Commands {
645680

646681
Ok(())
647682
}
683+
684+
async fn migrate(self, secret: Option<oo7::Secret>) -> Result<(), Error> {
685+
let Self::Migrate { file, format } = self else {
686+
unreachable!();
687+
};
688+
if !file.exists() {
689+
return Err(Error::Owned(format!(
690+
"File '{}' does not exist.",
691+
file.display()
692+
)));
693+
}
694+
695+
let name = file
696+
.file_stem()
697+
.and_then(|s| s.to_str())
698+
.ok_or_else(|| Error::new("Cannot determine keyring name from file path"))?;
699+
700+
let parent = file
701+
.parent()
702+
.ok_or_else(|| Error::new("Cannot determine parent directory of keyring file"))?;
703+
let target_path = parent.join("v1").join(format!("{name}.keyring"));
704+
705+
match format {
706+
MigrateFormat::V0 => {
707+
let keyring =
708+
oo7::file::UnlockedKeyring::load_from_v0(&file, &target_path, secret).await?;
709+
let n_items = keyring.n_items().await;
710+
keyring.write().await?;
711+
712+
println!(
713+
"Migrated v0 keyring '{name}' ({n_items} items) to {}.",
714+
target_path.display()
715+
);
716+
}
717+
#[cfg(feature = "kwallet_migration")]
718+
MigrateFormat::Kwallet => {
719+
let secret = secret.ok_or_else(|| {
720+
Error::new("KWallet migration requires a secret (use --secret)")
721+
})?;
722+
723+
let path = file.to_path_buf();
724+
let password = secret.to_vec();
725+
let wallet = tokio::task::spawn_blocking(move || {
726+
kwallet_parser::KWalletFile::open(&path, &password)
727+
})
728+
.await
729+
.map_err(|e| Error::Owned(format!("Task join error: {e}")))?
730+
.map_err(|e| Error::Owned(format!("Failed to open KWallet file: {e}")))?;
731+
732+
let keyring =
733+
oo7::file::UnlockedKeyring::open_at(parent, name, Some(secret)).await?;
734+
735+
let mut n_items = 0usize;
736+
for (folder_name, folder) in wallet.wallet() {
737+
for (entry_key, entry) in folder {
738+
match kwallet_parser::convert_entry(folder_name, entry_key, entry) {
739+
Ok(ss_entry) => {
740+
keyring
741+
.create_item(
742+
ss_entry.label(),
743+
ss_entry.attributes(),
744+
oo7::Secret::blob(ss_entry.secret()),
745+
true,
746+
)
747+
.await?;
748+
n_items += 1;
749+
}
750+
Err(e) => {
751+
eprintln!("Skipping entry {folder_name}/{entry_key}: {e}");
752+
}
753+
}
754+
}
755+
}
756+
keyring.write().await?;
757+
758+
println!(
759+
"Migrated KWallet '{name}' ({n_items} items) to {}.",
760+
target_path.display()
761+
);
762+
}
763+
}
764+
765+
Ok(())
766+
}
648767
}
649768

650769
#[derive(Parser)]

client/src/file/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ impl std::fmt::Display for Error {
116116
Self::WeakKey(err) => write!(f, "{err}"),
117117
Self::Io(e) => write!(f, "IO error {e}"),
118118
Self::MacError => write!(f, "Mac digest is not equal to the expected value"),
119-
Self::ChecksumMismatch => write!(f, "Checksum is not equal to the expected value"),
119+
Self::ChecksumMismatch => write!(f, "Incorrect secret or corrupted keyring data"),
120120
Self::HashedAttributeMac(e) => write!(f, "Failed to validate hashed attribute {e}"),
121121
Self::NoDataDir => write!(f, "Couldn't retrieve XDG_DATA_DIR"),
122122
Self::TargetFileChanged(e) => write!(f, "The target file has changed {e}"),

client/src/file/unlocked_keyring.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ impl UnlockedKeyring {
119119
})
120120
}
121121

122+
/// Load a v0 (legacy gnome-keyring) file and migrate it to v1 format.
123+
///
124+
/// The migrated keyring will be written to `target_path` when
125+
/// [`write()`](Self::write) is called.
126+
///
127+
/// # Arguments
128+
///
129+
/// * `source` - Path to the legacy v0 keyring file.
130+
/// * `target_path` - Where the v1 keyring should be stored.
131+
/// * `secret` - The encryption secret, or `None` for unencrypted keyrings.
132+
pub async fn load_from_v0(
133+
source: impl AsRef<Path>,
134+
target_path: impl Into<PathBuf>,
135+
secret: Option<Secret>,
136+
) -> Result<Self, Error> {
137+
let mut file = fs::File::open(source.as_ref()).await?;
138+
Self::migrate(&mut file, target_path.into(), secret).await
139+
}
140+
122141
#[cfg_attr(feature = "tracing", tracing::instrument(skip(file, secret), fields(path = ?path.as_ref())))]
123142
async fn migrate(
124143
file: &mut fs::File,

0 commit comments

Comments
 (0)