Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ APPNAME ="Passwords"
# Application version
APPVERSION_M=1
APPVERSION_N=3
APPVERSION_P=2
APPVERSION_P=3
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)

# Application source files
Expand Down
50 changes: 33 additions & 17 deletions clients/wui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,50 @@ import packageJson from "../package.json";
const passwords = new PasswordsManager();
listen((log) => console.log(log));

async function saveJSON(payload, suggestedName) {
const text = JSON.stringify(payload, null, 4);

// showSaveFilePicker() requires transient user activation (a recent gesture),
// so it must be called synchronously from the click handler — before the
// device exchange, which takes too long and would otherwise let the activation
// expire ("Must be handling a user gesture"). This picks the destination up
// front and returns a target the caller writes to once the data is ready.
// Falls back to a Blob/anchor download when the File System Access API is
// unavailable or refuses (returns { cancelled } if the user dismisses it).
async function pickSaveTarget(suggestedName) {
if (window.showSaveFilePicker) {
let handle;
try {
handle = await window.showSaveFilePicker({
const handle = await window.showSaveFilePicker({
suggestedName,
types: [
{ description: "Passwords backup", accept: { "application/json": [".json"] } },
],
});
return { handle, suggestedName };
} catch (error) {
if (error.name === "AbortError") return false;
throw error;
if (error.name === "AbortError") return { cancelled: true };
// SecurityError / NotAllowedError / unsupported: fall back to anchor.
}
const writable = await handle.createWritable();
}
return { suggestedName };
}

async function writeJSON(target, payload) {
const text = JSON.stringify(payload, null, 4);

if (target.handle) {
const writable = await target.handle.createWritable();
await writable.write(text);
await writable.close();
return true;
return;
}

const blob = new Blob([text], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = suggestedName;
a.download = target.suggestedName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return true;
}

export default function App() {
Expand Down Expand Up @@ -118,16 +130,20 @@ export default function App() {
}

async function onBackup() {
// Pick the destination first, while the click's user activation is still
// valid (the device exchange below is too slow for showSaveFilePicker).
const target = await pickSaveTarget("passwords_backup.json");
if (target.cancelled) {
setNotice({ appearance: "info", title: "Backup cancelled" });
return;
}

setBusy(true);
setNotice({ appearance: "info", title: 'Approve "Transfer metadatas?" on your device' });
try {
const payload = await passwords.dump_metadatas();
const saved = await saveJSON(payload, "passwords_backup.json");
setNotice(
saved
? { appearance: "success", title: "Backup saved" }
: { appearance: "info", title: "Backup cancelled" }
);
await writeJSON(target, payload);
setNotice({ appearance: "success", title: "Backup saved" });
} catch (error) {
setNotice({ appearance: "error", title: "Backup failed", description: String(error) });
} finally {
Expand Down
23 changes: 16 additions & 7 deletions src/app_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
#include "metadata.h"
#include "password_typing.h"
#include "ui.h"
#if defined(POPULATE)
#include "password.h"
#endif // POPULATE

const internalStorage_t N_storage_real;
app_state_t app_state;
Expand All @@ -42,7 +45,7 @@ volatile unsigned int G_led_status;
void app_main() {
int input_len = 0;

init_storage();
bool storage_freshly_initialized = init_storage();
memset(&app_state, 0, sizeof(app_state));

ui_idle();
Expand All @@ -51,12 +54,18 @@ void app_main() {
app_state.output_len = 0;

#if defined(POPULATE)
#include "password.h"
// removing 1 as `sizeof` will include the trailing null byte in the result (10)
// but this app stores password without this trailing null byte.
create_new_password("password1", sizeof("password1") - 1);
create_new_password("password2", sizeof("password2") - 1);
create_new_password("password3", sizeof("password3") - 1);
// Only seed the demo passwords on a fresh storage (first run), otherwise
// they would be appended again on every app start and pile up as
// duplicates.
if (storage_freshly_initialized) {
// removing 1 as `sizeof` will include the trailing null byte in the result (10)
// but this app stores password without this trailing null byte.
create_new_password("password1", sizeof("password1") - 1);
create_new_password("password2", sizeof("password2") - 1);
create_new_password("password3", sizeof("password3") - 1);
}
#else
UNUSED(storage_freshly_initialized);
#endif // POPULATE

for (;;) {
Expand Down
5 changes: 3 additions & 2 deletions src/globals.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#include "globals.h"
#include "options.h"

void init_storage() {
bool init_storage() {
if (N_storage.magic == STORAGE_MAGIC) {
// already initialized
return;
return false;
}
uint32_t tmp = STORAGE_MAGIC;
nvm_write((void *) &N_storage.magic, (void *) &tmp, sizeof(uint32_t));
Expand All @@ -18,4 +18,5 @@ void init_storage() {
nvm_write((void *) &N_storage.metadata_count, (void *) &tmp, sizeof(N_storage.metadata_count));
nvm_write((void *) N_storage.metadatas, (void *) &tmp, 2);
init_charset_options();
return true;
}
4 changes: 3 additions & 1 deletion src/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ extern volatile unsigned int G_led_status;
#define CLA 0xE0
#define N_storage (*(volatile internalStorage_t *) PIC(&N_storage_real))

void init_storage(void);
// Returns true if the storage was freshly initialized (first run), false if it
// was already initialized on a previous boot.
bool init_storage(void);
6 changes: 3 additions & 3 deletions src/password_list.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void password_list_reset() {

size_t password_list_get_offset(const size_t index) {
if (index >= MAX_METADATA_COUNT) {
PRINTF("[password_list_get_offset] Index %ld out of bounds\n", index);
PRINTF("[password_list_get_offset] Index %lu out of bounds\n", index);
return -1;
}
return passwordList.offsets[index];
Expand All @@ -57,7 +57,7 @@ size_t password_list_get_current_offset() {

const char *password_list_get_password(const size_t index) {
if (index >= MAX_METADATA_COUNT) {
PRINTF("[password_list_get_password] Index %ld out of bounds\n", index);
PRINTF("[password_list_get_password] Index %lu out of bounds\n", index);
return NULL;
}
return passwordList.passwords[index];
Expand All @@ -72,7 +72,7 @@ bool password_list_add_password(const size_t index,
const char *const password,
const size_t length) {
if (index >= MAX_METADATA_COUNT) {
PRINTF("[password_list_add_password] Index %ld out of bounds\n", index);
PRINTF("[password_list_add_password] Index %lu out of bounds\n", index);
return false;
}
passwordList.offsets[index] = offset;
Expand Down
4 changes: 2 additions & 2 deletions src/ui/ui_passwords.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ static void confirm_password_deletion(const size_t index) {
all_passwords = true;
snprintf(msgBuffer,
sizeof(msgBuffer),
"Confirm the deletion\nof all passwords (%d)",
N_storage.metadata_count);
"Confirm the deletion\nof all passwords (%u)",
(uint32_t) N_storage.metadata_count);
} else {
// A single password
all_passwords = false;
Expand Down
10 changes: 10 additions & 0 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# functions intentionally match module-level fixture names. Silence the noisy
# pylint warning for the whole file rather than per-function.
# pylint: disable=redefined-outer-name
from pathlib import Path
import re
import pytest
from ragger.backend import RaisePolicy, BackendInterface
from ragger.navigator import Navigator
Expand Down Expand Up @@ -40,3 +42,11 @@ def navigator(custom_backend, device, golden_run):
yield CustomNanoNavigator(custom_backend, device, golden_run)
else:
yield CustomTouchNavigator(custom_backend, device, golden_run)

@pytest.fixture(name="app_version")
def app_version_fixture() -> tuple[int, int, int]:
with open(Path(__file__).parent.parent.parent / "Makefile", encoding="utf-8") as f:
parsed = {}
for m in re.findall(r"^APPVERSION_(\w)\s*=\s*(\d*)$", f.read(), re.MULTILINE):
parsed[m[0]] = int(m[1])
return (parsed["M"], parsed["N"], parsed["P"])
5 changes: 3 additions & 2 deletions tests/functional/test_cmd.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from passwordsManager_cmd import PasswordsManagerCommand


def test_app_info(cmd: PasswordsManagerCommand):
assert cmd.get_app_info() == ("Passwords", "1.3.2")
def test_app_info(cmd: PasswordsManagerCommand, app_version: tuple[int, int, int]):
vers_str = ".".join(map(str, app_version))
assert cmd.get_app_info() == ("Passwords", vers_str)


def test_app_config(cmd: PasswordsManagerCommand):
Expand Down
Loading