Files
bigbrother/utils/crypto.py
T
n0mad1k 62a1010ab4 Add Phase 1 utility modules: crypto, networking, logging, stealth, resource, permissions, config_loader, bettercap_api
9 files in utils/ providing the shared infrastructure layer:
- crypto: AES-256-GCM file encryption, argon2id/PBKDF2 key derivation, HKDF network unlock, LUKS container management
- networking: interface detection, MAC/IP helpers, BPF compilation via libpcap ctypes, gratuitous ARP, VLAN creation
- logging: encrypted log writer (BBLogger) with rotation, per-module files, stdout suppression for stealth
- stealth: process rename via prctl, cmdline spoofing, sysctl helpers, timestomping, core dump disable
- resource: hardware tier detection (OPi Zero 3 / Pi Zero / generic) via /proc/device-tree and cpuinfo, resource monitoring
- permissions: root/capability checks via capget ctypes, privilege dropping, directory permission enforcement
- config_loader: YAML merge hierarchy (hardware_tiers -> bigbrother -> modules -> stealth -> CLI), tier auto-detection, SIGHUP reload
- bettercap_api: REST client with per-session random credentials, session/events/command methods
2026-03-18 08:11:25 -04:00

190 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""AES-256-GCM encryption, key derivation, and LUKS container management."""
import os
import struct
import subprocess
import hashlib
from pathlib import Path
from typing import Optional
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
try:
from argon2.low_level import hash_secret_raw, Type
HAS_ARGON2 = True
except ImportError:
HAS_ARGON2 = False
NONCE_SIZE = 12
KEY_SIZE = 32
SALT_SIZE = 16
HEADER_MAGIC = b"BB01"
HEADER_FMT = f"!4s{SALT_SIZE}s{NONCE_SIZE}s"
HEADER_SIZE = struct.calcsize(HEADER_FMT)
def derive_key(password: bytes, salt: bytes, method: str = "argon2id") -> bytes:
if method == "argon2id" and HAS_ARGON2:
return hash_secret_raw(
secret=password,
salt=salt,
time_cost=3,
memory_cost=65536,
parallelism=4,
hash_len=KEY_SIZE,
type=Type.ID,
)
return PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_SIZE,
salt=salt,
iterations=600_000,
).derive(password)
def derive_network_key(network_secret: bytes, device_serial: bytes) -> bytes:
return HKDF(
algorithm=hashes.SHA256(),
length=KEY_SIZE,
salt=device_serial,
info=b"bigbrother-luks-unlock",
).derive(network_secret)
class CryptoEngine:
def __init__(self, key: bytes):
if len(key) != KEY_SIZE:
raise ValueError(f"Key must be {KEY_SIZE} bytes")
self._aesgcm = AESGCM(key)
self._key = key
def encrypt(self, plaintext: bytes, aad: Optional[bytes] = None) -> bytes:
nonce = os.urandom(NONCE_SIZE)
ct = self._aesgcm.encrypt(nonce, plaintext, aad)
return nonce + ct
def decrypt(self, data: bytes, aad: Optional[bytes] = None) -> bytes:
nonce = data[:NONCE_SIZE]
ct = data[NONCE_SIZE:]
return self._aesgcm.decrypt(nonce, ct, aad)
def encrypt_file(
src: str,
dst: str,
password: bytes,
method: str = "argon2id",
chunk_size: int = 64 * 1024,
) -> None:
salt = os.urandom(SALT_SIZE)
key = derive_key(password, salt, method)
engine = CryptoEngine(key)
with open(src, "rb") as fin:
plaintext = fin.read()
nonce = os.urandom(NONCE_SIZE)
aesgcm = AESGCM(key)
ct = aesgcm.encrypt(nonce, plaintext, None)
with open(dst, "wb") as fout:
fout.write(struct.pack(HEADER_FMT, HEADER_MAGIC, salt, nonce))
fout.write(ct)
def decrypt_file(
src: str,
dst: str,
password: bytes,
method: str = "argon2id",
) -> None:
with open(src, "rb") as fin:
header = fin.read(HEADER_SIZE)
magic, salt, nonce = struct.unpack(HEADER_FMT, header)
if magic != HEADER_MAGIC:
raise ValueError("Invalid encrypted file header")
ct = fin.read()
key = derive_key(password, salt, method)
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ct, None)
with open(dst, "wb") as fout:
fout.write(plaintext)
class LUKSContainer:
def __init__(self, container_path: str, mount_point: str, mapper_name: str = "bb_storage"):
self.container_path = container_path
self.mount_point = mount_point
self.mapper_name = mapper_name
self.device_path = f"/dev/mapper/{mapper_name}"
def create(self, size_mb: int, passphrase: bytes) -> bool:
try:
subprocess.run(
["dd", "if=/dev/zero", f"of={self.container_path}",
"bs=1M", f"count={size_mb}"],
check=True, capture_output=True,
)
proc = subprocess.run(
["cryptsetup", "luksFormat", "--type", "luks2",
"--cipher", "aes-xts-plain64", "--key-size", "512",
"--hash", "sha256", "--batch-mode", self.container_path],
input=passphrase + b"\n",
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
def open(self, passphrase: bytes) -> bool:
try:
subprocess.run(
["cryptsetup", "open", self.container_path,
self.mapper_name, "--type", "luks2"],
input=passphrase + b"\n",
check=True, capture_output=True,
)
Path(self.mount_point).mkdir(parents=True, exist_ok=True)
subprocess.run(
["mount", self.device_path, self.mount_point],
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
def close(self) -> bool:
try:
subprocess.run(
["umount", self.mount_point],
check=True, capture_output=True,
)
subprocess.run(
["cryptsetup", "close", self.mapper_name],
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
def destroy_header(self) -> bool:
try:
subprocess.run(
["dd", "if=/dev/urandom", f"of={self.container_path}",
"bs=1M", "count=2", "conv=notrunc"],
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
@property
def is_open(self) -> bool:
return Path(self.device_path).exists()