add passphrase reset
This commit is contained in:
parent
bf0a2ca72d
commit
504c0b4156
@ -46,6 +46,7 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
ipc::unlock,
|
ipc::unlock,
|
||||||
ipc::lock,
|
ipc::lock,
|
||||||
|
ipc::reset_session,
|
||||||
ipc::set_passphrase,
|
ipc::set_passphrase,
|
||||||
ipc::respond,
|
ipc::respond,
|
||||||
ipc::get_session_status,
|
ipc::get_session_status,
|
||||||
|
@ -151,9 +151,11 @@ impl CredentialRecord {
|
|||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
todo!()
|
for record in Self::list(old, pool).await? {
|
||||||
|
record.save(new, pool).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,6 +342,22 @@ mod tests {
|
|||||||
assert_eq!(aws_record(), records[0]);
|
assert_eq!(aws_record(), records[0]);
|
||||||
assert_eq!(aws_record_2(), records[1]);
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
|
async fn test_rekey(pool: SqlitePool) {
|
||||||
|
let old = Crypto::fixed();
|
||||||
|
let new = Crypto::random();
|
||||||
|
|
||||||
|
CredentialRecord::rekey(&old, &new, &pool).await
|
||||||
|
.expect("Failed to rekey credentials");
|
||||||
|
|
||||||
|
let records = CredentialRecord::list(&new, &pool).await
|
||||||
|
.expect("Failed to re-list credentials");
|
||||||
|
|
||||||
|
assert_eq!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,6 +79,17 @@ impl AppSession {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn reset(&mut self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Unlocked {..} | Self::Locked {..} => {
|
||||||
|
kv::delete_multi(pool, &["salt", "verify_nonce", "verify_blob"]).await?;
|
||||||
|
*self = Self::Empty;
|
||||||
|
},
|
||||||
|
Self::Empty => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
|
pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Empty => Err(GetCredentialsError::Empty),
|
Self::Empty => Err(GetCredentialsError::Empty),
|
||||||
|
13
src-tauri/src/fixtures/kv.sql
Normal file
13
src-tauri/src/fixtures/kv.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
INSERT INTO kv (name, value)
|
||||||
|
VALUES
|
||||||
|
-- b"hello world" (raw bytes)
|
||||||
|
('test_bytes', X'68656C6C6F20776F726C64'),
|
||||||
|
|
||||||
|
-- b"\"hello world\"" (JSON string)
|
||||||
|
('test_string', X'2268656C6C6F20776F726C6422'),
|
||||||
|
|
||||||
|
-- b"123" (JSON integer)
|
||||||
|
('test_int', X'313233'),
|
||||||
|
|
||||||
|
-- b"true" (JSON bool)
|
||||||
|
('test_bool', X'74727565')
|
@ -80,6 +80,12 @@ pub async fn lock(app_state: State<'_, AppState>) -> Result<(), LockError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_session(app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
|
app_state.reset_session().await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
pub async fn set_passphrase(passphrase: &str, app_state: State<'_, AppState>) -> Result<(), SaveCredentialsError> {
|
||||||
app_state.set_passphrase(passphrase).await
|
app_state.set_passphrase(passphrase).await
|
||||||
|
@ -6,7 +6,7 @@ use crate::errors::*;
|
|||||||
|
|
||||||
|
|
||||||
pub async fn save<T>(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error>
|
pub async fn save<T>(pool: &SqlitePool, name: &str, value: &T) -> Result<(), sqlx::Error>
|
||||||
where T: Serialize
|
where T: Serialize + ?Sized
|
||||||
{
|
{
|
||||||
let bytes = serde_json::to_vec(value).unwrap();
|
let bytes = serde_json::to_vec(value).unwrap();
|
||||||
save_bytes(pool, name, &bytes).await
|
save_bytes(pool, name, &bytes).await
|
||||||
@ -44,9 +44,33 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||||
|
let placeholder = names.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(",");
|
||||||
|
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||||
|
|
||||||
|
let mut q = sqlx::query(&query);
|
||||||
|
for name in names {
|
||||||
|
q = q.bind(name);
|
||||||
|
}
|
||||||
|
q.execute(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
macro_rules! load_bytes_multi {
|
macro_rules! load_bytes_multi {
|
||||||
(
|
(
|
||||||
$pool:ident,
|
$pool:expr,
|
||||||
$($name:literal),*
|
$($name:literal),*
|
||||||
) => {
|
) => {
|
||||||
// wrap everything up in an async block for easy short-circuiting...
|
// wrap everything up in an async block for easy short-circuiting...
|
||||||
@ -78,7 +102,7 @@ pub(crate) use load_bytes_multi;
|
|||||||
|
|
||||||
// macro_rules! load_multi {
|
// macro_rules! load_multi {
|
||||||
// (
|
// (
|
||||||
// $pool:ident,
|
// $pool:expr,
|
||||||
// $($name:literal),*
|
// $($name:literal),*
|
||||||
// ) => {
|
// ) => {
|
||||||
// (|| {
|
// (|| {
|
||||||
@ -93,3 +117,94 @@ pub(crate) use load_bytes_multi;
|
|||||||
// })()
|
// })()
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save_bytes(pool: SqlitePool) {
|
||||||
|
save_bytes(&pool, "test_bytes", b"hello world").await
|
||||||
|
.expect("Failed to save bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn test_save(pool: SqlitePool) {
|
||||||
|
save(&pool, "test_string", "hello world").await
|
||||||
|
.expect("Failed to save string");
|
||||||
|
save(&pool, "test_int", &123).await
|
||||||
|
.expect("Failed to save integer");
|
||||||
|
save(&pool, "test_bool", &true).await
|
||||||
|
.expect("Failed to save bool");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_bytes(pool: SqlitePool) {
|
||||||
|
let bytes = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load(pool: SqlitePool) {
|
||||||
|
let string: String = load(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(string, "hello world".to_string());
|
||||||
|
|
||||||
|
let integer: usize = load(&pool, "test_int").await
|
||||||
|
.expect("Failed to load integer")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(integer, 123);
|
||||||
|
|
||||||
|
let boolean: bool = load(&pool, "test_bool").await
|
||||||
|
.expect("Failed to load boolean")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
assert_eq!(boolean, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_load_multi(pool: SqlitePool) {
|
||||||
|
let (bytes, boolean) = load_bytes_multi!(&pool, "test_bytes", "test_bool")
|
||||||
|
.await
|
||||||
|
.expect("Failed to load items")
|
||||||
|
.expect("Test data not found in database");
|
||||||
|
|
||||||
|
assert_eq!(bytes, Vec::from(b"hello world"));
|
||||||
|
assert_eq!(boolean, Vec::from(b"true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete(pool: SqlitePool) {
|
||||||
|
delete(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to delete data");
|
||||||
|
|
||||||
|
let loaded = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load data");
|
||||||
|
assert_eq!(loaded, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("kv"))]
|
||||||
|
async fn test_delete_multi(pool: SqlitePool) {
|
||||||
|
delete_multi(&pool, &["test_bytes", "test_string"]).await
|
||||||
|
.expect("Failed to delete keys");
|
||||||
|
|
||||||
|
let bytes_opt = load_bytes(&pool, "test_bytes").await
|
||||||
|
.expect("Failed to load bytes");
|
||||||
|
assert_eq!(bytes_opt, None);
|
||||||
|
|
||||||
|
let string_opt = load_bytes(&pool, "test_string").await
|
||||||
|
.expect("Failed to load string");
|
||||||
|
assert_eq!(string_opt, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -254,6 +254,13 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn reset_session(&self) -> Result<(), SaveCredentialsError> {
|
||||||
|
let mut session = self.app_session.write().await;
|
||||||
|
session.reset(&self.pool).await?;
|
||||||
|
sqlx::query!("DELETE FROM credentials").execute(&self.pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_aws_base(&self, name: &str) -> Result<AwsBaseCredential, GetCredentialsError> {
|
pub async fn get_aws_base(&self, name: &str) -> Result<AwsBaseCredential, GetCredentialsError> {
|
||||||
let app_session = self.app_session.read().await;
|
let app_session = self.app_session.read().await;
|
||||||
let crypto = app_session.try_get_crypto()?;
|
let crypto = app_session.try_get_crypto()?;
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getRootCause } from '../lib/errors.js';
|
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
|
||||||
import { navigate } from '../lib/routing.js';
|
|
||||||
import Link from '../ui/Link.svelte';
|
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
|
||||||
|
|
||||||
|
|
||||||
let errorMsg = null;
|
|
||||||
let alert;
|
|
||||||
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
|
|
||||||
|
|
||||||
function confirm() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
errorMsg = 'Passphrases do not match.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let saving = false;
|
|
||||||
async function save() {
|
|
||||||
if (passphrase !== confirmPassphrase) {
|
|
||||||
alert.shake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let credentials = {AccessKeyId, SecretAccessKey};
|
|
||||||
try {
|
|
||||||
saving = true;
|
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
|
||||||
emit('credentials-event', 'entered');
|
|
||||||
if ($appState.currentRequest) {
|
|
||||||
navigate('Approve');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
const root = getRootCause(e);
|
|
||||||
if (e.code === 'GetSession' && root.code) {
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// some of the built-in Tauri errors are plain strings,
|
|
||||||
// so fall back to e if e.msg doesn't exist
|
|
||||||
errorMsg = e.msg || e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the alert already existed, shake it
|
|
||||||
if (alert) {
|
|
||||||
alert.shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
saving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('credentials-event', 'enter-canceled');
|
|
||||||
navigate('Home');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form action="#" on:submit|preventDefault="{save}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
|
|
||||||
<h2 class="text-2xl font-bold text-center">Enter your credentials</h2>
|
|
||||||
|
|
||||||
{#if errorMsg}
|
|
||||||
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
|
|
||||||
<input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" class="input input-bordered" />
|
|
||||||
<input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
|
|
||||||
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{#if saving }
|
|
||||||
<Spinner class="w-5 h-5" thickness="12"/>
|
|
||||||
{:else}
|
|
||||||
Submit
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<Link target={cancel} hotkey="Escape">
|
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
@ -10,6 +10,7 @@
|
|||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import PassphraseInput from '../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './passphrase/ResetPassphrase.svelte';
|
||||||
import Spinner from '../ui/Spinner.svelte';
|
import Spinner from '../ui/Spinner.svelte';
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
||||||
|
|
||||||
@ -75,4 +76,6 @@
|
|||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<ResetPassphrase />
|
||||||
</form>
|
</form>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
import Link from '../../ui/Link.svelte';
|
import Link from '../../ui/Link.svelte';
|
||||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
import ResetPassphrase from './ResetPassphrase.svelte';
|
||||||
import Spinner from '../../ui/Spinner.svelte';
|
import Spinner from '../../ui/Spinner.svelte';
|
||||||
|
|
||||||
export let cancellable = false;
|
export let cancellable = false;
|
||||||
@ -81,4 +82,8 @@
|
|||||||
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button type="button" class="btn btn-outline btn-sm w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $appState.sessionStatus === 'locked'}
|
||||||
|
<ResetPassphrase />
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
42
src/views/passphrase/ResetPassphrase.svelte
Normal file
42
src/views/passphrase/ResetPassphrase.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script>
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { appState } from '../../lib/state.js';
|
||||||
|
|
||||||
|
|
||||||
|
let modal, error, alert;
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
try {
|
||||||
|
invoke('reset_session');
|
||||||
|
$appState.sessionStatus = 'empty';
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (alert) alert.shake();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" class="self-end text-sm text-secondary/75 hover:text-secondary hover:underline focus:ring-accent" on:click={modal.showModal()}>
|
||||||
|
Reset passphrase
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog class="modal" bind:this={modal}>
|
||||||
|
<div class="modal-box space-y-6">
|
||||||
|
{#if error}
|
||||||
|
<ErrorAlert>{error}</ErrorAlert>
|
||||||
|
{/if}
|
||||||
|
<h3 class="text-lg font-bold">Delete all credentials?</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>Credentials are encrypted with your current passphrase and will be lost if the passphrase is reset.</p>
|
||||||
|
<p>Are you sure you want to reset your passphrase and delete all saved credentials?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" class="flex gap-x-4">
|
||||||
|
<button autofocus class="btn btn-outline">Cancel</button>
|
||||||
|
<button class="btn btn-error" on:click={reset}>Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
Loading…
x
Reference in New Issue
Block a user