From acd8b55344693576511366ee95a66fb1a10166cc Mon Sep 17 00:00:00 2001 From: Stefan Claas Date: Tue, 2 Sep 2025 21:15:58 +0200 Subject: Add files via upload --- README.md | 45 +++ go.mod | 46 +++ go.sum | 88 ++++++ img/1.png | Bin 0 -> 76435 bytes yubicrypt.go | 996 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yubicrypt.png | Bin 0 -> 2127 bytes 6 files changed, 1175 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 img/1.png create mode 100644 yubicrypt.go create mode 100644 yubicrypt.png 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 Binary files /dev/null and b/img/1.png 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 Binary files /dev/null and b/yubicrypt.png differ -- cgit v1.2.3