Ctrl K
ComunidadGuías…

internal/crypto/crypto.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package crypto

import (
	"crypto"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/sha512"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"fmt"
	"hash"
	"io"
	"math/big"
	"strings"
	"time"
)

const (
	PrefixActive = "eoo_"
	EntropyBytes = 32
)

func CreateHash(data []byte, hashingAlgorithm, encoding string) (string, error) {
	var h hash.Hash
	switch hashingAlgorithm {
	case "sha256":
		h = sha256.New()
	case "sha512":
		h = sha512.New()
	case "md5":
		h = md5.New()
	default:
		return "", fmt.Errorf("unsupported hashing algorithm: %s", hashingAlgorithm)
	}
	if _, err := h.Write(data); err != nil {
		return "", fmt.Errorf("unable to write data into hasher: %w", err)
	}
	sum := h.Sum(nil)
	switch encoding {
	case "hex":
		return hex.EncodeToString(sum), nil
	case "base64":
		return base64.StdEncoding.EncodeToString(sum), nil
	default:
		return "", fmt.Errorf("unsupported encoding: %s", encoding)
	}
}

func ConvertSHA256HashToUUID(value string) string {
	if len(value) < 32 {
		return ""
	}
	return fmt.Sprintf("%s-%s-%s-%s-%s",
		value[:8],
		value[8:12],
		value[12:16],
		value[16:20],
		value[20:32],
	)
}

func GetBase64URLEncoding(encodedString string) string {
	base64EncodedString := strings.ReplaceAll(encodedString, "+", "-")
	base64EncodedString = strings.ReplaceAll(base64EncodedString, "/", "_")
	base64EncodedString = strings.TrimRight(base64EncodedString, "=")
	return base64EncodedString
}

func SignRSASHA256(data, privateKeyPEM string) (string, error) {
	block, _ := pem.Decode([]byte(privateKeyPEM))
	if block == nil {
		return "", errors.New("invalid private key PEM format")
	}
	var privateKey *rsa.PrivateKey
	var err error
	if privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
		parsedKey, parseErr := x509.ParsePKCS8PrivateKey(block.Bytes)
		if parseErr != nil {
			return "", fmt.Errorf("failed to parse private key: %w", parseErr)
		}
		var ok bool
		privateKey, ok = parsedKey.(*rsa.PrivateKey)
		if !ok {
			return "", errors.New("key is not an RSA private key")
		}
	}
	hashed := sha256.Sum256([]byte(data))
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
	if err != nil {
		return "", fmt.Errorf("failed to sign data: %w", err)
	}
	return base64.StdEncoding.EncodeToString(signature), nil
}

// GenerateRSAKeyPair generates a new 2048-bit RSA key pair formatted as PEM strings.
func GenerateRSAKeyPair() (string, string, error) {
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return "", "", fmt.Errorf("failed to generate rsa private key: %w", err)
	}
	privBlock := &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
	}
	privPEM := pem.EncodeToMemory(privBlock)
	pubASN1, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
	if err != nil {
		return "", "", fmt.Errorf("failed to marshal public key: %w", err)
	}
	pubBlock := &pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pubASN1,
	}
	pubPEM := pem.EncodeToMemory(pubBlock)
	return string(pubPEM), string(privPEM), nil
}

// SealAESGCM encrypts plaintext using a 32-byte master key and returns a base64 encoded string.
// aad is authenticated but not encrypted: it binds the ciphertext to the context it belongs
// to, so a blob sealed under one context cannot be opened under another. UnsealAESGCM must be
// given the exact same aad. Pass nil for data that has no context to bind to.
func SealAESGCM(plaintext []byte, masterKey []byte, aad []byte) (string, error) {
	if len(masterKey) != 32 {
		return "", errors.New("master key must be exactly 32 bytes for AES-256")
	}
	block, err := aes.NewCipher(masterKey)
	if err != nil {
		return "", fmt.Errorf("failed to create cipher block: %w", err)
	}
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return "", fmt.Errorf("failed to create GCM: %w", err)
	}
	// Create a unique nonce for this encryption execution
	nonce := make([]byte, aesGCM.NonceSize())
	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
		return "", fmt.Errorf("failed to generate nonce: %w", err)
	}
	// Encrypt the data, appending the ciphertext directly to the nonce payload
	ciphertext := aesGCM.Seal(nonce, nonce, plaintext, aad)
	return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// UnsealAESGCM decrypts a base64 encoded ciphertext string using the 32-byte master key.
