Browse Source

WIP: Crypto stuff

Philipp Heckel 2 years ago
parent
commit
febe45818c
2 changed files with 133 additions and 0 deletions
  1. 90 0
      crypto/crypto.go
  2. 43 0
      crypto/crypto_test.go

+ 90 - 0
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
+}

+ 43 - 0
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)
+}