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