Last active
July 6, 2022 19:32
-
-
Save komuw/4d44a25e1b6786100ffe0308106e80f2 to your computer and use it in GitHub Desktop.
encrypt and decrypt in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/[email protected]/-/blob/cmd/kubeadm/app/util/crypto/crypto.go | |
// 2. https://sourcegraph.com/github.com/gofiber/[email protected]/-/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/[email protected]/-/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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"crypto/aes" | |
"crypto/cipher" | |
cryptoRand "crypto/rand" | |
"fmt" | |
mathRand "math/rand" | |
"time" | |
) | |
// - https://sourcegraph.com/github.com/schollz/[email protected]/-/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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
https://github.com/FiloSottile/age/blob/891be91d42721088db0d0496ed3b3a8f3bcbdddc/scrypt.go#L64-L81