-
-
Save kbsriram/6093167 to your computer and use it in GitHub Desktop.
| // 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); | |
| } | |
| } | |
| } |
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.
this is really great! should be quite easy to incorporate into our apps. I think we might also want to do a
OF1:for OTR fingerprint scanning. One question: why not use the base44 since the QR reduced set has 44 chars?.hc