summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefan Claas <sacenator@gmail.com>2025-09-02 21:15:58 +0200
committerGitHub <noreply@github.com>2025-09-02 21:15:58 +0200
commitacd8b55344693576511366ee95a66fb1a10166cc (patch)
treeac14bba5b3537bcb591743f78a72918ef70c304d
parentd83f545f2f6ed2dc270103447db8e1f1b7a00d71 (diff)
downloadyubicrpt-cli-acd8b55344693576511366ee95a66fb1a10166cc.tar.gz
yubicrpt-cli-acd8b55344693576511366ee95a66fb1a10166cc.tar.xz
yubicrpt-cli-acd8b55344693576511366ee95a66fb1a10166cc.zip
Add files via upload
-rw-r--r--README.md45
-rw-r--r--go.mod46
-rw-r--r--go.sum88
-rw-r--r--img/1.pngbin0 -> 76435 bytes
-rw-r--r--yubicrypt.go996
-rw-r--r--yubicrypt.pngbin0 -> 2127 bytes
6 files changed, 1175 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3387b06
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# yubicrypt
+
+An easy to use public key encryption program for your YubiKey.
+
+Supported YubiKey algorithms:
+
+For encryption with slot 9d:
+
+RSA: 2048, 3072, 4096 bits
+
+For signing with slot 9c:
+
+ECC: P-256, P-384, Ed25519
+
+Simply use Yubico Authenticator to create your signing and
+encryption certificate and export your encryption certificate,
+to give it to your friends. You don't have to Export your signing
+certificate, because it is not needed for signature verification.
+
+If you use Linux (Debian/Ubuntu) you may have to do the following
+in order that yubicrypt works.
+
+Install PC/SC daemon and tools
+sudo apt update
+sudo apt install pcscd pcsc-tools
+
+Start the PC/SC daemon
+sudo systemctl start pcscd
+
+Enable it to start automatically on boot
+sudo systemctl enable pcscd
+
+Check the status to ensure it's running
+sudo systemctl status pcscd
+
+![yubicrypt](img/1.png)
+
+If you like yubicrypt, as much as I do, consider a small donation.
+```
+BTC: 129yB8kL8mQVZufNS4huajsdJPa48aHwHz
+Nym: n1yql04xjhmlhfkjsk8x8g7fynm27xzvnk23wfys
+XMR: 45TJx8ZHngM4GuNfYxRw7R7vRyFgfMVp862JqycMrPmyfTfJAYcQGEzT27wL1z5RG1b5XfRPJk97KeZr1svK8qES2z1uZrS
+```
+yubicrypt is dedicated to Alice and Bob.
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..6bdb82f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,46 @@
+module yubicrypt
+
+go 1.25.0
+
+require (
+ fyne.io/fyne/v2 v2.6.3
+ github.com/awnumar/memguard v0.23.0
+ github.com/go-piv/piv-go/v2 v2.4.0
+)
+
+require (
+ fyne.io/systray v1.11.0 // indirect
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/awnumar/memcall v0.4.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/fredbi/uri v1.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fyne-io/gl-js v0.2.0 // indirect
+ github.com/fyne-io/glfw-js v0.3.0 // indirect
+ github.com/fyne-io/image v0.1.1 // indirect
+ github.com/fyne-io/oksvg v0.1.0 // indirect
+ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
+ github.com/go-text/render v0.2.0 // indirect
+ github.com/go-text/typesetting v0.2.1 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/hack-pad/go-indexeddb v0.3.2 // indirect
+ github.com/hack-pad/safejs v0.1.0 // indirect
+ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
+ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rymdport/portal v0.4.1 // indirect
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
+ golang.org/x/image v0.24.0 // indirect
+ golang.org/x/net v0.42.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..417de17
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,88 @@
+fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
+fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
+fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
+fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
+github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
+github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
+github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
+github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
+github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
+github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
+github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
+github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
+github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
+github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
+github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-piv/piv-go/v2 v2.4.0 h1:xamQ/fR4MJiw/Ndbk6yi7MVwhjrwlnDAPuaH9zcGb+I=
+github.com/go-piv/piv-go/v2 v2.4.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=
+github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
+github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
+github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
+github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
+github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
+github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
+github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
+github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
+github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/img/1.png b/img/1.png
new file mode 100644
index 0000000..c1d7e30
--- /dev/null
+++ b/img/1.png
Binary files differ
diff --git a/yubicrypt.go b/yubicrypt.go
new file mode 100644
index 0000000..b187ad0
--- /dev/null
+++ b/yubicrypt.go
@@ -0,0 +1,996 @@
+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.NewHBox(
+ widget.NewLabel("PIN:"),
+ g.pinEntry,
+ clearBtn,
+ )
+
+ 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")
+}
+
+func (g *GUI) signData(data []byte, pin string) (string, error) {
+ pinGuard := memguard.NewBufferFromBytes([]byte(pin))
+ defer pinGuard.Destroy()
+
+ sig, curveType, err := g.signDataInternal(pinGuard.Bytes(), data)
+ if err != nil {
+ return "", fmt.Errorf("signing failed: %v", err)
+ }
+
+ return string(data) + "\r\n-----BEGIN " + curveType + " SIGNATURE-----\r\n" +
+ formatSignature(sig) + "-----END " + curveType + " SIGNATURE-----\r\n", nil
+}
+
+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
+}
+
+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
+}
+
+func (g *GUI) verifyData(data []byte) error {
+ s := string(data)
+
+ // Search for all supported algorithms
+ 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 == "" {
+ 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")
+ }
+
+ original := []byte(s[:i])
+ hexPart := s[i+len(beg):j]
+ hexPart = strings.ReplaceAll(hexPart, "\r\n", "")
+ hexPart = strings.ReplaceAll(hexPart, " ", "")
+
+ combined, err := hex.DecodeString(hexPart)
+ if err != nil {
+ return fmt.Errorf("hex decode failed: %v", err)
+ }
+
+ switch algorithm {
+ case AlgorithmED25519:
+ return g.verifyEd25519(original, combined)
+ case AlgorithmECCP256, AlgorithmECCP384:
+ return g.verifyECDSA(original, combined, algorithm)
+ default:
+ return fmt.Errorf("unsupported algorithm: %s", algorithm)
+ }
+}
+
+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
+}
+
+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
+}
+
+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
+}
+
+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
+}
+
+func normalizeCRLF(data []byte) []byte {
+ s := string(data)
+ s = strings.ReplaceAll(s, "\r\n", "\n")
+ s = strings.ReplaceAll(s, "\r", "\n")
+ return []byte(strings.ReplaceAll(s, "\n", "\r\n"))
+}
+
+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()
+}
+
+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()
+}
+
+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
+}
+
+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
+}
+
+// Helper function to format only the padding part 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: " + err.Error())
+ 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: " + err.Error())
+ 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
+}
+
+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)
+ }
+}
+
+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
+}
+
+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
+}
+
+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)
+}
diff --git a/yubicrypt.png b/yubicrypt.png
new file mode 100644
index 0000000..beb9aad
--- /dev/null
+++ b/yubicrypt.png
Binary files differ