Skip to content

Instantly share code, notes, and snippets.

@kbsriram
Created July 27, 2013 00:44
Show Gist options
  • Save kbsriram/6093167 to your computer and use it in GitHub Desktop.
Save kbsriram/6093167 to your computer and use it in GitHub Desktop.
PF1 encoding for PGP fingerprints.
// PF1-encoding for PGP fingerprints; optimized to fit
// within a Version 2 QR code using M-level error-correction
//
// Format is simple.
// PF1:<32 characters -- Base32 (rfc4648) encoded 160 bit fingerprint>
//
// To test:
//
// $ java PF1Coder encode <40-character hex fingerprint>
// PF1:<....>
//
// $ java PF1Coder decode PF1:<.....>
// <hex fingerprint>
import java.util.Random; // only needed for testing.
public class PF1Coder
{
// rfc4648
private final static char[] ENCODE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
public static byte[] decode(char[] in)
{
if (in.length != 32) {
throw new IllegalArgumentException("bad length: "+in.length);
}
byte[] ret = new byte[20];
// go through base32 40 bits (8 characters) at a time.
for (int i=0; i<in.length; i+= 8) {
long tmp =
(decodeBase32(in[i+0]) << 35) |
(decodeBase32(in[i+1]) << 30) |
(decodeBase32(in[i+2]) << 25) |
(decodeBase32(in[i+3]) << 20) |
(decodeBase32(in[i+4]) << 15) |
(decodeBase32(in[i+5]) << 10) |
(decodeBase32(in[i+6]) << 5) |
(decodeBase32(in[i+7]) << 0);
int idx = i/8*5;
ret[idx+0] = (byte) ((tmp >> 32) & 0xff);
ret[idx+1] = (byte) ((tmp >> 24) & 0xff);
ret[idx+2] = (byte) ((tmp >> 16) & 0xff);
ret[idx+3] = (byte) ((tmp >> 8) & 0xff);
ret[idx+4] = (byte) ((tmp >> 0) & 0xff);
}
return ret;
}
private final static long decodeBase32(char c)
{
if ((c >= 'A') && (c <= 'Z')) {
return (long) (c - 'A');
}
if ((c >= '2') && (c <= '7')) {
return (long) (26 + c - '2');
}
throw new IllegalArgumentException("bad base32: '"+c+"'");
}
public static char[] encode(byte[] in)
{
if (in.length != 20) {
throw new IllegalArgumentException("bad length: "+in.length);
}
char[] ret = new char[32];
long tmp;
// Go through fingerprint 40 bits (5 bytes) at a time.
for (int i=0; i<in.length; i+= 5) {
tmp =
(long) (in[i + 0] & 0xff) << 32 |
(long) (in[i + 1] & 0xff) << 24 |
(long) (in[i + 2] & 0xff) << 16 |
(long) (in[i + 3] & 0xff) << 8 |
(long) (in[i + 4] & 0xff);
// output 8 base32 characters for these
// 5 bytes
int idx = i/5*8;
ret[idx + 0] = ENCODE[(int) (tmp >> 35) & 0x1f];
ret[idx + 1] = ENCODE[(int) (tmp >> 30) & 0x1f];
ret[idx + 2] = ENCODE[(int) (tmp >> 25) & 0x1f];
ret[idx + 3] = ENCODE[(int) (tmp >> 20) & 0x1f];
ret[idx + 4] = ENCODE[(int) (tmp >> 15) & 0x1f];
ret[idx + 5] = ENCODE[(int) (tmp >> 10) & 0x1f];
ret[idx + 6] = ENCODE[(int) (tmp >> 5) & 0x1f];
ret[idx + 7] = ENCODE[(int) (tmp >> 0) & 0x1f];
}
return ret;
}
private final static void usage()
{
System.err.println("Usage:");
System.err.println(" PF1Coder encode <hex fingerprint>");
System.err.println(" or");
System.err.println(" PF1Coder decode <PF1 fingerprint>");
System.exit(1);
}
public static void main(String args[])
{
if (args.length != 2) {
usage();
}
else if ("encode".equals(args[0])) {
System.out.println(doEncode(args[1]));
}
else if ("decode".equals(args[0])) {
System.out.println(doDecode(args[1]));
}
else if ("test".equals(args[0])) {
runTest(Integer.parseInt(args[1]));
}
else {
usage();
}
}
public final static String doEncode(String hex)
{
if (hex.length() != 40) {
throw new RuntimeException("bad length: "+hex.length());
}
byte[] in = new byte[20];
for (int i=0; i<40; i+=2) {
in[i/2] = (byte) Integer.parseInt(hex.substring(i, i+2), 16);
}
return "PF1:"+new String(encode(in));
}
public static String doDecode(String pf1)
{
if (pf1.length() != 36) {
throw new RuntimeException("bad length: "+pf1.length());
}
char[] in = pf1.substring(4).toCharArray();
byte[] decoded = decode(in);
StringBuilder sb = new StringBuilder();
for (int i=0; i<20; i++) {
int v = ((int) decoded[i] & 0xff);
if (v < 0x10) {
sb.append("0");
}
sb.append(Integer.toHexString(v));
}
return sb.toString();
}
public static void runTest(int count)
{
Random r = new Random();
// Test by ensuring that decode and encode
// are symmetric.
byte[] test_fp = new byte[20];
for (int i=0; i<count; i++) {
r.nextBytes(test_fp);
char[] pf1 = encode(test_fp);
byte[] decode_fp = decode(pf1);
for (int j=0; j<20; j++) {
if (decode_fp[j] != test_fp[j]) {
throw new RuntimeException(new String(pf1));
}
}
System.out.println(pf1);
}
}
}
@kbsriram
Copy link
Author

The real answer is that I found it too fiddly to deal with base44 :-) but you're quite right of course. It should gain another 2-3 characters (log_44(2^160) ~ 29.3, down from 32)

However, that didn't seem enough to get it past the next interesting breakpoint (29 characters, for Q-level error-correction) so I stuck with base32.

By the same token, I should say that straight upper-case hex-coding with a smaller prefix -- say, PF1:<40 char hex> will also get it into a version 2 qrcode, though at the L-level (7%) error correction. The base32 encoding is interesting only because this just squeaks it into the M-level (15%) error correction area. I haven't done enough tests to see whether this increased error-correction is actually useful; but since base32 is itself a bit more fiddly than hex encoding, figured I'd also point out this tradeoff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment