Skip to content

Desktop Binary Hardening

The desktop app (Flutter/Dart) runs on the user's machine. Unlike the web frontend, the compiled binary can be analyzed with reverse engineering tools: decompilers, strings, hex editors, and debuggers. This layer hardens the binary against static and dynamic analysis.


HMAC Key Derivation

Problem: Hardcoded Key

The HMAC signing key was previously hardcoded as byte literals in security_service.dart:

// BEFORE: Key visible via `strings` on the binary
static Uint8List _deriveKey() {
  final parts = <int>[
    0x42, 0x44, 0x2D, 0x41, 0x50, 0x50, 0x2D, 0x48, // BD-APP-H
    0x4D, 0x41, 0x43, 0x2D, 0x4B, 0x45, 0x59, 0x2D, // MAC-KEY-
    0x56, 0x31, 0x2E, 0x30, 0x2E, 0x30, 0x2D, 0x53, // V1.0.0-S
    0x45, 0x43, 0x55, 0x52, 0x45, 0x2D, 0x42, 0x44, // ECURE-BD
  ];
  final keyBytes = sha256.convert(parts).bytes;
  return Uint8List.fromList(keyBytes);
}

Running strings desktop_app.exe | grep BD-APP would reveal the key. Even without the literal string, the byte values are constant in the compiled code and extractable via disassembly.

Solution: Server + Device Derivation

The key is now derived from two runtime components that are not present in the binary:

static Uint8List _deriveKey() {
  // Component 1: Build-time secret (injected via CI, not in source)
  const basePlaceholder = String.fromEnvironment(
    'HMAC_BASE_SECRET',
    defaultValue: 'dev-only-replace-in-ci',
  );

  // Component 2: Device fingerprint (runtime, not in binary)
  final deviceFp = _computeDeviceFingerprint();

  // HKDF-like derivation: HMAC(deviceFp, basePlaceholder)
  final hmac = Hmac(sha256, utf8.encode(deviceFp));
  final derived = hmac.convert(utf8.encode(basePlaceholder));
  return Uint8List.fromList(derived.bytes);
}

Key Properties

Property Before After
Extractable from binary Yes (strings) No
Same across all devices Yes No (device fingerprint)
Same across all builds Yes No (--dart-define)
Usable after copying binary Yes No (wrong device FP)

Device Fingerprint

The device fingerprint binds the key to the specific machine:

static String _computeDeviceFingerprint() {
  final components = <String>[
    Platform.localHostname,          // Machine name
    Platform.operatingSystem,        // "windows", "macos", "linux"
    Platform.operatingSystemVersion, // OS build number
    Platform.resolvedExecutable,     // Full path to this binary
  ];
  final raw = components.join(':');
  return sha256.convert(utf8.encode(raw)).toString();
}

This is not a secret -- it merely ensures that a key derived on Machine A is useless on Machine B.

Build-Time Secret Injection

The base secret is injected during CI/CD builds:

flutter build windows --release \
  --dart-define=HMAC_BASE_SECRET="$(vault read -field=hmac_secret secret/bd-app)" \
  --obfuscate \
  --split-debug-info=build/debug-info

In development, the default value dev-only-replace-in-ci is used. This value should never appear in production builds.


Certificate Pinning

Purpose

Certificate pinning prevents proxy tools (Burp Suite, mitmproxy, Charles, Fiddler) from intercepting HTTPS traffic by rejecting any TLS certificate that doesn't match pre-configured fingerprints.

Configuration

static const List<String> _pinnedCertFingerprints = [
  // Pin 1: Current production certificate
  // 'SHA256:AA:BB:CC:...',
  // Pin 2: Next rotation certificate (backup)
  // 'SHA256:DD:EE:FF:...',
];

