Skip to content

Commit d67b164

Browse files
authored
fix(scanner): guard against out-of-range currency byte in scan frame (#1124)
A foreign or corrupt Kik code can pass error-correction yet carry a currency byte outside CurrencyCode's range. PayloadKind.Payment.decode indexed CurrencyCode.entries with that untrusted 0-255 byte, so a frame with byte[1] = 232 threw IndexOutOfBoundsException against the 171-entry enum. Because the scanner decodes every camera frame, this stormed Bugsnag (~every 50ms) and silently broke scanning for affected users. PayloadKind.decode is now nullable: Payment.decode returns null when the currency index is out of range instead of throwing, and OpenCodePayload.fromList maps a null decode to Empty (Unknown), which the scanner ignores — no crash and no bogus grab attempt. Adds regression + boundary tests. Bugsnag: 6a5a4523e96556123eba0d68 Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 5629441 commit d67b164

3 files changed

Lines changed: 45 additions & 5 deletions

File tree

services/opencode/src/main/kotlin/com/getcode/opencode/model/core/OpenCodePayload.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ data class OpenCodePayload(
5353
}
5454

5555
val kind = PayloadKind.from(frame[0].toInt())
56-
return OpenCodePayload(kind, kind.decode(frame), kind.decodeNonce(frame))
56+
57+
// `decode` returns null for a frame this kind can't parse — e.g. a foreign or corrupt
58+
// Kik code whose currency byte is out of range (Bugsnag 6a5a4523:
59+
// IndexOutOfBoundsException on CurrencyCode.entries[232], a 171-entry enum). Treat it
60+
// as Empty so the scanner ignores it instead of crashing or attempting a bogus grab.
61+
val value = kind.decode(frame) ?: return Empty
62+
return OpenCodePayload(kind, value, kind.decodeNonce(frame))
5763
}
5864
}
5965
}

services/opencode/src/main/kotlin/com/getcode/opencode/model/core/PayloadKind.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ sealed interface PayloadKind {
1919
/** Encodes [value] (and [nonce], where applicable) into the fixed-length scan frame. */
2020
fun encode(value: PayloadValue, nonce: List<Byte>): List<Byte>
2121

22-
/** Reads this kind's [PayloadValue] out of a (zero-padded) scan [frame]. */
23-
fun decode(frame: List<Byte>): PayloadValue
22+
/**
23+
* Reads this kind's [PayloadValue] out of a (zero-padded) scan [frame], or null if the frame
24+
* can't be parsed as this kind (e.g. a cash frame whose currency byte is out of range).
25+
*/
26+
fun decode(frame: List<Byte>): PayloadValue?
2427

2528
/** Reads the nonce out of a scan [frame]; empty for kinds that carry none. */
2629
fun decodeNonce(frame: List<Byte>): List<Byte>
@@ -45,8 +48,11 @@ sealed interface PayloadKind {
4548
return data
4649
}
4750

48-
override fun decode(frame: List<Byte>): PayloadValue {
49-
val currency = CurrencyCode.entries[frame[1].byteToUnsignedInt()]
51+
override fun decode(frame: List<Byte>): PayloadValue? {
52+
// Byte 1 is an index into CurrencyCode. A foreign or corrupt Kik code can carry a byte
53+
// outside that range; such a frame isn't a valid cash code, so fail decoding rather
54+
// than throw (Bugsnag 6a5a4523: IndexOutOfBoundsException on a 171-entry enum).
55+
val currency = CurrencyCode.entries.getOrNull(frame[1].byteToUnsignedInt()) ?: return null
5056
val quarks = frame.subList(OpenCodePayload.OFFSET_QUARKS, OpenCodePayload.OFFSET_NONCE)
5157
.toByteArray().byteArrayToLong()
5258
return Fiat(currencyCode = currency, quarks = quarks)

services/opencode/src/test/kotlin/com/getcode/opencode/model/core/OpenCodePayloadTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,34 @@ class OpenCodePayloadTest {
9292
assertIs<Fiat>(decoded.value)
9393
}
9494

95+
@Test
96+
fun `fromList does not throw on out-of-range currency byte`() {
97+
// A foreign / corrupt Kik code can pass error-correction yet carry a currency byte
98+
// outside CurrencyCode's range. Regression for IndexOutOfBoundsException (Bugsnag
99+
// 6a5a4523): `CurrencyCode.entries[232]` on a 171-element enum. Such a frame is not a
100+
// valid Flipcash cash code, so it must decode to Unknown (which the scanner ignores).
101+
val frame = MutableList<Byte>(OpenCodePayload.LENGTH) { 0 }
102+
frame[0] = PayloadKind.Cash.value.toByte()
103+
frame[1] = 232.toByte() // >= CurrencyCode.entries.size
104+
105+
val decoded = OpenCodePayload.fromList(frame)
106+
107+
assertEquals(PayloadKind.Unknown, decoded.kind)
108+
}
109+
110+
@Test
111+
fun `fromList decodes highest valid currency index`() {
112+
// Boundary: the last valid ordinal must still decode as Cash, not be rejected.
113+
val lastCurrency = CurrencyCode.entries.last()
114+
val fiat = Fiat(quarks = 7L, currencyCode = lastCurrency)
115+
val encoded = encodePayload(PayloadKind.Cash, fiat, List(10) { 0.toByte() })
116+
117+
val decoded = OpenCodePayload.fromList(encoded)
118+
119+
assertEquals(PayloadKind.Cash, decoded.kind)
120+
assertEquals(lastCurrency, decoded.fiat!!.currencyCode)
121+
}
122+
95123
@Test
96124
fun `fromList preserves nonce bytes`() {
97125
val nonce = List(10) { (it * 3).toByte() }

0 commit comments

Comments
 (0)