A crypto/crypto.go => crypto/crypto.go +99 -0
@@ 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
+}
A crypto/crypto_test.go => crypto/crypto_test.go +85 -0
@@ 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)
+}
D crypto/webhook.go => crypto/webhook.go +0 -51
@@ 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
-}
M go.mod => go.mod +2 -0
@@ 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
)
M go.sum => go.sum +12 -0
@@ 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=