Lose Blätter - Software

Bauanleitung: Pico-TOTP Hardware Token (Split-Knowledge)

Dieses Projekt verwandelt einen Raspberry Pi Pico in einen hochsicheren Hardware-Token für Zwei-Faktor-Authentifizierung (2FA). Um die fehlende Hardware-Verschlüsselung des Pico auszugleichen, wird ein "Split-Knowledge" (Air-Gapped) Ansatz verwendet:

Dieses Projekt ist nichts für Anfänger. Man sollte zumindest in Arduino mit dem Raspberry Pico und am PC mit Python etwas grundlegende Erfahrung haben. Rust ist hier nicht unbedingt notwendig, aber ein Nice-To-Have. Wenn man das Projekt nicht nur auf Steckbrett, sondern in einem kleinen Gehäuse aufbauen möchte, sollte man etwas Löterfahrung und Designkenntnisse in Sachen 3D besitzen. Die Python Version sollte direkt unter Windows 11 wie auch Linux funktionieren. Wie es mit der seriellen Schnittstelle unter Rust aussieht, kann ich momentan nicht sagen. Probiert es aus. ;)

0. Das Prinzip

Wer das Prinzip genauer wissen will, dem lege ich die Einführung des Projekts ans Herz. Danach kann jeder entscheiden, ob es für ihn sinnvoll ist.

1. Die Hardware & Verdrahtung

Verdrahtung:

Je nach verwendetem Pico-Board kann man natürlich die Pinbelegungen im Arduino-Sketch anpassen. Allerdings muss man bei dem I2C Bus die Restriktionen beachten.

2. Die Secrets aus dem Smartphone pulen 🦐

Es wird angenommen, Du nutzt den Google Authenticator. Falls Du hingegen einen anderen Authenticator nutzt, schau nach, ob dieser überhaupt einen Export anbietet. Wenn nicht, kommst du nicht umhin, alle Codes neu zu erstellen. So oder so, wir benötigen den Aegis Authenticator. Gibt es im Google Play Store oder bei F-Droid . Diesen Authenticator kann man wärmstens empfehlen, da er nicht nach Hause telefoniert. Er bietet den Import aus vielen anderen Authenticators an. Von Google Authenticator ist es möglich, aber leider nicht trivial. So geht es:

3. Die Software-Vorbereitung (Aegis-Export)

Die Basis für die TOTP-Codes liefert ein unverschlüsselter JSON-Export aus der Android-App Aegis. Um diese Daten sicher in C++ Code (secrets.h) zu verwandeln, gibt es folgendes Exporter-Tool. Du kannst wählen, ob du die Python- oder die Rust-Version nutzen möchtest. Beide generieren beim Start auch den für den PC nötigen 16-stelligen pico_aes.key zum Entschlüsseln des Dongles.

Beide Varianten funktionieren gleich. Nach dem Öffnen erscheint ein Dateiauswahlmenu. Mit diesem findet man die Datei auf dem USB-Speicherstick und wählt sie aus. Danach wird secrets.h und pico_aes.key im gleichen Verzeichnis wie das Programm erzeugt und das Programm selbst schließt sich. Der USB-Speicherstick kann nun abgezogen und sicher verwahrt werden. Oder direkt formatiert. pico_aes.key brauchen wir für den PC und secrets.h für die Programmierung des Pico unter Arduino.

Variante A: Der Exporter in Python

Benötigt ein installiertes Python sowie die Pakete: pip install pycryptodome

aegis_exporter.py

import json
import base64
import os
import string
import secrets
import tkinter as tk
from tkinter import filedialog
from Crypto.Cipher import AES

# --- KONFIGURATION ---
KEY_FILE = "pico_aes.key"
HEADER_FILE = "secrets.h"
# Erhöht auf 64 Bytes (4 AES-Blöcke). 
# Das reicht für Base32-Secrets mit bis zu 102 Zeichen aus.
BUFFER_SIZE = 64 
# ---------------------

