NPCI CL's "Custom" Base64 #
This was taken from FamApp (v4.1.7.0).
A reverse-engineered, compilable reconstruction of the obfuscated OooOO0O Base64
class extracted from cl.jar (org.npci.upi.security.pinactivitycomponent).
JADX could not decompile the encoder's encodeBytes body, and the original bytecode
fails the JVM verifier (VerifyError), so it only runs with -noverify. This project
rebuilds it from clean source and verifies it byte-for-byte against the original.
What it is #
Standard RFC 4648 Base64. The bit-packing, alphabet, and decode tables are identical
to java.util.Base64; there is no custom alphabet or modified bit ordering. What's
non-standard is the wrapper behavior: a single-int options bitmask, LF line
separators by default, a lenient decoder, and streaming methods.
This is a fork of (Robert Harder's iharder Base64)[https://iharder.sourceforge.net/current/java/base64/]. It adds NO_PADDING and CRLF.
Options bitmask #
Passed as the int flags argument to every public method:
| bit | value | name | meaning |
|---|---|---|---|
| 0 | 1 |
NO_PADDING |
drop the = padding chars |
| 1 | 2 |
NO_NEWLINES |
do not insert line breaks |
| 2 | 4 |
CRLF |
use \r\n instead of \n (with newlines) |
| 3 | 8 |
URL_SAFE |
use - / _ instead of + / / |
- Line length is 76 chars (19 groups of 4), MIME-style.
- A line separator is emitted after every 19 groups and a trailing one at flush whenever any output was produced.
- Default newline mode uses LF (
\n); CRLF only when bit4is set.
How it differs from java.util.Base64 #
- Flag bitmask instead of separate encoder objects (
getEncoder()/getUrlEncoder()/withoutPadding()). - LF as the default line separator, whereas
Base64.getMimeEncoder()uses CRLF. - Lenient decoder: silently skips any character not in the alphabet (table
value
-1), not just whitespace.java.util.Base64rejects invalid chars;getMimeDecoder()skips only whitespace. - Streaming methods
encodeBytes(src, off, len, flush)/decodeBytes(src, off, len, flush)that the JDK class does not expose.
For the basic modes the output is identical to the JDK:
flags = 2≡Base64.getEncoder()flags = 10≡Base64.getUrlEncoder()- adding
& 1≡.withoutPadding()
Build & test #
make test # rebuild original jar, compile, run 40,071-case diff
make clean # rm -rf out custom_base64.jar
Expected:
== checks=40071 fails=0 ==
The test covers all 16 flag combinations, input sizes 0–300, 2000 random inputs, the full encode/decode cross-flag matrix, and streaming encode/decode across every chunk-split pattern (including the quirks above).
Public API (CustomBase64) #
static String toUrlSafe(String s); // + -> -, / -> _
static byte[] encodeBytes(byte[] src, int flags);
static byte[] encodeBytesRange(byte[] src, int off, int len, int flags);
static String encodeToString(byte[] src, int flags);
static byte[] decodeBytes(byte[] src, int flags);
static byte[] decodeBytesRange(byte[] src, int off, int len, int flags);
static byte[] decodeString(String s, int flags);