diff options
| -rw-r--r-- | yubicrypt.go | 1037 |
1 files changed, 0 insertions, 1037 deletions
diff --git a/yubicrypt.go b/yubicrypt.go deleted file mode 100644 index 1a87a96..0000000 --- a/yubicrypt.go +++ /dev/null @@ -1,1037 +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"
- "regexp"
- "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,
-}
-
-// Mapping from elliptic curve to algorithm name
-var curveToAlgorithm = map[elliptic.Curve]string{
- elliptic.P256(): elliptic.P256().Params().Name,
- elliptic.P384(): elliptic.P384().Params().Name,
-}
-
-// 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
- 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(800, 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("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)
-}
-
-// createMainUI builds the main layout
-func (g *GUI) createMainUI() fyne.CanvasObject {
- signTextBtn := widget.NewButtonWithIcon("Sign Text", theme.ConfirmIcon(), g.onSignText)
- verifyTextBtn := widget.NewButtonWithIcon("Verify Text", theme.VisibilityIcon(), g.onVerifyText)
-
- 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)
-
- buttonContainer := container.NewHBox(
- layout.NewSpacer(),
- signTextBtn,
- verifyTextBtn,
- 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
-}
-
-// 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.SetIcon(theme.ViewRefreshIcon())
- } else {
- g.app.Settings().SetTheme(theme.DarkTheme())
- g.currentTheme = "dark"
- g.themeToggle.SetIcon(theme.ViewRefreshIcon())
- }
-}
-
-// applyTheme sets the initial theme
-func (g *GUI) applyTheme() {
- if g.currentTheme == "dark" {
- g.app.Settings().SetTheme(theme.DarkTheme())
- } else {
- g.app.Settings().SetTheme(theme.LightTheme())
- }
-}
-
-// onSignText triggers the signing process for text in the GUI
-func (g *GUI) onSignText() {
- 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
- }
-
- // 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("Error: Message already contains a signature")
- return
- }
- }
-
- // Sign data in the text area
- 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 (" + 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("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")
-}
-
-// onEncrypt triggers encryption using a public key
-func (g *GUI) onEncrypt() {
- if g.encryptionUsed {
- g.statusLabel.SetText("Please select a new certificate for encryption.")
- g.publicKeyPath = ""
- g.choosePublicKey()
- return
- }
-
- 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
- }
-
- 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("Error selecting file: " + err.Error())
- return
- }
- if reader == nil {
- return
- }
- 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
- g.statusLabel.SetText("Selected public key: " + filepath.Base(path) + " - Encrypting...")
-
- 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: %v" + err.Error())
- return
- }
-
- g.encryptionUsed = true
- g.textArea.SetText(result)
- g.statusLabel.SetText("✓ Encrypted with: " + filepath.Base(path))
- }, g.window)
-}
-
-// onDecrypt triggers decryption using the YubiKey
-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")
-}
-
-// 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("Cleared text area, clipboard and reset encryption state")
-}
-
-// 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 CRLF before hashing
- normalizedData := normalizeToRFCCompliantCRLFSimple(data)
-
- // Display status that we're hashing large document
- if len(normalizedData) > 1024*1024 { // > 1MB
- g.statusLabel.SetText("Hashing large document (" + formatByteSize(len(normalizedData)) + ")...")
- g.window.Canvas().Refresh(g.statusLabel)
- }
-
- sig, algo, err := g.signDataInternal(pinGuard.Bytes(), normalizedData)
- if err != nil {
- return "", fmt.Errorf("signing failed: %v", err)
- }
-
- return string(normalizedData) + "\r\n-----BEGIN " + algo + " SIGNATURE-----\r\n" +
- formatSignature(sig) + "-----END " + algo + " SIGNATURE-----\r\n", 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("failed to get certificate from signature slot: %v", err)
- }
-
- // Handle Ed25519 signing
- if ed25519PubKey, ok := cert.PublicKey.(ed25519.PublicKey); ok {
- // Ed25519 signs the hash of the data, not the raw data
- 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("public key is not ECDSA or Ed25519")
- }
-
- algorithm, exists := curveToAlgorithm[pubKey.Curve]
- if !exists {
- return "", "", fmt.Errorf("unsupported curve: %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("unsupported hash algorithm for 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")
- }
-
- asn1sig, err := signer.Sign(rand.Reader, digest, nil)
- if err != nil {
- return "", "", fmt.Errorf("signing failed: %v", err)
- }
-
- var sig ecSignature
- if _, err := asn1.Unmarshal(asn1sig, &sig); err != nil {
- return "", "", fmt.Errorf("ASN.1 unmarshal failed: %v", err)
- }
-
- 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
-}
-
-// 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("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, hash, crypto.Hash(0))
- if err != nil {
- return "", "", fmt.Errorf("Ed25519 signing failed: %v", err)
- }
-
- combined := append(pubKey, signature...)
- return hex.EncodeToString(combined), AlgorithmED25519, nil
-}
-
-// verifyData verifies a signed message
-func (g *GUI) verifyData(data []byte) error {
- s := string(data)
-
- var algorithm string
- var beg, end string
-
- // Find the signature block and determine algorithm
- for algo := range supportedAlgorithms {
- begTestCRLF := fmt.Sprintf("\r\n-----BEGIN %s SIGNATURE-----\r\n", algo)
- endTestCRLF := fmt.Sprintf("-----END %s SIGNATURE-----\r\n", algo)
- begTestLF := fmt.Sprintf("\n-----BEGIN %s SIGNATURE-----\n", algo)
- endTestLF := fmt.Sprintf("-----END %s SIGNATURE-----\n", algo)
-
- if strings.Contains(s, begTestCRLF) && strings.Contains(s, endTestCRLF) {
- algorithm = algo
- beg = begTestCRLF
- end = endTestCRLF
- break
- } else if strings.Contains(s, begTestLF) && strings.Contains(s, endTestLF) {
- algorithm = algo
- beg = begTestLF
- end = endTestLF
- 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 and signature
- 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 {
- return fmt.Errorf("hex decode failed: %v", err)
- }
-
- // Normalize line endings before verification
- normalizedMessage := normalizeToRFCCompliantCRLFSimple(originalMessage)
-
- // Display status for large documents
- if len(normalizedMessage) > 1024*1024 {
- g.statusLabel.SetText("Verifying large document (" + formatByteSize(len(normalizedMessage)) + ")...")
- g.window.Canvas().Refresh(g.statusLabel)
- }
-
- // Verify based on algorithm type
- switch algorithm {
- case AlgorithmED25519:
- // Ed25519 for text signature
- return g.verifyEd25519(normalizedMessage, combined)
- case AlgorithmECCP256, AlgorithmECCP384:
- return g.verifyECDSA(normalizedMessage, combined, algorithm)
- default:
- return fmt.Errorf("unsupported algorithm: %s", algorithm)
- }
-}
-
-// verifyEd25519 verifies an Ed25519 signature
-func (g *GUI) verifyEd25519(data, combined []byte) error {
- if len(combined) != Ed25519CombinedSize {
- return fmt.Errorf("invalid Ed25519 signature block")
- }
-
- publicKey := combined[:Ed25519PublicKeySize]
- signature := combined[Ed25519PublicKeySize:]
-
- // Ed25519 requires the hash of the data for verification
- hash := sha256.Sum256(data)
-
- if !ed25519.Verify(ed25519.PublicKey(publicKey), hash[:], signature) {
- return fmt.Errorf("Ed25519 signature verification failed")
- }
-
- return nil
-}
-
-// verifyECDSA verifies an ECDSA signature
-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("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: expected %d, got %d", expectedBytes, len(combined))
- }
-
- // Extract public key components and signature values
- 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:])
-
- pub := &ecdsa.PublicKey{Curve: curve, X: x, Y: y}
-
- // Hash the data for verification
- 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)
- }
-
- // Verify the signature
- if !ecdsa.Verify(pub, digest, r, sVal) {
- return fmt.Errorf("signature verification failed")
- }
-
- return nil
-}
-
-// 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("failed to load public key: %v", err)
- }
-
- // Generate random AES key
- aesKeyGuard := memguard.NewBuffer(32)
- defer aesKeyGuard.Destroy()
- if _, err := rand.Read(aesKeyGuard.Bytes()); err != nil {
- return "", fmt.Errorf("failed to generate AES key: %v", err)
- }
-
- // Encrypt AES key with RSA
- 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
- 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 private key
-func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
- pinGuard := memguard.NewBufferFromBytes([]byte(pin))
- defer pinGuard.Destroy()
-
- // Clean up base64 input
- 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()
-
- // Get certificate from key management slot
- 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")
- }
-
- // Split encrypted key and data
- 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")
- }
-
- // Decrypt AES key with RSA
- 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")
- }
-
- // Decrypt data with AES
- decryptedData, err := decryptAES(encryptedData, decryptedPayload)
- if err != nil {
- return nil, fmt.Errorf("AES decryption failed: %v", err)
- }
-
- return decryptedData, nil
-}
-
-// normalizeToRFCCompliantCRLFSimple converts all line endings to CRLF
-func normalizeToRFCCompliantCRLFSimple(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)
-}
-
-// formatSignature formats hex 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")
- }
- return result.String()
-}
-
-// formatBase64 formats base64 string with 76 characters per line and CRLF
-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()
-}
-
-// 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("cannot unpad empty data")
- }
-
- if len(data)%4096 != 0 {
- return nil, errors.New("invalid block size for unpadding")
- }
-
- 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("invalid padding format: unexpected non-zero byte")
- }
- }
-
- if lastIndex == -1 {
- return nil, errors.New("no padding marker found")
- }
-
- return data[:lastIndex], nil
-}
-
-func (g *GUI) onPad() {
- input := g.textArea.Text
- if input == "" {
- g.statusLabel.SetText("Error: No text to pad")
- return
- }
-
- paddedData := securePadMessage([]byte(input))
- base64String := base64.StdEncoding.EncodeToString(paddedData)
- formattedBase64 := formatBase64(base64String)
-
- g.textArea.SetText(formattedBase64)
-
- originalLen := len(input)
- paddedLen := len(paddedData)
- g.statusLabel.SetText(fmt.Sprintf("✓ Padded: %d -> %d bytes (Base64)", originalLen, paddedLen))
-}
-
-func (g *GUI) onUnpad() {
- input := g.textArea.Text
- if input == "" {
- g.statusLabel.SetText("Error: No text to unpad")
- return
- }
-
- binaryData, err := base64.StdEncoding.DecodeString(input)
- if err != nil {
- g.statusLabel.SetText("Unpadding failed: Invalid Base64 data")
- return
- }
-
- unpaddedData, err := secureUnpadMessage(binaryData)
- if err != nil {
- g.statusLabel.SetText("Unpadding failed: " + err.Error())
- return
- }
-
- g.textArea.SetText(string(unpaddedData))
- g.statusLabel.SetText("✓ Message unpadded successfully")
-}
-
-// checkRSASecurity validates RSA key size
-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)
- }
-
- 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 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("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")
- }
- 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")
- }
- 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)
- }
- 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-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("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)
-}
\ No newline at end of file |