def get_or_create_key():
    """Lädt den AES-Key oder generiert einen neuen, falls keiner existiert."""
    if os.path.exists(KEY_FILE):
        print(f"[+] Lese existierenden Key aus '{KEY_FILE}'...")
        with open(KEY_FILE, "r", encoding="utf-8") as f:
            key = f.read().strip()
            if len(key) != 16:
                raise ValueError("Der Key in der Datei muss exakt 16 Zeichen lang sein!")
            return key.encode('utf-8')
    else:
        print(f"[+] Generiere neuen 16-Byte AES-Key und speichere in '{KEY_FILE}'...")
        alphabet = string.ascii_letters + string.digits
        key = ''.join(secrets.choice(alphabet) for _ in range(16))
        with open(KEY_FILE, "w", encoding="utf-8") as f:
            f.write(key)
        return key.encode('utf-8')

def clean_base32(secret):
    """Entfernt Leerzeichen und fügt Base32-Padding hinzu, falls nötig."""
    secret = secret.replace(" ", "").upper()
    missing_padding = len(secret) % 8
    if missing_padding:
        secret += '=' * (8 - missing_padding)
    return secret

def pad_bytes(data, size):
    """Füllt die Bytes mit Nullen auf (Zero-Padding für optimale Kompatibilität)."""
    if len(data) > size:
        raise ValueError(f"Secret ist mit {len(data)} Bytes zu lang für den {size}-Byte Puffer!")
    return data + b'\x00' * (size - len(data))

def select_json_file():
    """Öffnet einen Datei-Explorer zur Auswahl des Aegis-Exports."""
    root = tk.Tk()
    root.withdraw()
    print("[*] Bitte wähle die unverschlüsselte Aegis JSON-Exportdatei aus...")
    file_path = filedialog.askopenfilename(
        title="Aegis JSON Export auswählen",
        filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")]
    )
    return file_path

def main():
    print("=== Pico-TOTP Exporter ===")

    # 1. Key Management (AES-128)
    aes_key = get_or_create_key()
    cipher = AES.new(aes_key, AES.MODE_ECB)

    # 2. JSON Datei auswählen
    json_path = select_json_file()
    if not json_path:
        print("[-] Keine Datei ausgewählt. Abbruch.")
        return

    # 3. JSON parsen
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    entries = data.get("db", {}).get("entries", [])
    if not entries:
        print("[-] Keine Einträge in der JSON gefunden. Ist es ein unverschlüsselter Export?")
        return

    # 4. Daten verarbeiten und Header aufbauen
    header_content = [
        "#pragma once",
        "#include <Arduino.h>",
        "",
        "struct Account {",
        "  char name[16];",
        f"  uint8_t encrypted_secret[{BUFFER_SIZE}];",
        "  size_t secret_len;",
        f"  uint8_t decrypted_secret[{BUFFER_SIZE}];",
        "};",
        "",
        f"const int NUM_ACCOUNTS = {len(entries)};",
        "Account accounts[NUM_ACCOUNTS] = {"
    ]

    print(f"[+] Verarbeite {len(entries)} Konten...")

    for i, entry in enumerate(entries):
        name = entry.get("issuer") or entry.get("name") or "Unbekannt"
        name_clean = name[:15].replace('"', '') 

        base32_secret = entry.get("info", {}).get("secret", "")
        raw_secret = base64.b32decode(clean_base32(base32_secret))
        secret_len = len(raw_secret)

        padded_secret = pad_bytes(raw_secret, BUFFER_SIZE)
        encrypted_secret = cipher.encrypt(padded_secret)

        hex_array = ", ".join([f"0x{b:02X}" for b in encrypted_secret])

        comma = "," if i < len(entries) - 1 else ""
        header_content.append(f'  {{"{name_clean}", {{{hex_array}}}, {secret_len}, {{0}}}}{comma}')

    header_content.append("};")

    with open(HEADER_FILE, "w", encoding="utf-8") as f:
        f.write("\n".join(header_content))

    print(f"[+] Fertig! Die Datei '{HEADER_FILE}' wurde generiert.")
    print(f"[!] Der AES-Schlüssel steht in '{KEY_FILE}'.")

