From bacb5ed34cf3871a2c59e5c8544e32d790ea3cd0 Mon Sep 17 00:00:00 2001 From: Ch1ffr3punk Date: Wed, 11 Mar 2026 19:45:26 +0100 Subject: Add files via upload --- localization/yubicrypt-de.go | 1641 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1641 insertions(+) create mode 100644 localization/yubicrypt-de.go (limited to 'localization') diff --git a/localization/yubicrypt-de.go b/localization/yubicrypt-de.go new file mode 100644 index 0000000..d72f787 --- /dev/null +++ b/localization/yubicrypt-de.go @@ -0,0 +1,1641 @@ +package main + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "image" + "image/color" + "math" + "math/big" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + + "github.com/awnumar/memguard" + "github.com/go-piv/piv-go/v2/piv" +) + +// ecSignature represents an ECDSA signature with R and S components +type ecSignature struct{ R, S *big.Int } + +// Supported algorithms +const ( + AlgorithmECCP256 = "ECCP256" + AlgorithmECCP384 = "ECCP384" + AlgorithmED25519 = "ED25519" +) + +var supportedAlgorithms = map[string]bool{ + AlgorithmECCP256: true, + AlgorithmECCP384: true, + AlgorithmED25519: true, +} + +// Mapping from elliptic curve to YOUR algorithm name (not default) +var curveToAlgorithm = map[elliptic.Curve]string{ + elliptic.P256(): AlgorithmECCP256, + elliptic.P384(): AlgorithmECCP384, +} + +// Mapping from elliptic curve to hash function +var curveToHash = map[elliptic.Curve]crypto.Hash{ + elliptic.P256(): crypto.SHA256, + elliptic.P384(): crypto.SHA384, +} + +// Ed25519 constants +const ( + Ed25519SignatureSize = 64 + Ed25519PublicKeySize = 32 + Ed25519CombinedSize = Ed25519SignatureSize + Ed25519PublicKeySize // 96 bytes +) + +const ( + minRSABits = 2048 // Minimum accepted RSA key size +) + +// Supported RSA key sizes +var supportedRSASizes = map[int]string{ + 2048: "RSA2048", + 3072: "RSA3072", + 4096: "RSA4096", +} + +// GUI structure +type GUI struct { + app fyne.App + window fyne.Window + themeToggle *widget.Button + infoBtn *widget.Button + textArea *widget.Entry + pinEntry *widget.Entry + statusLabel *widget.Label + publicKeyPath string + currentTheme string + encryptionUsed bool // Tracks if encryption was used in this session +} + +func main() { + defer memguard.Purge() + + gui := &GUI{ + app: app.NewWithID("oc2mx.net.yubicrypt"), + currentTheme: "dark", + encryptionUsed: false, + } + + gui.window = gui.app.NewWindow("yubicrypt") + gui.window.Resize(fyne.NewSize(600, 600)) + gui.createUI() + gui.applyTheme() + gui.window.SetContent(gui.createMainUI()) + gui.window.ShowAndRun() +} + +// createUI initializes all UI components +func (g *GUI) createUI() { + monospace := &fyne.TextStyle{Monospace: true} + + g.textArea = widget.NewMultiLineEntry() + g.textArea.Wrapping = fyne.TextWrapOff + g.textArea.TextStyle = *monospace + g.textArea.SetPlaceHolder("Text zum Verschlüsseln, Signieren eingeben\noder verschlüsselten Inhalt hier einfügen.") + + g.pinEntry = widget.NewPasswordEntry() + g.pinEntry.SetPlaceHolder("") + g.pinEntry.Validator = func(s string) error { + if len(s) > 8 { + return fmt.Errorf("PIN darf maximal 8 Zeichen lang sein") + } + return nil + } + + g.statusLabel = widget.NewLabel("Bereit") + g.statusLabel.Wrapping = fyne.TextWrapWord + + // Theme toggle button with emoji (starts with Sun for dark mode) + g.themeToggle = widget.NewButton("☀️", g.toggleTheme) + + // Info Button (top left) + g.infoBtn = widget.NewButtonWithIcon("", theme.InfoIcon(), g.showInfoPopup) +} + +// createMainUI builds the main layout +func (g *GUI) createMainUI() fyne.CanvasObject { + // Buttons without icons, new order + signTextBtn := widget.NewButton("Signieren", g.onSignText) + padBtn := widget.NewButton("Polstern", g.onPad) + encryptBtn := widget.NewButton("Verschlüsseln", g.onEncrypt) + encryptBtn.Importance = widget.HighImportance // Blue + decryptBtn := widget.NewButton("Entschlüsseln", g.onDecrypt) + decryptBtn.Importance = widget.HighImportance // Blue + unpadBtn := widget.NewButton("Entpolstern", g.onUnpad) + verifyTextBtn := widget.NewButton("Verifizieren", g.onVerifyText) + + // Grid layout to fill width without side spaces + buttonContainer := container.NewGridWithColumns(6, + signTextBtn, + padBtn, + encryptBtn, + decryptBtn, + unpadBtn, + verifyTextBtn, + ) + + // Clear Button (Blue, no icon) + clearBtn := widget.NewButton("Löschen", g.onClear) + clearBtn.Importance = widget.HighImportance + + pinContainer := container.NewVBox( + container.NewHBox( + layout.NewSpacer(), + widget.NewLabel("PIN:"), + g.pinEntry, + clearBtn, + layout.NewSpacer(), + ), + ) + + // Top Bar: Info (Left), Theme (Right) + topBar := container.NewHBox( + g.infoBtn, + layout.NewSpacer(), + g.themeToggle, + ) + + mainContainer := container.NewBorder( + container.NewVBox( + topBar, + buttonContainer, + widget.NewSeparator(), + ), + container.NewVBox( + widget.NewSeparator(), + pinContainer, + g.statusLabel, + ), + nil, + nil, + container.NewScroll(g.textArea), + ) + + return mainContainer +} + +// toggleTheme switches between light and dark theme +func (g *GUI) toggleTheme() { + if g.currentTheme == "dark" { + g.app.Settings().SetTheme(theme.LightTheme()) + g.currentTheme = "light" + g.themeToggle.SetText("🌙") // Moon for light mode + } else { + g.app.Settings().SetTheme(theme.DarkTheme()) + g.currentTheme = "dark" + g.themeToggle.SetText("☀️") // Sun for dark mode + } +} + +// applyTheme sets the initial theme +func (g *GUI) applyTheme() { + if g.currentTheme == "dark" { + g.app.Settings().SetTheme(theme.DarkTheme()) + g.themeToggle.SetText("☀️") // Sun for dark mode + } else { + g.app.Settings().SetTheme(theme.LightTheme()) + g.themeToggle.SetText("🌙") // Moon for light mode + } +} + +// show info pop-up +func (g *GUI) showInfoPopup() { + projURL, _ := url.Parse("https://github.com/Ch1ffr3punk/yubicrypt") + projectLink := widget.NewHyperlink("Ein Open Source Projekt", projURL) + + okButton := widget.NewButton("OK", func() { + // Close dialog + g.window.Canvas().Overlays().Remove(g.window.Canvas().Overlays().Top()) + }) + okButton.Importance = widget.HighImportance + + content := container.NewVBox( + widget.NewLabelWithStyle("yubicrypt v0.1.8", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + widget.NewSeparator(), + container.NewHBox( + layout.NewSpacer(), + projectLink, + layout.NewSpacer(), + ), + widget.NewLabelWithStyle("veröffentlicht unter der Apache 2.0 Lizenz", fyne.TextAlignCenter, fyne.TextStyle{}), + widget.NewLabelWithStyle("© 2026 Ch1ffr3punk", fyne.TextAlignCenter, fyne.TextStyle{}), + widget.NewLabel(""), + container.NewHBox( + layout.NewSpacer(), + okButton, + layout.NewSpacer(), + ), + ) + + dialog.ShowCustom("", "", content, g.window) +} + +// onSignText triggers the signing process for text in the GUI +func (g *GUI) onSignText() { + if g.pinEntry.Text == "" { + g.statusLabel.SetText("Fehler: PIN zum Signieren erforderlich") + return + } + + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Signieren") + return + } + + // Check for existing signature to prevent double signing + s := string(input) + for algo := range supportedAlgorithms { + if strings.Contains(s, "-----BEGIN "+algo+" SIGNATURE-----") { + g.statusLabel.SetText("Fehler: Nachricht enthält bereits eine Signatur") + return + } + } + + // Sign data in the text area + result, err := g.signData([]byte(input), g.pinEntry.Text) + if err != nil { + g.statusLabel.SetText("Signieren fehlgeschlagen: " + err.Error()) + return + } + + g.textArea.SetText(result) + g.statusLabel.SetText("✓ Nachricht erfolgreich signiert (" + formatByteSize(len(input)) + ")") +} + +// onVerifyText triggers the verification process for text in the GUI +func (g *GUI) onVerifyText() { + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Verifizieren") + return + } + + err := g.verifyData([]byte(input)) + if err != nil { + g.statusLabel.SetText("Verifizierung fehlgeschlagen: " + err.Error()) + return + } + + g.statusLabel.SetText("✓ Signatur ist gültig") +} + +// onEncrypt triggers encryption using a public key +func (g *GUI) onEncrypt() { + if g.encryptionUsed { + g.statusLabel.SetText("Bitte wählen Sie ein neues Zertifikat für die Verschlüsselung.") + g.publicKeyPath = "" + g.choosePublicKey() + return + } + + if g.publicKeyPath != "" { + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Verschlüsseln") + return + } + + result, err := g.encryptData([]byte(input), g.publicKeyPath) + if err != nil { + g.statusLabel.SetText("Verschlüsselung fehlgeschlagen: " + err.Error()) + return + } + + g.encryptionUsed = true + g.textArea.SetText(result) + g.statusLabel.SetText("✓ Verschlüsselt mit: " + filepath.Base(g.publicKeyPath)) + return + } + + g.choosePublicKey() +} + +// choosePublicKey opens a file dialog to select a PEM certificate +func (g *GUI) choosePublicKey() { + dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { + if err != nil { + g.statusLabel.SetText("Fehler beim Auswählen der Datei: " + err.Error()) + return + } + if reader == nil { + return + } + defer reader.Close() + + path := reader.URI().Path() + if filepath.Ext(path) != ".pem" { + g.statusLabel.SetText("Fehler: Bitte wählen Sie eine .pem Datei") + return + } + + g.publicKeyPath = path + g.encryptionUsed = false + g.statusLabel.SetText("Ausgewählter öffentlicher Schlüssel: " + filepath.Base(path) + " - Verschlüssle...") + + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Ausgewählt: " + filepath.Base(path) + " - Kein Text zum Verschlüsseln") + return + } + + result, err := g.encryptData([]byte(input), g.publicKeyPath) + if err != nil { + g.statusLabel.SetText("Verschlüsselung fehlgeschlagen: %v" + err.Error()) + return + } + + g.encryptionUsed = true + g.textArea.SetText(result) + g.statusLabel.SetText("✓ Verschlüsselt mit: " + filepath.Base(path)) + }, g.window) +} + +// onDecrypt triggers decryption using the YubiKey +func (g *GUI) onDecrypt() { + if g.pinEntry.Text == "" { + g.statusLabel.SetText("Fehler: PIN zum Entschlüsseln erforderlich") + return + } + + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Entschlüsseln") + return + } + + result, err := g.decryptData([]byte(input), g.pinEntry.Text) + if err != nil { + g.statusLabel.SetText("Entschlüsselung fehlgeschlagen: " + err.Error()) + return + } + + g.textArea.SetText(string(result)) + g.statusLabel.SetText("✓ Nachricht erfolgreich entschlüsselt") +} + +// onClear resets the UI state +func (g *GUI) onClear() { + g.textArea.SetText("") + g.publicKeyPath = "" + g.encryptionUsed = false + + clipboard := g.app.Clipboard() + if clipboard != nil { + clipboard.SetContent("") + } + + g.statusLabel.SetText("Textbereich, Zwischenablage und Verschlüsselungsstatus zurückgesetzt") +} + +// safePad ensures byte slice is exactly 'size' bytes long, padded with leading zeros. +func safePad(b []byte, size int) []byte { + if len(b) > size { + return b[len(b)-size:] // Truncate from left if too long + } + return append(make([]byte, size-len(b)), b...) // Pad with leading zeros +} + +// signData signs the input data using the YubiKey after hashing +func (g *GUI) signData(data []byte, pin string) (string, error) { + pinGuard := memguard.NewBufferFromBytes([]byte(pin)) + defer pinGuard.Destroy() + + // Normalize line endings to RFC-compliant CRLF before hashing + normalizedData := normalizeToRFCCompliantCRLF(data) + + // Display status that we're hashing large document + if len(normalizedData) > 1024*1024 { // > 1MB + g.statusLabel.SetText("Hashing des großen Dokuments (" + formatByteSize(len(normalizedData)) + ")...") + g.window.Canvas().Refresh(g.statusLabel) + } + + sig, algo, err := g.signDataInternal(pinGuard.Bytes(), normalizedData) + if err != nil { + return "", fmt.Errorf("signieren fehlgeschlagen: %v", err) + } + + // Ensure clean separation with CRLF + sep := "\r\n" + if len(normalizedData) > 0 { + last := string(normalizedData[len(normalizedData)-1:]) + if last == "\n" && !(len(normalizedData) >= 2 && string(normalizedData[len(normalizedData)-2:]) == "\r\n") { + sep = "\n" + } + } + + return string(normalizedData) + sep + + "-----BEGIN " + algo + " SIGNATURE-----" + sep + + formatSignatureRFC(sig) + + "-----END " + algo + " SIGNATURE-----" + sep, nil +} + +// signDataInternal performs the actual signing operation +// Uses proper PIV-compliant hash formatting for YubiKey +func (g *GUI) signDataInternal(pin, data []byte) (string, string, error) { + yk, err := openYubiKey(0) + if err != nil { + return "", "", err + } + defer yk.Close() + + cert, err := yk.Certificate(piv.SlotSignature) + if err != nil { + return "", "", fmt.Errorf("Zertifikat vom Signatur-Slot konnte nicht abgerufen werden: %v", err) + } + + // Handle Ed25519 signing + if ed25519PubKey, ok := cert.PublicKey.(ed25519.PublicKey); ok { + // Ed25519 signs the hash of the data, not the raw data (YubiKey requirement) + hash := sha256.Sum256(data) + return g.signEd25519Data(string(pin), hash[:], ed25519PubKey, yk) + } + + // Handle ECDSA signing + pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return "", "", fmt.Errorf("öffentlicher Schlüssel ist nicht ECDSA oder Ed25519") + } + + // Algorithm name is "ECCP256", not "P-256" + algorithm, exists := curveToAlgorithm[pubKey.Curve] + if !exists { + return "", "", fmt.Errorf("nicht unterstützte Kurve: %v", pubKey.Curve) + } + + hashFunc := curveToHash[pubKey.Curve] + + // Create hash of the data for ECDSA signing + var digest []byte + switch hashFunc { + case crypto.SHA256: + h := sha256.New() + h.Write(data) + digest = h.Sum(nil) + case crypto.SHA384: + h := sha512.New384() + h.Write(data) + digest = h.Sum(nil) + default: + return "", "", fmt.Errorf("nicht unterstützter Hash-Algorithmus für Kurve") + } + + auth := piv.KeyAuth{PIN: string(pin)} + priv, err := yk.PrivateKey(piv.SlotSignature, cert.PublicKey, auth) + if err != nil { + return "", "", fmt.Errorf("privater Schlüssel konnte nicht abgerufen werden: %v", err) + } + + signer, ok := priv.(crypto.Signer) + if !ok { + return "", "", fmt.Errorf("Schlüssel implementiert nicht crypto.Signer") + } + + asn1sig, err := signer.Sign(rand.Reader, digest, nil) + if err != nil { + return "", "", fmt.Errorf("Signieren fehlgeschlagen: %v", err) + } + + var sig ecSignature + if _, err := asn1.Unmarshal(asn1sig, &sig); err != nil { + return "", "", fmt.Errorf("ASN.1 unmarshalling fehlgeschlagen: %v", err) + } + + curveSize := (pubKey.Curve.Params().BitSize + 7) / 8 + + // Build combined signature: X || Y || R || S (all padded to curveSize) + var raw []byte + raw = append(raw, safePad(pubKey.X.Bytes(), curveSize)...) + raw = append(raw, safePad(pubKey.Y.Bytes(), curveSize)...) + raw = append(raw, safePad(sig.R.Bytes(), curveSize)...) + raw = append(raw, safePad(sig.S.Bytes(), curveSize)...) + + return hex.EncodeToString(raw), algorithm, nil +} + +// signEd25519Data handles Ed25519 signing +func (g *GUI) signEd25519Data(pin string, hash []byte, pubKey ed25519.PublicKey, yk *piv.YubiKey) (string, string, error) { + auth := piv.KeyAuth{PIN: pin} + priv, err := yk.PrivateKey(piv.SlotSignature, pubKey, auth) + if err != nil { + return "", "", fmt.Errorf("privater Schlüssel konnte nicht abgerufen werden: %v", err) + } + + signer, ok := priv.(crypto.Signer) + if !ok { + return "", "", fmt.Errorf("Schlüssel implementiert nicht crypto.Signer") + } + + signature, err := signer.Sign(rand.Reader, hash, crypto.Hash(0)) + if err != nil { + return "", "", fmt.Errorf("Ed25519 Signieren fehlgeschlagen: %v", err) + } + + combined := append(pubKey, signature...) + + return hex.EncodeToString(combined), AlgorithmED25519, nil +} + +// verifyData verifies a signed message +func (g *GUI) verifyData(data []byte) error { + // Normalize input to handle both LF and CRLF + s := string(normalizeToRFCCompliantCRLF(data)) + + var algorithm string + var beg, end string + + // Try to find BEGIN/END block with CRLF or LF + for algo := range supportedAlgorithms { + begCRLF := "\r\n-----BEGIN " + algo + " SIGNATURE-----\r\n" + endCRLF := "-----END " + algo + " SIGNATURE-----\r\n" + begLF := "\n-----BEGIN " + algo + " SIGNATURE-----\n" + endLF := "-----END " + algo + " SIGNATURE-----\n" + + if strings.Contains(s, begCRLF) { + algorithm = algo + beg = begCRLF + end = endCRLF + break + } else if strings.Contains(s, begLF) { + algorithm = algo + beg = begLF + end = endLF + break + } + } + + if algorithm == "" { + g.showErrorPopup("Keine unterstützte Signatur gefunden", []byte{}, "") + return fmt.Errorf("kein unterstützter Signaturblock gefunden") + } + + i := strings.Index(s, beg) + j := strings.Index(s, end) + if i == -1 || j == -1 || j <= i { + g.showErrorPopup("Ungültiges Signaturformat", []byte{}, algorithm) + return fmt.Errorf("ungültiges Signaturblock-Format") + } + + originalMessage := []byte(s[:i]) + hexPart := s[i+len(beg) : j] + hexPart = regexp.MustCompile(`[\r\n\s\t]+`).ReplaceAllString(hexPart, "") + + combined, err := hex.DecodeString(hexPart) + if err != nil { + g.showErrorPopup("Hex-Dekodierung fehlgeschlagen", []byte{}, algorithm) + return fmt.Errorf("Hex-Dekodierung fehlgeschlagen: %v", err) + } + + // Status for large files + if len(originalMessage) > 1024*1024 { + g.statusLabel.SetText("Verifiziere großes Dokument (" + formatByteSize(len(originalMessage)) + ")...") + g.window.Canvas().Refresh(g.statusLabel) + } + + var verificationErr error + switch algorithm { + case AlgorithmED25519: + hash := sha256.Sum256(originalMessage) + verificationErr = g.verifyEd25519(hash[:], combined) + case AlgorithmECCP256, AlgorithmECCP384: + verificationErr = g.verifyECDSA(originalMessage, combined, algorithm) + default: + verificationErr = fmt.Errorf("nicht unterstützter Algorithmus: %s", algorithm) + } + + if verificationErr != nil { + publicKeyBytes, _ := extractPublicKeyFromSignature(combined, algorithm) + g.showErrorPopup("Signaturverifizierung fehlgeschlagen: "+verificationErr.Error(), publicKeyBytes, algorithm) + return verificationErr + } + + publicKeyBytes, err := extractPublicKeyFromSignature(combined, algorithm) + if err != nil { + g.showErrorPopup("Fehler beim Extrahieren des öffentlichen Schlüssels: "+err.Error(), []byte{}, algorithm) + return err + } + + // Successfully verified - show identicon from public key (hashed!) + g.showSuccessPopup(publicKeyBytes, algorithm) + + return nil +} + +// extractPublicKeyFromSignature extracts the public key from the signature +func extractPublicKeyFromSignature(combined []byte, algorithm string) ([]byte, error) { + switch algorithm { + case AlgorithmED25519: + if len(combined) != Ed25519CombinedSize { + return nil, fmt.Errorf("ungültiger Ed25519 Signaturblock") + } + // Return only the public key (first 32 bytes) + return combined[:Ed25519PublicKeySize], nil + + case AlgorithmECCP256, AlgorithmECCP384: + var curve elliptic.Curve + switch algorithm { + case AlgorithmECCP256: + curve = elliptic.P256() + case AlgorithmECCP384: + curve = elliptic.P384() + default: + return nil, fmt.Errorf("nicht unterstützter ECDSA Algorithmus: %s", algorithm) + } + + curveSize := (curve.Params().BitSize + 7) / 8 + expectedBytes := 4 * curveSize + + if len(combined) != expectedBytes { + return nil, fmt.Errorf("ungültige Signaturblockgröße: erwartet %d, erhalten %d", expectedBytes, len(combined)) + } + + // Return only the public key (X || Y) + return combined[:2*curveSize], nil + + default: + return nil, fmt.Errorf("nicht unterstützter Algorithmus: %s", algorithm) + } +} + +// verifyEd25519 verifies an Ed25519 signature +func (g *GUI) verifyEd25519(dataHash, combined []byte) error { + if len(combined) != Ed25519CombinedSize { + return fmt.Errorf("ungültiger Ed25519 Signaturblock") + } + + publicKey := combined[:Ed25519PublicKeySize] + signature := combined[Ed25519PublicKeySize:] + + if !ed25519.Verify(ed25519.PublicKey(publicKey), dataHash, signature) { + return fmt.Errorf("Ed25519 Signaturverifizierung fehlgeschlagen") + } + + return nil +} + +// verifyECDSA verifies an ECDSA signature with embedded public key (X,Y) +func (g *GUI) verifyECDSA(data, combined []byte, algorithm string) error { + var curve elliptic.Curve + var hashFunc crypto.Hash + + switch algorithm { + case AlgorithmECCP256: + curve = elliptic.P256() + hashFunc = crypto.SHA256 + case AlgorithmECCP384: + curve = elliptic.P384() + hashFunc = crypto.SHA384 + default: + return fmt.Errorf("nicht unterstützter ECDSA Algorithmus: %s", algorithm) + } + + curveSize := (curve.Params().BitSize + 7) / 8 + expectedBytes := 4 * curveSize + + if len(combined) != expectedBytes { + return fmt.Errorf("ungültige Signaturblockgröße: erwartet %d, erhalten %d", expectedBytes, len(combined)) + } + + X := new(big.Int).SetBytes(safePad(combined[0:curveSize], curveSize)) + Y := new(big.Int).SetBytes(safePad(combined[curveSize:2*curveSize], curveSize)) + R := new(big.Int).SetBytes(safePad(combined[2*curveSize:3*curveSize], curveSize)) + S := new(big.Int).SetBytes(safePad(combined[3*curveSize:], curveSize)) + + if !curve.IsOnCurve(X, Y) { + return fmt.Errorf("öffentlicher Schlüsselpunkt (X,Y) liegt nicht auf der Kurve %s", curve.Params().Name) + } + + pub := &ecdsa.PublicKey{ + Curve: curve, + X: X, + Y: Y, + } + + var digest []byte + switch hashFunc { + case crypto.SHA256: + h := sha256.New() + h.Write(data) + digest = h.Sum(nil) + case crypto.SHA384: + h := sha512.New384() + h.Write(data) + digest = h.Sum(nil) + } + + if !ecdsa.Verify(pub, digest, R, S) { + return fmt.Errorf("Signaturverifizierung fehlgeschlagen") + } + + return nil +} + +// stripLeadingZeros removes leading zero bytes, but keeps at least one byte. +func stripLeadingZeros(b []byte) []byte { + i := 0 + for i < len(b)-1 && b[i] == 0 { + i++ + } + return b[i:] +} + +// extractPublicKeyDisplayBytes returns the public key bytes for display/hashing — +// with leading zeros stripped from X and Y for ECC keys (for cleaner hex strings), +// but full raw bytes for Ed25519. +func extractPublicKeyDisplayBytes(combined []byte, algorithm string) ([]byte, error) { + switch algorithm { + case AlgorithmED25519: + if len(combined) != Ed25519CombinedSize { + return nil, fmt.Errorf("ungültiger Ed25519 Signaturblock") + } + // Return full 32 bytes — no stripping + return combined[:Ed25519PublicKeySize], nil + + case AlgorithmECCP256, AlgorithmECCP384: + var curve elliptic.Curve + switch algorithm { + case AlgorithmECCP256: + curve = elliptic.P256() + case AlgorithmECCP384: + curve = elliptic.P384() + default: + return nil, fmt.Errorf("nicht unterstützter ECDSA Algorithmus: %s", algorithm) + } + + curveSize := (curve.Params().BitSize + 7) / 8 + expectedBytes := 4 * curveSize + + if len(combined) != expectedBytes { + return nil, fmt.Errorf("ungültige Signaturblockgröße: erwartet %d, erhalten %d", expectedBytes, len(combined)) + } + + // Extract X and Y with leading zeros (as stored) + XBytes := combined[0:curveSize] + YBytes := combined[curveSize : 2*curveSize] + + // Strip leading zeros for display — but keep at least one byte! + XStripped := stripLeadingZeros(XBytes) + YStripped := stripLeadingZeros(YBytes) + + // Concatenate stripped X and Y for display/hashing + result := make([]byte, 0, len(XStripped)+len(YStripped)) + result = append(result, XStripped...) + result = append(result, YStripped...) + + return result, nil + + default: + return nil, fmt.Errorf("nicht unterstützter Algorithmus: %s", algorithm) + } +} + +// showSuccessPopup shows the identicon popup for successful verification +func (g *GUI) showSuccessPopup(publicKeyBytes []byte, algorithm string) { + displayBytes, err := extractPublicKeyDisplayBytes(publicKeyBytes, algorithm) + if err != nil { + displayBytes = publicKeyBytes + } + + hexString := hex.EncodeToString(displayBytes) + hash := sha256.Sum256([]byte(hexString)) + + identicon := NewClassicIdenticon(hash[:]) + img := identicon.Generate() + + fyneImg := canvas.NewImageFromImage(img) + fyneImg.FillMode = canvas.ImageFillContain + fyneImg.SetMinSize(fyne.NewSize(128, 128)) + + successLabel := widget.NewLabel("Signatur ist gültig") + successLabel.Alignment = fyne.TextAlignCenter + + copyBtn := widget.NewButton("Signaturkomponente kopieren", func() { + clipboard := g.app.Clipboard() + if clipboard != nil { + clipboard.SetContent(hexString) + g.statusLabel.SetText("✓ Signaturkomponente in Zwischenablage kopiert") + time.AfterFunc(2*time.Second, func() { + g.statusLabel.SetText("Bereit") + }) + } + }) + + content := container.NewVBox( + container.NewCenter(fyneImg), + container.NewCenter(successLabel), + container.NewCenter(copyBtn), + ) + + d := dialog.NewCustom("", "OK", content, g.window) + d.Show() +} + +// showErrorPopup shows an error popup with identicon for failed verification +func (g *GUI) showErrorPopup(message string, publicKeyBytes []byte, algorithm string) { + if len(publicKeyBytes) == 0 { + errorLabel := widget.NewLabel(message) + errorLabel.Alignment = fyne.TextAlignCenter + + content := container.NewVBox(container.NewCenter(errorLabel)) + d := dialog.NewCustom("", "OK", content, g.window) + d.Show() + return + } +} + +// encryptData encrypts data using RSA-OAEP and AES-GCM +func (g *GUI) encryptData(data []byte, pubKeyFile string) (string, error) { + pubKey, err := loadRSAPublicKey(pubKeyFile) + if err != nil { + return "", fmt.Errorf("öffentlicher Schlüssel konnte nicht geladen werden: %v", err) + } + + aesKeyGuard := memguard.NewBuffer(32) + defer aesKeyGuard.Destroy() + + if _, err := rand.Read(aesKeyGuard.Bytes()); err != nil { + return "", fmt.Errorf("AES-Schlüsselgenerierung fehlgeschlagen: %v", err) + } + + encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, aesKeyGuard.Bytes()) + if err != nil { + return "", fmt.Errorf("RSA-Verschlüsselung fehlgeschlagen: %v", err) + } + defer memguard.WipeBytes(encryptedKey) + + encryptedData, err := encryptAES(data, aesKeyGuard.Bytes()) + if err != nil { + return "", fmt.Errorf("AES-Verschlüsselung fehlgeschlagen: %v", err) + } + defer memguard.WipeBytes(encryptedData) + + combined := append(encryptedKey, encryptedData...) + defer memguard.WipeBytes(combined) + + base64Str := base64.StdEncoding.EncodeToString(combined) + + return formatBase64RFC(base64Str), nil +} + +// decryptData decrypts data using YubiKey's private key +func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) { + pinGuard := memguard.NewBufferFromBytes([]byte(pin)) + defer pinGuard.Destroy() + + s := string(data) + s = strings.ReplaceAll(s, "\r\n", "") + s = strings.ReplaceAll(s, " ", "") + + combined, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("Base64-Dekodierung fehlgeschlagen: %v", err) + } + defer memguard.WipeBytes(combined) + + yk, err := openYubiKey(0) + if err != nil { + return nil, fmt.Errorf("YubiKey konnte nicht geöffnet werden: %v", err) + } + defer yk.Close() + + cert, err := yk.Certificate(piv.SlotKeyManagement) + if err != nil { + return nil, fmt.Errorf("Zertifikat von Slot 9d konnte nicht abgerufen werden: %v", err) + } + + rsaPubKey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("Zertifikat enthält keinen RSA öffentlichen Schlüssel") + } + + if err := checkRSASecurity(rsaPubKey, "auf YubiKey"); err != nil { + return nil, err + } + + keySize := rsaPubKey.Size() + if len(combined) < keySize { + return nil, fmt.Errorf("Chiffretext zu kurz") + } + + encryptedKey := combined[:keySize] + encryptedData := combined[keySize:] + defer memguard.WipeBytes(encryptedKey) + + auth := piv.KeyAuth{PIN: pin} + priv, err := yk.PrivateKey(piv.SlotKeyManagement, cert.PublicKey, auth) + if err != nil { + return nil, fmt.Errorf("privater Schlüssel konnte nicht abgerufen werden: %v", err) + } + + decrypter, ok := priv.(crypto.Decrypter) + if !ok { + return nil, fmt.Errorf("privater Schlüssel unterstützt keine Entschlüsselung") + } + + decryptedPayload, err := decrypter.Decrypt(rand.Reader, encryptedKey, nil) + if err != nil { + return nil, fmt.Errorf("RSA-Entschlüsselung fehlgeschlagen: %v", err) + } + defer memguard.WipeBytes(decryptedPayload) + + if len(decryptedPayload) != 32 { + return nil, fmt.Errorf("ungültige AES-Schlüsselgröße") + } + + decryptedData, err := decryptAES(encryptedData, decryptedPayload) + if err != nil { + return nil, fmt.Errorf("AES-Entschlüsselung fehlgeschlagen: %v", err) + } + + return decryptedData, nil +} + +// normalizeToRFCCompliantCRLF converts all line endings to RFC-compliant CRLF +func normalizeToRFCCompliantCRLF(data []byte) []byte { + s := string(data) + s = strings.ReplaceAll(s, "\r\n", "\n") + s = strings.ReplaceAll(s, "\r", "\n") + s = strings.ReplaceAll(s, "\n", "\r\n") + return []byte(s) +} + +// formatSignatureRFC formats hex signature with 64 characters per line and RFC-compliant CRLF +func formatSignatureRFC(sig string) string { + var result strings.Builder + for i := 0; i < len(sig); i += 64 { + end := i + 64 + if end > len(sig) { + end = len(sig) + } + result.WriteString(sig[i:end]) + result.WriteString("\r\n") + } + return result.String() +} + +// formatBase64RFC formats base64 string with 76 characters per line and RFC-compliant CRLF +func formatBase64RFC(data string) string { + var result strings.Builder + for i := 0; i < len(data); i += 76 { + end := i + 76 + if end > len(data) { + end = len(data) + } + result.WriteString(data[i:end]) + result.WriteString("\r\n") + } + return result.String() +} + +// formatByteSize formats bytes into human-readable format +func formatByteSize(bytes int) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// securePadMessage adds ISO/IEC 7816-4 padding to align data to 4096-byte blocks +func securePadMessage(data []byte) []byte { + const blockSize = 4096 + paddingNeeded := blockSize - (len(data) % blockSize) + if paddingNeeded == blockSize { + return data + } + + paddedData := make([]byte, len(data)+paddingNeeded) + copy(paddedData, data) + paddedData[len(data)] = 0x80 + + return paddedData +} + +// secureUnpadMessage removes padding added by securePadMessage +func secureUnpadMessage(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("Polsterung kann nicht von leeren Daten entfernt werden") + } + if len(data)%4096 != 0 { + return nil, errors.New("ungültige Blockgröße für Polster-Entfernung") + } + + lastIndex := -1 + for i := len(data) - 1; i >= 0; i-- { + if data[i] == 0x80 { + lastIndex = i + break + } + if data[i] != 0x00 { + return nil, errors.New("ungültiges Polster-Format: unerwartetes Nicht-Null-Byte") + } + } + + if lastIndex == -1 { + return nil, errors.New("kein Polster-Marker gefunden") + } + + return data[:lastIndex], nil +} + +func (g *GUI) onPad() { + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Polstern") + return + } + + paddedData := securePadMessage([]byte(input)) + base64String := base64.StdEncoding.EncodeToString(paddedData) + formattedBase64 := formatBase64RFC(base64String) + + g.textArea.SetText(formattedBase64) + + originalLen := len(input) + paddedLen := len(paddedData) + + g.statusLabel.SetText(fmt.Sprintf("✓ Gepolstert: %d -> %d Bytes (Base64)", originalLen, paddedLen)) +} + +func (g *GUI) onUnpad() { + input := g.textArea.Text + if input == "" { + g.statusLabel.SetText("Fehler: Kein Text zum Entpolstern") + return + } + + binaryData, err := base64.StdEncoding.DecodeString(input) + if err != nil { + g.statusLabel.SetText("Entpolstern fehlgeschlagen: Ungültige Base64-Daten") + return + } + + unpaddedData, err := secureUnpadMessage(binaryData) + if err != nil { + g.statusLabel.SetText("Entpolstern fehlgeschlagen: " + err.Error()) + return + } + + g.textArea.SetText(string(unpaddedData)) + g.statusLabel.SetText("✓ Polsterung erfolgreich entfernt") +} + +// checkRSASecurity validates RSA key size +func checkRSASecurity(pubKey *rsa.PublicKey, context string) error { + keySize := pubKey.N.BitLen() + if keySize < minRSABits { + return fmt.Errorf("unsicherer %d-Bit RSA Schlüssel %s - Minimum ist %d-Bit", keySize, context, minRSABits) + } + if _, supported := supportedRSASizes[keySize]; !supported { + fmt.Fprintf(os.Stderr, "WARNUNG: %d-Bit RSA Schlüssel %s - unterstützte Größen sind 2048, 3072, 4096 Bits\n", keySize, context) + } + if keySize == 1024 { + fmt.Fprintf(os.Stderr, "KRITISCHE WARNUNG: 1024-Bit RSA Schlüssel %s sind unsicher und sollten nicht verwendet werden!\n", context) + } + return nil +} + +// loadRSAPublicKey loads RSA public key from PEM file +func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("öffentliche Schlüsseldatei konnte nicht gelesen werden: %v", err) + } + defer memguard.WipeBytes(data) + + block, _ := pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("keine PEM-Daten in der Datei gefunden") + } + + switch block.Type { + case "CERTIFICATE": + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("Zertifikat konnte nicht geparst werden: %v", err) + } + pubKey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("Zertifikat enthält keinen RSA öffentlichen Schlüssel") + } + if err := checkRSASecurity(pubKey, "im Zertifikat "+filename); err != nil { + return nil, err + } + return pubKey, nil + + case "PUBLIC KEY": + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("öffentlicher Schlüssel konnte nicht geparst werden: %v", err) + } + pubKey, ok := pubInterface.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("kein RSA öffentlicher Schlüssel") + } + if err := checkRSASecurity(pubKey, "in Datei "+filename); err != nil { + return nil, err + } + return pubKey, nil + + case "RSA PUBLIC KEY": + pubKey, err := x509.ParsePKCS1PublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("RSA öffentlicher Schlüssel konnte nicht geparst werden: %v", err) + } + if err := checkRSASecurity(pubKey, "in Datei "+filename); err != nil { + return nil, err + } + return pubKey, nil + + default: + return nil, fmt.Errorf("nicht unterstützter PEM-Typ: %s, erwartet CERTIFICATE, PUBLIC KEY oder RSA PUBLIC KEY", block.Type) + } +} + +// encryptAES encrypts data using AES-256-GCM +func encryptAES(data, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nonce, nonce, data, nil) + + return ciphertext, nil +} + +// decryptAES decrypts data using AES-256-GCM +func decryptAES(data, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("Chiffretext zu kurz") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +// openYubiKey opens a connection to the YubiKey +func openYubiKey(index int) (*piv.YubiKey, error) { + cards, err := piv.Cards() + if err != nil { + return nil, fmt.Errorf("Kartenliste konnte nicht abgerufen werden: %v", err) + } + if len(cards) == 0 { + return nil, fmt.Errorf("keine Smartcard gefunden") + } + + count := 0 + for _, card := range cards { + if strings.Contains(strings.ToLower(card), "yubikey") { + if count == index { + return piv.Open(card) + } + count++ + } + } + + return nil, fmt.Errorf("kein YubiKey am Index %d gefunden", index) +} + +// ClassicIdenticon with 100% deterministic, bit-perfect design + 2-color mode +type ClassicIdenticon struct { + source []byte + size int +} + +// NewClassicIdenticon creates a generator with classic look +func NewClassicIdenticon(source []byte) *ClassicIdenticon { + return &ClassicIdenticon{ + source: source, + size: 256, + } +} + +// mapValue maps a value from one range to another +func mapValue(value uint32, vmin, vmax, dmin, dmax uint32) float32 { + if vmax == vmin { + return float32(dmin) + } + return float32(dmin) + float32(value-vmin)*float32(dmax-dmin)/float32(vmax-vmin) +} + +// getBit returns the n-th bit (0-indexed) from source +func (identicon *ClassicIdenticon) getBit(n int) bool { + if len(identicon.source) == 0 || n < 0 { + return false + } + byteIndex := n / 8 + bitIndex := n % 8 + if byteIndex >= len(identicon.source) { + return false + } + return (identicon.source[byteIndex]>>bitIndex)&1 == 1 +} + +// getByte returns the n-th byte, wraps around if needed +func (identicon *ClassicIdenticon) getByte(n int) byte { + if len(identicon.source) == 0 { + return 0 + } + return identicon.source[n%len(identicon.source)] +} + +// foreground computes primary color using indexed palette (same as identicons program) +func (identicon *ClassicIdenticon) foreground() color.Color { + if len(identicon.source) < 32 { + return color.RGBA{0, 0, 0, 255} + } + + // Primary color index (4 bits → 16 colors) - EXACTLY like identicons program + colorIndex := 0 + for i := 0; i < 4; i++ { + if identicon.getBit(248 + i) { + colorIndex |= 1 << i + } + } + colorIndex %= 16 + + // Vibrant color palette — 16 beautiful, distinct colors (SAME as identicons program) + palette := []color.RGBA{ + {0x00, 0xbf, 0x93, 0xff}, // turquoise + {0x2d, 0xcc, 0x70, 0xff}, // mint + {0x42, 0xe4, 0x53, 0xff}, // green + {0xf1, 0xc4, 0x0f, 0xff}, // yellowOrange + {0xe6, 0x7f, 0x22, 0xff}, // brown + {0xff, 0x94, 0x4e, 0xff}, // orange + {0xe8, 0x4c, 0x3d, 0xff}, // red + {0x35, 0x98, 0xdb, 0xff}, // blue + {0x9a, 0x59, 0xb5, 0xff}, // purple + {0xef, 0x3e, 0x96, 0xff}, // magenta + {0xdf, 0x21, 0xb9, 0xff}, // violet + {0x7d, 0xc2, 0xd2, 0xff}, // lightBlue + {0x16, 0xa0, 0x86, 0xff}, // turquoiseIntense + {0x27, 0xae, 0x61, 0xff}, // mintIntense + {0x24, 0xc3, 0x33, 0xff}, // greenIntense + {0x1c, 0xab, 0xbb, 0xff}, // lightBlueIntense + } + + return palette[colorIndex] +} + +// secondaryColor computes second color using indexed palette (same as identicons program) +func (identicon *ClassicIdenticon) secondaryColor() color.Color { + if len(identicon.source) < 32 { + return color.RGBA{100, 100, 100, 255} + } + + // Secondary color index (4 bits → 16 colors) - EXACTLY like identicons program + colorIndex := 0 + for i := 0; i < 4; i++ { + if identicon.getBit(244 + i) { + colorIndex |= 1 << i + } + } + colorIndex %= 16 + + // Secondary color palette — 16 distinct colors (SAME as identicons program) + palette := []color.RGBA{ + {0x34, 0x49, 0x5e, 0xff}, // darkBlue + {0x95, 0xa5, 0xa5, 0xff}, // grey + {0xd2, 0x54, 0x00, 0xff}, // brownIntense + {0xc1, 0x39, 0x2b, 0xff}, // redIntense + {0x29, 0x7f, 0xb8, 0xff}, // blueIntense + {0x8d, 0x44, 0xad, 0xff}, // purpleIntense + {0xbe, 0x12, 0x7e, 0xff}, // violetIntense + {0xe5, 0x23, 0x83, 0xff}, // magentaIntense + {0x27, 0xae, 0x61, 0xff}, // mintIntense + {0x24, 0xc3, 0x33, 0xff}, // greenIntense + {0xd9, 0xd9, 0x21, 0xff}, // yellowIntense + {0xf3, 0x9c, 0x11, 0xff}, // yellowOrangeIntense + {0xff, 0x55, 0x00, 0xff}, // orangeIntense + {0x1c, 0xab, 0xbb, 0xff}, // lightBlueIntense + {0x23, 0x23, 0x23, 0xff}, // lightBlackIntense + {0x7e, 0x8c, 0x8d, 0xff}, // greyIntense + } + + return palette[colorIndex] +} + +// hslToRgb converts HSL to RGB in original style +func (identicon *ClassicIdenticon) hslToRgb(h, s, l float32) color.Color { + hue := h / 360.0 + sat := s / 100.0 + lum := l / 100.0 + + var b float32 + if lum <= 0.5 { + b = lum * (sat + 1.0) + } else { + b = lum + sat - lum*sat + } + + a := lum*2.0 - b + + red := identicon.hueToRgb(a, b, hue+1.0/3.0) + green := identicon.hueToRgb(a, b, hue) + blue := identicon.hueToRgb(a, b, hue-1.0/3.0) + + return color.RGBA{ + R: uint8(math.Round(float64(red * 255.0))), + G: uint8(math.Round(float64(green * 255.0))), + B: uint8(math.Round(float64(blue * 255.0))), + A: 255, + } +} + +// hueToRgb helper for color conversion +func (identicon *ClassicIdenticon) hueToRgb(a, b, hue float32) float32 { + if hue < 0 { + hue += 1.0 + } else if hue >= 1.0 { + hue -= 1.0 + } + + switch { + case hue < 1.0/6.0: + return a + (b-a)*6.0*hue + case hue < 0.5: + return b + case hue < 2.0/3.0: + return a + (b-a)*(2.0/3.0-hue)*6.0 + default: + return a + } +} + +// drawRect draws a solid rectangle +func (identicon *ClassicIdenticon) drawRect(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) { + rect := img.Bounds() + x0 = max(x0, rect.Min.X) + y0 = max(y0, rect.Min.Y) + x1 = min(x1, rect.Max.X) + y1 = min(y1, rect.Max.Y) + + if x0 >= x1 || y0 >= y1 { + return + } + + r, g, b, a := c.RGBA() + rgba := color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + } + + for y := y0; y < y1; y++ { + rowStart := img.PixOffset(x0, y) + for x := 0; x < x1-x0; x++ { + idx := rowStart + x*4 + img.Pix[idx] = rgba.R + img.Pix[idx+1] = rgba.G + img.Pix[idx+2] = rgba.B + img.Pix[idx+3] = rgba.A + } + } +} + +// generatePixelPattern generates 5x5 symmetric pixel grid — using individual bits +// Returns two layers: primary and secondary +func (identicon *ClassicIdenticon) generatePixelPattern() ([]bool, []bool) { + primary := make([]bool, 25) + secondary := make([]bool, 25) + + // Use bits 0-14 for primary pattern (15 bits) + bitIndex := 0 + for row := 0; row < 5; row++ { + for col := 0; col < 3; col++ { + paint := identicon.getBit(bitIndex) + bitIndex++ + ix := row*5 + col + mirrorIx := row*5 + (4 - col) + primary[ix] = paint + primary[mirrorIx] = paint + } + } + + // Use bits 15-29 for secondary pattern (next 15 bits) + for row := 0; row < 5; row++ { + for col := 0; col < 3; col++ { + paint := identicon.getBit(bitIndex) + bitIndex++ + ix := row*5 + col + mirrorIx := row*5 + (4 - col) + secondary[ix] = paint + secondary[mirrorIx] = paint + } + } + + return primary, secondary +} + +// Generate creates the identicon for UI display (respects theme) +func (identicon *ClassicIdenticon) Generate() image.Image { + const ( + pixelSize = 36 + spriteSize = 5 + margin = (256 - pixelSize*spriteSize) / 2 + ) + + primaryColor := identicon.foreground() + secondaryColor := identicon.secondaryColor() + + img := image.NewRGBA(image.Rect(0, 0, identicon.size, identicon.size)) + + // Background adapts to theme — use bits 252-253 to pick variation (2 bits → 3 options) + bgChoice := 0 + for i := 0; i < 2; i++ { // Use only 2 bits as in identicons program + if identicon.getBit(252 + i) { + bgChoice |= 1 << i + } + } + bgChoice %= 3 + + lightBackgrounds := []color.RGBA{ + {255, 255, 255, 255}, // pure white + {243, 245, 247, 255}, // light1 + {236, 240, 241, 255}, // light2 + } + + darkBackgrounds := []color.RGBA{ + {30, 30, 30, 255}, // dark gray + {45, 62, 80, 255}, // darkBlueIntense + {57, 57, 57, 255}, // dark2 + } + + var bg color.RGBA + if fyne.CurrentApp().Settings().ThemeVariant() == theme.VariantDark { + bg = darkBackgrounds[bgChoice] + } else { + bg = lightBackgrounds[bgChoice] + } + + for i := 0; i < len(img.Pix); i += 4 { + img.Pix[i] = bg.R + img.Pix[i+1] = bg.G + img.Pix[i+2] = bg.B + img.Pix[i+3] = bg.A + } + + primaryPixels, secondaryPixels := identicon.generatePixelPattern() + + // Draw secondary pixels first (background layer) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if secondaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + identicon.drawRect(img, x, y, x+pixelSize, y+pixelSize, secondaryColor) + } + } + } + + // Draw primary pixels on top (foreground layer) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if primaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + identicon.drawRect(img, x, y, x+pixelSize, y+pixelSize, primaryColor) + } + } + } + + return img +} + +// GenerateForExport generates identicon with fixed background for saving +func (identicon *ClassicIdenticon) GenerateForExport(transparent bool) image.Image { + const ( + pixelSize = 36 + spriteSize = 5 + margin = (256 - pixelSize*spriteSize) / 2 + ) + + primaryColor := identicon.foreground() + secondaryColor := identicon.secondaryColor() + + img := image.NewRGBA(image.Rect(0, 0, identicon.size, identicon.size)) + + // Set export background + var bg color.RGBA + if transparent { + bg = color.RGBA{0, 0, 0, 0} // fully transparent + } else { + // Use bits 252-253 for background choice (2 bits → 3 options) + bgChoice := 0 + for i := 0; i < 2; i++ { + if identicon.getBit(252 + i) { + bgChoice |= 1 << i + } + } + bgChoice %= 3 + + lightBackgrounds := []color.RGBA{ + {255, 255, 255, 255}, + {243, 245, 247, 255}, + {236, 240, 241, 255}, + } + bg = lightBackgrounds[bgChoice] + } + + for i := 0; i < len(img.Pix); i += 4 { + img.Pix[i] = bg.R + img.Pix[i+1] = bg.G + img.Pix[i+2] = bg.B + img.Pix[i+3] = bg.A + } + + primaryPixels, secondaryPixels := identicon.generatePixelPattern() + + // Draw secondary pixels first + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if secondaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + identicon.drawRect(img, x, y, x+pixelSize, y+pixelSize, secondaryColor) + } + } + } + + // Draw primary pixels on top + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if primaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + identicon.drawRect(img, x, y, x+pixelSize, y+pixelSize, primaryColor) + } + } + } + + return img +} -- cgit v1.2.3