// aad must match the value passed to SealAESGCM, otherwise decryption fails.
func UnsealAESGCM(base64Ciphertext string, masterKey []byte, aad []byte) ([]byte, error) {
	if len(masterKey) != 32 {
		return nil, errors.New("master key must be exactly 32 bytes for AES-256")
	}
	ciphertext, err := base64.StdEncoding.DecodeString(base64Ciphertext)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base64 ciphertext: %w", err)
	}
	block, err := aes.NewCipher(masterKey)
	if err != nil {
		return nil, fmt.Errorf("failed to create cipher block: %w", err)
	}
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("failed to create GCM: %w", err)
	}
	nonceSize := aesGCM.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, errors.New("ciphertext too short")
	}
	// Extract the nonce from the front and the actual encrypted text from the back
	nonce, encryptedPayload := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plaintext, err := aesGCM.Open(nil, nonce, encryptedPayload, aad)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt (key may be invalid, data was tampered with, or the blob belongs to another context): %w", err)
	}
	return plaintext, nil
}

func GenerateSelfSignedCodeSigningCertificateFromPEM(publicKeyPEM string, privateKeyPEM string, commonName string, serialNumber *big.Int, notBefore time.Time) (string, error) {
	privBlock, _ := pem.Decode([]byte(privateKeyPEM))
	if privBlock == nil {
		return "", errors.New("invalid private key PEM format")
	}
	var privateKey *rsa.PrivateKey
	var err error
	if privateKey, err = x509.ParsePKCS1PrivateKey(privBlock.Bytes); err != nil {
		parsedKey, parseErr := x509.ParsePKCS8PrivateKey(privBlock.Bytes)
		if parseErr != nil {
			return "", fmt.Errorf("failed to parse private key: %w", parseErr)
		}
		var ok bool
		privateKey, ok = parsedKey.(*rsa.PrivateKey)
		if !ok {
			return "", errors.New("key is not an RSA private key")
		}
	}
	pubBlock, _ := pem.Decode([]byte(publicKeyPEM))
	if pubBlock == nil {
		return "", errors.New("invalid public key PEM format")
	}
	var publicKey *rsa.PublicKey
	if publicKey, err = x509.ParsePKCS1PublicKey(pubBlock.Bytes); err != nil {
		parsedPub, parseErr := x509.ParsePKIXPublicKey(pubBlock.Bytes)
		if parseErr != nil {
			return "", fmt.Errorf("failed to parse public key: %w", parseErr)
		}
		var ok bool
		publicKey, ok = parsedPub.(*rsa.PublicKey)
		if !ok {
			return "", errors.New("key is not an RSA public key")
		}
	}
	notAfter := notBefore.AddDate(10, 0, 0) // Exactly 10 years from app creation date
	return generateSelfSignedCodeSigningCertificate(publicKey, privateKey, commonName, serialNumber, notBefore, notAfter)
}

// GenerateSelfSignedCodeSigningCertificate creates a PEM certificate string tailored for Expo OTA code signing.
// It matches the critical extensions defined in @expo/code-signing-certificates.
func generateSelfSignedCodeSigningCertificate(publicKey *rsa.PublicKey, privateKey *rsa.PrivateKey, commonName string, serialNumber *big.Int, notBefore time.Time, notAfter time.Time) (string, error) {
	// OID 2.5.29.15 = id-ce-keyUsage
	keyUsageBytes, _ := asn1.Marshal(asn1.BitString{
		Bytes:     []byte{0x80},
		BitLength: 1,
	})
	// OID 2.5.29.37 = id-ce-extKeyUsage
	extKeyUsageBytes, _ := asn1.Marshal([]asn1.ObjectIdentifier{{1, 3, 6, 1, 5, 5, 7, 3, 3}}) // 1.3.6.1.5.5.7.3.3 = Code Signing
	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			CommonName: commonName,
		},
		Issuer: pkix.Name{
			CommonName: commonName,
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,
		ExtraExtensions: []pkix.Extension{
			{
				Id:       asn1.ObjectIdentifier{2, 5, 29, 15},
				Critical: true,
				Value:    keyUsageBytes,
			},
			{
				Id:       asn1.ObjectIdentifier{2, 5, 29, 37},
				Critical: true,
				Value:    extKeyUsageBytes,
			},
		},
	}
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey)
	if err != nil {
		return "", fmt.Errorf("failed to sign and create x509 certificate: %w", err)
	}
	pemBlock := &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: derBytes,
	}
	return string(pem.EncodeToMemory(pemBlock)), nil
}

// GenerateAPIKey creates a secure, unique plaintext API token for the user
func GenerateAPIKey() (string, error) {
	tokenBytes := make([]byte, EntropyBytes)
	_, err := rand.Read(tokenBytes)
	if err != nil {
		return "", fmt.Errorf("failed to source system entropy: %w", err)
	}
	secretBody := hex.EncodeToString(tokenBytes)
	plaintextKey := fmt.Sprintf("%s%s", PrefixActive, secretBody)
	return plaintextKey, nil
}

func HashPlaintextAPIKey(plaintextKey string) (string, error) {
	hashedValue, err := CreateHash([]byte(plaintextKey), "sha256", "hex")
	if err != nil {
		return "", fmt.Errorf("failed to compute token hash: %w", err)
	}
	return hashedValue, nil
}