summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefan Claas <sacenator@gmail.com>2025-09-04 21:42:55 +0200
committerGitHub <noreply@github.com>2025-09-04 21:42:55 +0200
commit4045fba4466d5b351ed64458aab30f0b08ac774b (patch)
treefb8de772890f1d5d33a4b3e9f7ed5032942754f9
parenta75e4f18a8171e3be26d357ce34887d277a5b1d2 (diff)
downloadyubicrpt-cli-4045fba4466d5b351ed64458aab30f0b08ac774b.tar.gz
yubicrpt-cli-4045fba4466d5b351ed64458aab30f0b08ac774b.tar.xz
yubicrpt-cli-4045fba4466d5b351ed64458aab30f0b08ac774b.zip
Add files via upload
-rw-r--r--yubicrypt.go605
1 files changed, 278 insertions, 327 deletions
diff --git a/yubicrypt.go b/yubicrypt.go
index 7f1a54c..1a87a96 100644
--- a/yubicrypt.go
+++ b/yubicrypt.go
@@ -21,6 +21,7 @@ import (
"math/big"
"os"
"path/filepath"
+ "regexp"
"strings"
"fyne.io/fyne/v2"
@@ -51,18 +52,19 @@ var supportedAlgorithms = map[string]bool{
AlgorithmED25519: true,
}
-// Curve to algorithm mapping
+// Mapping from elliptic curve to algorithm name
var curveToAlgorithm = map[elliptic.Curve]string{
- elliptic.P256(): AlgorithmECCP256,
- elliptic.P384(): AlgorithmECCP384,
+ 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,
}
-// For Ed25519 - fixed sizes
+// Ed25519 constants
const (
Ed25519SignatureSize = 64
Ed25519PublicKeySize = 32
@@ -70,7 +72,7 @@ const (
)
const (
- minRSABits = 2048 // Minimum RSA key size accepted
+ minRSABits = 2048 // Minimum accepted RSA key size
)
// Supported RSA key sizes
@@ -80,7 +82,7 @@ var supportedRSASizes = map[int]string{
4096: "RSA4096",
}
-// GUI Structure
+// GUI structure
type GUI struct {
app fyne.App
window fyne.Window
@@ -90,15 +92,15 @@ type GUI struct {
statusLabel *widget.Label
publicKeyPath string
currentTheme string
- encryptionUsed bool // Flag to track if encryption was already used in this session
+ 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",
+ app: app.NewWithID("oc2mx.net.yubicrypt"),
+ currentTheme: "dark",
encryptionUsed: false,
}
@@ -112,6 +114,7 @@ func main() {
gui.window.ShowAndRun()
}
+// createUI initializes all UI components
func (g *GUI) createUI() {
monospace := &fyne.TextStyle{Monospace: true}
@@ -132,24 +135,24 @@ func (g *GUI) createUI() {
g.statusLabel = widget.NewLabel("Ready")
g.statusLabel.Wrapping = fyne.TextWrapWord
- // Theme Toggle Button
+ // Theme toggle button
g.themeToggle = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), g.toggleTheme)
}
+// createMainUI builds the main layout
func (g *GUI) createMainUI() fyne.CanvasObject {
- // Operation Buttons
- signBtn := widget.NewButtonWithIcon("Sign", theme.ConfirmIcon(), g.onSign)
- verifyBtn := widget.NewButtonWithIcon("Verify", theme.VisibilityIcon(), g.onVerify)
+ 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)
- // Button Container
buttonContainer := container.NewHBox(
layout.NewSpacer(),
- signBtn,
- verifyBtn,
+ signTextBtn,
+ verifyTextBtn,
padBtn,
unpadBtn,
encryptBtn,
@@ -190,6 +193,7 @@ func (g *GUI) createMainUI() fyne.CanvasObject {
return mainContainer
}
+// toggleTheme switches between light and dark theme
func (g *GUI) toggleTheme() {
if g.currentTheme == "dark" {
g.app.Settings().SetTheme(theme.LightTheme())
@@ -202,6 +206,7 @@ func (g *GUI) toggleTheme() {
}
}
+// applyTheme sets the initial theme
func (g *GUI) applyTheme() {
if g.currentTheme == "dark" {
g.app.Settings().SetTheme(theme.DarkTheme())
@@ -210,7 +215,8 @@ func (g *GUI) applyTheme() {
}
}
-func (g *GUI) onSign() {
+// 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
@@ -222,6 +228,16 @@ func (g *GUI) onSign() {
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())
@@ -229,10 +245,11 @@ func (g *GUI) onSign() {
}
g.textArea.SetText(result)
- g.statusLabel.SetText("✓ Message signed successfully")
+ g.statusLabel.SetText("✓ Message signed successfully (" + formatByteSize(len(input)) + ")")
}
-func (g *GUI) onVerify() {
+// 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")
@@ -248,16 +265,15 @@ func (g *GUI) onVerify() {
g.statusLabel.SetText("✓ Signature is valid")
}
+// onEncrypt triggers encryption using a public key
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.publicKeyPath = ""
g.choosePublicKey()
return
}
- // If certificate already selected, encrypt immediately
if g.publicKeyPath != "" {
input := g.textArea.Text
if input == "" {
@@ -277,10 +293,10 @@ func (g *GUI) onEncrypt() {
return
}
- // No certificate selected yet, open file dialog
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 {
@@ -288,7 +304,7 @@ func (g *GUI) choosePublicKey() {
return
}
if reader == nil {
- return // Dialog cancelled
+ return
}
defer reader.Close()
@@ -299,10 +315,9 @@ func (g *GUI) choosePublicKey() {
}
g.publicKeyPath = path
- g.encryptionUsed = false // Reset encryption flag when new certificate is selected
+ g.encryptionUsed = false
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")
@@ -311,17 +326,17 @@ func (g *GUI) choosePublicKey() {
result, err := g.encryptData([]byte(input), g.publicKeyPath)
if err != nil {
- g.statusLabel.SetText("Encryption failed: " + err.Error())
+ g.statusLabel.SetText("Encryption failed: %v" + 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)
}
+// onDecrypt triggers decryption using the YubiKey
func (g *GUI) onDecrypt() {
if g.pinEntry.Text == "" {
g.statusLabel.SetText("Error: PIN required for decryption")
@@ -344,10 +359,11 @@ func (g *GUI) onDecrypt() {
g.statusLabel.SetText("✓ Message decrypted successfully")
}
+// onClear resets the UI state
func (g *GUI) onClear() {
g.textArea.SetText("")
g.publicKeyPath = ""
- g.encryptionUsed = false // Reset encryption state on clear
+ g.encryptionUsed = false
clipboard := g.app.Clipboard()
if clipboard != nil {
@@ -357,23 +373,31 @@ func (g *GUI) onClear() {
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()
-
- // Use the simple normalization that preserves all line breaks
- normalizedData := normalizeToRFCCompliantCRLFSimple(data)
-
- sig, curveType, err := g.signDataInternal(pinGuard.Bytes(), normalizedData)
- if err != nil {
- return "", fmt.Errorf("signing failed: %v", err)
- }
-
- return string(normalizedData) + "\r\n-----BEGIN " + curveType + " SIGNATURE-----\r\n" +
- formatSignature(sig) + "-----END " + curveType + " SIGNATURE-----\r\n", nil
+ 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 with the YubiKey
+// 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 {
@@ -386,23 +410,41 @@ func (g *GUI) signDataInternal(pin, data []byte) (string, string, error) {
return "", "", fmt.Errorf("failed to get certificate from signature slot: %v", err)
}
- // Check for Ed25519 support
+ // Handle Ed25519 signing
if ed25519PubKey, ok := cert.PublicKey.(ed25519.PublicKey); ok {
- return g.signDataEd25519(pin, data, ed25519PubKey, yk)
+ // Ed25519 signs the hash of the data, not the raw data
+ hash := sha256.Sum256(data)
+ return g.signEd25519Data(string(pin), hash[:], ed25519PubKey, yk)
}
- // ECDSA support
+ // Handle ECDSA signing
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)
}
+ 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 {
@@ -414,24 +456,9 @@ func (g *GUI) signDataInternal(pin, data []byte) (string, string, error) {
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)
+ asn1sig, err := signer.Sign(rand.Reader, digest, nil)
if err != nil {
- return "", "", fmt.Errorf("ECDSA signing failed: %v", err)
+ return "", "", fmt.Errorf("signing failed: %v", err)
}
var sig ecSignature
@@ -439,7 +466,6 @@ func (g *GUI) signDataInternal(pin, data []byte) (string, string, error) {
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 {
@@ -457,9 +483,9 @@ func (g *GUI) signDataInternal(pin, data []byte) (string, string, error) {
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)}
+// 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)
@@ -470,175 +496,175 @@ func (g *GUI) signDataEd25519(pin, data []byte, pubKey ed25519.PublicKey, yk *pi
return "", "", fmt.Errorf("key does not implement crypto.Signer")
}
- signature, err := signer.Sign(rand.Reader, data, crypto.Hash(0))
+ signature, err := signer.Sign(rand.Reader, hash, crypto.Hash(0))
if err != nil {
return "", "", fmt.Errorf("Ed25519 signing failed: %v", err)
}
- combined := append(signature, pubKey...)
+ combined := append(pubKey, signature...)
return hex.EncodeToString(combined), AlgorithmED25519, nil
}
-// verifyData handles verification with proper message normalization
+// verifyData verifies a signed message
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 := normalizeToRFCCompliantCRLFSimple(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)
- }
+ 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 Ed25519 signatures
+// verifyEd25519 verifies an Ed25519 signature
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:]
+ publicKey := combined[:Ed25519PublicKeySize]
+ signature := combined[Ed25519PublicKeySize:]
+
+ // Ed25519 requires the hash of the data for verification
+ hash := sha256.Sum256(data)
- if !ed25519.Verify(publicKey, data, signature) {
+ if !ed25519.Verify(ed25519.PublicKey(publicKey), hash[:], signature) {
return fmt.Errorf("Ed25519 signature verification failed")
}
return nil
}
-// verifyECDSA verifies ECDSA signatures
+// 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")
+ 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:curveSize*4])
+ 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}
- hashFunc := curveToHash[curve]
+ // Hash the data for verification
var digest []byte
-
switch hashFunc {
case crypto.SHA256:
- h := sha256.Sum256(data)
- digest = h[:]
+ h := sha256.New()
+ h.Write(data)
+ digest = h.Sum(nil)
case crypto.SHA384:
- h := sha512.Sum384(data)
- digest = h[:]
- default:
- return fmt.Errorf("unsupported hash algorithm")
+ h := sha512.New384()
+ h.Write(data)
+ digest = h.Sum(nil)
}
+ // Verify the signature
if !ecdsa.Verify(pub, digest, r, sVal) {
- return fmt.Errorf("signature is not valid")
+ return fmt.Errorf("signature verification failed")
}
return nil
}
-// encryptData encrypts data using RSA public key and AES encryption
+// 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 AES key - will be automatically cleaned up by memguard
+ // Generate random AES key
aesKeyGuard := memguard.NewBuffer(32)
- defer aesKeyGuard.Destroy() // Secure cleanup after use
+ 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 public key
+ // 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 key
+ // Encrypt data with AES
encryptedData, err := encryptAES(data, aesKeyGuard.Bytes())
if err != nil {
return "", fmt.Errorf("AES encryption failed: %v", err)
@@ -653,11 +679,12 @@ func (g *GUI) encryptData(data []byte, pubKeyFile string) (string, error) {
return formatBase64(base64Str), nil
}
-// decryptData decrypts data using YubiKey's RSA private key
+// 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, " ", "")
@@ -674,6 +701,7 @@ func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
}
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)
@@ -693,6 +721,7 @@ func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
return nil, fmt.Errorf("ciphertext too short")
}
+ // Split encrypted key and data
encryptedKey := combined[:keySize]
encryptedData := combined[keySize:]
defer memguard.WipeBytes(encryptedKey)
@@ -708,6 +737,7 @@ func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
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)
@@ -718,6 +748,7 @@ func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
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)
@@ -726,72 +757,30 @@ func (g *GUI) decryptData(data []byte, pin string) ([]byte, error) {
return decryptedData, nil
}
-// Alternative: Simple normalization that preserves all line breaks
+// normalizeToRFCCompliantCRLFSimple converts all line endings to CRLF
func normalizeToRFCCompliantCRLFSimple(data []byte) []byte {
- s := string(data)
-
- // Convert all line endings to CRLF while preserving structure
- s = strings.ReplaceAll(s, "\r\n", "\n") // First normalize to LF
- s = strings.ReplaceAll(s, "\r", "\n") // Handle old Mac format
- s = strings.ReplaceAll(s, "\n", "\r\n") // Convert all to CRLF
-
- return []byte(s)
-}
-
-// 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 normalizeToRFCCompliantCRLFSimple(data)
- }
-
- // Extract message part (before signature)
- messagePart := s[:signatureStart]
-
- // Normalize only the message part
- normalizedMessage := normalizeToRFCCompliantCRLFSimple([]byte(messagePart))
-
- // Keep signature block unchanged
- signatureBlock := s[signatureStart:signatureEnd]
-
- // Reconstruct with normalized message and original signature
- return append(normalizedMessage, []byte(signatureBlock)...)
+ 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 creates RFC-compliant signature with 64 characters per line and CRLF
+// 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") // RFC-compliant CRLF
- }
- return result.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
+// 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 {
@@ -805,94 +794,60 @@ func formatBase64(data string) string {
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")
+// formatByteSize formats bytes into human-readable format
+func formatByteSize(bytes int) string {
+ const unit = 1024
+ if bytes < unit {
+ return fmt.Sprintf("%d B", bytes)
}
-
- // 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)
+ 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])
+}
- randomHex := hex.EncodeToString(randomBytes)
- if len(randomHex) > paddingNeeded {
- randomHex = randomHex[:paddingNeeded]
+// 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
}
- // 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
+ paddedData := make([]byte, len(data)+paddingNeeded)
+ copy(paddedData, data)
+ paddedData[len(data)] = 0x80
+ return paddedData
}
-// secureUnpadMessage removes cryptographic padding from the message
-func secureUnpadMessage(data string) ([]byte, error) {
- if data == "" {
- return []byte{}, nil
+// secureUnpadMessage removes padding added by securePadMessage
+func secureUnpadMessage(data []byte) ([]byte, error) {
+ if len(data) == 0 {
+ return nil, errors.New("cannot unpad empty data")
}
- // 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
+ if len(data)%4096 != 0 {
+ return nil, errors.New("invalid block size for unpadding")
}
- // 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)
+ 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")
}
- result.WriteString(cleanText[i:end])
- result.WriteString("\r\n")
}
- return result.String()
+
+ if lastIndex == -1 {
+ return nil, errors.New("no padding marker found")
+ }
+
+ return data[:lastIndex], nil
}
func (g *GUI) onPad() {
@@ -902,20 +857,15 @@ func (g *GUI) onPad() {
return
}
- result, err := securePadMessage([]byte(input))
- if err != nil {
- g.statusLabel.SetText("Padding failed: %v")
- return
- }
+ paddedData := securePadMessage([]byte(input))
+ base64String := base64.StdEncoding.EncodeToString(paddedData)
+ formattedBase64 := formatBase64(base64String)
- g.textArea.SetText(result)
+ g.textArea.SetText(formattedBase64)
- // 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))
+ paddedLen := len(paddedData)
+ g.statusLabel.SetText(fmt.Sprintf("✓ Padded: %d -> %d bytes (Base64)", originalLen, paddedLen))
}
func (g *GUI) onUnpad() {
@@ -925,17 +875,23 @@ func (g *GUI) onUnpad() {
return
}
- result, err := secureUnpadMessage(input)
+ binaryData, err := base64.StdEncoding.DecodeString(input)
if err != nil {
- g.statusLabel.SetText("Unpadding failed: %v")
+ g.statusLabel.SetText("Unpadding failed: Invalid Base64 data")
return
}
- g.textArea.SetText(string(result))
+ 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 checks RSA key length and issues warnings/errors
+// checkRSASecurity validates RSA key size
func checkRSASecurity(pubKey *rsa.PublicKey, context string) error {
keySize := pubKey.N.BitLen()
@@ -943,10 +899,8 @@ func checkRSASecurity(pubKey *rsa.PublicKey, context string) error {
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)
+ fmt.Fprintf(os.Stderr, "WARNING: %d-bit RSA key %s - supported sizes are 2048, 3072, 4096 bits\n", keySize, context)
}
if keySize == 1024 {
@@ -956,7 +910,7 @@ func checkRSASecurity(pubKey *rsa.PublicKey, context string) error {
return nil
}
-// loadRSAPublicKey loads an RSA public key from a PEM file
+// loadRSAPublicKey loads RSA public key from PEM file
func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) {
data, err := os.ReadFile(filename)
if err != nil {
@@ -979,7 +933,6 @@ func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) {
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
}
@@ -994,7 +947,6 @@ func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) {
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
}
@@ -1005,7 +957,6 @@ func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) {
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
}
@@ -1016,7 +967,7 @@ func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) {
}
}
-// encryptAES encrypts data using AES-GCM
+// encryptAES encrypts data using AES-256-GCM
func encryptAES(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
@@ -1037,7 +988,7 @@ func encryptAES(data, key []byte) ([]byte, error) {
return ciphertext, nil
}
-// decryptAES decrypts data using AES-GCM
+// decryptAES decrypts data using AES-256-GCM
func decryptAES(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
@@ -1083,4 +1034,4 @@ func openYubiKey(index int) (*piv.YubiKey, error) {
}
}
return nil, fmt.Errorf("no YubiKey found at index %d", index)
-}
+} \ No newline at end of file