Files
overlord/Overlord-Client/cmd/agent/securelog/securelog_noprint.go
T

264 lines
5.6 KiB
Go

//go:build noprint
// +build noprint
package securelog
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
const (
maxEntries = 2048
maxLineBytes = 64 * 1024
maxSnapshotCount = 512
)
type encryptedEnvelope struct {
V int `json:"v"`
Alg string `json:"alg"`
Seq uint64 `json:"seq"`
At int64 `json:"at"`
Source string `json:"source"`
WrappedKey string `json:"wrappedKey"`
Nonce string `json:"nonce"`
Ciphertext string `json:"ciphertext"`
}
type captureWriter struct {
source string
}
var (
mu sync.Mutex
entries []Entry
dropped uint64
seq uint64
enabled atomic.Bool
lastError atomic.Value
publicKey *rsa.PublicKey
originalOut *os.File
originalErr *os.File
)
func Install(publicKeyB64 string) {
if enabled.Load() {
return
}
originalOut = os.Stdout
originalErr = os.Stderr
key, err := parsePublicKey(publicKeyB64)
if err != nil {
setError(err.Error())
log.SetOutput(io.Discard)
redirectToNull()
return
}
publicKey = key
enabled.Store(true)
log.SetOutput(captureWriter{source: "log"})
log.SetFlags(log.LstdFlags)
redirectPipe("stdout", originalOut, func(f *os.File) { os.Stdout = f })
redirectPipe("stderr", originalErr, func(f *os.File) { os.Stderr = f })
}
func SnapshotLogs(sinceSeq uint64, limit int) Snapshot {
mu.Lock()
defer mu.Unlock()
if limit <= 0 || limit > maxSnapshotCount {
limit = maxSnapshotCount
}
out := make([]Entry, 0, min(limit, len(entries)))
for _, entry := range entries {
if entry.Seq <= sinceSeq {
continue
}
out = append(out, entry)
if len(out) >= limit {
break
}
}
var from, to uint64
if len(out) > 0 {
from = out[0].Seq
to = out[len(out)-1].Seq
}
return Snapshot{
Entries: out,
Dropped: dropped,
FromSeq: from,
ToSeq: to,
Enabled: enabled.Load(),
Error: currentError(),
}
}
func (w captureWriter) Write(p []byte) (int, error) {
record(w.source, p)
return len(p), nil
}
func redirectPipe(source string, mirror *os.File, assign func(*os.File)) {
reader, writer, err := os.Pipe()
if err != nil {
setError(fmt.Sprintf("%s pipe failed: %v", source, err))
redirectToNull()
return
}
assign(writer)
go readPipe(source, reader, mirror)
}
func readPipe(source string, reader *os.File, mirror *os.File) {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), maxLineBytes)
for scanner.Scan() {
record(source, append(scanner.Bytes(), '\n'))
}
if err := scanner.Err(); err != nil {
record("securelog", []byte(fmt.Sprintf("%s capture error: %v\n", source, err)))
}
}
func record(source string, raw []byte) {
if !enabled.Load() || publicKey == nil || len(raw) == 0 {
return
}
if len(raw) > maxLineBytes {
raw = append(raw[:maxLineBytes], []byte("\n[securelog] line truncated\n")...)
}
raw = bytes.TrimRight(raw, "\r\n")
if len(raw) == 0 {
return
}
nextSeq := atomic.AddUint64(&seq, 1)
now := time.Now().UnixMilli()
blob, err := encryptEntry(nextSeq, now, source, raw)
if err != nil {
setError(err.Error())
return
}
entry := Entry{Seq: nextSeq, At: now, Source: source, Blob: blob}
mu.Lock()
if len(entries) >= maxEntries {
copy(entries, entries[1:])
entries[len(entries)-1] = entry
dropped++
} else {
entries = append(entries, entry)
}
mu.Unlock()
mirrorEncrypted(entry)
}
func encryptEntry(seq uint64, at int64, source string, plaintext []byte) (string, error) {
aesKey := make([]byte, 32)
nonce := make([]byte, 12)
if _, err := rand.Read(aesKey); err != nil {
return "", err
}
if _, err := rand.Read(nonce); err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
aad := []byte(fmt.Sprintf("%d:%d:%s", seq, at, source))
ciphertext := gcm.Seal(nil, nonce, plaintext, aad)
wrapped, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, aesKey, aad)
if err != nil {
return "", err
}
env := encryptedEnvelope{
V: 1,
Alg: "RSA-OAEP-SHA256+A256GCM",
Seq: seq,
At: at,
Source: source,
WrappedKey: base64.StdEncoding.EncodeToString(wrapped),
Nonce: base64.StdEncoding.EncodeToString(nonce),
Ciphertext: base64.StdEncoding.EncodeToString(ciphertext),
}
jsonBytes, err := json.Marshal(env)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(jsonBytes), nil
}
func mirrorEncrypted(entry Entry) {
if originalOut == nil {
return
}
_, _ = fmt.Fprintf(originalOut, "OVERLORD-SECURE-LOG v1 seq=%d source=%s %s\n", entry.Seq, entry.Source, entry.Blob)
}
func parsePublicKey(publicKeyB64 string) (*rsa.PublicKey, error) {
raw := strings.TrimSpace(publicKeyB64)
if raw == "" {
return nil, fmt.Errorf("secure log public key missing")
}
der, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("secure log public key decode failed: %w", err)
}
pub, err := x509.ParsePKIXPublicKey(der)
if err != nil {
return nil, fmt.Errorf("secure log public key parse failed: %w", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("secure log public key is not RSA")
}
return rsaPub, nil
}
func redirectToNull() {
file, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return
}
os.Stdout = file
os.Stderr = file
}
func setError(msg string) {
lastError.Store(msg)
}
func currentError() string {
if v := lastError.Load(); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}