From a9e49d17a1e63903135b9b3c9a9a69b24451226e Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 5 Oct 2020 13:32:46 -0400 Subject: [PATCH] Overhaul crypto module Part 1 of merging gql.sr.ht with core-go --- crypto/crypto.go | 99 +++++++++++++++++++++++++++++++++++++++++++ crypto/crypto_test.go | 85 +++++++++++++++++++++++++++++++++++++ crypto/webhook.go | 51 ---------------------- go.mod | 2 + go.sum | 12 ++++++ 5 files changed, 198 insertions(+), 51 deletions(-) create mode 100644 crypto/crypto.go create mode 100644 crypto/crypto_test.go delete mode 100644 crypto/webhook.go diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000000000000000000000000000000000000..7e28e7dcbb30f09d2c69748031d78564d3793a1f --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,99 @@ +package crypto + +import ( + "crypto/ed25519" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "log" + "time" + + "github.com/fernet/fernet-go" + "github.com/vaughan0/go-ini" +) + +var ( + privateKey ed25519.PrivateKey + publicKey ed25519.PublicKey + macKey []byte + fernetKey *fernet.Key +) + +func InitCrypto(config ini.File) { + b64key, ok := config.Get("webhooks", "private-key") + if !ok { + log.Fatalf("No webhook key configured") + } + seed, err := base64.StdEncoding.DecodeString(b64key) + if err != nil { + log.Fatalf("base64 decode webhooks private key: %v", err) + } + privateKey = ed25519.NewKeyFromSeed(seed) + publicKey, _ = privateKey.Public().(ed25519.PublicKey) + + b64fernet, ok := config.Get("sr.ht", "network-key") + if !ok { + log.Fatalf("No network key configured") + } + fernetKey, err = fernet.DecodeKey(b64fernet) + if err != nil { + log.Fatalf("Load Fernet network encryption key: %v", err) + } + mac := hmac.New(sha256.New, privateKey) + mac.Write([]byte("sr.ht HMAC key")) + macKey = mac.Sum(nil) +} + +func Sign(payload []byte) []byte { + return ed25519.Sign(privateKey, payload) +} + +func Verify(payload, signature []byte) bool { + return ed25519.Verify(publicKey, payload, signature) +} + +func Encrypt(payload []byte) []byte { + msg, err := fernet.EncryptAndSign(payload, fernetKey) + if err != nil { + log.Fatalf("Error encrypting payload: %v", err) + } + return msg +} + +func Decrypt(payload []byte) []byte { + return fernet.VerifyAndDecrypt(payload, + time.Duration(0), []*fernet.Key{fernetKey}) +} + +func DecryptWithExpiration(payload []byte, expiry time.Duration) []byte { + return fernet.VerifyAndDecrypt(payload, expiry, []*fernet.Key{fernetKey}) +} + +func HMAC(payload []byte) []byte { + mac := hmac.New(sha256.New, macKey) + mac.Write(payload) + return mac.Sum(nil) +} + +func HMACVerify(payload []byte, signature []byte) bool { + mac := hmac.New(sha256.New, macKey) + mac.Write(payload) + expected := mac.Sum(nil) + return hmac.Equal(expected, signature) +} + +func SignWebhook(payload []byte) (string, string) { + var nonceSeed [8]byte + _, err := rand.Read(nonceSeed[:]) + if err != nil { + panic(fmt.Errorf("Failed to generate nonce: %w", err)) + } + + nonce := hex.EncodeToString(nonceSeed[:]) + signature := base64.StdEncoding.EncodeToString( + Sign(append(payload, []byte(nonce)...))) + return nonce, signature +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 0000000000000000000000000000000000000000..81d872bf47fbf8216c5f280692d6b472af176dc0 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,85 @@ +package crypto + +import ( + "encoding/base64" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/vaughan0/go-ini" +) + +func init() { + config, err := ini.Load(strings.NewReader(` +[webhooks] +private-key=ebzsjPaN6E13ln/FeNWly1C92q6bVMVdOnDo1HPl5fc= + +[sr.ht] +network-key=tbuG-7Vh44vrDq1L_HKWkHnWrDOtJhEkPKPiauaLeuk=`)) + if err != nil { + panic(err) + } + InitCrypto(config) +} + +func TestSignWebhook(t *testing.T) { + payload := []byte("Hello world!") + nonce, signature := SignWebhook(payload) + + sig, err := base64.StdEncoding.DecodeString(signature) + assert.Nil(t, err) + valid := Verify(append(payload, []byte(nonce)...), sig) + assert.True(t, valid) +} + +func TestSign(t *testing.T) { + payload := []byte("Hello world!") + signature := Sign(payload) + + valid := Verify(payload, signature) + assert.True(t, valid) + + valid = Verify([]byte("Something else"), signature) + assert.False(t, valid) +} + +func TestEncrypt(t *testing.T) { + payload := []byte("Hello, world!") + + enc := Encrypt(payload) + assert.NotNil(t, enc) + assert.NotEqual(t, enc, []byte("Hello, world!")) + + dec := Decrypt(enc) + assert.NotNil(t, dec) + assert.Equal(t, dec, []byte("Hello, world!")) +} + +func TestEncryptWithExpire(t *testing.T) { + payload := []byte("Hello, world!") + + enc := Encrypt(payload) + assert.NotNil(t, enc) + assert.NotEqual(t, enc, []byte("Hello, world!")) + + dec := DecryptWithExpiration(enc, 30*time.Minute) + assert.NotNil(t, dec) + assert.Equal(t, dec, []byte("Hello, world!")) + + time.Sleep(time.Duration(1)) + + dec = DecryptWithExpiration(enc, time.Duration(2)) + assert.Nil(t, dec) +} + +func TestHMAC(t *testing.T) { + payload := []byte("Hello, world!") + mac := HMAC(payload) + + valid := HMACVerify(payload, mac) + assert.True(t, valid) + + valid = HMACVerify([]byte("Something else"), mac) + assert.False(t, valid) +} diff --git a/crypto/webhook.go b/crypto/webhook.go deleted file mode 100644 index c2f133dfb0e78666bd53c1b7c37b2b3e358d8c27..0000000000000000000000000000000000000000 --- a/crypto/webhook.go +++ /dev/null @@ -1,51 +0,0 @@ -package crypto - -import ( - "crypto/rand" - "encoding/base64" - "encoding/hex" - "log" - - "github.com/vaughan0/go-ini" - "golang.org/x/crypto/ed25519" -) - -var ( - webhookKey ed25519.PrivateKey -) - -func initWebhookKey(logger *log.Logger, config ini.File) { - if webhookKey != nil { - return - } - - b64key, ok := config.Get("webhooks", "private-key") - if !ok { - logger.Fatalf("No webhook key configured") - } - seed, err := base64.StdEncoding.DecodeString(b64key) - if err != nil { - logger.Fatalf("base64 decode webhooks private key: %v", err) - } - webhookKey = ed25519.NewKeyFromSeed(seed) -} - -func SignWebhookPayload(payload []byte, logger *log.Logger, config ini.File) (string, string) { - var ( - nonceSeed [8]byte - nonceHex [16]byte - ) - - _, err := rand.Read(nonceSeed[:]) - if err != nil { - logger.Fatalf("generate nonce: %v", err) - } - hex.Encode(nonceHex[:], nonceSeed[:]) - nonce := string(nonceHex[:]) - - initWebhookKey(logger, config) - signature := base64.StdEncoding.EncodeToString( - ed25519.Sign(webhookKey, append(payload, nonceHex[:]...))) - - return nonce, signature -} diff --git a/go.mod b/go.mod index 6f954976182772c12e589cd406e99e3668a996d3..57f8c8ba5095a04eb6e86a83a37702570cb5f56a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module git.sr.ht/~sircmpwn/core-go go 1.13 require ( + github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 + github.com/stretchr/testify v1.6.1 github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de ) diff --git a/go.sum b/go.sum index 62028af0e6da7327332ce95285708e3aa0ab4c8d..55ceb904c4b35a438570460875e860044061c395 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 h1:/UMxx5lGDg30aioUL9e7xJnbJfJeX7vhcm57fa5udaI= +github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c= +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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks= github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -7,3 +16,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=