Skip to content

Instantly share code, notes, and snippets.

@komuw
Last active July 6, 2022 19:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save komuw/4d44a25e1b6786100ffe0308106e80f2 to your computer and use it in GitHub Desktop.
Save komuw/4d44a25e1b6786100ffe0308106e80f2 to your computer and use it in GitHub Desktop.
encrypt and decrypt in Go
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"github.com/komuw/ong/id"
"golang.org/x/crypto/pbkdf2"
)
// from: https://sourcegraph.com/github.com/grafana/grafana/-/blob/pkg/services/encryption/ossencryption/ossencryption.go
// license(GNU Affero General Public License v3.0): https://github.com/grafana/grafana/blob/v9.0.2/LICENSE
//
// Also:
// 1. https://sourcegraph.com/github.com/kubernetes/kubernetes@v1.23.8/-/blob/cmd/kubeadm/app/util/crypto/crypto.go
// 2. https://sourcegraph.com/github.com/gofiber/fiber@v2.34.1/-/blob/middleware/encryptcookie/utils.go
// The recommendation from Go authors seems to be to use `crypto/cipher.NewGCM` or `XChaCha20-Poly1305`
// see; https://github.com/golang/crypto/blob/05595931fe9d3f8894ab063e1981d28e9873e2cb/tea/cipher.go#L13-L14
// examples:
// - https://sourcegraph.com/github.com/schollz/croc@v9.5.6/-/blob/src/crypt/crypt.go?L36-74
// - https://github.com/golang/go/blob/go1.18.3/src/crypto/cipher/example_test.go#L18-L47
// Latacora seems to recommend XSalsa20+Poly1305
// - https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html
const (
saltLength = 8
// from the docs of aes.NewCipher, key should be 32bytes.
keyLength = 32
)
func main() {
secret := "1234"
payload := []byte("hello world")
encrypted, err := encrypt(payload, secret)
if err != nil {
panic(err)
}
fmt.Println("encrypted: ", encrypted, string(encrypted))
decrypted, err := decrypt(encrypted, secret)
if err != nil {
panic(err)
}
fmt.Println("decrypted: ", decrypted, string(decrypted))
}
func encode(payload []byte) string {
return base64.RawURLEncoding.EncodeToString(payload)
}
func decode(payload string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(payload)
}
func encrypt(payload []byte, secret string) ([]byte, error) {
salt := id.Random(16)[:saltLength]
key := encryptionKeyToBytes(secret, salt)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
copy(ciphertext[:saltLength], salt)
iv := ciphertext[saltLength : saltLength+aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
return ciphertext, nil
}
func decrypt(payload []byte, secret string) ([]byte, error) {
if len(payload) < saltLength {
return nil, errors.New("unable to compute salt")
}
salt := payload[:saltLength]
key := encryptionKeyToBytes(secret, string(salt))
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// The IV needs to be unique, but not secure. Therefore, it's common to
// include it at the beginning of the ciphertext.
if len(payload) < aes.BlockSize {
return nil, errors.New("payload too short")
}
iv := payload[saltLength : saltLength+aes.BlockSize]
payload = payload[saltLength+aes.BlockSize:]
payloadDst := make([]byte, len(payload))
stream := cipher.NewCFBDecrypter(block, iv)
// XORKeyStream can work in-place if the two arguments are the same.
stream.XORKeyStream(payloadDst, payload)
return payloadDst, nil
}
func encryptionKeyToBytes(secret, salt string) []byte {
return pbkdf2.Key([]byte(secret), []byte(salt), 10000, keyLength, sha256.New)
}
package main
import (
"testing"
"github.com/akshayjshah/attest"
"github.com/komuw/ong/id"
)
/*
goimports -w .;gofumpt -extra -lang 1.18 -w .;gofmt -w -s .;go mod tidy;go test -race ./... -v
*/
func TestEncrypt(t *testing.T) {
t.Run("success", func(t *testing.T) {
secret := id.Random(32)
payload := []byte("hello world")
encrypted, err := encrypt(payload, secret)
attest.Ok(t, err)
decrypted, err := decrypt(encrypted, secret)
attest.Ok(t, err)
attest.Equal(t, decrypted, payload)
attest.Equal(t, string(decrypted), string(payload))
})
t.Run("eoncode/decode", func(t *testing.T) {
secret := id.Random(32)
payload := []byte("hello world")
encrypted, err := encrypt(payload, secret)
attest.Ok(t, err)
encodedEncrypted := encode(encrypted)
_encrypted, err := decode(encodedEncrypted)
attest.Ok(t, err)
decrypted, err := decrypt(_encrypted, secret)
attest.Ok(t, err)
attest.Equal(t, decrypted, payload)
attest.Equal(t, string(decrypted), string(payload))
})
}
package main
import (
"crypto/aes"
"crypto/cipher"
cryptoRand "crypto/rand"
"fmt"
mathRand "math/rand"
"time"
)
// - https://sourcegraph.com/github.com/schollz/croc@v9.5.6/-/blob/src/crypt/crypt.go?L36-74
// - https://github.com/golang/go/blob/go1.18.3/src/crypto/cipher/example_test.go#L18-L47
// why 12?
// https://crypto.stackexchange.com/a/78165
const noncelength = 12
func main() {
plaintext := []byte("hello world")
secret := []byte("the key should 32bytes & random.")
encrypted, err := encrypt(plaintext, secret)
if err != nil {
panic(err)
}
decrypted, err := decrypt(encrypted, secret)
if err != nil {
panic(err)
}
if string(decrypted) != string(plaintext) {
panic("error")
}
fmt.Println("decrypted: ", string(decrypted))
}
// encrypt will encrypt using the pre-generated key
func encrypt(plaintext, key []byte) ([]byte, error) {
// from the docs of aes.NewCipher, key should be 32bytes.
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// generate a random iv/nonce each time
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf, Section 8.2
// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
// ie, you can only send a maximum of 2^32 messages for any given key.
// after that(ideally prior to that), you should generate a new key; https://security.stackexchange.com/a/202071
nonce := make([]byte, noncelength)
if _, err := cryptoRand.Read(nonce); err != nil {
// Is it safe to use mathRand here?
// According to agl(the one and only);
// "The nonce itself does not have to be random, it can be a counter.
// But it absolutely must be unique"
// see: https://crypto.stackexchange.com/a/5818
mathRand.Seed(time.Now().UTC().UnixNano())
_, _ = mathRand.Read(nonce) // docs say that it always returns a nil error.
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
encrypted := aesgcm.Seal(nil, nonce, plaintext, nil)
encrypted = append(
// "you can send the nonce in the clear before each message; so long as it's unique." - agl
// see: https://crypto.stackexchange.com/a/5818
nonce,
encrypted...,
)
return encrypted, err
}
// decrypt using the pre-generated key
func decrypt(encrypted, key []byte) ([]byte, error) {
if len(encrypted) < 13 {
return nil, fmt.Errorf("incorrect passphrase")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, encrypted[:noncelength], encrypted[noncelength:], nil)
return plaintext, err
}
package main
import (
cryptoRand "crypto/rand"
"fmt"
"golang.org/x/crypto/chacha20poly1305"
)
// as recommended by Latacora.
// from: https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
// XChaCha20-Poly1305, unlinke aes-gcm, has no message limit per key.
// It can safely encrypt an unlimited number of messages with the same key, without any limit to the size of a message.
// see: https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction
func main() {
/*
key should be randomly generated or derived from a function like Argon2.
import "golang.org/x/crypto/argon2"
key := argon2.Key([]byte("some password"), salt, 3, 32*1024, 4, chacha20poly1305.KeySize)
*/
key := make([]byte, chacha20poly1305.KeySize)
if _, err := cryptoRand.Read(key); err != nil {
panic(err)
}
// xchacha20poly1305 takes a longer nonce, suitable to be generated randomly without risk of collisions.
// It should be preferred when nonce uniqueness cannot be trivially ensured
aead, err := chacha20poly1305.NewX(key)
if err != nil {
panic(err)
}
// Encryption.
var encryptedMsg []byte
{
msg := []byte("hello world")
fmt.Println("aead.NonceSize(); ", aead.NonceSize())
// Select a random nonce, and leave capacity for the ciphertext.
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
if _, err := cryptoRand.Read(nonce); err != nil {
panic(err)
}
// Encrypt the message and append the ciphertext to the nonce.
encryptedMsg = aead.Seal(nonce, nonce, msg, nil)
fmt.Println("encryptedMsg: ", encryptedMsg)
}
// Decryption.
{
if len(encryptedMsg) < aead.NonceSize() {
panic("ciphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err)
}
fmt.Println("plaintext: ", string(plaintext))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment