/****************************************************************************
* Copyright (C) 2012 ecsec GmbH.
* All rights reserved.
* Contact: ecsec GmbH (info@ecsec.de)
*
* This file is part of the Open eCard App.
*
* GNU General Public License Usage
* This file may be used under the terms of the GNU General Public
* License version 3.0 as published by the Free Software Foundation
* and appearing in the file LICENSE.GPL included in the packaging of
* this file. Please review the following information to ensure the
* GNU General Public License version 3.0 requirements will be met:
* http://www.gnu.org/copyleft/gpl.html.
*
* Other Usage
* Alternatively, this file may be used in accordance with the terms
* and conditions contained in a signed written agreement between
* you and ecsec GmbH.
*
***************************************************************************/
package org.openecard.common.util;
import iso.std.iso_iec._24727.tech.schema.InputAPDUInfoType;
import iso.std.iso_iec._24727.tech.schema.PasswordAttributesType;
import static iso.std.iso_iec._24727.tech.schema.PasswordTypeType.*;
import iso.std.iso_iec._24727.tech.schema.PasswordTypeType;
import iso.std.iso_iec._24727.tech.schema.Transmit;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.openecard.common.ECardConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements convenience methods for dealing with PINs.
*
* @author Johannes Schmoelz <johannes.schmoelz@ecsec.de>
* @author Tobias Wich <tobias.wich@ecsec.de>
* @author Dirk Petrautzki <petrautzki@hs-coburg.de>
*/
public class PINUtils {
private static final Logger logger = LoggerFactory.getLogger(PINUtils.class);
/**
* Build a Transmit containing a verify APDU.
*
* @param rawPIN the pin as entered by the user
* @param attributes attributes of the password (e.g. encoding and length)
* @param template the verify template
* @param slotHandle slot handle
* @return Transmit containing the built verify APDU
* @throws UtilException if an pin related error occurs (e.g. wrong PIN length)
*/
public static Transmit buildVerifyTransmit(String rawPIN, PasswordAttributesType attributes, byte[] template,
byte[] slotHandle) throws UtilException {
// concatenate template with encoded pin
byte[] pin = PINUtils.encodePin(rawPIN, attributes);
byte[] pinCmd = ByteUtils.concatenate(template, (byte) pin.length);
pinCmd = ByteUtils.concatenate(pinCmd, pin);
Transmit transmit = new Transmit();
transmit.setSlotHandle(slotHandle);
InputAPDUInfoType pinApdu = new InputAPDUInfoType();
pinApdu.setInputAPDU(pinCmd);
pinApdu.getAcceptableStatusCode().add(new byte[] {(byte)0x90, (byte)0x00});
transmit.getInputAPDUInfo().add(pinApdu);
return transmit;
}
public static byte[] encodePin(String rawPin, PasswordAttributesType attributes) throws UtilException {
// extract attributes
PasswordTypeType pwdType = attributes.getPwdType();
int minLen = attributes.getMinLength().intValue();
int maxLen = (attributes.getMaxLength() == null) ? 0 : attributes.getMaxLength().intValue();
int storedLen = attributes.getStoredLength().intValue();
boolean needsPadding = needsPadding(attributes);
// check if padding is inferred
byte padChar = getPadChar(attributes, needsPadding);
// helper variables
String encoding = "UTF-8";
try {
switch (pwdType) {
case ASCII_NUMERIC:
encoding = "US-ASCII";
case UTF_8:
byte[] textPin = encodeTextPin(encoding, rawPin, minLen, storedLen, maxLen, needsPadding, padChar);
return textPin;
case ISO_9564_1:
case BCD:
case HALF_NIBBLE_BCD:
byte[] bcdPin = encodeBcdPin(pwdType, rawPin, minLen, storedLen, maxLen, needsPadding, padChar);
return bcdPin;
default:
String msg = "Unsupported PIN encoding requested.";
UtilException ex = new UtilException(ECardConstants.Minor.IFD.IO.UNKNOWN_PIN_FORMAT, msg);
logger.error(ex.getMessage(), ex);
throw ex;
}
} catch (UnsupportedEncodingException ex) {
throw new UtilException(ex);
} catch (IOException ex) {
throw new UtilException(ex);
}
}
public static byte[] createPinMask(PasswordAttributesType attributes) throws UtilException {
// extract attributes
PasswordTypeType pwdType = attributes.getPwdType();
int minLen = attributes.getMinLength().intValue();
int maxLen = (attributes.getMaxLength() == null) ? 0 : attributes.getMaxLength().intValue();
int storedLen = attributes.getStoredLength().intValue();
boolean needsPadding = needsPadding(attributes);
// opt out if needs-padding is not on
if (! needsPadding) {
return new byte[0];
}
byte padChar = getPadChar(attributes, needsPadding);
if (storedLen <= 0) {
throw new UtilException("PIN mask can only be created when storage size is known.");
}
// they are all the same except half nibble which
if (HALF_NIBBLE_BCD == pwdType) {
padChar = (byte) (padChar | 0xF0);
}
byte[] mask = new byte[storedLen];
Arrays.fill(mask, padChar);
// iso needs a sligth correction
if (ISO_9564_1 == pwdType) {
mask[0] = 0x20;
}
return mask;
}
public static byte[] encodeTextPin(String encoding, String rawPin, int minLen, int storedLen, int maxLen,
boolean needsPadding, byte padChar) throws UnsupportedEncodingException, UtilException {
// perform some basic checks
if (needsPadding && storedLen <= 0) {
String msg = "Padding is required, but no stored length is given.";
throw new UtilException(msg);
}
if (rawPin.length() < minLen) {
String msg = String.format("Entered PIN is too short, enter at least %d characters.", minLen);
throw new UtilException(msg);
//throw new UtilException("PIN contains invalid symbols.");
}
if (maxLen > 0 && rawPin.length() > maxLen) {
String msg = String.format("Entered PIN is too long, enter at most %d characters.", maxLen);
throw new UtilException(msg);
}
// get the pin string and validate it is within stored length
Charset charset = Charset.forName(encoding);
byte[] pinBytes = rawPin.getBytes(charset);
if (storedLen > 0 && pinBytes.length > storedLen) {
String msg = String.format("Storage size for PIN exceeded, only %d bytes are allowed.", storedLen);
throw new UtilException(msg);
}
// if the pin is too short, append the necessary padding bytes
if (needsPadding && pinBytes.length < storedLen) {
int missingBytes = storedLen - pinBytes.length;
byte[] filler = new byte[missingBytes];
Arrays.fill(filler, padChar);
pinBytes = ByteUtils.concatenate(pinBytes, filler);
}
return pinBytes;
}
public static byte[] encodeBcdPin(PasswordTypeType pwdType, String rawPin, int minLen, int storedLen, int maxLen,
boolean needsPadding, byte padChar) throws UtilException, IOException {
ByteArrayOutputStream o = new ByteArrayOutputStream();
int pinSize = rawPin.length();
if (ISO_9564_1 == pwdType) {
byte head = (byte) (0x20 | (0x0F & pinSize));
o.write(head);
}
if (HALF_NIBBLE_BCD == pwdType) {
for (int i = 0; i < pinSize; i++) {
char nextChar = rawPin.charAt(i);
byte digit = (byte) (0xF0 | getByte(nextChar));
o.write(digit);
}
} else if (BCD == pwdType || ISO_9564_1 == pwdType) {
for (int i = 0; i < pinSize; i += 2) {
byte b1 = (byte) (getByte(rawPin.charAt(i)) << 4);
byte b2 = (byte) (padChar & 0x0F); // lower nibble set to pad byte
// one char left, replace pad nibble with it
if (i + 1 < pinSize) {
b2 = (byte) (getByte(rawPin.charAt(i + 1)) & 0x0F);
}
byte b = (byte) (b1 | b2);
o.write(b);
}
}
// add padding bytes if needed
if (needsPadding && o.size() < storedLen) {
int missingBytes = storedLen - o.size();
byte[] filler = new byte[missingBytes];
Arrays.fill(filler, padChar);
o.write(filler);
}
return o.toByteArray();
}
private static byte getByte(char c) throws UtilException {
if (c >= '0' && c <= '9') {
return (byte) (c - '0');
} else {
UtilException ex = new UtilException("Entered PIN contains invalid characters.");
logger.error(ex.getMessage(), ex);
throw ex;
}
}
private static byte getPadChar(PasswordAttributesType attributes, boolean needsPadding) throws UtilException {
if (PasswordTypeType.ISO_9564_1.equals(attributes.getPwdType())) {
return (byte) 0xFF;
} else {
byte[] padChars = attributes.getPadChar();
if (padChars != null && padChars.length == 1) {
return padChars[0];
} else if (needsPadding) {
UtilException ex = new UtilException("Unsupported combination of PIN parameters concerning padding.");
throw ex;
} else {
// just return a value, it is not gonna be used in this case
return 0;
}
}
}
private static boolean needsPadding(PasswordAttributesType attributes) {
PasswordTypeType pwdType = attributes.getPwdType();
if (ISO_9564_1 == pwdType) {
return true;
} else {
boolean needsPadding = attributes.getPwdFlags().contains("needs-padding");
return needsPadding;
}
}
}