all these changes were made about 7 months ago. forgot to commit. also forgot about this project
sophuwu sophie@sophuwu.com
Sun, 13 Jul 2025 02:01:09 +0200
8 files changed,
311 insertions(+),
16 deletions(-)
A
argon2id/argon2id.go
@@ -0,0 +1,145 @@
+package argon2id + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +var ( + // ErrInvalidHash is returned if the required parameters can not be obtained + // from the hash. + ErrInvalidHash = fmt.Errorf("argon2id: invalid hash format") + + // ErrVersionIncompatible is returned if the Argon2id version generating + // the hash does not match the version validating it. + ErrVersionIncompatible = fmt.Errorf("argon2id: incompatible version") + + // Prefix is set to be compatible with Dovecot. Can be set to an empty string. + Prefix = "{ARGON2ID}" +) + +// DefaultParams provides sane default parameters for password hashing as of +// 2021. Depending on your environment you will need to adjust these. +var DefaultParams = &Params{ + Memory: 512 * 1024, + Iterations: 3, + Parallelism: 1, // changed from 4 to 1 + SaltLen: 32, + KeyLen: 32, +} + +// Params contains the input parameters for the Argon2id algorithm. Memory and +// Iterations tweak the computational cost. If you have more cores available +// you can change the parallelism to reduce runtime without reducing cost. But +// note that this will change the hash. +// +// See https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4 +type Params struct { + Memory uint32 + Iterations uint32 + Parallelism uint8 + SaltLen uint32 + KeyLen uint32 +} + +// Generate generates a new Argon2ID hash with recommended values for it's +// complexity parameters. By default the generated hash is compatible with +// the Dovecot Password Scheme. +// +// See https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ +// +// It looks like this +// +// {ARGON2ID}$argon2id$v=19$m=524288,t=3,p=4$464unwkIcBGXjqWBZ0A5FWClURgYdWFqRlQaBJOE5fs$5ofdht4OkXsg/tftXGgxNchAdgHzpe+QJyizabiKZFk +func Generate(password string, saltLen uint32) (string, []byte, error) { + params := DefaultParams + if saltLen > 0 { + params.SaltLen = saltLen + } + + salt := make([]byte, params.SaltLen) + if _, err := rand.Read(salt); err != nil { + return "", nil, fmt.Errorf("failed to read rand: %w", err) + } + + hash := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen) + + b64hash := base64.RawStdEncoding.EncodeToString(hash) + b64salt := base64.RawStdEncoding.EncodeToString(salt) + + return fmt.Sprintf(Prefix+"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64salt, b64hash), hash, nil +} + +// Validate unpacks the parameters from the hash, computes the hash of the given +// password with these parameters and performs a constant time comparison between +// both hashes. +func Validate(password string, hash string) (bool, error) { + params, salt, key, err := unpackHash(hash) + if err != nil { + return false, err + } + + otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen) + + if subtle.ConstantTimeEq(int32(len(key)), int32(len(otherKey))) == 0 { + return false, nil + } + + if subtle.ConstantTimeCompare(key, otherKey) == 1 { + return true, nil + } + + return false, nil +} + +func unpackHash(hash string) (*Params, []byte, []byte, error) { + hash = strings.TrimPrefix(hash, Prefix) + + p := strings.Split(hash, "$") + if len(p) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + if p[1] != "argon2id" { + return nil, nil, nil, ErrInvalidHash + } + + var version int + + _, err := fmt.Sscanf(p[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to scan version %s: %w", p[2], err) + } + + if version != argon2.Version { + return nil, nil, nil, ErrVersionIncompatible + } + + params := &Params{} + + _, err = fmt.Sscanf(p[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to scan header %s: %w", p[3], err) + } + + salt, err := base64.RawStdEncoding.DecodeString(p[4]) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode salt %s: %w", p[4], err) + } + + params.SaltLen = uint32(len(salt)) + + key, err := base64.RawStdEncoding.DecodeString(p[5]) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode hash %s: %w", p[5], err) + } + + params.KeyLen = uint32(len(key)) + + return params, salt, key, nil +}
A
argon2id/wrap.go
@@ -0,0 +1,57 @@
+package argon2id + +import ( + "crypto/aes" + "crypto/cipher" +) + +func Encrypt(plaintext string, secret []byte) string { + aes, err := aes.NewCipher(secret) + if err != nil { + panic(err) + } + + gcm, err := cipher.NewGCM(aes) + if err != nil { + panic(err) + } + + // We need a 12-byte nonce for GCM (modifiable if you use cipher.NewGCMWithNonceSize()) + // A nonce should always be randomly generated for every encryption. + nonce := make([]byte, gcm.NonceSize()) + _, err = rand.Read(nonce) + if err != nil { + panic(err) + } + + // ciphertext here is actually nonce+ciphertext + // So that when we decrypt, just knowing the nonce size + // is enough to separate it from the ciphertext. + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + + return string(ciphertext) +} + +func Decrypt(ciphertext string, secret []byte) string { + aes, err := aes.NewCipher(secret) + if err != nil { + panic(err) + } + + gcm, err := cipher.NewGCM(aes) + if err != nil { + panic(err) + } + + // Since we know the ciphertext is actually nonce+ciphertext + // And len(nonce) == NonceSize(). We can separate the two. + nonceSize := gcm.NonceSize() + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + + plaintext, err := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil) + if err != nil { + panic(err) + } + + return string(plaintext) +}
M
cmd/cli.go
→
cmd/cli.go
@@ -4,6 +4,7 @@ import (
"fmt" "os" "sophuwu.site/seks" + "sophuwu.site/seks/keyring" "strings" )@@ -84,7 +85,7 @@ }
func main() { if len(os.Args) == 2 && (os.Args[1] == "g" || os.Args[1] == "keygen") { - pub, priv := seks.KeyGen() + pub, priv := keyring.KeyGen() fmt.Println(pub) fmt.Println(priv) return
M
example/keygen.go
→
example/keygen.go
@@ -1,9 +1,9 @@
package main -import "sophuwu.site/seks" +import "sophuwu.site/seks/keyring" func main() { - pub, priv := seks.KeyGen() + pub, priv := keyring.KeyGen() println(pub) println(priv) -}+}
M
go.mod
→
go.mod
@@ -2,6 +2,12 @@ module sophuwu.site/seks
go 1.21.3 -require golang.org/x/crypto v0.14.0 +require ( + github.com/asdine/storm/v3 v3.2.1 + golang.org/x/crypto v0.14.0 +) -require golang.org/x/sys v0.13.0 // indirect +require ( + go.etcd.io/bbolt v1.3.4 // indirect + golang.org/x/sys v0.13.0 // indirect +)
M
go.sum
→
go.sum
@@ -1,4 +1,41 @@
+github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= +github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= +github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= +github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= +github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= +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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
A
keyring/keyring.go
@@ -0,0 +1,59 @@
+package keyring + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "github.com/asdine/storm/v3" + "golang.org/x/crypto/nacl/box" + "strings" +) + +const KEYHEADER = "----BEGIN SEKS PUBLIC KEY BLOCK-----" +const KEYFOOTER = "-----END SEKS PUBLIC KEY BLOCK-----" +const PRIVHEAD = "----BEGIN SEKS PRIVATE KEY BLOCK-----" +const PRIVFOOT = "-----END SEKS PRIVATE KEY BLOCK-----" + +const KeyVersion = "SEKS1.0" + +type PubKey struct { + Name string `storm:"index"` + Email string `storm:"index"` + PubKey [32]byte `storm:"unique"` + KeyID string `storm:"id"` + Tags map[string]string `storm:"index"` + Version string `storm:"index"` +} + +func (p *PubKey) Export() (string, error) { + s := KEYHEADER + "\n" + s += "Version: " + p.Version + "\n" + s += "Name: " + p.Name + if p.Email != "" { + s += " <" + p.Email + ">" + } + s += "\n" + jsn, err := json.Marshal(p) + if err != nil { + return "", err + } + return s + "\n" + hex.EncodeToString(p.PubKey[:]) + "\n" + string(jsn) + "\n" + KEYFOOTER, nil +} + +type PrivKey struct { + PubID string `storm:"index"` + Encrypted string `storm:"index"` +} + +func KeyGen() (string, string) { + pub, priv, err := box.GenerateKey(rand.Reader) + if err != nil { + return "", "" + } + + pubs := "public-key-" + hex.EncodeToString(pub[:]) + privs := "SECRET-KEY-" + strings.ToUpper(hex.EncodeToString(priv[:])) + return pubs, privs +} + +var DB *storm.DB
M
seks.go
→
seks.go
@@ -9,16 +9,6 @@ "golang.org/x/crypto/nacl/box"
"strings" ) -func KeyGen() (string, string) { - pub, priv, err := box.GenerateKey(rand.Reader) - if err != nil { - return "", "" - } - pubs := "public-key-" + hex.EncodeToString(pub[:]) - privs := "SECRET-KEY-" + strings.ToUpper(hex.EncodeToString(priv[:])) - return pubs, privs -} - func ReadKey(s string) ([32]byte, error) { s = strings.ToLower(s) s = strings.TrimPrefix(s, "public-key-")