Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
)
|
|
|
|
const liveAttestationHeader = "x-oai-attestation"
|
|
|
|
type liveAttestationAES struct {
|
|
key [32]byte
|
|
}
|
|
|
|
func newLiveAttestationCipher(cfg *config.Config) SecretEncryptor {
|
|
if cfg == nil || strings.TrimSpace(cfg.JWT.Secret) == "" {
|
|
return nil
|
|
}
|
|
return &liveAttestationAES{
|
|
key: sha256.Sum256([]byte("sub2api/live-attestation/v1\x00" + cfg.JWT.Secret)),
|
|
}
|
|
}
|
|
|
|
func (c *liveAttestationAES) Encrypt(plaintext string) (string, error) {
|
|
block, err := aes.NewCipher(c.key[:])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return "", fmt.Errorf("generate Live attestation nonce: %w", err)
|
|
}
|
|
encrypted := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
|
return base64.RawStdEncoding.EncodeToString(encrypted), nil
|
|
}
|
|
|
|
func (c *liveAttestationAES) Decrypt(ciphertext string) (string, error) {
|
|
encrypted, err := base64.RawStdEncoding.DecodeString(ciphertext)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode Live attestation: %w", err)
|
|
}
|
|
block, err := aes.NewCipher(c.key[:])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(encrypted) < gcm.NonceSize() {
|
|
return "", errors.New("encrypted Live attestation is too short")
|
|
}
|
|
plaintext, err := gcm.Open(nil, encrypted[:gcm.NonceSize()], encrypted[gcm.NonceSize():], nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decrypt Live attestation: %w", err)
|
|
}
|
|
return string(plaintext), nil
|
|
}
|
|
|
|
func (s *OpenAIGatewayService) prepareLiveAttestation(ctx context.Context) (string, string, error) {
|
|
if s == nil || s.liveAttestation == nil {
|
|
return "", "", &LiveAttestationUnavailableError{
|
|
Reason: "Sub2API has no platform DeviceCheck provider",
|
|
}
|
|
}
|
|
if s.liveAttestationCipher == nil {
|
|
return "", "", &LiveAttestationUnavailableError{
|
|
Reason: "JWT secret is required to protect the Sideband attestation",
|
|
}
|
|
}
|
|
header, err := s.liveAttestation.Generate(ctx)
|
|
if err != nil {
|
|
return "", "", &LiveAttestationUnavailableError{Reason: err.Error()}
|
|
}
|
|
ciphertext, err := s.liveAttestationCipher.Encrypt(header)
|
|
if err != nil {
|
|
return "", "", &LiveAttestationUnavailableError{
|
|
Reason: "failed to protect the generated DeviceCheck attestation",
|
|
}
|
|
}
|
|
return header, ciphertext, nil
|
|
}
|
|
|
|
func (s *OpenAIGatewayService) decryptLiveAttestation(record *LiveCallRecord) (string, error) {
|
|
if record == nil || strings.TrimSpace(record.AttestationCiphertext) == "" || s.liveAttestationCipher == nil {
|
|
return "", &LiveAttestationUnavailableError{
|
|
Reason: "the Live call has no reusable DeviceCheck attestation",
|
|
}
|
|
}
|
|
header, err := s.liveAttestationCipher.Decrypt(record.AttestationCiphertext)
|
|
if err != nil {
|
|
return "", &LiveAttestationUnavailableError{
|
|
Reason: "the Live call DeviceCheck attestation cannot be decrypted on this instance",
|
|
}
|
|
}
|
|
return header, nil
|
|
}
|