From 04a5b7f513fe598b00302c389de935e0d10e63a5 Mon Sep 17 00:00:00 2001 From: Stefan Claas Date: Wed, 3 Sep 2025 20:14:28 +0200 Subject: Delete yubicrypt.go --- yubicrypt.go | 1100 ---------------------------------------------------------- 1 file changed, 1100 deletions(-) delete mode 100644 yubicrypt.go (limited to 'yubicrypt.go') diff --git a/yubicrypt.go b/yubicrypt.go deleted file mode 100644 index 44d32ae..0000000 --- a/yubicrypt.go +++ /dev/null @@ -1,1100 +0,0 @@ -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" - "math/big" - "os" - "path/filepath" - "strings" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "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, -} - -// Curve to algorithm mapping -var curveToAlgorithm = map[elliptic.Curve]string{ - elliptic.P256(): AlgorithmECCP256, - elliptic.P384(): AlgorithmECCP384, -} - -var curveToHash = map[elliptic.Curve]crypto.Hash{ - elliptic.P256(): crypto.SHA256, - elliptic.P384(): crypto.SHA384, -} - -// For Ed25519 - fixed sizes -const ( - Ed25519SignatureSize = 64 - Ed25519PublicKeySize = 32 - Ed25519CombinedSize = Ed25519SignatureSize + Ed25519PublicKeySize // 96 bytes -) - -const ( - minRSABits = 2048 // Minimum RSA key size accepted -) - -// 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 - textArea *widget.Entry - pinEntry *widget.Entry - statusLabel *widget.Label - publicKeyPath string - currentTheme string - encryptionUsed bool // Flag to track if encryption was already 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(800, 600)) - - gui.createUI() - gui.applyTheme() - - gui.window.SetContent(gui.createMainUI()) - gui.window.ShowAndRun() -} - -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("Enter text to encrypt, sign, or paste encrypted content here...") - - g.pinEntry = widget.NewPasswordEntry() - g.pinEntry.SetPlaceHolder("Enter PIN (max 8 chars)") - g.pinEntry.Validator = func(s string) error { - if len(s) > 8 { - return fmt.Errorf("PIN must be max 8 characters") - } - return nil - } - - g.statusLabel = widget.NewLabel("Ready") - g.statusLabel.Wrapping = fyne.TextWrapWord - - // Theme Toggle Button - g.themeToggle = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), g.toggleTheme) -} - -func (g *GUI) createMainUI() fyne.CanvasObject { - // Operation Buttons - signBtn := widget.NewButtonWithIcon("Sign", theme.ConfirmIcon(), g.onSign) - verifyBtn := widget.NewButtonWithIcon("Verify", theme.VisibilityIcon(), g.onVerify) - padBtn := widget.NewButtonWithIcon("Pad", theme.ContentAddIcon(), g.onPad) - unpadBtn := widget.NewButtonWithIcon("Unpad", theme.ContentRemoveIcon(), g.onUnpad) - encryptBtn := widget.NewButtonWithIcon("Encrypt", theme.MailComposeIcon(), g.onEncrypt) - decryptBtn := widget.NewButtonWithIcon("Decrypt", theme.MailForwardIcon(), g.onDecrypt) - - // Button Container - buttonContainer := container.NewHBox( - layout.NewSpacer(), - signBtn, - verifyBtn, - padBtn, - unpadBtn, - encryptBtn, - decryptBtn, - layout.NewSpacer(), - ) - - clearBtn := widget.NewButtonWithIcon("Clear", theme.DeleteIcon(), g.onClear) - pinContainer := container.NewVBox( - container.NewHBox( - layout.NewSpacer(), - widget.NewLabel("PIN:"), - g.pinEntry, - clearBtn, - layout.NewSpacer(), - ), - ) - - mainContainer := container.NewBorder( - container.NewVBox( - container.NewHBox( - layout.NewSpacer(), - g.themeToggle, - ), - buttonContainer, - widget.NewSeparator(), - ), - container.NewVBox( - widget.NewSeparator(), - pinContainer, - g.statusLabel, - ), - nil, - nil, - container.NewScroll(g.textArea), - ) - - return mainContainer -} - -func (g *GUI) toggleTheme() { - if g.currentTheme == "dark" { - g.app.Settings().SetTheme(theme.LightTheme()) - g.currentTheme = "light" - g.themeToggle.SetIcon(theme.ViewRefreshIcon()) - } else { - g.app.Settings().SetTheme(theme.DarkTheme()) - g.currentTheme = "dark" - g.themeToggle.SetIcon(theme.ViewRefreshIcon()) - } -} - -func (g *GUI) applyTheme() { - if g.currentTheme == "dark" { - g.app.Settings().SetTheme(theme.DarkTheme()) - } else { - g.app.Settings().SetTheme(theme.LightTheme()) - } -} - -func (g *GUI) onSign() { - if g.pinEntry.Text == "" { - g.statusLabel.SetText("Error: PIN required for signing") - return - } - - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to sign") - return - } - - result, err := g.signData([]byte(input), g.pinEntry.Text) - if err != nil { - g.statusLabel.SetText("Signing failed: " + err.Error()) - return - } - - g.textArea.SetText(result) - g.statusLabel.SetText("✓ Message signed successfully") -} - -func (g *GUI) onVerify() { - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to verify") - return - } - - err := g.verifyData([]byte(input)) - if err != nil { - g.statusLabel.SetText("Verification failed: " + err.Error()) - return - } - - g.statusLabel.SetText("✓ Signature is valid") -} - -func (g *GUI) onEncrypt() { - // If encryption was already used in this session, require new certificate - if g.encryptionUsed { - g.statusLabel.SetText("Please select a new certificate for encryption.") - g.publicKeyPath = "" // Reset public key path to force new selection - g.choosePublicKey() - return - } - - // If certificate already selected, encrypt immediately - if g.publicKeyPath != "" { - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to encrypt") - return - } - - result, err := g.encryptData([]byte(input), g.publicKeyPath) - if err != nil { - g.statusLabel.SetText("Encryption failed: " + err.Error()) - return - } - - g.encryptionUsed = true - g.textArea.SetText(result) - g.statusLabel.SetText("✓ Encrypted with: " + filepath.Base(g.publicKeyPath)) - return - } - - // No certificate selected yet, open file dialog - g.choosePublicKey() -} - -func (g *GUI) choosePublicKey() { - dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { - if err != nil { - g.statusLabel.SetText("Error selecting file: " + err.Error()) - return - } - if reader == nil { - return // Dialog cancelled - } - defer reader.Close() - - path := reader.URI().Path() - if filepath.Ext(path) != ".pem" { - g.statusLabel.SetText("Error: Please select a .pem file") - return - } - - g.publicKeyPath = path - g.encryptionUsed = false // Reset encryption flag when new certificate is selected - g.statusLabel.SetText("Selected public key: " + filepath.Base(path) + " - Encrypting...") - - // AUTOMATIC ENCRYPTION AFTER CERTIFICATE SELECTION - LIKE GNUPG/AGE - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Selected: " + filepath.Base(path) + " - No text to encrypt") - return - } - - result, err := g.encryptData([]byte(input), g.publicKeyPath) - if err != nil { - g.statusLabel.SetText("Encryption failed: " + err.Error()) - return - } - - // Mark encryption as used for this session - g.encryptionUsed = true - g.textArea.SetText(result) - g.statusLabel.SetText("✓ Encrypted with: " + filepath.Base(path)) - }, g.window) -} - -func (g *GUI) onDecrypt() { - if g.pinEntry.Text == "" { - g.statusLabel.SetText("Error: PIN required for decryption") - return - } - - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to decrypt") - return - } - - result, err := g.decryptData([]byte(input), g.pinEntry.Text) - if err != nil { - g.statusLabel.SetText("Decryption failed: " + err.Error()) - return - } - - g.textArea.SetText(string(result)) - g.statusLabel.SetText("✓ Message decrypted successfully") -} - -func (g *GUI) onClear() { - g.textArea.SetText("") - g.publicKeyPath = "" - g.encryptionUsed = false // Reset encryption state on clear - - clipboard := g.app.Clipboard() - if clipboard != nil { - clipboard.SetContent("") - } - - g.statusLabel.SetText("Cleared text area, clipboard and reset encryption state") -} - -// signData signs with RFC-compliant normalization -func (g *GUI) signData(data []byte, pin string) (string, error) { - pinGuard := memguard.NewBufferFromBytes([]byte(pin)) - defer pinGuard.Destroy() - - // Normalize to RFC-compliant CRLF before signing - normalizedData := normalizeToRFCCompliantCRLF(data) - - sig, curveType, err := g.signDataInternal(pinGuard.Bytes(), normalizedData) - if err != nil { - return "", fmt.Errorf("signing failed: %v", err) - } - - // RFC-compliant signature block - return string(normalizedData) + "\r\n-----BEGIN " + curveType + " SIGNATURE-----\r\n" + - formatSignature(sig) + "-----END " + curveType + " SIGNATURE-----\r\n", nil -} - -// signDataInternal performs the actual signing operation with the 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("failed to get certificate from signature slot: %v", err) - } - - // Check for Ed25519 support - if ed25519PubKey, ok := cert.PublicKey.(ed25519.PublicKey); ok { - return g.signDataEd25519(pin, data, ed25519PubKey, yk) - } - - // ECDSA support - pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey) - if !ok { - return "", "", fmt.Errorf("public key is not ECDSA or Ed25519") - } - - // Determine curve type - algorithm, exists := curveToAlgorithm[pubKey.Curve] - if !exists { - return "", "", fmt.Errorf("unsupported curve: %v", pubKey.Curve) - } - - auth := piv.KeyAuth{PIN: string(pin)} - priv, err := yk.PrivateKey(piv.SlotSignature, cert.PublicKey, auth) - if err != nil { - return "", "", fmt.Errorf("failed to get private key: %v", err) - } - - signer, ok := priv.(crypto.Signer) - if !ok { - return "", "", fmt.Errorf("key does not implement crypto.Signer") - } - - // Use appropriate hash for the curve - hashFunc := curveToHash[pubKey.Curve] - var digest []byte - - switch hashFunc { - case crypto.SHA256: - h := sha256.Sum256(data) - digest = h[:] - case crypto.SHA384: - h := sha512.Sum384(data) - digest = h[:] - default: - return "", "", fmt.Errorf("unsupported hash algorithm for curve") - } - - asn1sig, err := signer.Sign(rand.Reader, digest, hashFunc) - if err != nil { - return "", "", fmt.Errorf("ECDSA signing failed: %v", err) - } - - var sig ecSignature - if _, err := asn1.Unmarshal(asn1sig, &sig); err != nil { - return "", "", fmt.Errorf("ASN.1 unmarshal failed: %v", err) - } - - // Calculate appropriate padding based on curve - curveSize := (pubKey.Curve.Params().BitSize + 7) / 8 - pad := func(b []byte) []byte { - if len(b) > curveSize { - b = b[len(b)-curveSize:] - } - return append(make([]byte, curveSize-len(b)), b...) - } - - var raw []byte - raw = append(raw, pad(pubKey.X.Bytes())...) - raw = append(raw, pad(pubKey.Y.Bytes())...) - raw = append(raw, pad(sig.R.Bytes())...) - raw = append(raw, pad(sig.S.Bytes())...) - - return hex.EncodeToString(raw), algorithm, nil -} - -// signDataEd25519 handles Ed25519 signing operations -func (g *GUI) signDataEd25519(pin, data []byte, pubKey ed25519.PublicKey, yk *piv.YubiKey) (string, string, error) { - auth := piv.KeyAuth{PIN: string(pin)} - priv, err := yk.PrivateKey(piv.SlotSignature, pubKey, auth) - if err != nil { - return "", "", fmt.Errorf("failed to get private key: %v", err) - } - - signer, ok := priv.(crypto.Signer) - if !ok { - return "", "", fmt.Errorf("key does not implement crypto.Signer") - } - - signature, err := signer.Sign(rand.Reader, data, crypto.Hash(0)) - if err != nil { - return "", "", fmt.Errorf("Ed25519 signing failed: %v", err) - } - - combined := append(signature, pubKey...) - return hex.EncodeToString(combined), AlgorithmED25519, nil -} - -// verifyData handles verification with proper message normalization -func (g *GUI) verifyData(data []byte) error { - s := string(data) - - // Strict RFC-compliant detection - only CRLF format accepted - var algorithm string - var beg, end string - - for algo := range supportedAlgorithms { - begTest := fmt.Sprintf("\r\n-----BEGIN %s SIGNATURE-----\r\n", algo) - endTest := fmt.Sprintf("-----END %s SIGNATURE-----\r\n", algo) - - if strings.Contains(s, begTest) && strings.Contains(s, endTest) { - algorithm = algo - beg = begTest - end = endTest - break - } - } - - if algorithm == "" { - // Try without CRLF in case of Usenet/email - for algo := range supportedAlgorithms { - begTest := fmt.Sprintf("-----BEGIN %s SIGNATURE-----", algo) - endTest := fmt.Sprintf("-----END %s SIGNATURE-----", algo) - - if strings.Contains(s, begTest) && strings.Contains(s, endTest) { - algorithm = algo - beg = begTest - end = endTest - break - } - } - - if algorithm == "" { - return fmt.Errorf("no supported signature block found") - } - } - - i := strings.Index(s, beg) - j := strings.Index(s, end) - - if i == -1 || j == -1 || j < i { - return fmt.Errorf("invalid signature block format") - } - - // Extract original message - originalMessage := []byte(s[:i]) - hexPart := s[i+len(beg):j] - - // Remove all line breaks and whitespace from hex part - hexPart = strings.ReplaceAll(hexPart, "\r\n", "") - hexPart = strings.ReplaceAll(hexPart, "\n", "") - hexPart = strings.ReplaceAll(hexPart, "\r", "") - hexPart = strings.ReplaceAll(hexPart, " ", "") - hexPart = strings.ReplaceAll(hexPart, "\t", "") - - combined, err := hex.DecodeString(hexPart) - if err != nil { - return fmt.Errorf("hex decode failed: %v", err) - } - - // Normalize ONLY the message part for verification - normalizedMessage := normalizeToRFCCompliantCRLF(originalMessage) - - switch algorithm { - case AlgorithmED25519: - return g.verifyEd25519(normalizedMessage, combined) - case AlgorithmECCP256, AlgorithmECCP384: - return g.verifyECDSA(normalizedMessage, combined, algorithm) - default: - return fmt.Errorf("unsupported algorithm: %s", algorithm) - } -} - -// verifyEd25519 verifies Ed25519 signatures -func (g *GUI) verifyEd25519(data, combined []byte) error { - if len(combined) != Ed25519CombinedSize { - return fmt.Errorf("invalid Ed25519 signature block") - } - - signature := combined[:Ed25519SignatureSize] - publicKey := combined[Ed25519SignatureSize:] - - if !ed25519.Verify(publicKey, data, signature) { - return fmt.Errorf("Ed25519 signature verification failed") - } - - return nil -} - -// verifyECDSA verifies ECDSA signatures -func (g *GUI) verifyECDSA(data, combined []byte, algorithm string) error { - var curve elliptic.Curve - switch algorithm { - case AlgorithmECCP256: - curve = elliptic.P256() - case AlgorithmECCP384: - curve = elliptic.P384() - default: - return fmt.Errorf("unsupported ECDSA algorithm: %s", algorithm) - } - - curveSize := (curve.Params().BitSize + 7) / 8 - expectedBytes := curveSize * 4 - - if len(combined) != expectedBytes { - return fmt.Errorf("invalid signature block size") - } - - x := new(big.Int).SetBytes(combined[0:curveSize]) - y := new(big.Int).SetBytes(combined[curveSize:curveSize*2]) - r := new(big.Int).SetBytes(combined[curveSize*2:curveSize*3]) - sVal := new(big.Int).SetBytes(combined[curveSize*3:curveSize*4]) - - pub := &ecdsa.PublicKey{Curve: curve, X: x, Y: y} - - hashFunc := curveToHash[curve] - var digest []byte - - switch hashFunc { - case crypto.SHA256: - h := sha256.Sum256(data) - digest = h[:] - case crypto.SHA384: - h := sha512.Sum384(data) - digest = h[:] - default: - return fmt.Errorf("unsupported hash algorithm") - } - - if !ecdsa.Verify(pub, digest, r, sVal) { - return fmt.Errorf("signature is not valid") - } - - return nil -} - -// encryptData encrypts data using RSA public key and AES encryption -func (g *GUI) encryptData(data []byte, pubKeyFile string) (string, error) { - pubKey, err := loadRSAPublicKey(pubKeyFile) - if err != nil { - return "", fmt.Errorf("failed to load public key: %v", err) - } - - // Generate AES key - will be automatically cleaned up by memguard - aesKeyGuard := memguard.NewBuffer(32) - defer aesKeyGuard.Destroy() // Secure cleanup after use - if _, err := rand.Read(aesKeyGuard.Bytes()); err != nil { - return "", fmt.Errorf("failed to generate AES key: %v", err) - } - - // Encrypt AES key with RSA public key - encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, aesKeyGuard.Bytes()) - if err != nil { - return "", fmt.Errorf("RSA encryption failed: %v", err) - } - defer memguard.WipeBytes(encryptedKey) - - // Encrypt data with AES key - encryptedData, err := encryptAES(data, aesKeyGuard.Bytes()) - if err != nil { - return "", fmt.Errorf("AES encryption failed: %v", err) - } - defer memguard.WipeBytes(encryptedData) - - // Combine encrypted key and data - combined := append(encryptedKey, encryptedData...) - defer memguard.WipeBytes(combined) - - base64Str := base64.StdEncoding.EncodeToString(combined) - return formatBase64(base64Str), nil -} - -// decryptData decrypts data using YubiKey's RSA 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 decode failed: %v", err) - } - defer memguard.WipeBytes(combined) - - yk, err := openYubiKey(0) - if err != nil { - return nil, fmt.Errorf("failed to open YubiKey: %v", err) - } - defer yk.Close() - - cert, err := yk.Certificate(piv.SlotKeyManagement) - if err != nil { - return nil, fmt.Errorf("failed to get certificate from slot 9d: %v", err) - } - - rsaPubKey, ok := cert.PublicKey.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("certificate does not contain RSA public key") - } - - if err := checkRSASecurity(rsaPubKey, "on YubiKey"); err != nil { - return nil, err - } - - keySize := rsaPubKey.Size() - if len(combined) < keySize { - return nil, fmt.Errorf("ciphertext too short") - } - - 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("failed to get private key: %v", err) - } - - decrypter, ok := priv.(crypto.Decrypter) - if !ok { - return nil, fmt.Errorf("private key does not support decryption") - } - - decryptedPayload, err := decrypter.Decrypt(rand.Reader, encryptedKey, nil) - if err != nil { - return nil, fmt.Errorf("RSA decryption failed: %v", err) - } - defer memguard.WipeBytes(decryptedPayload) - - if len(decryptedPayload) != 32 { - return nil, fmt.Errorf("invalid AES key size") - } - - decryptedData, err := decryptAES(encryptedData, decryptedPayload) - if err != nil { - return nil, fmt.Errorf("AES decryption failed: %v", err) - } - - return decryptedData, nil -} - -// normalizeToRFCCompliantCRLF normalizes all line endings to RFC-compliant CRLF -// and ensures proper line length for cryptographic operations -func normalizeToRFCCompliantCRLF(data []byte) []byte { - s := string(data) - - // First convert all line endings to LF - s = strings.ReplaceAll(s, "\r\n", "\n") - s = strings.ReplaceAll(s, "\r", "\n") - - // Now convert to RFC-compliant CRLF - lines := strings.Split(s, "\n") - var result strings.Builder - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" { - result.WriteString(trimmed) - result.WriteString("\r\n") - } - } - - return []byte(result.String()) -} - -// normalizeMessageOnly normalizes only the message part, preserves signature blocks -func normalizeMessageOnly(data []byte) []byte { - s := string(data) - - // Check if we have a signature block - var signatureStart, signatureEnd int = -1, -1 - - for algo := range supportedAlgorithms { - begTest := fmt.Sprintf("-----BEGIN %s SIGNATURE-----", algo) - endTest := fmt.Sprintf("-----END %s SIGNATURE-----", algo) - - if start := strings.Index(s, begTest); start != -1 { - if end := strings.Index(s, endTest); end != -1 && end > start { - signatureStart = start - signatureEnd = end + len(endTest) - _ = algo - break - } - } - } - - if signatureStart == -1 { - // No signature found, normalize entire content - return normalizeToRFCCompliantCRLF(data) - } - - // Extract message part (before signature) - messagePart := s[:signatureStart] - - // Normalize only the message part - normalizedMessage := normalizeToRFCCompliantCRLF([]byte(messagePart)) - - // Keep signature block unchanged - signatureBlock := s[signatureStart:signatureEnd] - - // Reconstruct with normalized message and original signature - return append(normalizedMessage, []byte(signatureBlock)...) -} - -// formatSignature creates RFC-compliant signature with 64 characters per line and CRLF -func formatSignature(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") // RFC-compliant CRLF - } - return result.String() -} - -// formatBase64 formats base64 string with 76 characters per line -func formatBase64(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() -} - -// securePadMessage adds cryptographic padding to the message -func securePadMessage(data []byte) (string, error) { - const blockSize = 4096 - const minSize = 4096 - const lineLength = 76 - - if len(data) == 0 { - return "", errors.New("empty data cannot be padded") - } - - // Preserve original text with line breaks intact - originalText := string(data) - currentSize := len(originalText) - - // Determine target size - var targetSize int - if currentSize < minSize { - targetSize = minSize - } else { - targetSize = ((currentSize/blockSize) + 1) * blockSize - } - - // Length information - IMPORTANT: length of ORIGINAL text - lengthInfo := fmt.Sprintf("===LENGTH:%d===", len(originalText)) - totalMarkerLength := len("===PADDING===") + len(lengthInfo) - paddingNeeded := targetSize - currentSize - totalMarkerLength - - if paddingNeeded < 0 { - targetSize += blockSize - paddingNeeded = targetSize - currentSize - totalMarkerLength - } - - // Generate random padding - randomBytes := make([]byte, (paddingNeeded+1)/2) - if _, err := rand.Read(randomBytes); err != nil { - return "", fmt.Errorf("error generating random padding: %v", err) - } - - randomHex := hex.EncodeToString(randomBytes) - if len(randomHex) > paddingNeeded { - randomHex = randomHex[:paddingNeeded] - } - - // Append padding at the END (after signature) and format it properly - paddingContent := "===PADDING===" + randomHex + lengthInfo - formattedPadding := formatTo76Chars(paddingContent) - - // Combine original text with formatted padding - paddedContent := originalText + formattedPadding - - return paddedContent, nil -} - -// secureUnpadMessage removes cryptographic padding from the message -func secureUnpadMessage(data string) ([]byte, error) { - if data == "" { - return []byte{}, nil - } - - // Search for padding marker in the original formatted text - paddingIndex := strings.Index(data, "===PADDING===") - if paddingIndex == -1 { - // No padding found, return original text with all formatting - return []byte(data), nil - } - - // Return everything before the padding marker (preserves signature structure) - return []byte(data[:paddingIndex]), nil -} - -// formatTo76Chars formats text to 76 characters per line with CRLF -func formatTo76Chars(text string) string { - const lineLength = 76 - var result strings.Builder - - // Remove existing line breaks first to avoid double formatting - cleanText := strings.ReplaceAll(text, "\r\n", "") - cleanText = strings.ReplaceAll(cleanText, "\n", "") - - for i := 0; i < len(cleanText); i += lineLength { - end := i + lineLength - if end > len(cleanText) { - end = len(cleanText) - } - result.WriteString(cleanText[i:end]) - result.WriteString("\r\n") - } - return result.String() -} - -func (g *GUI) onPad() { - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to pad") - return - } - - result, err := securePadMessage([]byte(input)) - if err != nil { - g.statusLabel.SetText("Padding failed: %v") - return - } - - g.textArea.SetText(result) - - // Show padding statistics - originalLen := len(input) - paddedLen := len(result) - lines := strings.Count(result, "\r\n") + 1 - g.statusLabel.SetText(fmt.Sprintf("✓ Padded: %d → %d bytes (%d lines)", - originalLen, paddedLen, lines)) -} - -func (g *GUI) onUnpad() { - input := g.textArea.Text - if input == "" { - g.statusLabel.SetText("Error: No text to unpad") - return - } - - result, err := secureUnpadMessage(input) - if err != nil { - g.statusLabel.SetText("Unpadding failed: %v") - return - } - - g.textArea.SetText(string(result)) - g.statusLabel.SetText("✓ Message unpadded successfully") -} - -// checkRSASecurity checks RSA key length and issues warnings/errors -func checkRSASecurity(pubKey *rsa.PublicKey, context string) error { - keySize := pubKey.N.BitLen() - - if keySize < minRSABits { - return fmt.Errorf("insecure %d-bit RSA key %s - minimum is %d-bit", keySize, context, minRSABits) - } - - // Check if key size is supported - if _, supported := supportedRSASizes[keySize]; !supported { - fmt.Fprintf(os.Stderr, "WARNING: %d-bit RSA key %s - supported sizes are 2048, 3072, 4096 bits\n", - keySize, context) - } - - if keySize == 1024 { - fmt.Fprintf(os.Stderr, "CRITICAL WARNING: 1024-bit RSA keys %s are insecure and should not be used!\n", context) - } - - return nil -} - -// loadRSAPublicKey loads an RSA public key from a PEM file -func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) { - data, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("failed to read public key file: %v", err) - } - defer memguard.WipeBytes(data) - - block, _ := pem.Decode(data) - if block == nil { - return nil, fmt.Errorf("no PEM data found in file") - } - - switch block.Type { - case "CERTIFICATE": - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse certificate: %v", err) - } - pubKey, ok := cert.PublicKey.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("certificate does not contain RSA public key") - } - // Security check for certificate - if err := checkRSASecurity(pubKey, "in certificate "+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("failed to parse public key: %v", err) - } - pubKey, ok := pubInterface.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("not an RSA public key") - } - // Security check for public key - if err := checkRSASecurity(pubKey, "in file "+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("failed to parse RSA public key: %v", err) - } - // Security check for RSA public key - if err := checkRSASecurity(pubKey, "in file "+filename); err != nil { - return nil, err - } - return pubKey, nil - - default: - return nil, fmt.Errorf("unsupported PEM type: %s, expected CERTIFICATE, PUBLIC KEY or RSA PUBLIC KEY", block.Type) - } -} - -// encryptAES encrypts data using AES-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-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("ciphertext too short") - } - - 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("failed to list cards: %v", err) - } - if len(cards) == 0 { - return nil, fmt.Errorf("no smart card found") - } - - 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("no YubiKey found at index %d", index) -} -- cgit v1.2.3