Skip to content

Instantly share code, notes, and snippets.

@trptcolin
Last active March 31, 2022 20:29
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 trptcolin/6e6bf5257b646dac117e22c2dddfe353 to your computer and use it in GitHub Desktop.
Save trptcolin/6e6bf5257b646dac117e22c2dddfe353 to your computer and use it in GitHub Desktop.
// Package secret provides encrypt/decrypt functionality with a string-oriented API.
//
// The design choice to use strings is purely for ease of API use, almost certainly not a good idea.
// To be clear, this thing is not intended for production use, just for poking around the Go ecosystem.
// In particular, there are performance implications with all the stringification, and I wouldn't trust the error handling.
// There may be additional / worse issues too!
//
// Uses `golang.org/x/crypto/nacl/secretbox` for the actual cryptography, and `encoding/base64` to round-trip into strings.
package secret
import (
"crypto/rand"
"encoding/base64"
"errors"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/scrypt"
)
// EncryptionKey represents an encryption key.
type EncryptionKey [32]byte
// Plaintext represents a plaintext string.
type Plaintext string
// Ciphertext represents a ciphertext string.
type Ciphertext string
// GenerateKey generates a new encryption key based on a given passphrase string.
// The key is generated via scrypt, and the 8-byte (64-bit) salt will be randomly generated when the zero value is passed.
// ... so the passphrase needn't be cryptographically random.
func GenerateKey(passphrase string, saltBytes [8]byte) EncryptionKey {
// Get 8 random bytes for the scrypt salt
salt := saltBytes[:]
if saltBytes == [8]byte{} {
_, err := rand.Read(salt)
if err != nil {
panic(err)
}
}
// Use scrypt parameters from docs (https://pkg.go.dev/golang.org/x/crypto/scrypt)
keySlice, err := scrypt.Key([]byte(passphrase), salt, 1<<15, 8, 1, 32)
if err != nil {
panic(err)
}
var key [32]byte
copy(key[:], keySlice)
return key
}
// Encrypt encrypts a plaintext value into a base64-encoded nacl/secretbox with the given key.
func Encrypt(key EncryptionKey, value Plaintext) Ciphertext {
// Ensure that we've got a locally-immutable copy of the key
// IMPORTANT NOTE:
// - if the given key is shorter than 32 bytes, the rest will be zeroed
// - if the given key is *longer*, it'll get truncated to 32 bytes
var secretKeyBytes [32]byte
copy(secretKeyBytes[:], key[:])
// Get 24 random bytes
// nonce[:] instead of nonce directly, because Go slices are not the same type as arrays
var nonce [24]byte
_, err := rand.Read(nonce[:])
if err != nil {
panic(err)
}
// the first param (`out`) is a slice to which the plaintext message gets appended.
// so we end up actually using the nonce as the first bytes in the "ciphertext"
// something like this:
// |---PLAINTEXT_NONCE---|---ENCRYPTED_MESSAGE_THAT_INCLUDES_NONCE_IN_ENCRYPTION_ALGORITHM---|
// and that means when decrypting, as long as we know the nonce size, we can extract that and use to decrypt
encrypted := secretbox.Seal(nonce[:], []byte(value), &nonce, &secretKeyBytes)
// finally, wrap in base64 encoding to make it easy to write to disk
result := base64.RawStdEncoding.EncodeToString(encrypted)
return Ciphertext(result)
}
// Decrypt decrypts a base64-encoded nacl/secretbox with the given key.
func Decrypt(key EncryptionKey, value Ciphertext) (Plaintext, error) {
// Ensure that we've got a locally-immutable copy of the key
var secretKeyBytes [32]byte
copy(secretKeyBytes[:], key[:])
// first, unwrap the base64 encoding (from disk, or wherever)
decodedCiphertextBytes, err := base64.RawStdEncoding.DecodeString(string(value))
if err != nil {
return "", errors.New("could not decode the base64 value")
}
// First extract the first 24 bytes from the ciphertext - that's the nonce we encrypted with
var decryptNonce [24]byte
copy(decryptNonce[:], decodedCiphertextBytes[:24])
// The later bytes [24:] are the actual ciphertext we want to decrypt
decrypted, ok := secretbox.Open(nil, decodedCiphertextBytes[24:], &decryptNonce, &secretKeyBytes)
if !ok {
return "", errors.New("could not decrypt the given value")
}
return Plaintext(decrypted), nil
}
package secret
import (
"fmt"
)
func Example() {
// Don't use this actual string, of course
secretKey := GenerateKey("hi everybody, here's the secret key we're going to be using...", [8]byte{})
fmt.Printf("len(secretKey): %v\n", len(secretKey))
plaintext := Plaintext("top$3kr3Tpassworld")
encrypted1 := Encrypt(EncryptionKey(secretKey), plaintext)
fmt.Printf("len(encrypted1): %v\n", len(encrypted1))
encrypted2 := Encrypt(EncryptionKey(secretKey), plaintext)
fmt.Printf("len(encrypted2): %v\n", len(encrypted2))
fmt.Println("encrypted1 == encrypted2: ", encrypted1 == encrypted2)
decrypted1, err := Decrypt(EncryptionKey(secretKey), encrypted1)
if err != nil {
panic(err)
}
fmt.Printf("decrypted1: %q\n", decrypted1)
decrypted2, err := Decrypt(EncryptionKey(secretKey), encrypted2)
if err != nil {
panic(err)
}
fmt.Printf("decrypted2: %q\n", decrypted2)
fmt.Println("decrypted1 == decrypted2: ", decrypted1 == decrypted2)
// Output:
// len(secretKey): 32
// len(encrypted1): 78
// len(encrypted2): 78
// encrypted1 == encrypted2: false
// decrypted1: "top$3kr3Tpassworld"
// decrypted2: "top$3kr3Tpassworld"
// decrypted1 == decrypted2: true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment