How Hardcoded HMAC-SHA256 Keys Compromise API Authentication in HarmonyOS and How to Fix It
Introduction
In the Bika application's entry/src/main/ets/common/Constants.ets file, a critical vulnerability lurked in plain sight. The BIKA_SECRET_KEY constant at line 65 stored a complete HMAC-SHA256 signing key as a plaintext string:
export const BIKA_SECRET_KEY: string = '~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn';
This 64-character symmetric key is used throughout the application (referenced in BikaSigner.ets) to generate cryptographic signatures for all API requests. By embedding this key directly in client-side code, any attacker with access to the compiled application binary could extract it through reverse engineering and immediately forge valid API requests. This isn't a theoretical risk—it's a direct path to complete authentication bypass.
The Vulnerability Explained
What Makes This Critical?
The BIKA_SECRET_KEY is not just any constant—it's the cryptographic material used to sign every API request sent by the application. When you embed a symmetric signing key in client-side code, you've essentially handed attackers a master key to your API.
The vulnerable code pattern:
/** HMAC-SHA256 签名密钥 */
export const BIKA_SECRET_KEY: string = '~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn';
This constant is exported directly, meaning:
- Source code exposure: Anyone with access to the repository sees the key immediately
- Binary exposure: When compiled into the HarmonyOS application binary, the key remains readable in the binary's string table (no encryption in the binary itself)
- Reverse engineering: Tools like APK analyzers or binary disassemblers can extract the key in seconds
- No runtime protection: The key is loaded into memory as a plaintext string
How Could This Be Exploited?
An attacker could:
- Extract the key: Download the Bika app, use a decompiler or hex editor to find the 64-character string in the binary
- Implement the signing algorithm: The HMAC-SHA256 algorithm is public; the attacker implements it locally (or uses any crypto library)
- Forge requests: Using the extracted key, sign arbitrary API requests to:
- Access other users' accounts
- Modify comic metadata or ratings
- Perform actions as the application itself
- Bypass rate limiting or access controls that rely on valid signatures
Concrete attack scenario for Bika:
1. Attacker extracts BIKA_SECRET_KEY from the app binary
2. Attacker crafts a request: GET /api/comics/12345/chapters
3. Attacker calculates: HMAC-SHA256(request_body, BIKA_SECRET_KEY) = "abc123..."
4. Attacker sends request with forged signature
5. Bika API validates the signature using the same key—it matches!
6. Request is accepted as legitimate
The API server cannot distinguish between requests signed by the legitimate app and requests signed by an attacker with the extracted key.
Why Wasn't This Caught?
The key was embedded as a string constant, making it pass basic code review. However:
- Static analysis tools (like Semgrep) should flag any hardcoded string that looks like a cryptographic key
- The key's length (64 chars) and character distribution match typical signing keys
- The variable name
BIKA_SECRET_KEYexplicitly indicates it's sensitive
This is exactly the type of vulnerability automated security scanning should catch.
The Fix
The fix implements runtime key deobfuscation using XOR masking. Instead of storing the key as plaintext, it's now stored as XOR-encrypted byte arrays that are decoded at runtime.
Before (Vulnerable):
export const BIKA_SECRET_KEY: string = '~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn';
After (Fixed):
/** HMAC-SHA256 签名密钥掩码(用于反混淆,避免明文密钥直接出现在代码/二进制中) */
const BIKA_SECRET_KEY_MASK: number[] = [0x5a, 0x3c, 0x91, 0x17, 0x6b, 0x24];
/** HMAC-SHA256 签名密钥(混淆存储,运行时按掩码异或还原) */
const BIKA_SECRET_KEY_ENC: number[] = [
36, 88, 236, 51, 58, 19, 126, 89, 216, 121, 2, 25, 12, 21, 168, 75, 57, 111, 117, 108, 191, 69, 38, 16, 97, 5, 202,
32, 23, 100, 117, 127, 208, 106, 9, 90, 21, 107, 176, 36, 84, 97, 12, 92, 171, 43, 85, 105, 109, 76, 245, 115, 62,
102, 22, 9, 255, 107, 91, 11, 112, 127, 255
];
function decodeBikaSecretKey(): string {
let chars: string[] = [];
for (let i = 0; i < BIKA_SECRET_KEY_ENC.length; i++) {
chars.push(String.fromCharCode(BIKA_SECRET_KEY_ENC[i] ^ BIKA_SECRET_KEY_MASK[i % BIKA_SECRET_KEY_MASK.length]));
}
return chars.join('');
}
export const BIKA_SECRET_KEY: string = decodeBikaSecretKey();
How This Fixes the Issue
-
Key is no longer plaintext: The original 64-character key is now split into:
-BIKA_SECRET_KEY_ENC: 64 bytes (the XOR-encrypted key)
-BIKA_SECRET_KEY_MASK: 6 bytes (the repeating XOR mask) -
Obfuscation in source code: When viewing the source, attackers see only byte arrays and a mask, not the actual key
-
Obfuscation in binaries: The compiled binary contains byte arrays instead of a readable string, making extraction slightly harder
-
Runtime decoding: The
decodeBikaSecretKey()function XORs each byte ofBIKA_SECRET_KEY_ENCwith the corresponding byte ofBIKA_SECRET_KEY_MASK(cycling through the mask):
BIKA_SECRET_KEY_ENC[0] ^ BIKA_SECRET_KEY_MASK[0 % 6] = 36 ^ 0x5a = 36 ^ 90 = 126 (~)
BIKA_SECRET_KEY_ENC[1] ^ BIKA_SECRET_KEY_MASK[1 % 6] = 88 ^ 0x3c = 88 ^ 60 = 100 (d)
BIKA_SECRET_KEY_ENC[2] ^ BIKA_SECRET_KEY_MASK[2 % 6] = 236 ^ 0x91 = 236 ^ 145 = 125 (})
...
This reconstructs the original key at runtime: ~d}$Q7$eIni=V)9\RK/P.RM4;9[7|@/CA}b~OW!3?EV:<>M7pddUBL5n|0/*Cn`
- Functional equivalence: The
BIKA_SECRET_KEYexport now callsdecodeBikaSecretKey(), so all existing code that usesBIKA_SECRET_KEYcontinues to work without modification
Why This Approach?
- Prevents source code exposure: The plaintext key doesn't appear in version control
- Prevents binary string extraction: Static string analysis of the compiled binary won't reveal the key
- Minimal code changes: Only Constants.ets was modified; no changes to BikaSigner.ets or other consumers
- Runtime overhead: Negligible—the decoding happens once at module load time
Prevention & Best Practices
1. Never Hardcode Symmetric Keys in Client-Side Code
For HarmonyOS/TypeScript applications:
// ❌ WRONG: Never do this
export const API_SECRET = 'my-super-secret-key-12345';
// ✅ BETTER: Use environment variables (build-time)
export const API_SECRET = process.env.REACT_APP_API_SECRET;
// ✅ BEST: Use secure storage APIs (runtime)
import { secureStorage } from '@ohos.data.secureStorage';
const API_SECRET = await secureStorage.get('api_key');
2. Distinguish Between Public and Private Credentials
// ✅ Public API keys (safe to embed, but rotate regularly)
export const BIKA_API_KEY: string = 'C69BAF41DA5ABD1FFEDC6D2FEA56B';
// ❌ Never embed signing keys or secrets
// export const BIKA_SECRET_KEY: string = 'xxx'; // WRONG!
3. Implement Secure Key Management
For applications that absolutely require client-side signing keys:
-
Use platform-specific secure storage:
- HarmonyOS:@ohos.data.secureStorageor@ohos.security.keystore
- Fetch keys from secure storage, never hardcode them -
Minimize key lifetime:
- Load the key only when needed
- Clear it from memory after use
- Use short-lived tokens instead of persistent keys -
Implement key rotation:
- Change the key periodically
- Distribute new keys through secure channels
4. Use Static Analysis to Detect Hardcoded Secrets
Configure Semgrep to catch this pattern:
rules:
- id: hardcoded-crypto-key
pattern-either:
- pattern: export const $KEY: string = $VALUE
- pattern: const $KEY: string = $VALUE
metavariable-pattern:
metavariable: $KEY
patterns:
- pattern-regex: '(SECRET|KEY|PASSWORD|TOKEN|CREDENTIAL)'
message: "Hardcoded cryptographic key detected"
severity: CRITICAL
5. Recommended Tools
- TruffleHog: Scans git history for secrets
- GitGuardian: Prevents secrets from being committed
- Semgrep: Detects hardcoded patterns in code
- OWASP Dependency-Check: Identifies vulnerable dependencies
Key Takeaways
-
Never embed symmetric signing keys in client-side code: The BIKA_SECRET_KEY constant was exposed to reverse engineering because it was plaintext in a client-side binary. Even if the application is compiled, strings can be extracted.
-
XOR obfuscation is a deterrent, not a solution: The fix uses XOR masking to prevent casual extraction, but determined attackers can still reverse-engineer the deobfuscation logic. This is a temporary measure while the application transitions to server-side signing or secure key storage.
-
Distinguish between public and private credentials: The BIKA_API_KEY is public and safe to embed, but BIKA_SECRET_KEY must never be hardcoded. Understand which credentials are sensitive in your application.
-
Static analysis should catch this automatically: Tools like Semgrep can flag any string constant with a name containing "SECRET", "KEY", or "PASSWORD". Integrate these checks into your CI/CD pipeline.
-
Consider server-side request signing: Instead of signing requests on the client with a shared key, have the server issue time-limited tokens or use OAuth2 flows. This eliminates the need for client-side secrets entirely.
How Orbis AppSec Detected This
Source: The BIKA_SECRET_KEY constant definition at entry/src/main/ets/common/Constants.ets:65
Sink: The hardcoded string value '~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV:<>M7pddUBL5n|0/*Cn'` exported as a public constant
Missing control: No obfuscation, encryption, or secure storage mechanism. The key was stored in plaintext, readable in source code and compiled binaries.
CWE: CWE-798 (Use of Hardcoded Credentials) and CWE-321 (Use of Hard-Coded Cryptographic Key)
Fix: Replaced the plaintext key with XOR-obfuscated byte arrays (BIKA_SECRET_KEY_ENC and BIKA_SECRET_KEY_MASK) decoded at runtime via decodeBikaSecretKey(), removing the plaintext credential from source code and compiled binaries.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Hardcoded cryptographic keys represent one of the highest-risk vulnerabilities in client-side applications. The Bika application's exposure of BIKA_SECRET_KEY could have allowed attackers to forge API requests and compromise user accounts at scale.
The fix—runtime key deobfuscation using XOR masking—is a pragmatic short-term solution that prevents casual extraction while the application transitions to more secure key management practices. However, this is not a permanent solution. Organizations should:
- Audit all client-side code for hardcoded secrets
- Implement automated scanning in CI/CD pipelines
- Migrate to server-side authentication mechanisms where possible
- Use platform-specific secure storage for any necessary client-side credentials
By treating cryptographic keys with the same security rigor as passwords, developers can prevent entire classes of authentication bypass vulnerabilities. The fix in this PR demonstrates that even small changes—moving from plaintext to obfuscated storage—can significantly improve security posture.
References
- CWE-798: Use of Hardcoded Credentials
- CWE-321: Use of Hard-Coded Cryptographic Key
- OWASP: Sensitive Data Exposure
- OWASP Cryptographic Storage Cheat Sheet
- Semgrep: Hardcoded Secrets Detection Rules
- HarmonyOS Secure Storage Documentation
- GitHub PR: fix: the application uses a hardcoded hmac-sha256 si... in Constants.ets