if __name__ == "__main__":
    main()

Ausführen mit python aegis_exporter.py

Variante B: Der Exporter in Rust (Standalone)

Erstelle ein Projekt mit cargo new aegis_exporter. Cargo.toml (Abhängigkeiten):

[package]
name = "aegis_exporter"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rfd = "0.14"
rand = "0.8"
base32 = "0.4"
aes = "0.8"

src/main.rs:

use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
use rand::{distributions::Alphanumeric, Rng};
use rfd::FileDialog;
use serde::Deserialize;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;

// --- KONFIGURATION ---
const KEY_FILE: &str = "pico_aes.key";
const HEADER_FILE: &str = "secrets.h";
const BUFFER_SIZE: usize = 64;
// ---------------------

// --- Datenstrukturen für das JSON-Parsing (Serde) ---
#[derive(Deserialize, Debug)]
struct AegisExport {
    db: AegisDb,
}

#[derive(Deserialize, Debug)]
struct AegisDb {
    entries: Vec<AegisEntry>,
}

#[derive(Deserialize, Debug)]
struct AegisEntry {
    issuer: Option<String>,
    name: Option<String>,
    info: AegisInfo,
}

#[derive(Deserialize, Debug)]
struct AegisInfo {
    secret: String,
}

/// Lädt oder generiert den AES-Key
fn get_or_create_key() -> String {
    if Path::new(KEY_FILE).exists() {
        println!("[+] Lese existierenden Key aus '{}'...", KEY_FILE);
        let key = fs::read_to_string(KEY_FILE).expect("Fehler beim Lesen des Keys");
        let key = key.trim().to_string();
        if key.len() != 16 {
            panic!("Der Key in der Datei muss exakt 16 Zeichen lang sein!");
        }
        key
    } else {
        println!("[+] Generiere neuen 16-Byte AES-Key und speichere in '{}'...", KEY_FILE);
        let key: String = rand::thread_rng()
            .sample_iter(&Alphanumeric)
            .take(16)
            .map(char::from)
            .collect();
        fs::write(KEY_FILE, &key).expect("Fehler beim Speichern des Keys");
        key
    }
}

/// Säubert den Base32-String (entfernt Leerzeichen, fügt Padding hinzu)
fn clean_base32(secret: &str) -> String {
    let mut clean = secret.replace(" ", "").to_uppercase();
    let missing_padding = clean.len() % 8;
    if missing_padding != 0 {
        clean.push_str(&"=".repeat(8 - missing_padding));
    }
    clean
}