Setup Procedure

  1. Get the current certificate fingerprint:

    openssl s_client -connect api.bedefended.com:443 < /dev/null 2>/dev/null \
      | openssl x509 -fingerprint -sha256 -noout
    

  2. Add both current and backup pins (two-pin policy prevents lockout during rotation)

  3. Update pins during certificate rotation -- add the new cert pin before removing the old one

Behavior

Scenario Connection Result
Direct HTTPS (no proxy) Cert matches pin Allowed
Burp Suite with custom CA Cert does NOT match pin Rejected
mitmproxy with MITM cert Cert does NOT match pin Rejected
localhost / 127.0.0.1 Pin check skipped Allowed (dev mode)
No pins configured Pin check skipped Allowed (pre-production)

Proxy Detection

The app detects common proxy configurations at startup:

static bool detectProxy() {
  final proxyVars = [
    'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy',
    'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy',
  ];
  for (final varName in proxyVars) {
    if (Platform.environment[varName]?.isNotEmpty == true) {
      return true;
    }
  }
  return false;
}

Additionally, the HttpClient is configured to bypass system proxy settings:

client.findProxy = (uri) => 'DIRECT';

This forces all connections to go directly to the server, ignoring any system-level proxy configuration.


Reverse Engineering Tool Detection

The app detects common RE tools running on the system:

Platform Detected Tools
Windows x64dbg, x32dbg, OllyDbg, IDA, Ghidra, dnSpy, dotPeek, ILSpy, Fiddler, Charles, Wireshark, Burp Suite, mitmproxy, HTTP Toolkit
macOS LLDB, Hopper, IDA, Ghidra, Charles, Proxyman, mitmproxy, Wireshark, Burp Suite, HTTP Toolkit

Best-effort detection

Process enumeration can be bypassed by renaming executables, using process hollowing, or running tools in VMs. This is a deterrent, not a guarantee.


Response Validation

Outgoing requests include anti-tampering headers, and incoming responses are checked for proxy injection markers:

Request Signing

X-BD-Timestamp: 1710432000000
X-BD-Nonce: 42
X-BD-Signature: <HMAC-SHA256 of method:path:timestamp:nonce>
X-BD-Client: desktop
X-BD-Version: 1.0.0

Response Validation

The app checks for headers that indicate proxy interception:

  • X-Proxy-Id
  • X-Forwarded-By
  • Via
  • X-Burp-Version
  • X-Charles-Version

If any of these headers are present, validateResponse() returns false, alerting the app that a MITM proxy may be active.


Build Hardening Checklist

For production releases:

Step Command / Action Purpose
Obfuscation --obfuscate --split-debug-info=build/debug-info Mangle class/method names
Secret injection --dart-define=HMAC_BASE_SECRET=<value> No hardcoded secrets
Certificate pins Populate _pinnedCertFingerprints list Block proxy interception
Code signing Sign with company certificate Prevent binary patching
Debug info separation --split-debug-info= stores symbols separately No debug symbols in release binary

Using the Build Script

The desktop/build.sh script automates all hardening steps:

# Basic production build (auto-detects platform)
./build.sh

# With custom HMAC secret
./build.sh --secret "my-production-secret"

# With code signing (macOS or Windows)
./build.sh --sign

# Full CI/CD command
BD_HMAC_SECRET="$(vault read -field=hmac_secret secret/bd-app)" \
BD_CODESIGN_IDENTITY="Developer ID Application: BeDefended Srl" \
./build.sh --sign

The script:

  1. Auto-detects the target platform (Windows/macOS/Linux)
  2. Runs flutter build with --obfuscate and --split-debug-info
  3. Injects HMAC_BASE_SECRET via --dart-define
  4. Optionally signs the binary (codesign on macOS, signtool on Windows)
  5. Saves debug symbols to build/debug-info/ (keep for crash symbolication, never distribute)

Manual Build Command

flutter build windows --release \
  --obfuscate \
  --split-debug-info=build/debug-info \
  --dart-define=HMAC_BASE_SECRET="$(vault read -field=hmac_secret secret/bd-app)"