Revision 9:16125cca68e4 src/my/com/upass/gemalto/GemaltoTokenControllerImpl.java

View differences:

src/my/com/upass/gemalto/GemaltoTokenControllerImpl.java
10 10
 */
11 11
package my.com.upass.gemalto;
12 12

  
13

  
14 13
import java.util.Date;
15 14
import java.util.HashMap;
16 15
import ocra.Ocra;
......
50 49
public class GemaltoTokenControllerImpl implements TokenController
51 50
{
52 51
	private Ocra myocra = new Ocra();
53
	
52

  
54 53
	private TokenBean tb;
55 54
	private int retCode;
56 55
	private String serialNumber;
57 56
	private String authMode;
58
	private byte[] blob;	
57
	private byte[] blob;
59 58
	private int useCount;
60 59
	private int errorCount;
61 60
	private Date lastTimeUsed;
62 61
	private Date firstTimeUsed;
63 62
	private Date lastAuthentication;
64 63
	private Date tokenExpectedDateTime;
65
	
64

  
66 65
	/**
67 66
	 * this will override the existing tokenBean object
68 67
	 * this will override the existing serial number, authentication mode and blob
69 68
	 * 
70 69
	 * @param tokenBean
71 70
	 */
72
	public GemaltoTokenControllerImpl(TokenBean tokenBean) 
71
	public GemaltoTokenControllerImpl(TokenBean tokenBean)
73 72
	{
74 73
		this.tb = tokenBean;
75 74
		setSerialNumber();
76 75
		setAuthMode();
77 76
		setBlob();
78 77
	}
79
	
78

  
80 79
	/**
81 80
	 * this will override the existing last authentication date
82 81
	 * 
......
86 85
	{
87 86
		this.lastAuthentication = lastAuthentication;
88 87
	}
89
	
88

  
90 89
	/**
91 90
	 * this will return updated last authentication date
92 91
	 * 
......
96 95
	{
97 96
		return lastAuthentication;
98 97
	}
99
	
98

  
100 99
	/**
101 100
	 * this will override the existing token expected date and time
102 101
	 * 
......
106 105
	{
107 106
		this.tokenExpectedDateTime = tokenExpectedDateTime;
108 107
	}
109
	
108

  
110 109
	/**
111 110
	 * this will return updated token expected date and time
112 111
	 * 
......
116 115
	{
117 116
		return tokenExpectedDateTime;
118 117
	}
119
	
118

  
120 119
	/**
121 120
	 * this will override the existing tokenBean
122 121
	 * 
123 122
	 */
124
	public void setObject(TokenBean tokenBean) 
123
	public void setObject(TokenBean tokenBean)
125 124
	{
126 125
		this.tb = tokenBean;
127 126
	}
128
	
127

  
129 128
	/**
130 129
	 * this will return the updated object (tokenBean)
131 130
	 * 
132 131
	 */
133
	public TokenBean getUpdatedObject() 
132
	public TokenBean getUpdatedObject()
134 133
	{
135 134
		return tb;
136 135
	}
137
	
136

  
138 137
	/**
139 138
	 * this will override the existing serial number
140 139
	 * 
141 140
	 */
142
	private void setSerialNumber() 
141
	private void setSerialNumber()
143 142
	{
144 143
		this.serialNumber = tb.getVserialNumber();
145 144
	}
146
	
145

  
147 146
	/**
148 147
	 * this will override the existing authentication mode
149 148
	 * 
150 149
	 */
151
	private void setAuthMode() 
150
	private void setAuthMode()
152 151
	{
153 152
		this.authMode = tb.getVdpAuthMode();
154 153
	}
155
	
154

  
156 155
	/**
157 156
	 * this will override the existing blob
158 157
	 * 
159 158
	 */
160
	private void setBlob() 
159
	private void setBlob()
161 160
	{
162 161
		this.blob = tb.getVdpCipherText();
163 162
	}
164
	
163

  
165 164
	/**
166 165
	 * this will return the updated blob
167 166
	 * 
168 167
	 */
169
	public byte[] getUpdatedBlob() 
168
	public byte[] getUpdatedBlob()
170 169
	{
171 170
		return this.blob;
172 171
	}
173
	
172

  
174 173
	/**
175 174
	 * this will override the existing useCount
176 175
	 * 
......
179 178
	{
180 179
		this.useCount = tb.getVuseCount();
181 180
	}
182
	
181

  
183 182
	/**
184 183
	 * this will increase the existing useCount by 1 whenever it is invoked.
185 184
	 * 
......
188 187
	{
189 188
		this.useCount++;
190 189
	}
191
	
190

  
192 191
	/**
193 192
	 * this will return the updated useCount
194 193
	 * 
195 194
	 */
196
	public int getUseCount() 
195
	public int getUseCount()
197 196
	{
198 197
		return this.useCount;
199 198
	}
200
	
199

  
201 200
	/**
202 201
	 * this will override the existing errorCount
203 202
	 * 
......
206 205
	{
207 206
		this.errorCount = tb.getVerrorCount();
208 207
	}
209
	
208

  
210 209
	/**
211 210
	 * this will increase the existing errorCount by 1 whenever it is invoked.
212 211
	 * 
......
215 214
	{
216 215
		this.errorCount++;
217 216
	}
218
	
217

  
219 218
	/**
220 219
	 * this will return the updated errorCount
221 220
	 * 
222 221
	 */
223
	public int getErrorCount() 
222
	public int getErrorCount()
224 223
	{
225 224
		return this.errorCount;
226 225
	}
227
	
226

  
228 227
	/**
229 228
	 * this will override the existing first used date
230 229
	 * 
......
233 232
	{
234 233
		this.firstTimeUsed = tb.getVdateFirstUsed();
235 234
	}
236
	
235

  
237 236
	/**
238 237
	 * case 1: if first used date is null, then first used date will be override by current date.
239 238
	 * case 2: throw exception if first used date is after last used date.
......
243 242
	 */
244 243
	public void setFirstTimeUsed(Date currentDate) throws Exception
245 244
	{
246
		if(this.firstTimeUsed == null)
245
		if (this.firstTimeUsed == null)
247 246
		{
248 247
			this.firstTimeUsed = currentDate;
249 248
		}
250
		else if(this.firstTimeUsed.compareTo(this.lastTimeUsed) > 0)
251
		{		
249
		else if (this.firstTimeUsed.compareTo(this.lastTimeUsed) > 0)
250
		{
252 251
			throw new Exception("Invalid date. First used date cannot be after last used date.");
253 252
		}
254 253
	}
255
	
254

  
256 255
	/**
257 256
	 * this will return the updated first used date
258 257
	 * 
......
261 260
	{
262 261
		return firstTimeUsed;
263 262
	}
264
	
263

  
265 264
	/**
266 265
	 * this will override the existing last used date.
267 266
	 * 
......
270 269
	{
271 270
		this.lastTimeUsed = tb.getVdateFirstUsed();
272 271
	}
273
	
272

  
274 273
	/**
275 274
	 * case 1: if last used date is null, then last used date will be override by current date.
276 275
	 * case 2: if last used date is before the current date, then last used date will be override by current date.
......
282 281
	 */
283 282
	public void setLastTimeUsed(Date currentDate) throws Exception
284 283
	{
285
		if(this.lastTimeUsed == null)
286
		{
287
			this.lastTimeUsed = currentDate;
288
		}		
289
		else if(this.lastTimeUsed.compareTo(currentDate) < 0)
284
		if (this.lastTimeUsed == null)
290 285
		{
291 286
			this.lastTimeUsed = currentDate;
292 287
		}
293
		else if(this.lastTimeUsed.compareTo(currentDate) > 0)
288
		else if (this.lastTimeUsed.compareTo(currentDate) < 0)
289
		{
290
			this.lastTimeUsed = currentDate;
291
		}
292
		else if (this.lastTimeUsed.compareTo(currentDate) > 0)
294 293
		{
295 294
			throw new Exception("Invalid date. Last used date cannot be after current date.");
296 295
		}
297
		else if(this.lastTimeUsed.compareTo(this.firstTimeUsed) < 0)
296
		else if (this.lastTimeUsed.compareTo(this.firstTimeUsed) < 0)
298 297
		{
299 298
			throw new Exception("Invalid date. Last used date cannot be before first used date.");
300 299
		}
301 300
	}
302
	
301

  
303 302
	/**
304 303
	 * this will return the updated last used date
305 304
	 * 
......
314 313
	 * it also acts as last returned error code.
315 314
	 * 
316 315
	 */
317
	public int getRetCode() 
316
	public int getRetCode()
318 317
	{
319 318
		return retCode;
320 319
	}
......
323 322
	 * this will return the updated error text based on the error code
324 323
	 * 
325 324
	 */
326
	public String getLastError() 
325
	public String getLastError()
327 326
	{
328 327
		String code = null;
329
		
330
		switch(this.retCode)
328

  
329
		switch (this.retCode)
331 330
		{
332
			case 0: code = "0";
333
			case 1: code = "1";
334
			case 2: code = "18";
335
			case 3: code = "19";
336
			case 4: code = "20";
337
			case 5: code = "21";
338
			case 6: code = "22";
339
			case 7: code = "3";
340
			case 8: code = "23";
341
			case 9: code = "24";
342
			case 10: code = "25";
343
			case 11: code = "26";
344
			default: 
345
				try 
346
				{
347
					throw new Exception("Invalid error code.");
348
				}
349
				catch (Exception e) 
350
				{
351
					e.printStackTrace();
352
				}
331
		case 0:
332
			code = "0";
333
		case 1:
334
			code = "1";
335
		case 2:
336
			code = "18";
337
		case 3:
338
			code = "19";
339
		case 4:
340
			code = "20";
341
		case 5:
342
			code = "21";
343
		case 6:
344
			code = "22";
345
		case 7:
346
			code = "3";
347
		case 8:
348
			code = "23";
349
		case 9:
350
			code = "24";
351
		case 10:
352
			code = "25";
353
		case 11:
354
			code = "26";
355
		default:
356
			try
357
			{
358
				throw new Exception("Invalid error code.");
359
			} catch (Exception e)
360
			{
361
				e.printStackTrace();
362
			}
353 363
		}
354
		
355
		return Constants.getErrText(code).get(code);
364

  
365
		return (String) Constants.getErrText(code).get(code);
356 366
	}
357 367

  
358 368
	/**
......
363 373
	{
364 374
		return verifyToken(otp);
365 375
	}
376

  
366 377
	/**
367 378
	 * Verify the entered On Time Password (OTP/password) for the given token
368 379
	 * 
369 380
	 */
370
	
371
	public int verifyToken(String password) 
381

  
382
	public int verifyToken(String password)
372 383
	{
373 384
		int[] blobLen = new int[1];
374
		blobLen[0]=500;
375
		
385
		blobLen[0] = 500;
386

  
376 387
		String[] blobInString = new String[1];
377 388
		blobInString[0] = new String(this.blob);
378
		
379
		//Time based OTP verification
380
		//Authentication window need to be bigger for first OTP verification.
381
		int result = myocra.comAuthBlob(0, 0, this.serialNumber, Constants.getAuthenticationMode(this.authMode), 20, 1, 100, 
382
							new byte[0], 0, 
383
							new byte[0], 0,	
384
							new byte[0], 0, 
385
							6, password, 
386
							blobInString, blobLen);
387
		
389

  
390
		// Time based OTP verification
391
		// Authentication window need to be bigger for first OTP verification.
392
		int result = myocra.comAuthBlob(0, 0, this.serialNumber, Constants.getAuthenticationMode(this.authMode), 20, 1,
393
				100,
394
				new byte[0], 0,
395
				new byte[0], 0,
396
				new byte[0], 0,
397
				6, password,
398
				blobInString, blobLen);
399

  
388 400
		this.useCount++;
389 401
		this.firstTimeUsed = new Date();
390 402
		this.lastTimeUsed = new Date();
391
		
392
		if(result == 0)
403

  
404
		if (result == 0)
393 405
		{
394 406
			this.blob = blobInString[0].getBytes();
395 407
			this.retCode = result;
......
397 409
		}
398 410
		else
399 411
		{
400
			try 
412
			try
401 413
			{
402 414
				this.errorCount++;
403 415
				this.retCode = result;
404 416
				updateTokenObject();
405
				return errorCodeConversion(result);	
406
			} 
407
			catch (Exception e) 
417
				return errorCodeConversion(result);
418
			} catch (Exception e)
408 419
			{
409 420
				e.printStackTrace();
410 421
			}
......
412 423

  
413 424
		return Constants.ERR_SUCCESS;
414 425
	}
415
	
426

  
416 427
	/**
417 428
	 * Check last successful authentication
418 429
	 * 
......
422 433
	{
423 434
		String[] blobInString = new String[1];
424 435
		blobInString[0] = new String(this.blob);
425
		int [] LastAuthTime = new int[1];
426
        int [] TokenExpectedTime = new int[1];
427
				
428
		int result = myocra.comGetInfo(0, 0, serialNumber, Constants.getAuthenticationMode("AUTH_OCRA"), blobInString, blobInString[0].length(), LastAuthTime, TokenExpectedTime);
429
		
430
		if(result == 0)
436
		int[] LastAuthTime = new int[1];
437
		int[] TokenExpectedTime = new int[1];
438

  
439
		int result = myocra.comGetInfo(0, 0, serialNumber, Constants.getAuthenticationMode("AUTH_OCRA"), blobInString,
440
				blobInString[0].length(), LastAuthTime, TokenExpectedTime);
441

  
442
		if (result == 0)
431 443
		{
432
			//For first use of blob, LastAuthTimeDate returns a value of 0 or in Date format, Thu Jan 01 07:30:00 SGT 1970.
433
			if(LastAuthTime[0] == 0)
444
			// For first use of blob, LastAuthTimeDate returns a value of 0 or in Date format, Thu Jan 01 07:30:00 SGT
445
			// 1970.
446
			if (LastAuthTime[0] == 0)
434 447
			{
435 448
				System.out.println("No Last authentication.");
436 449
			}
437 450
			else
438 451
			{
439 452
				this.blob = blobInString[0].getBytes();
440
				this.lastAuthentication = new Date(Long.parseLong(""+LastAuthTime[0])* 1000);
441
	            this.tokenExpectedDateTime = new Date(Long.parseLong(""+TokenExpectedTime[0])* 1000);
442
	            this.retCode = result;
453
				this.lastAuthentication = new Date(Long.parseLong("" + LastAuthTime[0]) * 1000);
454
				this.tokenExpectedDateTime = new Date(Long.parseLong("" + TokenExpectedTime[0]) * 1000);
455
				this.retCode = result;
443 456
			}
444 457
		}
445 458
		else
446 459
		{
447
			try 
460
			try
448 461
			{
449 462
				this.errorCount++;
450 463
				this.retCode = result;
451 464
				updateTokenObject();
452 465
				return errorCodeConversion(result);
453
			} 
454
			catch (Exception e) 
466
			} catch (Exception e)
455 467
			{
456 468
				e.printStackTrace();
457 469
			}
458 470
		}
459
		
471

  
460 472
		return Constants.ERR_SUCCESS;
461 473
	}
462
	
474

  
463 475
	/**
464 476
	 * In order to perform blob synchronization, 2 token verifications shall be performed .
465 477
	 * First token verification must have larger time drift windows then the second's.
466 478
	 * First token verification must be executed first before execute the second.
467 479
	 * Second token verification must use new password (OTP) and lesser time drift window.
468 480
	 * If the returned result of first verification is successful or 0, then second verification will be executed.
469
	 * If the returned results of both first and second verifications are successful or 0, then blobSynchronization is successful,
481
	 * If the returned results of both first and second verifications are successful or 0, then blobSynchronization is
482
	 * successful,
470 483
	 * otherwise, it is failed.
471 484
	 * 
472 485
	 * 
......
477 490
	public int blobSynchronization(int timeDriftWindow1, String password1, int timeDriftWindow2, String password2)
478 491
	{
479 492
		int result = blobSynchronizationTokenVerification(timeDriftWindow1, password1);
480
				
481
		if(result == 0)
482
		{			
493

  
494
		if (result == 0)
495
		{
483 496
			result = blobSynchronizationTokenVerification(timeDriftWindow2, password2);
484
			
485
			if(result == 0)
497

  
498
			if (result == 0)
486 499
			{
487 500
				this.retCode = result;
488 501
				updateTokenObject();
489 502
			}
490 503
			else
491 504
			{
492
				try 
505
				try
493 506
				{
494 507
					this.errorCount++;
495 508
					this.retCode = result;
496 509
					updateTokenObject();
497
					return errorCodeConversion(result);	
498
				} 
499
				catch (Exception e) 
510
					return errorCodeConversion(result);
511
				} catch (Exception e)
500 512
				{
501 513
					e.printStackTrace();
502 514
				}
503
			}		
515
			}
504 516
		}
505 517
		else
506 518
		{
507
			try 
519
			try
508 520
			{
509 521
				this.retCode = result;
510
				return errorCodeConversion(result);	
511
			} 
512
			catch (Exception e) 
522
				return errorCodeConversion(result);
523
			} catch (Exception e)
513 524
			{
514 525
				e.printStackTrace();
515 526
			}
......
517 528

  
518 529
		return Constants.ERR_SUCCESS;
519 530
	}
520
	
531

  
521 532
	/**
522 533
	 * This method will be used in blobSynchronization to verify the token and blob synchronization
523 534
	 * 
......
527 538
	 */
528 539
	private int blobSynchronizationTokenVerification(int timeDriftWindow, String password)
529 540
	{
530
		if(this.retCode == 0)
541
		if (this.retCode == 0)
531 542
		{
532 543
			int[] blobLen = new int[1];
533
			blobLen[0]=500;
534
			
544
			blobLen[0] = 500;
545

  
535 546
			String[] blobInString = new String[1];
536 547
			blobInString[0] = new String(this.blob);
537
			
538
			//Time based OTP verification
539
			//Authentication window need to be bigger for first OTP verification.
540
			int result = myocra.comAuthBlob(0, 0, this.serialNumber, Constants.getAuthenticationMode(this.authMode), timeDriftWindow, 1, 100, 
541
								new byte[0], 0, 
542
								new byte[0], 0,	
543
								new byte[0], 0, 
544
								6, password, 
545
								blobInString, blobLen);
546
			
548

  
549
			// Time based OTP verification
550
			// Authentication window need to be bigger for first OTP verification.
551
			int result = myocra.comAuthBlob(0, 0, this.serialNumber, Constants.getAuthenticationMode(this.authMode),
552
					timeDriftWindow, 1, 100,
553
					new byte[0], 0,
554
					new byte[0], 0,
555
					new byte[0], 0,
556
					6, password,
557
					blobInString, blobLen);
558

  
547 559
			this.useCount++;
548 560
			this.firstTimeUsed = new Date();
549 561
			this.lastTimeUsed = new Date();
550
			
551
			if(result == 0)
562

  
563
			if (result == 0)
552 564
			{
553 565
				this.blob = blobInString[0].getBytes();
554 566
				this.retCode = result;
......
556 568
			}
557 569
			else
558 570
			{
559
				try 
571
				try
560 572
				{
561 573
					this.errorCount++;
562 574
					this.retCode = result;
563 575
					updateTokenObject();
564
					return errorCodeConversion(result);	
565
				} 
566
				catch (Exception e) 
576
					return errorCodeConversion(result);
577
				} catch (Exception e)
567 578
				{
568 579
					e.printStackTrace();
569 580
				}
570 581
			}
571 582

  
572 583
		}
573
		
584

  
574 585
		return Constants.ERR_SUCCESS;
575 586
	}
576 587

  
577 588
	/**
578 589
	 * This will reset the token back to the initial state
579
	 * The blob will be regenerated with the backup blob which is initially generated when it is imported from the pskc file.
590
	 * The blob will be regenerated with the backup blob which is initially generated when it is imported from the pskc
591
	 * file.
580 592
	 * 
581 593
	 */
582
	public int resetToken() 
594
	public int resetToken()
583 595
	{
584 596
		this.retCode = 0;
585
		this.blob = tb.getVbkCipherText();	
597
		this.blob = tb.getVbkCipherText();
586 598
		this.useCount = 0;
587 599
		this.errorCount = 0;
588 600
		this.lastTimeUsed = null;
589 601
		this.firstTimeUsed = null;
590 602
		this.lastAuthentication = null;
591
		this.tokenExpectedDateTime = null;		
603
		this.tokenExpectedDateTime = null;
592 604
		updateTokenObject();
593
		
605

  
594 606
		return Constants.ERR_SUCCESS;
595 607
	}
596 608

  
597
	public HashMap<String, String> getTokenBlobInfo() 
609
	public HashMap<String, String> getTokenBlobInfo()
598 610
	{
599
		HashMap<String,String>  map = new HashMap<String,String>();
600
		
611
		HashMap<String, String> map = new HashMap<String, String>();
612

  
601 613
		map.put("TOKEN_MODEL", tb.getVdpModel());
602
		map.put("USE_COUNT",  Integer.toString(tb.getVuseCount()));
603
		map.put("ERROR_COUNT",  Integer.toString(tb.getVerrorCount()));
604
		map.put("LAST_TIME_USED", String.valueOf (tb.getVdateLastUsed()));
614
		map.put("USE_COUNT", Integer.toString(tb.getVuseCount()));
615
		map.put("ERROR_COUNT", Integer.toString(tb.getVerrorCount()));
616
		map.put("LAST_TIME_USED", String.valueOf(tb.getVdateLastUsed()));
605 617
		map.put("TIME_STEP_USED", Integer.toString(Constants.TIMESTEPUSED));
606
		
618

  
607 619
		return map;
608 620
	}
609
	
621

  
610 622
	/**
611 623
	 * display gemalto token information
612 624
	 * In comparion with vasco, only limited information will be displayed.
613 625
	 * 
614 626
	 */
615
	public void displayTokenInfo() 
616
	{		
627
	public void displayTokenInfo()
628
	{
617 629
		System.out.println("--Info----------------------------------------------");
618 630
		System.out.println("TOKEN_MODEL......." + tb.getVdpModel());
619 631
		System.out.println("USE_COUNT........." + tb.getVuseCount());
......
622 634
		System.out.println("TIME_STEP_USED...." + Constants.TIMESTEPUSED);
623 635
		System.out.println("----------------------------------------------------");
624 636
	}
625
	
637

  
626 638
	/**
627 639
	 * This will override the existing tokenBean object
628 640
	 * 
629 641
	 */
630
	private void updateTokenObject() 
642
	private void updateTokenObject()
631 643
	{
632
		tb.setVuseCount(this.useCount );
633
		tb.setVerrorCount(this.errorCount );
644
		tb.setVuseCount(this.useCount);
645
		tb.setVerrorCount(this.errorCount);
634 646
		tb.setVdpCipherText(this.blob);
635 647
		tb.setVdateFirstUsed(this.firstTimeUsed);
636 648
		tb.setVdateLastUsed(this.lastTimeUsed);
637 649
	}
638
	
639 650

  
640
	
641 651
	/*
642 652
	 * Code the gemalto returned error code to match with vasco's
643
	 * 
644 653
	 */
645 654
	private int errorCodeConversion(int errorCode) throws Exception
646
	{		
647
		switch(errorCode)
655
	{
656
		switch (errorCode)
648 657
		{
649
			case 0: return Constants.ERR_SUCCESS;
650
			case 1: return Constants.ERR_INVALID_CREDENTIAL;
651
			case 2: return Constants.ERR_BLOBINVALID;
652
			case 3: return Constants.ERR_BUFFER;
653
			case 4: return Constants.ERR_PARAM;
654
			case 5: return Constants.ERR_SNOINVALID;
655
			case 6: return Constants.ERR_OTPINVALID;
656
			case 7: return Constants.ERR_REUSED_PASSWD;
657
			case 8: return Constants.ERR_BLOBOTINIT;
658
			case 9: return Constants.ERR_FAILED;
659
			case 10: return Constants.ERR_REQINVALID;
660
			case 11: return Constants.ERR_AUTH_MODE;
661
			default: throw new Exception("Invalid error code.");
658
		case 0:
659
			return Constants.ERR_SUCCESS;
660
		case 1:
661
			return Constants.ERR_INVALID_CREDENTIAL;
662
		case 2:
663
			return Constants.ERR_BLOBINVALID;
664
		case 3:
665
			return Constants.ERR_BUFFER;
666
		case 4:
667
			return Constants.ERR_PARAM;
668
		case 5:
669
			return Constants.ERR_SNOINVALID;
670
		case 6:
671
			return Constants.ERR_OTPINVALID;
672
		case 7:
673
			return Constants.ERR_REUSED_PASSWD;
674
		case 8:
675
			return Constants.ERR_BLOBOTINIT;
676
		case 9:
677
			return Constants.ERR_FAILED;
678
		case 10:
679
			return Constants.ERR_REQINVALID;
680
		case 11:
681
			return Constants.ERR_AUTH_MODE;
682
		default:
683
			throw new Exception("Invalid error code.");
662 684
		}
663 685
	}
664 686

  

Also available in: Unified diff