mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-08 10:46:58 +00:00
116 lines
3.8 KiB
Go
116 lines
3.8 KiB
Go
package common
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
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 decrypts a base64 RSA-OAEP/SHA-256 password submitted by a
|
|
// browser. All malformed inputs share one error so callers do not expose
|
|
// cryptographic details to unauthenticated clients.
|
|
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
|
|
}
|
|
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
|
|
}
|