Skip to content

Commit 532ecc0

Browse files
authored
Merge pull request #144 from BenCodez/copilot/improve-invalid-vote-error-handling
Fix test helper validation to match VoteReceiver implementation
2 parents 590b1f8 + 115c023 commit 532ecc0

2 files changed

Lines changed: 268 additions & 25 deletions

File tree

‎VotifierPlus/src/main/java/com/vexsoftware/votifier/net/VoteReceiver.java‎

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ public void run() {
240240
debug("Reading V1 vote block (256 bytes expected) at " + startTime + " ms");
241241

242242
if (in.available() < 256) {
243+
logWarning("Invalid vote format: Insufficient data for V1 vote block from " + address
244+
+ " (expected 256 bytes)");
243245
debug("Insufficient data available for V1 vote block; closing connection from " + address);
244246
writer.close();
245247
in.close();
@@ -263,20 +265,26 @@ public void run() {
263265
try {
264266
decrypted = RSA.decrypt(block, getKeyPair().getPrivate());
265267
} catch (BadPaddingException e) {
268+
// Log only the first 32 bytes as hex to avoid exposing full vote block
266269
StringBuilder blockHex = new StringBuilder();
267-
for (byte b : block) {
268-
blockHex.append(String.format("%02X ", b));
270+
int bytesToLog = Math.min(32, block.length);
271+
for (int i = 0; i < bytesToLog; i++) {
272+
blockHex.append(String.format("%02X ", block[i]));
273+
}
274+
if (block.length > bytesToLog) {
275+
blockHex.append("... (truncated)");
269276
}
270277
logWarning(
271-
"Decryption failed. Either the vote block is invalid or the public key does not match the server list from "
278+
"Decryption failed: Invalid V1 vote block or public key mismatch from "
272279
+ address);
280+
debug("Vote block preview (first 32 bytes): " + blockHex.toString().trim());
273281
throw e;
274282
}
275283
int position = 0;
276284
String opcode = readString(decrypted, position);
277285
position += opcode.length() + 1;
278286
if (!opcode.equals("VOTE")) {
279-
throw new Exception("Unable to decode RSA: invalid opcode " + opcode);
287+
throw new Exception("Invalid vote format: Expected opcode 'VOTE' but got '" + opcode + "' from " + address);
280288
}
281289
String serviceName = readString(decrypted, position);
282290
position += serviceName.length() + 1;
@@ -290,6 +298,8 @@ public void run() {
290298
+ "\n";
291299
debug("Processed V1 vote block.");
292300
} else {
301+
logWarning("Invalid vote format: Failed to read complete V1 vote block from " + address
302+
+ " (expected 256 bytes, got " + totalRead + ")");
293303
debug("Failed to read V1 vote, random ping? expected 256 bytes, got " + totalRead);
294304
continue;
295305
// throw new Exception("Failed to read V1 vote block: expected 256 bytes, got "
@@ -322,7 +332,7 @@ public void run() {
322332
int jsonEnd = voteData.lastIndexOf("}");
323333
if (jsonStart == -1 || jsonEnd == -1 || jsonStart > jsonEnd) {
324334
throw new Exception(
325-
"Expected JSON-formatted vote payload, got: " + voteData + " from " + address);
335+
"Invalid vote format: Expected JSON-formatted vote payload, got: " + voteData + " from " + address);
326336
}
327337
String jsonPayloadRaw = voteData.substring(jsonStart, jsonEnd + 1).trim();
328338
debug("Extracted raw JSON payload: [" + jsonPayloadRaw + "]");
@@ -332,20 +342,53 @@ public void run() {
332342
if (jsonPayloadRaw.startsWith("[")) {
333343
JsonArray jsonArray = gson.fromJson(jsonPayloadRaw, JsonArray.class);
334344
if (jsonArray.size() == 0) {
335-
throw new Exception("Empty JSON array in vote payload from " + address);
345+
throw new Exception("Invalid vote format: Empty JSON array in vote payload from " + address);
336346
}
337347
voteMessage = jsonArray.get(0).getAsJsonObject();
338348
} else {
339349
voteMessage = gson.fromJson(jsonPayloadRaw, JsonObject.class);
340350
}
341351

342-
// Extract the inner payload and signature.
352+
// Validate and extract the outer payload and signature fields.
353+
if (!voteMessage.has("payload")) {
354+
throw new Exception("Invalid vote format: Missing required 'payload' field in outer JSON from " + address);
355+
}
356+
if (!voteMessage.has("signature")) {
357+
throw new Exception("Invalid vote format: Missing required 'signature' field in outer JSON from " + address);
358+
}
343359
String payload = voteMessage.get("payload").getAsString();
344360
String sigHash = voteMessage.get("signature").getAsString();
345-
byte[] sigBytes = Base64.getDecoder().decode(sigHash);
361+
byte[] sigBytes;
362+
try {
363+
sigBytes = Base64.getDecoder().decode(sigHash);
364+
} catch (IllegalArgumentException e) {
365+
throw new Exception("Invalid vote format: Signature is not valid Base64 from " + address + ": " + e.getMessage());
366+
}
346367

347368
// Parse the inner payload JSON.
348-
JsonObject votePayload = gson.fromJson(payload, JsonObject.class);
369+
JsonObject votePayload;
370+
try {
371+
votePayload = gson.fromJson(payload, JsonObject.class);
372+
} catch (Exception e) {
373+
throw new Exception("Invalid vote format: Inner payload is not valid JSON from " + address + ": " + e.getMessage());
374+
}
375+
376+
// Validate required fields in inner payload.
377+
if (!votePayload.has("serviceName")) {
378+
throw new Exception("Invalid vote format: Missing required 'serviceName' field in vote payload from " + address);
379+
}
380+
if (!votePayload.has("username")) {
381+
throw new Exception("Invalid vote format: Missing required 'username' field in vote payload from " + address);
382+
}
383+
if (!votePayload.has("address")) {
384+
throw new Exception("Invalid vote format: Missing required 'address' field in vote payload from " + address);
385+
}
386+
if (!votePayload.has("timestamp")) {
387+
throw new Exception("Invalid vote format: Missing required 'timestamp' field in vote payload from " + address);
388+
}
389+
if (!votePayload.has("challenge")) {
390+
throw new Exception("Invalid vote format: Missing required 'challenge' field in vote payload from " + address);
391+
}
349392

350393
// Retrieve serviceName from the inner JSON.
351394
String serviceNameFromPayload = votePayload.get("serviceName").getAsString();
@@ -355,16 +398,19 @@ public void run() {
355398
if (key == null) {
356399
key = getTokens().get("default");
357400
if (key == null) {
358-
throw new Exception("Unknown token for service '" + serviceNameFromPayload + "'");
401+
throw new Exception("Authentication failed: Unknown token for service '" + serviceNameFromPayload + "' from " + address);
359402
}
403+
debug("Using default token for service: " + serviceNameFromPayload);
404+
} else {
405+
debug("Using service-specific token for: " + serviceNameFromPayload);
360406
}
361407

362408
// Debug: log the payload string and its computed HMAC for comparison.
363409
debug("Inner payload string: [" + payload + "]");
364410

365411
// Verify HMAC signature using the payload bytes.
366412
if (!hmacEqual(sigBytes, payload.getBytes(StandardCharsets.UTF_8), key)) {
367-
throw new Exception("Signature is not valid (invalid token?) from " + address);
413+
throw new Exception("Authentication failed: Signature verification failed (invalid token?) for service '" + serviceNameFromPayload + "' from " + address);
368414
}
369415

370416
// Extract vote fields from the inner payload.
@@ -373,14 +419,11 @@ public void run() {
373419
address1 = votePayload.get("address").getAsString();
374420
timeStamp = votePayload.get("timestamp").getAsString();
375421

376-
// Check the challenge.
377-
if (!votePayload.has("challenge")) {
378-
throw new Exception("Vote payload missing challenge field from " + address);
379-
}
422+
// Verify the challenge.
380423
String receivedChallenge = votePayload.get("challenge").getAsString().trim();
381424
if (!receivedChallenge.equals(challenge.trim())) {
382425
throw new Exception(
383-
"Invalid challenge: expected " + challenge + " but got " + receivedChallenge);
426+
"Authentication failed: Invalid challenge (expected '" + challenge + "' but got '" + receivedChallenge + "') from " + address);
384427
}
385428
} else {
386429
String[] fields = voteData.split("\n");
@@ -428,25 +471,32 @@ public void run() {
428471
in.close();
429472
socket.close();
430473
} catch (MalformedJsonException ex) {
431-
logWarning("Malformed JSON payload received from: " + address + " - " + ex.getMessage());
474+
logWarning("Invalid vote format: Malformed JSON payload received from " + address + " - " + ex.getMessage());
432475
debug(ex);
433476
} catch (SocketException ex) {
434477
if (running) {
435-
logWarning("Protocol error from: " + address + " - " + ex.getLocalizedMessage());
478+
logWarning("Connection error: Protocol error from " + address + " - " + ex.getLocalizedMessage());
436479
debug(ex);
437480
} else {
438481
logWarning("Votifier socket closed.");
439482
}
440483
} catch (BadPaddingException ex) {
441-
logWarning("Unable to decrypt vote record from: " + address
484+
logWarning("Authentication failed: Unable to decrypt V1 vote record from " + address
442485
+ ". Make sure that your public key matches the one you gave the server list.");
443486
debug(ex);
444487
} catch (SocketTimeoutException ex) {
445-
logWarning("Socket timeout while waiting for vote payload from: " + address + " - " + ex.getMessage());
488+
logWarning("Connection timeout: Socket timeout while waiting for vote payload from " + address + " - " + ex.getMessage());
446489
debug(ex);
447490
} catch (Exception ex) {
448-
logWarning("Exception caught while receiving a vote notification from: " + address + " - "
449-
+ ex.getLocalizedMessage());
491+
String errorMsg = ex.getMessage();
492+
if (errorMsg != null && (errorMsg.startsWith("Invalid vote format:") ||
493+
errorMsg.startsWith("Authentication failed:"))) {
494+
// These are validation errors with detailed messages, log them as-is
495+
logWarning(errorMsg);
496+
} else {
497+
// Generic exception with less context
498+
logWarning("Error processing vote from " + address + ": " + ex.getLocalizedMessage());
499+
}
450500
debug(ex);
451501
}
452502
}

0 commit comments

Comments
 (0)