<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>reversed (Posts about qr)</title><link>https://desrever.dev/</link><description></description><atom:link href="https://desrever.dev/categories/qr.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:serv0id@desrever.dev"&gt;serv0id&lt;/a&gt; </copyright><lastBuildDate>Fri, 08 May 2026 21:44:26 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>On Enhanced PAN QRs</title><link>https://desrever.dev/posts/opanqr/</link><dc:creator>serv0id</dc:creator><description>&lt;p&gt;The Indian government introduced a QR code on newly issued PAN
(Permanent Account Number) cards around 2017 to assist in quick
verification at various government outlets.&lt;/p&gt;
&lt;p&gt;These QR codes have gone through updates over the years. This post tries
to analyse what is actually contained in these QR codes and how they
fare in terms of being secure.&lt;/p&gt;
&lt;section id="pan-qr-v1"&gt;
&lt;h2&gt;PAN QR v1&lt;/h2&gt;
&lt;p&gt;The first version of the PAN QR code was located on the left side of the
physical PAN card. It simply contained the personal details on the PAN
card in a key-value form in plaintext. It did not have any sort of
cryptographic mechanism that ensured the authenticity of the QR code.&lt;/p&gt;
&lt;img alt="/images/image4.png" class="align-center" src="https://desrever.dev/images/image4.png" style="width: 3.35243in; height: 2.15514in;"&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img alt="/images/image6.png" class="align-center" src="https://desrever.dev/images/image6.png" style="width: 2.79688in; height: 2.3536in;"&gt;
&lt;p&gt;Of course, such a verification mechanism cannot be relied on since it
can be very easily altered.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="pan-qr-v2"&gt;
&lt;h2&gt;PAN QR v2&lt;/h2&gt;
&lt;p&gt;The enhanced PAN QR was introduced as an improvement towards the v1 QR
code. The
&lt;a class="reference external" href="https://proteantech.in/articles/pan-card-security-upgrade/"&gt;protean&lt;/a&gt;
website describes this QR code containing “encrypted data” that can
“only” be read by certified scanners. However, the claim of the QR data
being “encrypted” is an inaccuracy as will be shown further.&lt;/p&gt;
&lt;p&gt;On first glance, the enhanced QR code seems bigger which means it
contains more information than the older version. The new QR code is now
at the right side of the PAN card.&lt;/p&gt;
&lt;img alt="/images/image7.png" class="align-center" src="https://desrever.dev/images/image7.png" style="width: 5.47746in; height: 3.48118in;"&gt;
&lt;p&gt;However, the content in the newer QR code seems to just be a very long
integer rather than anything that is plaintext.&lt;/p&gt;
&lt;img alt="/images/image5.png" class="align-center" src="https://desrever.dev/images/image5.png" style="width: 3.61979in; height: 2.01434in;"&gt;
&lt;section id="pan-qr-reader"&gt;
&lt;h3&gt;PAN QR Reader&lt;/h3&gt;
&lt;p&gt;Protean provides an android
&lt;a class="reference external" href="https://play.google.com/store/apps/details?id=com.pv.scr.pancardreader&amp;amp;hl=en_IN"&gt;app&lt;/a&gt;
called “PAN QR Code Reader” which allows the reading and parsing of the
enhanced QR code. This is obviously the first entry point to figuring
out how the enhanced QR codes work.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="the-obfuscation"&gt;
&lt;h2&gt;The obfuscation&lt;/h2&gt;
&lt;p&gt;The major observation with this application is that it is obfuscated
more than normally what such utility apps are and employs a very custom
data format for the QR code content.&lt;/p&gt;
&lt;p&gt;Many classes contain a custom string encryption method that takes two
integer arguments and spits out a string based on some XORs.&lt;/p&gt;
&lt;img alt="/images/image3.png" class="align-center" src="https://desrever.dev/images/image3.png" style="width: 4.34375in; height: 2.41667in;"&gt;
&lt;p&gt;Fortunately, JEB handles this automatically for us and decrypts most of
the strings that are used in the logic.&lt;/p&gt;
&lt;img alt="/images/image1.png" class="align-center" src="https://desrever.dev/images/image1.png" style="width: 5.91381in; height: 1.03419in;"&gt;
&lt;p&gt;The decryption logic relies on some character arrays that are resolved
at the time of loading the particular class inside the &amp;lt;clinit&amp;gt;, logic
for which is also obfuscated but not handed gracefully by JEB.&lt;/p&gt;
&lt;img alt="/images/image2.png" class="align-center" src="https://desrever.dev/images/image2.png" style="width: 2.29167in; height: 2.66667in;"&gt;
&lt;p&gt;To retrieve desired static variables, frida can be used to hook the
object &lt;em&gt;after&lt;/em&gt; the class is instantiated. So reading something like
&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;ClassName.$new()._variable_name&lt;/span&gt;&lt;/code&gt; should work.&lt;/p&gt;
&lt;p&gt;A proof-of-concept repository for parsing and verifying this QR is
available at &lt;a class="reference external" href="https://github.com/serv0id/OPANqr"&gt;https://github.com/serv0id/OPANqr&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;section id="the-flow"&gt;
&lt;h2&gt;The flow&lt;/h2&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;p&gt;The seemingly integer looking string is passed to the class x1.b,
which is a subclass of FilterOutputStream; converts and parses the
stream to a byte array through a pseudo-unpacking function x1.b.a.
The python port for the function is at
&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/main/utils/unpacker.py"&gt;https://github.com/serv0id/OPANqr/blob/main/utils/unpacker.py&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The method
&lt;a class="reference external" href="http://com.pv"&gt;com.pv&lt;/a&gt;.scrapi.html.api.PanHtmlApi.decodeQRCode
handles parsing the custom data structure that the QR content is
after being “unpacked”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;decodeQRCode performs a bunch of checks detailed at
&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/dd85d86b26742febca869fb37467fd9217fa2283/utils/parser.py#L22"&gt;https://github.com/serv0id/OPANqr/blob/dd85d86b26742febca869fb37467fd9217fa2283/utils/parser.py#L22&lt;/a&gt;
that verifies that the QR version is valid and sets the public key
that will be used for the signature verification later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Through an intricate maze of nested structures detailed at
&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/main/constants/structs.py"&gt;https://github.com/serv0id/OPANqr/blob/main/constants/structs.py&lt;/a&gt;, the
relevant blocks containing the pictures and personal information are
parsed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The image block deliberately has a bad header for some reason, so the
WEBP header is fixed and the image is rendered.
(&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/main/utils/image.py"&gt;https://github.com/serv0id/OPANqr/blob/main/utils/image.py&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The personal information section is zlib inflated and individual
blocks are parsed within. A hacky way with regex is described at
&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/dd85d86b26742febca869fb37467fd9217fa2283/utils/parser.py#L78"&gt;https://github.com/serv0id/OPANqr/blob/dd85d86b26742febca869fb37467fd9217fa2283/utils/parser.py#L78&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The signature is verified against the chosen public key based on the
QR version. The signing algorithm is ECDSA on the curve P-384.
(&lt;a class="reference external" href="https://github.com/serv0id/OPANqr/blob/main/utils/verifier.py"&gt;https://github.com/serv0id/OPANqr/blob/main/utils/verifier.py&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;All in all, it’s not very clear why a simple task such as parsing and
verifying the QR content is obfuscated into custom data structures and
protected applications when it is already cryptographically signed and
hence resistant to forgery. As long as the private keys are safe, the QR
could even have been a plain JSON file containing a signature at the
end. It is a fun exercise parsing custom structures regardless.&lt;/p&gt;
&lt;/section&gt;</description><category>android</category><category>pan</category><category>protean</category><category>qr</category><guid>https://desrever.dev/posts/opanqr/</guid><pubDate>Wed, 27 Aug 2025 22:58:29 GMT</pubDate></item></channel></rss>