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¶
-
Get the current certificate fingerprint:
-
Add both current and backup pins (two-pin policy prevents lockout during rotation)
-
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:
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-IdX-Forwarded-ByViaX-Burp-VersionX-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:
- Auto-detects the target platform (Windows/macOS/Linux)
- Runs
flutter buildwith--obfuscateand--split-debug-info - Injects
HMAC_BASE_SECRETvia--dart-define - Optionally signs the binary (
codesignon macOS,signtoolon Windows) - Saves debug symbols to
build/debug-info/(keep for crash symbolication, never distribute)