feat: publish v1.0.0-rc.36-custom.1
This commit is contained in:
commit
3a7e8f3b54
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,157 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const passwordEncryptionKeyBits = 2048
|
||||
|
||||
var ErrPasswordEncryptionInvalid = errors.New("password encryption payload is invalid")
|
||||
|
||||
var passwordEncryptionState struct {
|
||||
sync.RWMutex
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey string
|
||||
keyID string
|
||||
}
|
||||
|
||||
// GeneratePasswordEncryptionPrivateKey creates the server key used to decrypt
|
||||
// browser login passwords. The caller is responsible for persisting the PEM.
|
||||
func GeneratePasswordEncryptionPrivateKey() (string, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, passwordEncryptionKeyBits)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate password encryption key: %w", err)
|
||||
}
|
||||
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal password encryption key: %w", err)
|
||||
}
|
||||
return string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privateKeyDER,
|
||||
})), nil
|
||||
}
|
||||
|
||||
// LoadPasswordEncryptionPrivateKey validates a persisted key before replacing
|
||||
// the active in-memory key used by request handlers.
|
||||
func LoadPasswordEncryptionPrivateKey(privateKeyPEM string) error {
|
||||
block, rest := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil || block.Type != "PRIVATE KEY" || strings.TrimSpace(string(rest)) != "" {
|
||||
return errors.New("password encryption key is not valid PKCS#8 PEM")
|
||||
}
|
||||
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse password encryption key: %w", err)
|
||||
}
|
||||
privateKey, ok := parsed.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return errors.New("password encryption key is not RSA")
|
||||
}
|
||||
if privateKey.N == nil || privateKey.N.BitLen() < passwordEncryptionKeyBits {
|
||||
return fmt.Errorf("password encryption key must be at least %d bits", passwordEncryptionKeyBits)
|
||||
}
|
||||
if err := privateKey.Validate(); err != nil {
|
||||
return fmt.Errorf("validate password encryption key: %w", err)
|
||||
}
|
||||
privateKey.Precompute()
|
||||
|
||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal password encryption public key: %w", err)
|
||||
}
|
||||
publicKeyPEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyDER,
|
||||
}))
|
||||
keyDigest := sha256.Sum256(publicKeyDER)
|
||||
keyID := hex.EncodeToString(keyDigest[:16])
|
||||
|
||||
passwordEncryptionState.Lock()
|
||||
defer passwordEncryptionState.Unlock()
|
||||
passwordEncryptionState.privateKey = privateKey
|
||||
passwordEncryptionState.publicKey = publicKeyPEM
|
||||
passwordEncryptionState.keyID = keyID
|
||||
return nil
|
||||
}
|
||||
|
||||
// PasswordEncryptionPublicKey returns the active key identifier and SPKI PEM
|
||||
// public key exposed to browser clients.
|
||||
func PasswordEncryptionPublicKey() (keyID string, publicKeyPEM string) {
|
||||
passwordEncryptionState.RLock()
|
||||
defer passwordEncryptionState.RUnlock()
|
||||
return passwordEncryptionState.keyID, passwordEncryptionState.publicKey
|
||||
}
|
||||
|
||||
// DecryptPassword accepts legacy RSA-OAEP/SHA-256 ciphertext and v2 envelopes.
|
||||
// V2 wraps a fresh AES-256 key with RSA-OAEP and encrypts the password with GCM,
|
||||
// allowing long Unicode passwords to work with existing 2048-bit server keys.
|
||||
// Both formats share one public error for all malformed inputs.
|
||||
func DecryptPassword(ciphertextBase64 string, keyID string) (string, error) {
|
||||
passwordEncryptionState.RLock()
|
||||
privateKey := passwordEncryptionState.privateKey
|
||||
activeKeyID := passwordEncryptionState.keyID
|
||||
passwordEncryptionState.RUnlock()
|
||||
if privateKey == nil || keyID == "" || keyID != activeKeyID {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
if strings.HasPrefix(ciphertextBase64, "v2.") {
|
||||
if len(ciphertextBase64) > 4096 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
parts := strings.Split(ciphertextBase64, ".")
|
||||
if len(parts) != 4 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
wrappedKey, err := base64.StdEncoding.Strict().DecodeString(parts[1])
|
||||
if err != nil || len(wrappedKey) != privateKey.Size() {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
nonce, err := base64.StdEncoding.Strict().DecodeString(parts[2])
|
||||
if err != nil || len(nonce) != 12 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.Strict().DecodeString(parts[3])
|
||||
if err != nil || len(ciphertext) <= 16 || len(ciphertext) > MaxAccountPasswordLength*utf8.UTFMax+16 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, wrappedKey, []byte("password-v2"))
|
||||
if err != nil || len(key) != 32 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, []byte("password-v2:"+keyID))
|
||||
if err != nil {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
|
||||
if err != nil || len(ciphertext) != privateKey.Size() {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, nil)
|
||||
if err != nil || len(plaintext) == 0 {
|
||||
return "", ErrPasswordEncryptionInvalid
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
Reference in new issue
Block a user