1 /** 2 * --------- 3 * |.##> <##.| SmartCard-HSM Support Scripts 4 * |# #| 5 * |# #| Copyright (c) 2011-2012 CardContact Software & System Consulting 6 * |'##> <##'| Andreas Schwier, 32429 Minden, Germany (www.cardcontact.de) 7 * --------- 8 * 9 * This file is part of OpenSCDP. 10 * 11 * OpenSCDP is free software; you can redistribute it and/or modify 12 * it under the terms of the GNU General Public License version 2 as 13 * published by the Free Software Foundation. 14 * 15 * OpenSCDP is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with OpenSCDP; if not, write to the Free Software 22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 * 24 * @fileoverview PaperKey Encoder and Decoder 25 */ 26 27 28 29 /** 30 * Create a PaperKey encoder/decoder 31 * 32 * @class Encoder / Decoder for PaperKeys 33 * @constructor 34 */ 35 function PaperKeyEncoding() { 36 this.base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 37 this.alpha = "ABCDEFGH!JKLMN*PQRSTUVWXYZabcdefghijk%mnopqrstuvwxyz0123456789+/"; 38 } 39 40 exports.PaperKeyEncoding = PaperKeyEncoding; 41 42 PaperKeyEncoding.E_INV_LENGTH = "Input does not contain 12 characters"; 43 PaperKeyEncoding.E_WRONG_SEGMENT = "Wrong segment number"; 44 PaperKeyEncoding.E_CHECK_FAILED = "Check value failed"; 45 46 47 48 /** 49 * Encode an 8 byte segment in human readable form. 50 * 51 * Based on BASE64 encoding with visually confusable characters replaced. 52 * 53 * I is replaced by ! 54 * O is replaced by * 55 * l is replaced by % 56 * 57 * A LUHN-64 character is appended. 58 * 59 * A segment identifier is encoded in 2 bit at the end of the encoded 64 bit string. 60 * 61 * @param {Number} segment 62 * @param {ByteString} data 63 * @type String 64 * @return the encoded string 65 */ 66 PaperKeyEncoding.prototype.encodeSegment = function(segment, data) { 67 if (typeof(segment) != "number" || segment < 0) { 68 throw new GPError(module.id, GPError.INVALID_DATA, 0, "segment must be a positive number"); 69 } 70 71 if (!(data instanceof ByteString)) { 72 throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be ByteString"); 73 } 74 75 if (data.length != 8) { 76 throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be 8 byte"); 77 } 78 79 var bb = new ByteBuffer(); 80 81 // Append segment number's less 2 bit 82 data = data.concat(ByteString.valueOf((segment & 3) << 6)); 83 84 // print(data.toString(BASE64)); 85 86 var cs = 0; 87 var cf = 1; 88 89 for (var i = 0; i < data.length; i += 3) { 90 var a = data.bytes(i, 3).toUnsigned(); 91 var s = (a >> 18) & 0x3F; 92 93 cs += s << cf; 94 // print("s = " + s + " cf = " + cf + " cs = " + cs); 95 cf = cf ^ 1; 96 97 bb.append(this.alpha.charAt(s)); 98 99 s = (a >> 12) & 0x3F; 100 101 cs += s << cf; 102 // print("s = " + s + " cf = " + cf + " cs = " + cs); 103 cf = cf ^ 1; 104 105 bb.append(this.alpha.charAt(s)); 106 107 s = (a >> 6) & 0x3F; 108 109 cs += s << cf; 110 // print("s = " + s + " cf = " + cf + " cs = " + cs); 111 cf = cf ^ 1; 112 113 bb.append(this.alpha.charAt(s)); 114 115 if (i <= 3) { 116 s = a & 0x3F; 117 118 cs += s << cf; 119 // print("s = " + s + " cf = " + cf + " cs = " + cs); 120 cf = cf ^ 1; 121 122 bb.append(this.alpha.charAt(s)); 123 } 124 } 125 126 bb.append(this.alpha.charAt(cs & 0x3F)); 127 128 return bb.toString(ASCII); 129 } 130 131 132 133 /** 134 * Decode an 8 byte segment from 12 input characters. 135 * 136 * Characters not in the alphabet are ignored. 137 * 138 * Throws GPError with GPError.INVALID_LENGTH if the input does not contain at least 12 usable characters. 139 * Throws GPError with GPError.SIGNATURE_FAILED if the check digit based validation fails. 140 * Throws GPError with GPError.INVALID_DATA if the segment number does not match the expected value. 141 * 142 * @param {Number} segment 143 * @param {String} str the user input 144 * @type ByteString 145 * @return the 8 byte segment 146 */ 147 PaperKeyEncoding.prototype.decodeSegment = function(segment, str) { 148 if (typeof(segment) != "number" || segment < 0) { 149 throw new GPError(module.id, GPError.INVALID_DATA, 0, "segment must be a positive number"); 150 } 151 152 if (typeof(str) != "string") { 153 throw new GPError(module.id, GPError.INVALID_DATA, 0, "str must be String"); 154 } 155 156 // print(str); 157 var bb = new ByteBuffer(); 158 159 var cs = 0; 160 var cf = 1; 161 var a = 0; 162 var p = 0; 163 164 for (var i = 0; i < 12; i++) { 165 do { 166 var ch = str.charAt(p++); 167 if (ch == "") { 168 throw new GPError(module.id, GPError.INVALID_LENGTH, 0, PaperKeyEncoding.E_INV_LENGTH); 169 } 170 var v = this.alpha.indexOf(ch); 171 if (v < 0) { 172 var v = this.base.indexOf(ch); 173 } 174 } while (v < 0); 175 176 cs += v << cf; 177 // print("v = " + v + " cf = " + cf + " cs = " + cs); 178 cf = cf ^ 1; 179 180 a = (a << 6) | v; 181 182 if ((i + 1 & 3) == 0) { 183 bb.append(ByteString.valueOf(a, 3)); 184 a = 0; 185 } 186 } 187 188 bb.append(ByteString.valueOf(a, 3)); 189 190 cs -= v << (1 - cf); 191 if (v != (cs & 0x3F)) { 192 throw new GPError(module.id, GPError.SIGNATURE_FAILED, 0, PaperKeyEncoding.E_CHECK_FAILED); 193 } 194 195 // print(bb); 196 if ((bb.byteAt(8) >> 6) != (segment & 3)) { 197 throw new GPError(module.id, GPError.INVALID_DATA, 0, PaperKeyEncoding.E_WRONG_SEGMENT); 198 } 199 200 var r = bb.toByteString().left(8); 201 bb.clear(); 202 return r; 203 } 204 205 206 207 /** 208 * Format a segment by splitting the input after each 4 characters and inserting " - " 209 * 210 * @param {String} str the unformatted string 211 * @type String 212 * @return the formatted string 213 */ 214 PaperKeyEncoding.formatSegment = function(str) { 215 var res = ""; 216 var j = 0; 217 while(j < str.length) { 218 if (j > 0) { 219 res += " - "; 220 } 221 res += str.substr(j, 4); 222 j += 4; 223 } 224 return res; 225 } 226 227 228 229 /** 230 * Dump a key 231 * 232 * @param {ByteString} data the key value (length must be a multiple of 8) 233 * @type String 234 * @return the formatted key with 3 groups of 4 digits per line 235 */ 236 PaperKeyEncoding.dumpKey = function(data) { 237 if (!(data instanceof ByteString)) { 238 throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be ByteString"); 239 } 240 241 if (data.length & 7 != 0) { 242 throw new GPError(module.id, GPError.INVALID_DATA, 0, "data must be a multiple of 8 byte"); 243 } 244 245 var pc = new PaperKeyEncoding(); 246 247 var segments = data.length >> 3; 248 249 var str = ""; 250 for (var i = 0; i < segments; i++) { 251 var s = pc.encodeSegment(i, data.bytes(i * 8, 8)); 252 str += PaperKeyEncoding.formatSegment(s) + "\n"; 253 } 254 return str; 255 } 256 257 258 259 PaperKeyEncoding.test = function() { 260 var pc = new PaperKeyEncoding(); 261 262 var ref = ByteString.valueOf(0, 8); 263 var enc = pc.encodeSegment(0, ref); 264 assert("AAAAAAAAAAAA" == enc, "All zero seg 0 encoding failed"); 265 var res = pc.decodeSegment(0, enc); 266 assert(ref.equals(res), "Decoding failed"); 267 268 var enc = pc.encodeSegment(3, ref); 269 assert("AAAAAAAAAADG" == enc, "All zero seg 3 encoding failed"); 270 var res = pc.decodeSegment(3, enc); 271 assert(ref.equals(res), "Decoding failed"); 272 273 var enc = pc.encodeSegment(7, ref); 274 assert("AAAAAAAAAADG" == enc, "All zero seg 7 encoding failed"); 275 276 try { 277 var res = pc.decodeSegment(6, enc); 278 assert(false, "Did not detect wrong segment"); 279 } 280 catch(e) { 281 assert(e.error == GPError.INVALID_DATA, "Must be GPError.INVALID_DATA"); 282 assert(e.message == PaperKeyEncoding.E_WRONG_SEGMENT, "Must be PaperKeyEncoding.E_WRONG_SEGMENT"); 283 } 284 285 try { 286 var res = pc.decodeSegment(7, "AAAAAAAAAADA"); 287 assert(false, "Did not detect wrong check digit"); 288 } 289 catch(e) { 290 assert(e.error == GPError.SIGNATURE_FAILED, "Must be GPError.SIGNATURE_FAILED"); 291 assert(e.message == PaperKeyEncoding.E_CHECK_FAILED, "Must be PaperKeyEncoding.E_CHECK_FAILED"); 292 } 293 294 var ref = ref.not(); 295 var enc = pc.encodeSegment(0, ref); 296 assert("//////////8p" == enc, "All one seg 0 encoding failed"); 297 var res = pc.decodeSegment(0, enc); 298 assert(ref.equals(res), "Decoding failed"); 299 var enc = pc.encodeSegment(3, ref); 300 assert("///////////v" == enc, "All one seg 3 encoding failed"); 301 var res = pc.decodeSegment(3, enc); 302 assert(ref.equals(res), "Decoding failed"); 303 304 var ref = ByteString.valueOf(0x04).concat(ByteString.valueOf(0, 7)); 305 var enc = pc.encodeSegment(0, ref); 306 assert("BAAAAAAAAAAC" == enc, "First one encoding failed"); 307 var res = pc.decodeSegment(0, enc); 308 assert(ref.equals(res), "Decoding failed"); 309 310 var ref = ByteString.valueOf(0x10, 2).concat(ByteString.valueOf(0, 6)); 311 var enc = pc.encodeSegment(0, ref); 312 assert("ABAAAAAAAAAB" == enc, "Second one encoding failed"); 313 var res = pc.decodeSegment(0, enc); 314 assert(ref.equals(res), "Decoding failed"); 315 316 var ref = ByteString.valueOf(0xFC).concat(ByteString.valueOf(0, 7)); 317 var enc = pc.encodeSegment(0, ref); 318 assert("/AAAAAAAAAA+" == enc, "First max encoding failed"); 319 var res = pc.decodeSegment(0, enc); 320 assert(ref.equals(res), "Decoding failed"); 321 322 var ref = ByteString.valueOf(0x03F0, 2).concat(ByteString.valueOf(0, 6)); 323 var enc = pc.encodeSegment(0, ref); 324 assert("A/AAAAAAAAA/" == enc, "Second max encoding failed"); 325 var res = pc.decodeSegment(0, enc); 326 assert(ref.equals(res), "Decoding failed"); 327 328 329 330 var crypto = new Crypto(); 331 332 var kv = crypto.generateRandom(32); 333 print(PaperKeyEncoding.dumpKey(kv)); 334 335 while (true) { 336 var kv = crypto.generateRandom(8); 337 var str = PaperKeyEncoding.formatSegment(pc.encodeSegment(0, kv)); 338 print(str); 339 var inp = Dialog.prompt("Enter segment " + str, ""); 340 if (inp == null) { 341 throw new Error("User abort"); 342 } 343 try { 344 var val = pc.decodeSegment(0, inp); 345 if (!kv.equals(val)) { 346 throw new GPError(module.id, GPError.INVALID_DATA, 0, "Entered value does not match key"); 347 } 348 } 349 catch(e) { 350 Dialog.prompt(e.message); 351 } 352 } 353 } 354