fn main() {
    println!("=== Pico-TOTP Exporter (Rust Edition) ===");

    // 1. Key Management (AES-128 init)
    let aes_key_str = get_or_create_key();
    let aes_key = GenericArray::from_slice(aes_key_str.as_bytes());
    let cipher = Aes128::new(&aes_key);

    // 2. JSON Datei über natives Fenster auswählen
    println!("[*] Bitte wähle die unverschlüsselte Aegis JSON-Exportdatei aus...");
    let file_path = FileDialog::new()
        .add_filter("JSON Dateien", &["json"])
        .set_title("Aegis JSON Export auswählen")
        .pick_file();

    let json_path = match file_path {
        Some(path) => path,
        None => {
            println!("[-] Keine Datei ausgewählt. Abbruch.");
            return;
        }
    };

    // 3. JSON parsen
    let json_content = fs::read_to_string(&json_path).expect("Fehler beim Lesen der JSON-Datei");
    let data: AegisExport = serde_json::from_str(&json_content)
        .expect("Fehler beim Parsen der JSON-Datei. Ist es der unverschlüsselte Export?");
    let entries = data.db.entries;

    if entries.is_empty() {
        println!("[-] Keine Einträge in der JSON gefunden.");
        return;
    }

    // 4. Daten verarbeiten und Header aufbauen
    let mut header_content = vec![
        "#pragma once".to_string(),
        "#include <Arduino.h>".to_string(),
        "".to_string(),
        "struct Account {".to_string(),
        "  char name[16];".to_string(),
        format!("  uint8_t encrypted_secret[{}];", BUFFER_SIZE),
        "  size_t secret_len;".to_string(),
        format!("  uint8_t decrypted_secret[{}];", BUFFER_SIZE),
        "};".to_string(),
        "".to_string(),
        format!("const int NUM_ACCOUNTS = {};", entries.len()),
        "Account accounts[NUM_ACCOUNTS] = {".to_string(),
    ];

    println!("[+] Verarbeite {} Konten...", entries.len());

    for (i, entry) in entries.iter().enumerate() {
        // Name holen und für C++ Array auf 15 Zeichen kürzen
        let name_raw = entry.issuer.clone().unwrap_or_else(|| {
            entry.name.clone().unwrap_or_else(|| "Unbekannt".to_string())
        });
        let name_clean: String = name_raw.chars().take(15).filter(|c| *c != '"').collect();

        // Base32 dekodieren
        let base32_secret = clean_base32(&entry.info.secret);
        let raw_secret = base32::decode(base32::Alphabet::RFC4648 { padding: true }, &base32_secret)
            .unwrap_or_else(|| panic!("Fehler beim Base32-Decodieren für Account: {}", name_clean));

        let secret_len = raw_secret.len();
        if secret_len > BUFFER_SIZE {
            panic!("Secret für {} ist zu lang ({} Bytes, max ist {})!", name_clean, secret_len, BUFFER_SIZE);
        }

        // Puffer mit Nullen füllen (Padding) und Secret kopieren
        let mut padded_secret = vec![0u8; BUFFER_SIZE];
        padded_secret[..secret_len].copy_from_slice(&raw_secret);

        // AES-128 ECB Verschlüsselung (Block für Block)
        for chunk in padded_secret.chunks_mut(16) {
            let block = GenericArray::from_mut_slice(chunk);
            cipher.encrypt_block(block);
        }

        // In C-Array Format umwandeln (0x1A, 0x2B, ...)
        let hex_array: Vec<String> = padded_secret.iter().map(|b| format!("0x{:02X}", b)).collect();
        let hex_str = hex_array.join(", ");
        let comma = if i < entries.len() - 1 { "," } else { "" };

        header_content.push(format!("  {{\"{}\", {{{}}}, {}, {{0}}}}{}", name_clean, hex_str, secret_len, comma));
    }

    header_content.push("};".to_string());

    // 5. secrets.h Datei schreiben
    let mut file = File::create(HEADER_FILE).expect("Fehler beim Erstellen der secrets.h");
    for line in header_content {
        writeln!(file, "{}", line).unwrap();
    }

    println!("[+] Fertig! Die C++ Datei '{}' wurde generiert.", HEADER_FILE);
    println!("[!] Der AES-Schlüssel steht in '{}'.", KEY_FILE);
}

Mit cargo build --release erzeugst du eine kleine aegis_exporter.exe

4. Die Pico Firmware (Arduino IDE)

Vorbereitung:

  1. Installiere den Raspberry Pi Pico/RP2040 Core von Earle F. Philhower über den Boardverwalter.
  2. Wähle als Board den Raspberry Pi Pico.
  3. Wichtig: Unter Werkzeuge -> USB Stack zwingend Adafruit TinyUSB auswählen!
  4. Bibliotheken installieren: Adafruit SSD1306, Crypto (von Rhys Weatherley) und TOTP library (von Luca Dentella).
  5. Kopiere die generierte secrets.h in denselben Ordner wie den .ino Sketch.

PicoTOTP.ino

#include <Adafruit_TinyUSB.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Crypto.h>
#include <AES.h>
#include <TOTP.h>
#include "secrets.h"

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define BUTTON_PIN 11 // Externer Taster an GPIO 11 und GND

// USB HID Konfiguration für Tastatur (Muss vor Serial.begin initialisiert werden!)
uint8_t const desc_hid_report[] = { TUD_HID_REPORT_DESC_KEYBOARD() };
Adafruit_USBD_HID usb_hid(desc_hid_report, sizeof(desc_hid_report), HID_ITF_PROTOCOL_KEYBOARD, 2, false);

