Skip to content

Commit 04ee821

Browse files
kwallet: Add a helper for converting to a secret service format
Following what kwallet c++ does.
1 parent 4b783c5 commit 04ee821

12 files changed

Lines changed: 132 additions & 98 deletions

File tree

.github/workflows/CI.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
4242
- name: Build kwallet-parser
4343
run: |
44-
cargo build --manifest-path ./kwallet/parser/Cargo.toml --features serde
44+
cargo build --manifest-path ./kwallet/parser/Cargo.toml
4545
4646
- name: Build kwallet-cli
4747
run: |
@@ -95,7 +95,7 @@ jobs:
9595
9696
- name: Test kwallet-parser
9797
run: |
98-
cargo test --manifest-path ./kwallet/parser/Cargo.toml --features serde
98+
cargo test --manifest-path ./kwallet/parser/Cargo.toml
9999
100100
cargo-deny:
101101
runs-on: ubuntu-latest

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ exclude = ["org.freedesktop.Secrets.xml"]
2929
[workspace.dependencies]
3030
zvariant = { version = "5.8", default-features = false, features = ["gvariant", "serde_bytes"]}
3131
ashpd = {version = "0.13", default-features = false}
32+
base64 = "0.22"
3233
endi = "1.1"
3334
clap = { version = "4.5", features = [ "cargo", "derive" ] }
3435
futures-channel = "0.3"
@@ -37,6 +38,7 @@ futures-util = "0.3"
3738
num-bigint-dig = { version = "0.9", features = ["zeroize"] }
3839
oo7 = { path = "client", version = "0.6.0-alpha", default-features = false, features = ["unstable", "tracing"]}
3940
serde = { version = "1.0", features = ["derive"] }
41+
serde_json = "1.0"
4042
tokio = { version = "1.50", default-features = false }
4143
tempfile = "3.26"
4244
tracing = "0.1"

cli/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ hex = "0.4"
2020
oo7 = { workspace = true, features = ["tokio"] }
2121
rpassword = "7.4.0"
2222
tokio = { workspace = true, features = [ "macros", "rt"] }
23-
serde_json = "1.0"
24-
serde = "1.0"
23+
serde_json = { workspace = true }
24+
serde = { workspace = true }
2525

2626
[features]
2727
default = ["native_crypto"]

kwallet/cli/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ license.workspace = true
1010
rust-version.workspace = true
1111

1212
[dependencies]
13-
kwallet-parser = { version = "0.6.0-alpha", path = "../parser", features = ["serde"] }
13+
kwallet-parser = { version = "0.6.0-alpha", path = "../parser" }
1414
oo7 = { workspace = true, features = ["tokio", "native_crypto"] }
1515
clap = { workspace = true }
1616
serde = { workspace = true }
17-
serde_json = "1.0"
17+
serde_json = { workspace = true }
1818
rpassword = "7.3"
1919
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }

