Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
196
pkg/cdn/encrypted_client.go
Normal file
196
pkg/cdn/encrypted_client.go
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
Encrypted CDN Client — Transparent AGE encryption layer over CDN operations.
|
||||
|
||||
Wraps the standard CDN Client to encrypt blobs before upload and decrypt
|
||||
on download. The encryption is transparent to callers — they push/pull
|
||||
plaintext and the encryption happens automatically.
|
||||
|
||||
Architecture:
|
||||
- PushBlob: plaintext → AGE encrypt → upload ciphertext
|
||||
- PullBlob: download ciphertext → AGE decrypt → return plaintext
|
||||
- Hash verification: hash is of PLAINTEXT (preserves CAS dedup)
|
||||
- Manifests are NOT encrypted (they contain only hashes, no sensitive data)
|
||||
|
||||
Copyright (c) Armored Gates LLC. All rights reserved.
|
||||
*/
|
||||
package cdn
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/armoredgate/volt/pkg/encryption"
|
||||
)
|
||||
|
||||
// ── Encrypted Client ─────────────────────────────────────────────────────────
|
||||
|
||||
// EncryptedClient wraps a CDN Client with transparent AGE encryption.
|
||||
type EncryptedClient struct {
|
||||
// Inner is the underlying CDN client that handles HTTP operations.
|
||||
Inner *Client
|
||||
|
||||
// Recipients are the AGE public keys to encrypt to.
|
||||
// Populated from encryption.BuildRecipients() on creation.
|
||||
Recipients []string
|
||||
|
||||
// IdentityPath is the path to the AGE private key for decryption.
|
||||
IdentityPath string
|
||||
}
|
||||
|
||||
// NewEncryptedClient creates a CDN client with transparent encryption.
|
||||
// It reads encryption keys from the standard locations.
|
||||
func NewEncryptedClient() (*EncryptedClient, error) {
|
||||
inner, err := NewClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypted cdn client: %w", err)
|
||||
}
|
||||
|
||||
return NewEncryptedClientFromInner(inner)
|
||||
}
|
||||
|
||||
// NewEncryptedClientFromInner wraps an existing CDN client with encryption.
|
||||
func NewEncryptedClientFromInner(inner *Client) (*EncryptedClient, error) {
|
||||
recipients, err := encryption.BuildRecipients()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypted cdn client: %w", err)
|
||||
}
|
||||
|
||||
return &EncryptedClient{
|
||||
Inner: inner,
|
||||
Recipients: recipients,
|
||||
IdentityPath: encryption.CDNIdentityPath(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ── Encrypted Push/Pull ──────────────────────────────────────────────────────
|
||||
|
||||
// PushBlob encrypts plaintext data and uploads the ciphertext to the CDN.
|
||||
// The hash parameter is the SHA-256 of the PLAINTEXT (for CAS addressing).
|
||||
// The CDN stores the ciphertext keyed by the plaintext hash.
|
||||
func (ec *EncryptedClient) PushBlob(hash string, plaintext []byte) error {
|
||||
// Verify plaintext hash matches
|
||||
actualHash := encSha256Hex(plaintext)
|
||||
if actualHash != hash {
|
||||
return fmt.Errorf("encrypted push: hash mismatch (expected %s, got %s)", hash[:12], actualHash[:12])
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext, err := encryption.Encrypt(plaintext, ec.Recipients)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypted push %s: %w", hash[:12], err)
|
||||
}
|
||||
|
||||
// Upload ciphertext — we bypass the inner client's hash check since the
|
||||
// ciphertext hash won't match the plaintext hash. We use the raw HTTP upload.
|
||||
return ec.pushRawBlob(hash, ciphertext)
|
||||
}
|
||||
|
||||
// PullBlob downloads ciphertext from the CDN, decrypts it, and returns plaintext.
|
||||
// The hash is verified against the decrypted plaintext.
|
||||
func (ec *EncryptedClient) PullBlob(hash string) ([]byte, error) {
|
||||
// Download raw (skip inner client's integrity check since it's ciphertext)
|
||||
ciphertext, err := ec.pullRawBlob(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := encryption.Decrypt(ciphertext, ec.IdentityPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypted pull %s: %w", hash[:12], err)
|
||||
}
|
||||
|
||||
// Verify plaintext integrity
|
||||
actualHash := encSha256Hex(plaintext)
|
||||
if actualHash != hash {
|
||||
return nil, fmt.Errorf("encrypted pull %s: plaintext integrity check failed (got %s)", hash[:12], actualHash[:12])
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// BlobExists checks if a blob exists on the CDN (delegates to inner client).
|
||||
func (ec *EncryptedClient) BlobExists(hash string) (bool, error) {
|
||||
return ec.Inner.BlobExists(hash)
|
||||
}
|
||||
|
||||
// PullManifest downloads a manifest (NOT encrypted — manifests contain only hashes).
|
||||
func (ec *EncryptedClient) PullManifest(name string) (*Manifest, error) {
|
||||
return ec.Inner.PullManifest(name)
|
||||
}
|
||||
|
||||
// PushManifest uploads a manifest (NOT encrypted).
|
||||
func (ec *EncryptedClient) PushManifest(name string, manifest *Manifest) error {
|
||||
return ec.Inner.PushManifest(name, manifest)
|
||||
}
|
||||
|
||||
// ── Raw HTTP Operations ──────────────────────────────────────────────────────
|
||||
|
||||
// pushRawBlob uploads raw bytes to the CDN without hash verification.
|
||||
// Used for ciphertext upload where the hash is of the plaintext.
|
||||
func (ec *EncryptedClient) pushRawBlob(hash string, data []byte) error {
|
||||
if ec.Inner.StorageAPIKey == "" {
|
||||
return fmt.Errorf("cdn push blob: StorageAPIKey not configured")
|
||||
}
|
||||
if ec.Inner.StorageZoneName == "" {
|
||||
return fmt.Errorf("cdn push blob: StorageZoneName not configured")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s.storage.bunnycdn.com/%s/sha256:%s",
|
||||
ec.Inner.Region, ec.Inner.StorageZoneName, hash)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, url, strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdn push blob %s: create request: %w", hash[:12], err)
|
||||
}
|
||||
req.Header.Set("AccessKey", ec.Inner.StorageAPIKey)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.ContentLength = int64(len(data))
|
||||
|
||||
resp, err := ec.Inner.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdn push blob %s: %w", hash[:12], err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("cdn push blob %s: HTTP %d: %s", hash[:12], resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pullRawBlob downloads raw bytes from the CDN without hash verification.
|
||||
// Used for ciphertext download where the hash is of the plaintext.
|
||||
func (ec *EncryptedClient) pullRawBlob(hash string) ([]byte, error) {
|
||||
url := fmt.Sprintf("%s/sha256:%s", ec.Inner.BlobsBaseURL, hash)
|
||||
|
||||
resp, err := ec.Inner.HTTPClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdn pull blob %s: %w", hash[:12], err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("cdn pull blob %s: HTTP %d", hash[:12], resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdn pull blob %s: read body: %w", hash[:12], err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func encSha256Hex(data []byte) string {
|
||||
h := sha256.Sum256(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
Reference in New Issue
Block a user