add alternate entry mode for ssh keys

This commit is contained in:
Joseph Montanaro 2024-07-03 16:28:12 -04:00
parent ae93a57aab
commit 10231df860
11 changed files with 71 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.5.0", "version": "0.5.1",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",

2
src-tauri/Cargo.lock generated
View File

@ -1196,7 +1196,7 @@ dependencies = [
[[package]] [[package]]
name = "creddy" name = "creddy"
version = "0.4.9" version = "0.5.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"auto-launch", "auto-launch",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy" name = "creddy"
version = "0.5.0" version = "0.5.1"
description = "A friendly AWS credentials manager" description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"] authors = ["Joseph Montanaro"]
license = "" license = ""

View File

@ -53,6 +53,7 @@ pub fn run() -> tauri::Result<()> {
ipc::delete_credential, ipc::delete_credential,
ipc::list_credentials, ipc::list_credentials,
ipc::sshkey_from_file, ipc::sshkey_from_file,
ipc::sshkey_from_private_key,
ipc::get_config, ipc::get_config,
ipc::save_config, ipc::save_config,
ipc::launch_terminal, ipc::launch_terminal,

View File

@ -74,6 +74,21 @@ impl SshKey {
}) })
} }
pub fn from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
let mut privkey = PrivateKey::from_openssh(private_key)?;
if privkey.is_encrypted() {
privkey = privkey.decrypt(passphrase)
.map_err(|_| LoadSshKeyError::InvalidPassphrase)?;
}
Ok(SshKey {
algorithm: privkey.algorithm(),
comment: privkey.comment().into(),
public_key: privkey.public_key().clone(),
private_key: privkey,
})
}
pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> { pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> {
let row = sqlx::query!( let row = sqlx::query!(
"SELECT c.name "SELECT c.name

View File

@ -142,6 +142,12 @@ pub async fn sshkey_from_file(path: &str, passphrase: &str) -> Result<SshKey, Lo
} }
#[tauri::command]
pub async fn sshkey_from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
SshKey::from_private_key(private_key, passphrase)
}
#[tauri::command] #[tauri::command]
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> { pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
let config = app_state.config.read().await; let config = app_state.config.read().await;

View File

@ -50,7 +50,7 @@
} }
}, },
"productName": "creddy", "productName": "creddy",
"version": "0.5.0", "version": "0.5.1",
"identifier": "creddy", "identifier": "creddy",
"plugins": {}, "plugins": {},
"app": { "app": {

View File

@ -9,7 +9,7 @@
// Extra 50ms so the window can finish disappearing before the redraw // Extra 50ms so the window can finish disappearing before the redraw
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50); const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);
let alert; let alert;
let success = false; let success = false;

View File

@ -15,7 +15,6 @@
async function saveCredential() { async function saveCredential() {
await invoke('save_credential', {record: local}); await invoke('save_credential', {record: local});
dispatch('save', local); dispatch('save', local);
showDetails = false;
} }
async function copyText(evt) { async function copyText(evt) {

View File

@ -13,20 +13,23 @@
let name; let name;
let file; let file;
let privateKey = '';
let passphrase = ''; let passphrase = '';
let showDetails = true; let showDetails = true;
let mode = 'file';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let defaultPath = null; let defaultPath = null;
homeDir().then(d => defaultPath = `${d}/.ssh`); homeDir().then(d => defaultPath = `${d}/.ssh`);
let alert; let alert;
let saving = false; let saving = false;
async function saveCredential() { async function saveCredential() {
saving = true; saving = true;
try { try {
let key = await invoke('sshkey_from_file', {path: file.path, passphrase}); let key = await getKey();
const payload = { const payload = {
id: record.id, id: record.id,
name, name,
@ -41,9 +44,40 @@
} }
} }
async function getKey() {
if (mode === 'file') {
return await invoke('sshkey_from_file', {path: file.path, passphrase});
}
else {
return await invoke('sshkey_from_private_key', {privateKey, passphrase});
}
}
</script> </script>
<div role="tablist" class="join max-w-sm mx-auto flex justify-center">
<button
type="button"
role="tab"
class="join-item flex-1 btn border border-primary"
class:btn-primary={mode === 'file'}
on:click={() => mode = 'file'}
>
From file
</button>
<button
type="button"
role="tab"
class="join-item flex-1 btn border border-primary"
class:btn-primary={mode === 'direct'}
on:click={() => mode = 'direct'}
>
From private key
</button>
</div>
<form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}> <form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}>
<ErrorAlert bind:this={alert} /> <ErrorAlert bind:this={alert} />
@ -55,15 +89,20 @@
bind:value={name} bind:value={name}
> >
<span class="justify-self-end">File</span> {#if mode === 'file'}
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} /> <span class="justify-self-end">File</span>
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
{:else}
<span class="justify-self-end">Private key</span>
<textarea bind:value={privateKey} rows="5" class="textarea textarea-bordered bg-transparent font-mono whitespace-pre overflow-x-auto"></textarea>
{/if}
<span class="justify-self-end">Passphrase</span> <span class="justify-self-end">Passphrase</span>
<PassphraseInput class="bg-transparent" bind:value={passphrase} /> <PassphraseInput class="bg-transparent" bind:value={passphrase} />
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
{#if file?.path} {#if file?.path || privateKey !== ''}
<button <button
transition:fade={{duration: 100}} transition:fade={{duration: 100}}
type="submit" type="submit"

View File

@ -64,7 +64,7 @@
{#if record.isNew} {#if record.isNew}
<NewSshKey {record} on:save on:save={handleSave} /> <NewSshKey {record} on:save on:save={handleSave} />
{:else} {:else}
<EditSshKey bind:local={local} {isModified} on:save /> <EditSshKey bind:local={local} {isModified} on:save={handleSave} on:save />
{/if} {/if}
</div> </div>
{/if} {/if}