mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-23 19:59:26 +01:00
WIP: Crypto stuff
This commit is contained in:
parent
e8953aea3b
commit
febe45818c
2 changed files with 133 additions and 0 deletions
90
crypto/crypto.go
Normal file
90
crypto/crypto.go
Normal file
|
@ -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
crypto/crypto_test.go
Normal file
43
crypto/crypto_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in a new issue