enum State { WAITING, DECRYPTING, READY };
State currentState = WAITING;

uint32_t currentUnixTime = 0;
uint32_t lastTimeSync = 0; 
char aesKey[17] = {0};
int currentAccountIdx = 0;

bool lastButtonState = false;
uint32_t buttonPressStart = 0;
const uint32_t LONG_PRESS_MS = 600;

void updateDisplay(const char* line1, const char* line2) {
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.println(line1);
  display.setCursor(0, 16);
  display.setTextSize(2); 
  display.println(line2);
  display.display();
}

void checkSerial() {
  if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n');
    if (input.startsWith("TIME:") && input.indexOf("|KEY:") > 0) {
      int splitIdx = input.indexOf("|KEY:");
      currentUnixTime = input.substring(5, splitIdx).toInt();
      lastTimeSync = millis();
      input.substring(splitIdx + 5).toCharArray(aesKey, 17);
      currentState = DECRYPTING;
    }
  }
}

void decryptSecrets() {
  AES128 aes128; 
  aes128.setKey((uint8_t*)aesKey, 16);

  for(int i = 0; i < NUM_ACCOUNTS; i++) {
    for(int block = 0; block < 4; block++) {
      aes128.decryptBlock(
        accounts[i].decrypted_secret + (block * 16), 
        accounts[i].encrypted_secret + (block * 16)
      );
    }
  }

  aes128.clear();
  memset(aesKey, 0, sizeof(aesKey)); // RAM sofort säubern!

  currentState = READY;
  updateDisplay("Entsperrt!", accounts[currentAccountIdx].name);
}

void typeTotpCode(int accountIndex) {
  updateDisplay("Tippe Code...", accounts[accountIndex].name);

  uint32_t realTime = currentUnixTime + ((millis() - lastTimeSync) / 1000);
  TOTP totp(accounts[accountIndex].decrypted_secret, accounts[accountIndex].secret_len);
  char* code = totp.getCode(realTime);

  usb_hid.keyboardRelease(0);
  delay(10);

  for(int i = 0; i < 6; i++) {
    char c = code[i];
    uint8_t keycode = (c == '0') ? HID_KEY_0 : HID_KEY_1 + (c - '1');
    uint8_t keys[6] = {keycode, 0, 0, 0, 0, 0};
    usb_hid.keyboardReport(0, 0, keys);
    delay(20);
    usb_hid.keyboardRelease(0);
    delay(20);
  }

  uint8_t enterKey[6] = {HID_KEY_ENTER, 0, 0, 0, 0, 0};
  usb_hid.keyboardReport(0, 0, enterKey);
  delay(20);
  usb_hid.keyboardRelease(0);

  delay(500);
  updateDisplay("Bereit", accounts[currentAccountIdx].name);
}

void handleButton() {
  bool isPressed = (digitalRead(BUTTON_PIN) == LOW);

  if (isPressed && !lastButtonState) {
    buttonPressStart = millis();
    lastButtonState = true;
  } else if (!isPressed && lastButtonState) {
    uint32_t pressDuration = millis() - buttonPressStart;
    lastButtonState = false;

    if (pressDuration > LONG_PRESS_MS) {
      typeTotpCode(currentAccountIdx);
    } else if (pressDuration > 50) {
      currentAccountIdx = (currentAccountIdx + 1) % NUM_ACCOUNTS;
      updateDisplay("Bereit", accounts[currentAccountIdx].name);
    }
  }
}

void setup() {
  usb_hid.begin(); // MUSS vor Serial initialisiert werden!
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  Wire.setSDA(4); 
  Wire.setSCL(5);
  Wire.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  display.setTextColor(SSD1306_WHITE);
  updateDisplay("Pico-TOTP", "Warte auf PC...");
}

void loop() {
  #ifdef USE_TINYUSB
    TinyUSBDevice.task();
  #endif

  switch (currentState) {
    case WAITING: checkSerial(); break;
    case DECRYPTING: decryptSecrets(); break;
    case READY: handleButton(); break;
  }
}

