Use the crypto/aes package for symmetric encryption (fast, same key) and crypto/rand with crypto/cipher for secure initialization vectors, while relying on crypto/rsa for asymmetric operations where you encrypt with a public key and decrypt with a private key. For production, always prefer AES-GCM mode over CBC to handle authentication and integrity automatically, and ensure you generate random nonces for every encryption operation.
Here is a practical implementation using AES-GCM for symmetric encryption:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)
func encryptAES(plaintext []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func decryptAES(ciphertextB64 string, key []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
func main() {
key := []byte("16-byte-long-key-!!") // Must be 16, 24, or 32 bytes
plaintext := []byte("Secret message")
enc, _ := encryptAES(plaintext, key)
fmt.Println("Encrypted:", enc)
dec, _ := decryptAES(enc, key)
fmt.Println("Decrypted:", string(dec))
}
For asymmetric encryption using RSA, you typically encrypt small data blocks or symmetric keys, then decrypt them with the private key. Note that RSA is slow for large payloads, so it's often used to exchange an AES key.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
)
func generateRSAKeyPair() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 2048)
}
func encryptRSA(plaintext []byte, pubKey *rsa.PublicKey) ([]byte, error) {
return rsa.EncryptPKCS1v15(rand.Reader, pubKey, plaintext)
}
func decryptRSA(ciphertext []byte, privKey *rsa.PrivateKey) ([]byte, error) {
return rsa.DecryptPKCS1v15(rand.Reader, privKey, ciphertext)
}
func main() {
priv, _ := generateRSAKeyPair()
pub := &priv.PublicKey
// RSA can only encrypt data smaller than the key size (minus padding)
plaintext := []byte("Short key")
encrypted, _ := encryptRSA(plaintext, pub)
decrypted, _ := decryptRSA(encrypted, priv)
fmt.Println("Decrypted RSA:", string(decrypted))
// Save private key to file (PEM format)
pemKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
// Save public key similarly using x509.MarshalPKIXPublicKey
}
Always store keys securely (e.g., environment variables or a secrets manager) and never hardcode them in source control. For RSA, remember the input size limit: with a 2048-bit key, you can only encrypt roughly 245 bytes of data.