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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 infrastructure/eid-wallet/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.7.1",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
package foundation.metastate.eid_wallet

import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge

class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}

// Kill the Android WebView overscroll glow/bounce. CSS
// `overscroll-behavior: none` doesn't reliably suppress it on all
// Android WebView versions because the glow is drawn natively by the
// OverScroller, not the renderer. Setting OVER_SCROLL_NEVER on the
// WebView itself stops it at the source.
override fun onWebViewCreate(webView: WebView) {
webView.overScrollMode = View.OVER_SCROLL_NEVER
}
}
17 changes: 16 additions & 1 deletion infrastructure/eid-wallet/src-tauri/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,20 @@ async fn get_platform() -> Result<String, String> {
return Ok("unknown".to_string());
}

/// Forwards a frontend log line to the Tauri host process stdout/stderr so
/// devs can see console output in the terminal that ran `tauri dev`, without
/// needing the WebView devtools to be attachable.
#[tauri::command]
fn log_to_terminal(level: String, message: String) {
match level.as_str() {
"error" => eprintln!("[FE error] {}", message),
"warn" => eprintln!("[FE warn] {}", message),
"info" => println!("[FE info] {}", message),
"debug" => println!("[FE debug] {}", message),
_ => println!("[FE log] {}", message),
}
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All@@ -97,7 +111,8 @@ pub fn run() {
hash,
verify,
get_device_id,
get_platform
get_platform,
log_to_terminal
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.7.1",
"version": "1.0.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand DownExpand Up@@ -29,7 +29,7 @@
"active": true,
"targets": "all",
"android": {
"versionCode": 25
"versionCode": 26
},
"icon": [
"icons/32x32.png",
Expand Down
51 changes: 51 additions & 0 deletions infrastructure/eid-wallet/src/app.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,31 @@
@import "@fontsource-variable/roboto-condensed";

@layer base {
/* Kill the Android WebView overscroll glow/bounce when scrolling past
* a container's edge. Applied at the root so it cascades into every
* scroll container; individual elements can opt back in with
* `overscroll-behavior: auto` if they need pull-to-refresh later. */
html,
body {
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
/* This is a native mobile app — text selection on tap-and-hold is
* a desktop-browser affordance that just feels broken here, and on
* iOS the magnifier loupe over scan buttons / PIN dots is a regular
* cause of misclicks. Opt back in below for inputs/textareas. */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}

/* Typography */
h1 {
@apply text-[90px]/[1.5] text-black font-semibold;
Expand DownExpand Up@@ -194,3 +219,29 @@ body {
position: relative;
z-index: 1;
}

/* WebView passthrough for the native camera scanner.
*
* The Tauri barcode-scanner plugin opens the camera in a native overlay
* BEHIND the WebView. Whichever page calls scan({ windowed: true }) must
* make the body + its route wrappers transparent for that scan's duration
* so the feed can show through. Toggled on/off by code (currently /scan-qr
* and /recover's notary path).
*
* Lives in app.css rather than per-layout because /recover is in (public)
* and (app)'s style block doesn't load there. */
body.custom-global-style {
background-color: transparent;
overflow: hidden;
}
body.custom-global-style [data-route-wrapper] {
background-color: transparent !important;
}
/* The notary-recovery scan opens from /recover (public group), whose page
* uses `<main class="bg-white …">` instead of [data-route-wrapper]. The
* recover <main> is nested under the root layout's slide-wrapper divs,
* so we need a descendant selector. Drawers/bottom-sheets use <div
* role="dialog"> not <main> so this won't clobber their backgrounds. */
body.custom-global-style main {
background-color: transparent !important;
}
108 changes: 37 additions & 71 deletions infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,127 +6,93 @@ import {
verifySignature as hwVerifySignature,
} from "@auvo/tauri-plugin-crypto-hw-api";
import type { KeyManager } from "./types";
import { KeyManagerError, KeyManagerErrorCodes } from "./types";
import {
KeyManagerError,
KeyManagerErrorCodes,
WALLET_KEY_ALIAS,
} from "./types";

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

async exists(keyId: string): Promise<boolean> {
async exists(): Promise<boolean> {
try {
return await hwExists(keyId);
return await hwExists(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key exists check failed:", error);
throw new KeyManagerError(
"Failed to check if hardware key exists",
`Hardware key exists check failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.HARDWARE_UNAVAILABLE,
keyId,
);
}
}

async generate(keyId: string): Promise<string | undefined> {
async generate(): Promise<void> {
try {
const result = await hwGenerate(keyId);
console.log(`Hardware key generated for ${keyId}:`, result);
return result;
await hwGenerate(WALLET_KEY_ALIAS);
} catch (error) {
console.error("Hardware key generation failed:", error);
throw new KeyManagerError(
"Failed to generate hardware key",
`Hardware key generation failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_GENERATION_FAILED,
keyId,
);
}
}

async getPublicKey(keyId: string): Promise<string | undefined> {
async getPublicKey(): Promise<string> {
try {
const publicKey = await hwGetPublicKey(keyId);
console.log(
`Hardware public key retrieved for ${keyId}:`,
publicKey,
);
return publicKey;
const pk = await hwGetPublicKey(WALLET_KEY_ALIAS);
if (!pk) {
throw new KeyManagerError(
"Hardware key not found",
KeyManagerErrorCodes.KEY_NOT_FOUND,
);
}
return pk;
} catch (error) {
console.error("Hardware public key retrieval failed:", error);
if (error instanceof KeyManagerError) throw error;
throw new KeyManagerError(
"Failed to get hardware public key",
`Hardware public key retrieval failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.KEY_NOT_FOUND,
keyId,
);
}
}

async signPayload(keyId: string, payload: string): Promise<string> {
async signPayload(payload: string): Promise<string> {
try {
console.log("=".repeat(70));
console.log("🔐 [HardwareKeyManager] signPayload called");
console.log("=".repeat(70));
console.log(`Key ID: ${keyId}`);
console.log(`Payload: "${payload}"`);
console.log(`Payload length: ${payload.length} bytes`);
const payloadHex = Array.from(new TextEncoder().encode(payload))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
console.log(`Payload (hex): ${payloadHex}`);

// Get and log the public key
try {
const publicKey = await this.getPublicKey(keyId);
if (publicKey) {
console.log(`Public key: ${publicKey.substring(0, 60)}...`);
console.log(`Public key (full): ${publicKey}`);
} else {
console.log("⚠️ Public key not available");
}
} catch (error) {
console.log(
`⚠️ Failed to get public key: ${error instanceof Error ? error.message : String(error)}`,
);
}

console.log("Signing with hardware key...");
const signature = await hwSignPayload(keyId, payload);
console.log(`✅ Hardware signature created for ${keyId}`);
console.log(`Signature: ${signature.substring(0, 50)}...`);
console.log(`Signature (full): ${signature}`);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
return signature;
return await hwSignPayload(WALLET_KEY_ALIAS, payload);
} catch (error) {
console.error("❌ Hardware signing failed:", error);
throw new KeyManagerError(
"Failed to sign payload with hardware key",
`Hardware signing failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.SIGNING_FAILED,
keyId,
);
}
}

async verifySignature(
keyId: string,
payload: string,
signature: string,
): Promise<boolean> {
try {
const isValid = await hwVerifySignature(keyId, payload, signature);
console.log(
`Hardware signature verification for ${keyId}:`,
isValid,
return await hwVerifySignature(
WALLET_KEY_ALIAS,
payload,
signature,
);
return isValid;
} catch (error) {
console.error("Hardware signature verification failed:", error);
throw new KeyManagerError(
"Failed to verify signature with hardware key",
`Hardware signature verification failed: ${stringifyError(error)}`,
KeyManagerErrorCodes.VERIFICATION_FAILED,
keyId,
);
}
}
}

function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading