Skip to content

Commit 4af8f4b

Browse files
authored
Feat/eid wallet v1 (#972)
* fix: recovery flow * fix: prevent recovery back * fix: security question thing * fix: security question attmept migration * fix: UI and settings * feat: loading sheet * feat: new notary mechanism * feat: enotary jwt * fix: enotary cors issue on JWKs * fix: notary scan and scan drawer * chore: bump android version
1 parent 4dfada5 commit 4af8f4b

64 files changed

Lines changed: 3745 additions & 2037 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

infrastructure/eid-wallet/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eid-wallet",
3-
"version": "0.7.1",
3+
"version": "1.0.0",
44
"description": "",
55
"type": "module",
66
"scripts": {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
package foundation.metastate.eid_wallet
22

33
import android.os.Bundle
4+
import android.view.View
5+
import android.webkit.WebView
46
import androidx.activity.enableEdgeToEdge
57

68
class MainActivity : TauriActivity() {
79
override fun onCreate(savedInstanceState: Bundle?) {
810
enableEdgeToEdge()
911
super.onCreate(savedInstanceState)
1012
}
13+
14+
// Kill the Android WebView overscroll glow/bounce. CSS
15+
// `overscroll-behavior: none` doesn't reliably suppress it on all
16+
// Android WebView versions because the glow is drawn natively by the
17+
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
18+
// WebView itself stops it at the source.
19+
override fun onWebViewCreate(webView: WebView) {
20+
webView.overScrollMode = View.OVER_SCROLL_NEVER
21+
}
1122
}

infrastructure/eid-wallet/src-tauri/src/lib.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
7575
return Ok("unknown".to_string());
7676
}
7777

78+
/// Forwards a frontend log line to the Tauri host process stdout/stderr so
79+
/// devs can see console output in the terminal that ran `tauri dev`, without
80+
/// needing the WebView devtools to be attachable.
81+
#[tauri::command]
82+
fn log_to_terminal(level: String, message: String) {
83+
match level.as_str() {
84+
"error" => eprintln!("[FE error] {}", message),
85+
"warn" => eprintln!("[FE warn] {}", message),
86+
"info" => println!("[FE info] {}", message),
87+
"debug" => println!("[FE debug] {}", message),
88+
_ => println!("[FE log] {}", message),
89+
}
90+
}
91+
7892
#[cfg_attr(mobile, tauri::mobile_entry_point)]
7993
pub fn run() {
8094
tauri::Builder::default()
@@ -97,7 +111,8 @@ pub fn run() {
97111
hash,
98112
verify,
99113
get_device_id,
100-
get_platform
114+
get_platform,
115+
log_to_terminal
101116
])
102117
.run(tauri::generate_context!())
103118
.expect("error while running tauri application");

infrastructure/eid-wallet/src-tauri/tauri.conf.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "eID for W3DS",
4-
"version": "0.7.1",
4+
"version": "1.0.0",
55
"identifier": "foundation.metastate.eid-wallet",
66
"build": {
77
"beforeDevCommand": "pnpm dev",
@@ -29,7 +29,7 @@
2929
"active": true,
3030
"targets": "all",
3131
"android": {
32-
"versionCode": 25
32+
"versionCode": 26
3333
},
3434
"icon": [
3535
"icons/32x32.png",

infrastructure/eid-wallet/src/app.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@
44
@import "@fontsource-variable/roboto-condensed";
55

66
@layer base {
7+
/* Kill the Android WebView overscroll glow/bounce when scrolling past
8+
* a container's edge. Applied at the root so it cascades into every
9+
* scroll container; individual elements can opt back in with
10+
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
11+
html,
12+
body {
13+
overscroll-behavior: none;
14+
-webkit-tap-highlight-color: transparent;
15+
/* This is a native mobile app — text selection on tap-and-hold is
16+
* a desktop-browser affordance that just feels broken here, and on
17+
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
18+
* cause of misclicks. Opt back in below for inputs/textareas. */
19+
user-select: none;
20+
-webkit-user-select: none;
21+
-webkit-touch-callout: none;
22+
}
23+
24+
input,
25+
textarea,
26+
[contenteditable="true"] {
27+
user-select: text;
28+
-webkit-user-select: text;
29+
-webkit-touch-callout: default;
30+
}
31+
732
/* Typography */
833
h1 {
934
@apply text-[90px]/[1.5] text-black font-semibold;
@@ -194,3 +219,29 @@ body {
194219
position: relative;
195220
z-index: 1;
196221
}
222+
223+
/* WebView passthrough for the native camera scanner.
224+
*
225+
* The Tauri barcode-scanner plugin opens the camera in a native overlay
226+
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
227+
* make the body + its route wrappers transparent for that scan's duration
228+
* so the feed can show through. Toggled on/off by code (currently /scan-qr
229+
* and /recover's notary path).
230+
*
231+
* Lives in app.css rather than per-layout because /recover is in (public)
232+
* and (app)'s style block doesn't load there. */
233+
body.custom-global-style {
234+
background-color: transparent;
235+
overflow: hidden;
236+
}
237+
body.custom-global-style [data-route-wrapper] {
238+
background-color: transparent !important;
239+
}
240+
/* The notary-recovery scan opens from /recover (public group), whose page
241+
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
242+
* recover <main> is nested under the root layout's slide-wrapper divs,
243+
* so we need a descendant selector. Drawers/bottom-sheets use <div
244+
* role="dialog"> not <main> so this won't clobber their backgrounds. */
245+
body.custom-global-style main {
246+
background-color: transparent !important;
247+
}

infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts

Lines changed: 37 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,127 +6,93 @@ import {
66
verifySignature as hwVerifySignature,
77
} from "@auvo/tauri-plugin-crypto-hw-api";
88
import type { KeyManager } from "./types";
9-
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
9+
import {
10+
KeyManagerError,
11+
KeyManagerErrorCodes,
12+
WALLET_KEY_ALIAS,
13+
} from "./types";
1014

1115
/**
12-
* Hardware key manager implementation using Tauri crypto hardware API
16+
* Hardware-backed key manager. Uses Android Keystore / iOS Secure Enclave / TPM
17+
* via the Tauri crypto-hw plugin. Errors propagate to the caller; there is no
18+
* silent fallback to software at this layer.
1319
*/
1420
export class HardwareKeyManager implements KeyManager {
1521
getType(): "hardware" | "software" {
1622
return "hardware";
1723
}
1824

19-
async exists(keyId: string): Promise<boolean> {
25+
async exists(): Promise<boolean> {
2026
try {
21-
return await hwExists(keyId);
27+
return await hwExists(WALLET_KEY_ALIAS);
2228
} catch (error) {
23-
console.error("Hardware key exists check failed:", error);
2429
throw new KeyManagerError(
25-
"Failed to check if hardware key exists",
30+
`Hardware key exists check failed: ${stringifyError(error)}`,
2631
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
27-
keyId,
2832
);
2933
}
3034
}
3135

32-
async generate(keyId: string): Promise<string | undefined> {
36+
async generate(): Promise<void> {
3337
try {
34-
const result = await hwGenerate(keyId);
35-
console.log(`Hardware key generated for ${keyId}:`, result);
36-
return result;
38+
await hwGenerate(WALLET_KEY_ALIAS);
3739
} catch (error) {
38-
console.error("Hardware key generation failed:", error);
3940
throw new KeyManagerError(
40-
"Failed to generate hardware key",
41+
`Hardware key generation failed: ${stringifyError(error)}`,
4142
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
42-
keyId,
4343
);
4444
}
4545
}
4646

47-
async getPublicKey(keyId: string): Promise<string | undefined> {
47+
async getPublicKey(): Promise<string> {
4848
try {
49-
const publicKey = await hwGetPublicKey(keyId);
50-
console.log(
51-
`Hardware public key retrieved for ${keyId}:`,
52-
publicKey,
53-
);
54-
return publicKey;
49+
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
50+
if (!pk) {
51+
throw new KeyManagerError(
52+
"Hardware key not found",
53+
KeyManagerErrorCodes.KEY_NOT_FOUND,
54+
);
55+
}
56+
return pk;
5557
} catch (error) {
56-
console.error("Hardware public key retrieval failed:", error);
58+
if (error instanceof KeyManagerError) throw error;
5759
throw new KeyManagerError(
58-
"Failed to get hardware public key",
60+
`Hardware public key retrieval failed: ${stringifyError(error)}`,
5961
KeyManagerErrorCodes.KEY_NOT_FOUND,
60-
keyId,
6162
);
6263
}
6364
}
6465

65-
async signPayload(keyId: string, payload: string): Promise<string> {
66+
async signPayload(payload: string): Promise<string> {
6667
try {
67-
console.log("=".repeat(70));
68-
console.log("🔐 [HardwareKeyManager] signPayload called");
69-
console.log("=".repeat(70));
70-
console.log(`Key ID: ${keyId}`);
71-
console.log(`Payload: "${payload}"`);
72-
console.log(`Payload length: ${payload.length} bytes`);
73-
const payloadHex = Array.from(new TextEncoder().encode(payload))
74-
.map((b) => b.toString(16).padStart(2, "0"))
75-
.join("");
76-
console.log(`Payload (hex): ${payloadHex}`);
77-
78-
// Get and log the public key
79-
try {
80-
const publicKey = await this.getPublicKey(keyId);
81-
if (publicKey) {
82-
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
83-
console.log(`Public key (full): ${publicKey}`);
84-
} else {
85-
console.log("⚠️ Public key not available");
86-
}
87-
} catch (error) {
88-
console.log(
89-
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
90-
);
91-
}
92-
93-
console.log("Signing with hardware key...");
94-
const signature = await hwSignPayload(keyId, payload);
95-
console.log(`✅ Hardware signature created for ${keyId}`);
96-
console.log(`Signature: ${signature.substring(0, 50)}...`);
97-
console.log(`Signature (full): ${signature}`);
98-
console.log(`Signature length: ${signature.length} chars`);
99-
console.log("=".repeat(70));
100-
return signature;
68+
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
10169
} catch (error) {
102-
console.error("❌ Hardware signing failed:", error);
10370
throw new KeyManagerError(
104-
"Failed to sign payload with hardware key",
71+
`Hardware signing failed: ${stringifyError(error)}`,
10572
KeyManagerErrorCodes.SIGNING_FAILED,
106-
keyId,
10773
);
10874
}
10975
}
11076

11177
async verifySignature(
112-
keyId: string,
11378
payload: string,
11479
signature: string,
11580
): Promise<boolean> {
11681
try {
117-
const isValid = await hwVerifySignature(keyId, payload, signature);
118-
console.log(
119-
`Hardware signature verification for ${keyId}:`,
120-
isValid,
82+
return await hwVerifySignature(
83+
WALLET_KEY_ALIAS,
84+
payload,
85+
signature,
12186
);
122-
return isValid;
12387
} catch (error) {
124-
console.error("Hardware signature verification failed:", error);
12588
throw new KeyManagerError(
126-
"Failed to verify signature with hardware key",
89+
`Hardware signature verification failed: ${stringifyError(error)}`,
12790
KeyManagerErrorCodes.VERIFICATION_FAILED,
128-
keyId,
12991
);
13092
}
13193
}
13294
}
95+
96+
function stringifyError(error: unknown): string {
97+
return error instanceof Error ? error.message : String(error);
98+
}

0 commit comments

Comments
 (0)