package platform import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "errors" "fmt" "strconv" "strings" ) const passwordIterations = 210_000 type Vault struct { key []byte } func NewVault(encoded string) (*Vault, error) { encoded = strings.TrimSpace(encoded) if encoded == "" { return nil, errors.New("LICENSE_MASTER_KEY is required") } key, err := base64.RawStdEncoding.DecodeString(encoded) if err != nil { key, err = base64.StdEncoding.DecodeString(encoded) } if err != nil || len(key) != 32 { return nil, errors.New("LICENSE_MASTER_KEY must be a base64-encoded 32-byte key") } return &Vault{key: key}, nil } func (v *Vault) Encrypt(plaintext string) (string, error) { block, err := aes.NewCipher(v.key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", err } sealed := gcm.Seal(nil, nonce, []byte(plaintext), nil) payload := append(nonce, sealed...) return "v1." + base64.RawURLEncoding.EncodeToString(payload), nil } func (v *Vault) Decrypt(ciphertext string) (string, error) { parts := strings.SplitN(strings.TrimSpace(ciphertext), ".", 2) if len(parts) != 2 || parts[0] != "v1" { return "", errors.New("unsupported encrypted value") } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return "", err } block, err := aes.NewCipher(v.key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } if len(payload) < gcm.NonceSize() { return "", errors.New("encrypted value is truncated") } plain, err := gcm.Open(nil, payload[:gcm.NonceSize()], payload[gcm.NonceSize():], nil) if err != nil { return "", errors.New("encrypted value authentication failed") } return string(plain), nil } func HashPassword(password string) (string, error) { if len(password) < 12 { return "", errors.New("password must contain at least 12 characters") } if len(password) > 1024 { return "", errors.New("password is too long") } salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", err } dk := pbkdf2SHA256([]byte(password), salt, passwordIterations, 32) return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", passwordIterations, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(dk)), nil } func VerifyPassword(encoded, password string) bool { parts := strings.Split(encoded, "$") if len(parts) != 4 || parts[0] != "pbkdf2-sha256" { return false } iterations, err := strconv.Atoi(parts[1]) if err != nil || iterations < 100_000 || iterations > 2_000_000 { return false } salt, err1 := base64.RawStdEncoding.DecodeString(parts[2]) expected, err2 := base64.RawStdEncoding.DecodeString(parts[3]) if err1 != nil || err2 != nil || len(expected) == 0 { return false } actual := pbkdf2SHA256([]byte(password), salt, iterations, len(expected)) return subtle.ConstantTimeCompare(actual, expected) == 1 } func pbkdf2SHA256(password, salt []byte, iterations, keyLen int) []byte { hLen := sha256.Size blocks := (keyLen + hLen - 1) / hLen out := make([]byte, 0, blocks*hLen) for i := 1; i <= blocks; i++ { mac := hmac.New(sha256.New, password) mac.Write(salt) mac.Write([]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)}) u := mac.Sum(nil) t := append([]byte(nil), u...) for j := 1; j < iterations; j++ { mac = hmac.New(sha256.New, password) mac.Write(u) u = mac.Sum(nil) for k := range t { t[k] ^= u[k] } } out = append(out, t...) } return out[:keyLen] } func randomToken(bytes int) (string, error) { buf := make([]byte, bytes) if _, err := rand.Read(buf); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf), nil } func tokenHash(value string) string { sum := sha256.Sum256([]byte(value)) return base64.RawURLEncoding.EncodeToString(sum[:]) } func constantTokenEqual(expected, actual string) bool { if expected == "" || actual == "" { return false } return subtle.ConstantTimeCompare([]byte(tokenHash(expected)), []byte(tokenHash(actual))) == 1 }