Wichtig! Nach erfolgreicher Kompilierung die secrets.h aus dem Arduino-Verzeichnis aber auch aus dem Verzeichnis wo der aegis_exporter löschen! Ansonsten wäre es ein Sicherheitsrisiko, falls der PC kompromittiert würde.

5. Der PC-Daemon (Der "Schlüsselverwalter")

Dieser Dienst läuft dauerhaft im Hintergrund auf dem PC. Er überwacht die USB-Ports und schickt beim Einstecken des Picos vollautomatisch den AES-Schlüssel (pico_aes.key) sowie die aktuelle Uhrzeit los.

Wichtig: Die pico_aes.key Datei muss im selben Ordner wie der Daemon liegen.

Variante A: Der Daemon in Python

Benötigt: pip install pyserial

pc_daemon.py

import time
import serial
import serial.tools.list_ports
import os

# --- KONFIGURATION ---
KEY_FILE = "pico_aes.key"
USB_VID = 0x239A  # Standard Adafruit TinyUSB VID (wird vom Pico genutzt)
# ---------------------

def load_key():
    """Lädt den generierten AES-Schlüssel aus der Datei."""
    if not os.path.exists(KEY_FILE):
        print(f"[-] Fehler: Schlüsseldatei '{KEY_FILE}' nicht gefunden!")
        return None
    with open(KEY_FILE, "r", encoding="utf-8") as f:
        key = f.read().strip()
        if len(key) != 16:
            print("[-] Fehler: Der Key muss exakt 16 Zeichen lang sein.")
            return None
        return key

def find_pico():
    """Sucht nach einem USB-Gerät mit der passenden Vendor ID."""
    ports = serial.tools.list_ports.comports()
    for port in ports:
        if port.vid == USB_VID:
            return port.device
    return None

def send_auth_data(port_name, aes_key):
    """Sendet die aktuelle Unix-Zeit und den AES-Key an den Pico."""
    try:
        # Öffne die serielle Verbindung mit 115200 Baud
        with serial.Serial(port_name, 115200, timeout=1) as ser:
            unix_time = int(time.time())
            # Format: TIME:1718391900|KEY:MeinGeheimer1234\n
            payload = f"TIME:{unix_time}|KEY:{aes_key}\n"
            ser.write(payload.encode('utf-8'))
            print(f"[+] Daten gesendet an {port_name} (Zeit: {unix_time})")
    except Exception as e:
        print(f"[-] Fehler beim Senden: {e}")

def main():
    print("=== Pico-TOTP Schlüsselverwalter ===")
    aes_key = load_key()
    if not aes_key:
        return

    print("[*] Warte auf Pico...")
    pico_connected = False

    while True:
        port = find_pico()

        if port and not pico_connected:
            print(f"[+] Pico erkannt an {port}!")
            time.sleep(1.5)  # Kurz warten, bis der USB-Serial Stack auf dem Pico komplett hochgefahren ist
            send_auth_data(port, aes_key)
            pico_connected = True

        elif not port and pico_connected:
            print("[-] Pico wurde entfernt. Warte...")
            pico_connected = False

        time.sleep(1) # CPU schonen

if __name__ == "__main__":
    main()

Variante B: Der Daemon in Rust (Für den produktiven Dauerbetrieb)

Erstelle ein Projekt mit cargo new pico_daemon. Cargo.toml (Abhängigkeiten):

[package]
name = "pico_daemon"
version = "0.1.0"
edition = "2021"

[dependencies]
serialport = "4.3"

src/main.rs:

use serialport::{SerialPortType, UsbPortInfo};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

// --- KONFIGURATION ---
const KEY_FILE: &str = "pico_aes.key";
const USB_VID: u16 = 0x239A; // Adafruit TinyUSB VID
// ---------------------

