160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// SecureToken creates a new random token
|
|
func SecureToken() string {
|
|
b := make([]byte, 16)
|
|
must(io.ReadFull(rand.Reader, b))
|
|
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// GenerateOtp generates a random n digit otp
|
|
func GenerateOtp(digits int) string {
|
|
upper := math.Pow10(digits)
|
|
val := must(rand.Int(rand.Reader, big.NewInt(int64(upper))))
|
|
|
|
// adds a variable zero-padding to the left to ensure otp is uniformly random
|
|
expr := "%0" + strconv.Itoa(digits) + "v"
|
|
otp := fmt.Sprintf(expr, val.String())
|
|
|
|
return otp
|
|
}
|
|
func GenerateTokenHash(emailOrPhone, otp string) string {
|
|
return fmt.Sprintf("%x", sha256.Sum224([]byte(emailOrPhone+otp)))
|
|
}
|
|
|
|
// Generated a random secure integer from [0, max[
|
|
func secureRandomInt(max int) int {
|
|
randomInt := must(rand.Int(rand.Reader, big.NewInt(int64(max))))
|
|
return int(randomInt.Int64())
|
|
}
|
|
|
|
type EncryptedString struct {
|
|
KeyID string `json:"key_id"`
|
|
Algorithm string `json:"alg"`
|
|
Data []byte `json:"data"`
|
|
Nonce []byte `json:"nonce,omitempty"`
|
|
}
|
|
|
|
func (es *EncryptedString) IsValid() bool {
|
|
return es.KeyID != "" && len(es.Data) > 0 && len(es.Nonce) > 0 && es.Algorithm == "aes-gcm-hkdf"
|
|
}
|
|
|
|
// ShouldReEncrypt tells you if the value encrypted needs to be encrypted again with a newer key.
|
|
func (es *EncryptedString) ShouldReEncrypt(encryptionKeyID string) bool {
|
|
return es.KeyID != encryptionKeyID
|
|
}
|
|
|
|
func (es *EncryptedString) Decrypt(id string, decryptionKeys map[string]string) ([]byte, error) {
|
|
decryptionKey := decryptionKeys[es.KeyID]
|
|
|
|
if decryptionKey == "" {
|
|
return nil, fmt.Errorf("crypto: decryption key with name %q does not exist", es.KeyID)
|
|
}
|
|
|
|
key, err := deriveSymmetricKey(id, es.KeyID, decryptionKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block := must(aes.NewCipher(key))
|
|
cipher := must(cipher.NewGCM(block))
|
|
|
|
decrypted, err := cipher.Open(nil, es.Nonce, es.Data, nil) // #nosec G407
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decrypted, nil
|
|
}
|
|
|
|
func ParseEncryptedString(str string) *EncryptedString {
|
|
if !strings.HasPrefix(str, "{") {
|
|
return nil
|
|
}
|
|
|
|
var es EncryptedString
|
|
|
|
if err := json.Unmarshal([]byte(str), &es); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if !es.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
return &es
|
|
}
|
|
|
|
func (es *EncryptedString) String() string {
|
|
out := must(json.Marshal(es))
|
|
|
|
return string(out)
|
|
}
|
|
|
|
func deriveSymmetricKey(id, keyID, keyBase64URL string) ([]byte, error) {
|
|
hkdfKey, err := base64.RawURLEncoding.DecodeString(keyBase64URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(hkdfKey) != 256/8 {
|
|
return nil, fmt.Errorf("crypto: key with ID %q is not 256 bits", keyID)
|
|
}
|
|
|
|
// Since we use AES-GCM here, the same symmetric key *must not be used
|
|
// more than* 2^32 times. But, that's not that much. Suppose a system
|
|
// with 100 million users, then a user can only change their password
|
|
// 42 times. To prevent this, the actual symmetric key is derived by
|
|
// using HKDF using the encryption key and the "ID" of the object
|
|
// containing the encryption string. Ideally this ID is a UUID. This
|
|
// has the added benefit that the encrypted string is bound to that
|
|
// specific object, and can't accidentally be "moved" to other objects
|
|
// without changing their ID to the original one.
|
|
|
|
keyReader := hkdf.New(sha256.New, hkdfKey, nil, []byte(id))
|
|
key := make([]byte, 256/8)
|
|
|
|
must(io.ReadFull(keyReader, key))
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func NewEncryptedString(id string, data []byte, keyID string, keyBase64URL string) (*EncryptedString, error) {
|
|
key, err := deriveSymmetricKey(id, keyID, keyBase64URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block := must(aes.NewCipher(key))
|
|
cipher := must(cipher.NewGCM(block))
|
|
|
|
es := EncryptedString{
|
|
KeyID: keyID,
|
|
Algorithm: "aes-gcm-hkdf",
|
|
Nonce: make([]byte, 12),
|
|
}
|
|
|
|
must(io.ReadFull(rand.Reader, es.Nonce))
|
|
es.Data = cipher.Seal(nil, es.Nonce, data, nil) // #nosec G407
|
|
|
|
return &es, nil
|
|
}
|