cap bbqr display and pdf encodings at the 1295 parts the header can number, using larger parts for data that needs more rather than emitting a sequence that cannot be reassembled
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's BBQ QR code format. Previously, if a large transaction or data blob needed more than 1,295 QR-code-sized pieces, the app would generate pieces with impossible sequence numbers that could not be reassembled by a scanner. The fix caps the number of pieces at 1,295 and makes each piece bigger instead, so scanning still works. It is a reliability/data-integrity bug rather than a direct theft-of-funds vulnerability, but a malformed or unrecoverable QR sequence could disrupt wallet operations.
Treat as a bug-fix patch with low-to-moderate security relevance. Users relying on large QR exports should upgrade. No immediate incident response is indicated unless independent research shows the malformed sequences can be exploited beyond a failed scan.
Security signals we found
Integer/sequence-number overflow-like limit violation in a data-encoding protocol
Potential denial-of-service or data-integrity failure when exporting large transactions via QR
Round-trip unit test added to prevent regression
Evidence from the diff
BBQR’s header uses two base36 characters to number parts, giving a maximum of 36*36 - 1 = 1,295 parts. BBQREncoder previously computed numChunks from the desired chunk size without enforcing that limit, then numbered parts with a two-character base36 sequence. For inputs requiring more than 1,295 chunks, emitted parts would have sequence numbers that could not be represented or reassembled. The patch introduces MAX_PARTS, caps numChunks at 1,295, recalculates chunkSize upward to cover the data, and recomputes numChunks after alignment. A unit test verifies that 900 KB of random data, which would need 1,440 parts at the default fragment length, is encoded into ≤1,295 parts and round-trips through the decoder.
Changed components
src/main/java/com/sparrowwallet/sparrow/io/bbqr/BBQREncoder.javaBBQR export/display and PDF encoding flows that use BBQREncoderInspect captured patch +27 / −1
### src/main/java/com/sparrowwallet/sparrow/io/bbqr/BBQREncoder.java
@@ -4,6 +4,8 @@
import java.util.List;
public class BBQREncoder {
+ private static final int MAX_PARTS = 36 * 36 - 1;
+
private final String[] parts;
private int partIndex;
@@ -48,14 +50,17 @@ private List<String> encode(BBQRType type, BBQREncoding desiredEncoding, byte[]
}
int inputLength = encoded.length();
- int numChunks = (inputLength + desiredChunkSize - 1) / desiredChunkSize;
+ //The header numbers parts with two base36 characters, so data needing more parts is split into larger ones
+ int numChunks = Math.min((inputLength + desiredChunkSize - 1) / desiredChunkSize, MAX_PARTS);
int chunkSize = numChunks == 1 ? desiredChunkSize : (int)Math.ceil((double)inputLength / numChunks);
int modulo = chunkSize % encoding.getPartModulo();
if(modulo > 0) {
chunkSize += (encoding.getPartModulo() - modulo);
}
+ numChunks = (inputLength + chunkSize - 1) / chunkSize;
+
List<String> chunks = new ArrayList<>();
int startIndex = 0;
for(int i = 0; i < numChunks; i ++) {
### src/test/java/com/sparrowwallet/sparrow/io/bbqr/BBQREncoderTest.java
@@ -31,4 +31,25 @@ public void testEncoding() {
}
}
}
+
+ @Test
+ public void testEncodingBeyondMaxParts() {
+ //At 1000 character fragments this data needs 1440 parts, more than two base36 header characters can number
+ byte[] data = new byte[900000];
+ new Random().nextBytes(data);
+
+ BBQREncoder encoder = new BBQREncoder(BBQRType.BINARY, BBQREncoding.BASE32, data, QRDensity.LOW.getMaxBbqrFragmentLength(), 0);
+ Assertions.assertTrue(encoder.getNumParts() <= 1295, "Encoded " + encoder.getNumParts() + " parts");
+
+ BBQRDecoder decoder = new BBQRDecoder();
+ for(int i = 0; i < encoder.getNumParts(); i++) {
+ String part = encoder.nextPart();
+ Assertions.assertEquals(encoder.getNumParts(), BBQRHeader.fromString(part).seqTotal());
+ Assertions.assertTrue(part.length() > 8);
+ decoder.receivePart(part);
+ }
+
+ Assertions.assertEquals(BBQRDecoder.ResultType.SUCCESS, decoder.getResult().getResultType());
+ Assertions.assertArrayEquals(data, decoder.getResult().getData());
+ }
}Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.