/// Lädt den AES-Key aus der Datei und prüft die Länge
fn load_key() -> Option<String> {
    if !Path::new(KEY_FILE).exists() {
        eprintln!("[-] Fehler: Schlüsseldatei '{}' nicht gefunden!", KEY_FILE);
        eprintln!("    Bitte lege die Datei in denselben Ordner wie diese ausführbare Datei.");
        return None;
    }

    match fs::read_to_string(KEY_FILE) {
        Ok(content) => {
            let key = content.trim().to_string();
            if key.len() != 16 {
                eprintln!("[-] Fehler: Der Key muss exakt 16 Zeichen lang sein.");
                return None;
            }
            Some(key)
        }
        Err(e) => {
            eprintln!("[-] Fehler beim Lesen der Schlüsseldatei: {}", e);
            None
        }
    }
}

/// Sucht in allen seriellen Ports nach der Adafruit USB Vendor ID
fn find_pico() -> Option<String> {
    if let Ok(ports) = serialport::available_ports() {
        for port in ports {
            // Wir prüfen, ob es ein USB-Port ist und lesen die Vendor ID (VID) aus
            if let SerialPortType::UsbPort(UsbPortInfo { vid, .. }) = port.port_type {
                if vid == USB_VID {
                    return Some(port.port_name);
                }
            }
        }
    }
    None
}

/// Öffnet den seriellen Port und sendet Unix-Zeit und AES-Key
fn send_auth_data(port_name: &str, aes_key: &str) {
    // Port mit 115200 Baud öffnen und 1 Sekunde Timeout setzen
    match serialport::new(port_name, 115_200)
        .timeout(Duration::from_secs(1))
        .open()
    {
        Ok(mut port) => {
            // Aktuelle Unix-Zeit generieren
            let unix_time = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();

            // Payload im Format "TIME:1718391900|KEY:MeinGeheimer1234\n" bauen
            let payload = format!("TIME:{}|KEY:{}\n", unix_time, aes_key);

            // Senden
            match port.write_all(payload.as_bytes()) {
                Ok(_) => println!("[+] Daten gesendet an {} (Zeit: {})", port_name, unix_time),
                Err(e) => eprintln!("[-] Fehler beim Senden an den Pico: {}", e),
            }
        }
        Err(e) => eprintln!("[-] Fehler beim Öffnen des Ports {}: {}", port_name, e),
    }
}

fn main() {
    println!("=== Pico-TOTP Schlüsselverwalter (Rust Edition) ===");
    println!("=== Beenden mit Strg-C == Fenster kann minimiert werden ===");

    // 1. Key laden
    let aes_key = match load_key() {
        Some(key) => key,
        None => return, // Abbruch, wenn kein Key da ist
    };

    println!("[*] Warte im Hintergrund auf Pico...");
    let mut pico_connected = false;

    // 2. Endlosschleife zur Geräteüberwachung
    loop {
        let port_opt = find_pico();

        match port_opt {
            // Fall A: Pico wurde gerade neu eingesteckt
            Some(port_name) if !pico_connected => {
                println!("[+] Pico erkannt an {}!", port_name);

                // WICHTIG: 1.5 Sekunden warten, bis der USB-Serial Stack auf dem Pico hochgefahren ist
                thread::sleep(Duration::from_millis(1500)); 

                send_auth_data(&port_name, &aes_key);
                pico_connected = true;
            }
            // Fall B: Pico wurde abgezogen
            None if pico_connected => {
                println!("[-] Pico wurde entfernt. Warte...");
                pico_connected = false;
            }
            // Fall C: Status unverändert -> Nichts tun
            _ => {}
        }

        // 1 Sekunde schlafen, um die CPU-Auslastung bei ~0% zu halten
        thread::sleep(Duration::from_secs(1));
    }
}

(Mit cargo build --release erzeugst du eine winzige .exe, die unsichtbar in den Autostart gelegt werden kann).

Der Ablauf in der Praxis: Aegis Export -> Exporter starten -> secrets.h in der Arduino IDE mit dem Code hochladen -> PC Daemon starten -> Pico anstecken -> Button klicken. Fertig ist der hochsichere 2FA-Stick!