kwallet/cli/src/main.rs

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -165,50 +165,27 @@ async fn migrate_to_secret_service(
165165

166166
for (folder_name, folder) in wallet.wallet() {
167167
for (key, entry) in folder {
168-
let label = format!("{}/{}", folder_name, key);
169-
let mut attributes = HashMap::new();
170-
attributes.insert("folder".to_string(), folder_name.to_string());
171-
attributes.insert("key".to_string(), key.to_string());
172-
attributes.insert("source".to_string(), "kwallet".to_string());
173-
174-
match entry.entry_type() {
175-
EntryType::Password => {
176-
if let Ok(password) = entry.as_password() {
177-
attributes.insert("type".to_string(), "password".to_string());
178-
keyring
179-
.create_item(&label, &attributes, oo7::Secret::text(&password), true)
180-
.await?;
181-
count += 1;
182-
println!(" ✓ Migrated {} (password)", label);
183-
}
184-
}
185-
EntryType::Map => {
186-
if let Ok(map) = entry.as_map() {
187-
attributes.insert("type".to_string(), "map".to_string());
188-
for (k, v) in &map {
189-
attributes.insert(k.clone(), v.clone());
190-
}
191-
keyring
192-
.create_item(&label, &attributes, oo7::Secret::text(""), true)
193-
.await?;
194-
count += 1;
195-
println!(" ✓ Migrated {} (map)", label);
196-
}
197-
}
198-
EntryType::Stream => {
199-
attributes.insert("type".to_string(), "stream".to_string());
168+
match kwallet_parser::convert_entry(folder_name, key, entry) {
169+
Ok(ss_entry) => {
200170
keyring
201171
.create_item(
202-
&label,
203-
&attributes,
204-
oo7::Secret::blob(entry.as_stream()),
172+
ss_entry.label(),
173+
ss_entry.attributes(),
174+
oo7::Secret::blob(ss_entry.secret()),
205175
true,
206176
)
207177
.await?;
208178
count += 1;
209-
println!(" ✓ Migrated {} (stream)", label);
179+
let entry_type = ss_entry
180+
.attributes()
181+
.get("type")
182+
.map(|s| s.as_str())
183+
.unwrap_or("unknown");
184+
println!(" ✓ Migrated {} ({})", ss_entry.label(), entry_type);
185+
}
186+
Err(e) => {
187+
eprintln!(" ✗ Skipped {}/{}: {}", folder_name, key, e);
210188
}
211-
EntryType::Unknown => {}
212189
}
213190
}
214191
}

kwallet/parser/Cargo.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ sha2 = "0.10"
2222
pbkdf2 = { version = "0.12", default-features = false, features = ["simple"] }
2323
cipher = { version = "0.4", features = ["block-padding"] }
2424
zeroize = { workspace = true }
25-
serde = { workspace = true, optional = true }
25+
serde = { workspace = true }
26+
serde_json = { workspace = true }
27+
base64 = { workspace = true }
2628

2729
[dev-dependencies]
2830
hex = "0.4"
29-
30-
[features]
31-
serde = ["dep:serde"]

kwallet/parser/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ mod crypto;
5151
mod error;
5252
mod format;
5353
mod qdata;
54+
pub mod secret_service;
5455
mod wallet;
5556

5657
use std::{fs, io::Cursor, path::Path};
5758

5859
pub use error::{Error, Result};
5960
pub use format::{CipherType, HashType};
61+
pub use secret_service::{SecretServiceEntry, convert_entry};
6062
pub use wallet::{Entry, EntryType, Folder, Wallet};
6163

6264
/// A parsed KWallet file
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Helper functions for migrating KWallet entries to Secret Service format
2+
//! matching the behavior of KWallet's own migration code.
3+
4+
use std::collections::HashMap;
5+
6+
use base64::Engine;
7+
8+
use crate::{Entry, EntryType};
9+
10+
/// Result of converting a KWallet entry to Secret Service format
11+
#[derive(Debug, Clone)]
12+
pub struct SecretServiceEntry {
13+
label: String,
14+
attributes: HashMap<String, String>,
15+
secret: Vec<u8>,
16+
}
17+
18+
impl SecretServiceEntry {
19+
/// The Secret Service label (format: "folder/key")
20+
pub fn label(&self) -> &str {
21+
&self.label
22+
}
23+
24+
/// Attributes that should be set on the Secret Service item
25+
pub fn attributes(&self) -> &HashMap<String, String> {
26+
&self.attributes
27+
}
28+
29+
/// The secret value (as bytes)
30+
pub fn secret(&self) -> &[u8] {
31+
&self.secret
32+
}
33+
}
34+
35+
/// Convert a KWallet entry to Secret Service format
36+
///
37+
/// This follows KWallet's migration behavior:
38+
/// - Attributes: `user` (key), `server` (folder), `type` (password/map/base64)
39+
/// - Label: "folder/key"
40+
/// - Secret:
41+
/// - Password: UTF-8 text
42+
/// - Map: JSON object
43+
/// - Stream: Base64-encoded binary data
44+
pub fn convert_entry(
45+
folder: &str,
46+
key: &str,
47+
entry: &Entry,
48+
) -> Result<SecretServiceEntry, Box<dyn std::error::Error>> {
49+
let label = format!("{}/{}", folder, key);
50+
let mut attributes = HashMap::new();
51+
52+
// Standard Secret Service attributes used by KWallet
53+
attributes.insert("user".to_string(), key.to_string());
54+
attributes.insert("server".to_string(), folder.to_string());
55+
56+
let (type_str, secret) = match entry.entry_type() {
57+
EntryType::Password => {
58+
let password = entry.as_password()?;
59+
("password".to_string(), password.into_bytes())
60+
}
61+
EntryType::Map => {
62+
let map = entry.as_map()?;
63+
// Convert map to JSON like KWallet does
64+
let json_value = serde_json::to_value(map)?;
65+
let json_bytes = serde_json::to_vec(&json_value)?;
66+
("map".to_string(), json_bytes)
67+
}
68+
EntryType::Stream => {
69+
// KWallet stores streams as base64
70+
let stream_data = entry.as_stream();
71+
let base64_data = base64::engine::general_purpose::STANDARD.encode(stream_data);
72+
("base64".to_string(), base64_data.into_bytes())
73+
}
74+
EntryType::Unknown => {
75+
return Err("Cannot convert unknown entry type".into());
76+
}
77+
};
78+
79+
attributes.insert("type".to_string(), type_str);
80+
81+
Ok(SecretServiceEntry {
82+
label,
83+
attributes,
84+
secret,
85+
})
86+
}

kwallet/parser/src/wallet.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ use crate::{
77
qdata::QDataStreamReader,
88
};
99

10-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11-
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
12-
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
10+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
11+
#[serde(rename_all = "lowercase")]
1312
#[repr(i32)]
1413
pub enum EntryType {
1514
Unknown = 0,

0 commit comments

Comments
 (0)