Padding Oracle in Fiber with Hmac Signatures
Padding Oracle in Fiber with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A padding oracle attack can occur in a Fiber application when encrypted data is verified using Hmac signatures in a way that reveals whether the padding of a decrypted payload is valid before checking the integrity of the Hmac. In this combination, an attacker can send modified ciphertexts to the server and observe differences in error responses or timing to iteratively decrypt or forge messages without knowing the key.
In Fiber, if you decrypt a JWE or AES-encrypted payload and then verify an Hmac on the plaintext or on structured data (e.g., JSON Web Token contents), an error such as ErrDecrypt or an invalid padding error returned before the Hmac check can act as an oracle. For example, a server might return distinct HTTP status codes or messages for padding failures versus Hmac mismatches, allowing an attacker to distinguish between the two conditions. This leakage enables adaptive chosen-ciphertext attacks where the attacker submits many modified ciphertexts and uses the server’s responses to recover plaintext or to forge valid authenticated messages.
Consider a route that accepts an encrypted token, decrypts it, and then checks an Hmac over the payload:
const crypto = require('crypto');
const express = require('express');
const app = express();
const KEY = crypto.randomBytes(32);
const IV_LENGTH = 16;
app.post('/verify', (req, res) => {
const { ciphertext, receivedMac, data } = req.body;
const iv = ciphertext.slice(0, IV_LENGTH);
const encrypted = ciphertext.slice(IV_LENGTH);
const decipher = crypto.createDecipheriv('aes-256-cbc', KEY, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8'); // throws if padding is invalid
const expectedMac = crypto.createHmac('sha256', KEY).update(decrypted).digest('hex');
if (expectedMac !== receivedMac) {
return res.status(401).send('Invalid signature');
}
res.json({ data: JSON.parse(decrypted) });
});
In the example above, if decipher.final throws due to bad padding, the error is raised before the Hmac comparison. An attacker can use this timing and error difference to learn about padding validity, effectively turning the endpoint into a padding oracle. Even if you use constant-time comparisons for the Hmac, failing to ensure that padding errors do not leak prior to the Hmac check defeats the purpose of authenticated encryption.
To avoid this, you must ensure that decryption and padding validation are performed in a way that does not expose distinct error paths before the Hmac verification. This typically means using an AEAD cipher (such as AES-GCM) or, when using AES-CBC with Hmac, ensuring that padding errors are not surfaced distinctly and that the Hmac is computed and compared in a manner that does not depend on the validity of the padding.
Hmac Signatures-Specific Remediation in Fiber — concrete code fixes
To remediate padding oracle risks when using Hmac Signatures in Fiber, structure your code so that padding validation does not leak and the Hmac is always verified in a constant-time fashion before any error is returned to the caller. Use an authenticated encryption mode like AES-GCM where possible, or, if you must use AES-CBC, defer any padding-related errors until after the Hmac is verified and use a constant-time comparison for the Hmac.
Prefer AES-GCM for authenticated encryption, as it provides both confidentiality and integrity in a single step, eliminating the padding oracle surface. Here is an example using crypto.createCipheriv with GCM:
const crypto = require('crypto');
const express = require('express');
const app = express();
const KEY = crypto.randomBytes(32);
app.post('/verify-gcm', express.json(), (req, res) => {
const { ciphertext, iv, authTag, receivedMac } = req.body;
// GCM: ciphertext includes encrypted data; authTag is the authentication tag
const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted;
try {
decrypted = decipher.update(Buffer.from(ciphertext, 'hex'), 'hex', 'utf8');
decrypted += decipher.final('utf8');
} catch (err) {
// Do not distinguish padding vs other errors; return a generic error
return res.status(400).send('Invalid message');
}
const expectedMac = crypto.createHmac('sha256', KEY).update(decrypted).digest('hex');
// Use timing-safe compare to avoid leaking via Hmac mismatch differences
const received = Buffer.from(receivedMac, 'hex');
const expected = Buffer.from(expectedMac, 'hex');
let valid = true;
if (received.length !== expected.length) valid = false;
for (let i = 0; i < expected.length; i++) {
valid |= (received[i] ^ expected[i]);
}
if (valid !== 0) {
return res.status(401).send('Invalid signature');
}
res.json({ data: JSON.parse(decrypted) });
});
If you must use AES-CBC with separate Hmac, ensure that you do not throw or return distinct errors for padding failures before verifying the Hmac, and use a constant-time Hmac comparison. Here is a safer pattern:
const crypto = require('crypto');
const express = require('express');
const app = express();
const KEY = crypto.randomBytes(32);
const IV_LENGTH = 16;
function timingSafeEqual(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
app.post('/verify-cbc', express.json(), (req, res) => {
const { ciphertext, receivedMac, data } = req.body;
const iv = ciphertext.slice(0, IV_LENGTH);
const encrypted = ciphertext.slice(IV_LENGTH);
let decrypted;
try {
const decipher = crypto.createDecipheriv('aes-256-cbc', KEY, iv);
decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
} catch (err) {
// Generic error to avoid oracle; do not reveal padding vs other issues
return res.status(400).send('Invalid message');
}
const expectedMac = crypto.createHmac('sha256', KEY).update(decrypted).digest('hex');
const received = Buffer.from(receivedMac, 'hex');
const expected = Buffer.from(expectedMac, 'hex');
if (!timingSafeEqual(received, expected)) {
return res.status(401).send('Invalid signature');
}
res.json({ data: JSON.parse(decrypted) });
});
Key remediation points:
- Always verify the Hmac before returning any error that could distinguish padding failures from authentication failures.
- Use constant-time comparison for the Hmac to prevent timing leaks.
- Avoid returning distinct error messages or status codes for padding errors versus Hmac mismatches.
- Prefer authenticated encryption (e.g., AES-GCM) to eliminate padding handling entirely.
These practices reduce the risk of a padding oracle when Hmac Signatures are used in Fiber endpoints.