chatdesk-ui/auth_v2.169.0/internal/crypto/crypto.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
}