use std::path::PathBuf;
use clap::Parser;
use kleos_cred::crypto::derive_key;
use kleos_cred::types::SecretData;
use kleos_lib::db::Database;
use serde::Deserialize;
#[derive(Parser)]
#[command(name = "migrate-cred")]
#[command(about = "Migrate credentials from JSON backup to engram-cred database")]
struct Args {
json_file: PathBuf,
db_path: PathBuf,
#[arg(long)]
software_hmac: bool,
}
#[derive(Debug, Deserialize)]
struct BackupEntry {
service: String,
key: String,
value: BackupValue,
}
#[derive(Debug, Deserialize)]
struct BackupValue {
#[serde(rename = "type")]
secret_type: String,
username: Option<String>,
password: Option<String>,
url: Option<String>,
#[serde(rename = "key")]
api_key: Option<String>,
endpoint: Option<String>,
client_id: Option<String>,
client_secret: Option<String>,
redirect_uri: Option<String>,
scopes: Option<Vec<String>>,
content: Option<String>,
notes: Option<String>,
}
impl BackupEntry {
fn to_secret_data(&self) -> Option<SecretData> {
match self.value.secret_type.as_str() {
"login" => Some(SecretData::Login {
username: self.value.username.clone().unwrap_or_default(),
password: self.value.password.clone().unwrap_or_default(),
url: self.value.url.clone(),
totp_seed: None,
notes: self.value.notes.clone(),
}),
"api_key" => Some(SecretData::ApiKey {
key: self
.value
.api_key
.clone()
.or(self.value.password.clone())
.unwrap_or_default(),
endpoint: self.value.endpoint.clone().or(self.value.url.clone()),
notes: self.value.notes.clone(),
}),
"o_auth_app" | "oauth_app" => Some(SecretData::OAuthApp {
client_id: self.value.client_id.clone().unwrap_or_default(),
client_secret: self.value.client_secret.clone().unwrap_or_default(),
redirect_uri: self.value.redirect_uri.clone(),
scopes: self.value.scopes.clone(),
}),
"note" => Some(SecretData::Note {
content: self.value.content.clone().unwrap_or_default(),
}),
_ => {
eprintln!("Unknown secret type: {}", self.value.secret_type);
None
}
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
println!("Reading backup from {:?}...", args.json_file);
let json_data = std::fs::read_to_string(&args.json_file)?;
let entries: Vec<BackupEntry> = serde_json::from_str(&json_data)?;
println!("Found {} entries", entries.len());
let encryption_key = if args.software_hmac {
println!("Using software HMAC (testing mode)");
let challenge = kleos_cred::yubikey::get_or_create_challenge()?;
let response = kleos_cred::yubikey::software_hmac(b"test-secret", &challenge);
derive_key(1, b"", Some(&response))
} else {
println!("Touch YubiKey slot 2 to derive encryption key...");
let challenge = kleos_cred::yubikey::get_or_create_challenge()?;
let response = kleos_cred::yubikey::challenge_response(&challenge)?;
derive_key(1, b"", Some(&response))
};
println!("Encryption key derived");
println!("Opening database at {:?}...", args.db_path);
let db_path_str = args.db_path.to_string_lossy();
let db = Database::connect(&db_path_str).await?;
db.write(|conn| {
Ok(conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cred_secrets (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL,
secret_type TEXT NOT NULL,
encrypted_data BLOB NOT NULL,
nonce BLOB NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, category, name)
);",
)?)
})
.await?;
let mut imported = 0;
let mut skipped = 0;
for entry in &entries {
let category = &entry.service;
let name = &entry.key;
match entry.to_secret_data() {
Some(data) => {
match kleos_cred::storage::store_secret(
&db,
0,
category,
name,
&data,
&encryption_key,
)
.await
{
Ok(id) => {
println!(" [OK] {}/{} -> id={}", category, name, id);
imported += 1;
}
Err(e) => {
if e.to_string().contains("UNIQUE constraint") {
match kleos_cred::storage::update_secret(
&db,
0,
category,
name,
&data,
&encryption_key,
)
.await
{
Ok(()) => {
println!(" [UPDATE] {}/{}", category, name);
imported += 1;
}
Err(e2) => {
eprintln!(" [ERROR] {}/{}: {}", category, name, e2);
skipped += 1;
}
}
} else {
eprintln!(" [ERROR] {}/{}: {}", category, name, e);
skipped += 1;
}
}
}
}
None => {
eprintln!(" [SKIP] {}/{}: unknown type", category, name);
skipped += 1;
}
}
}
println!(
"\nMigration complete: {} imported, {} skipped",
imported, skipped
);
Ok(())
}