Last active
March 31, 2022 20:29
-
-
Save trptcolin/6e6bf5257b646dac117e22c2dddfe353 to your computer and use it in GitHub Desktop.
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 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 | |
} |
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 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