From febe45818c9388414c937529dee6a05a5a0900d8 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 1 Jul 2022 15:48:49 -0400 Subject: [PATCH] WIP: Crypto stuff --- crypto/crypto.go | 90 +++++++++++++++++++++++++++++++++++++++++++ crypto/crypto_test.go | 43 +++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 crypto/crypto.go create mode 100644 crypto/crypto_test.go diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 00000000..f86f6219 --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,90 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" +) + +const ( + versionByte = 0x31 // "1" + gcmTagSize = 16 + gcmNonceSize = 12 +) + +var ( + errCiphertextTooShort = errors.New("ciphertext too short") + errCiphertextUnexpectedVersion = errors.New("unsupported ciphertext version") +) + +// Encrypt encrypts the given plaintext with the given key using AES-GCM, +// and encodes the (version, tag, nonce, ciphertext) set as base64. +// +// The output format is (|| means concatenate): +// "1" || tag (128 bits) || IV/nonce (96 bits) || ciphertext (remaining) +// +// This format is compatible with Pushbullet's encryption format. +// See https://docs.pushbullet.com/#encryption for details. +func Encrypt(plaintext string, key []byte) (string, error) { + nonce := make([]byte, gcmNonceSize) // Never use more than 2^32 random nonces + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + return encryptWithNonce(plaintext, nonce, key) +} + +func encryptWithNonce(plaintext string, nonce, key []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + ciphertextWithTag := aesgcm.Seal(nil, nonce, []byte(plaintext), nil) + tagIndex := len(ciphertextWithTag) - gcmTagSize + ciphertext, tag := ciphertextWithTag[:tagIndex], ciphertextWithTag[tagIndex:] + output := appendSlices([]byte{versionByte}, tag, nonce, ciphertext) + return base64.StdEncoding.EncodeToString(output), nil +} + +// Decrypt decodes and decrypts a message that was encrypted with the Encrypt function. +func Decrypt(input string, key []byte) (string, error) { + inputBytes, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "", err + } + if len(inputBytes) < 1+gcmTagSize+gcmNonceSize { + return "", errCiphertextTooShort + } + version, tag, nonce, ciphertext := inputBytes[0], inputBytes[1:gcmTagSize+1], inputBytes[1+gcmTagSize:1+gcmTagSize+gcmNonceSize], inputBytes[1+gcmTagSize+gcmNonceSize:] + if version != versionByte { + return "", errCiphertextUnexpectedVersion + } + cipherTextWithTag := append(ciphertext, tag...) + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + plaintext, err := aesgcm.Open(nil, nonce, cipherTextWithTag, nil) + if err != nil { + return "", err + } + return string(plaintext), nil +} + +func appendSlices(s ...[]byte) []byte { + var output []byte + for _, r := range s { + output = append(output, r...) + } + return output +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 00000000..f1ea3ec9 --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,43 @@ +package crypto + +import ( + "encoding/base64" + "encoding/hex" + "github.com/stretchr/testify/require" + "log" + "testing" +) + +func TestEncryptDecrypt(t *testing.T) { + message := "this is a message or is it?" + ciphertext, err := Encrypt(message, []byte("AES256Key-32Characters1234567890")) + require.Nil(t, err) + plaintext, err := Decrypt(ciphertext, []byte("AES256Key-32Characters1234567890")) + require.Nil(t, err) + log.Println(ciphertext) + require.Equal(t, message, plaintext) +} + +func TestEncryptExpectedOutputxxxxx(t *testing.T) { + // These values are taken from https://docs.pushbullet.com/#encryption + // The following expected ciphertext from the site was used as a baseline: + // MQS2K9l3G8YoLccJooY64kDeWjbkI3fAx4WcrYNtbz4p8Q== + // 31 04b62bd9771bc6282dc709a2863ae240 de5a36e42377c0c7859cad83 6d6f3e29f1 + // v tag nonce ciphertext + message := "meow!" + key, _ := base64.StdEncoding.DecodeString("1sW28zp7CWv5TtGjlQpDHHG4Cbr9v36fG5o4f74LsKg=") + nonce, _ := hex.DecodeString("de5a36e42377c0c7859cad83") + ciphertext, err := encryptWithNonce(message, nonce, key) + require.Nil(t, err) + require.Equal(t, "MQS2K9l3G8YoLccJooY64kDeWjbkI3fAx4WcrYNtbz4p8Q==", ciphertext) +} + +func TestEncryptExpectedOutput(t *testing.T) { + // These values are taken from https://docs.pushbullet.com/#encryption, meaning that + // all of this is compatible with how Pushbullet encrypts + encryptedMessage := "MSfJxxY5YdjttlfUkCaKA57qU9SuCN8+ZhYg/xieI+lDnQ==" + key, _ := base64.StdEncoding.DecodeString("1sW28zp7CWv5TtGjlQpDHHG4Cbr9v36fG5o4f74LsKg=") + plaintext, err := Decrypt(encryptedMessage, key) + require.Nil(t, err) + require.Equal(t, "meow!", plaintext) +}