/*
* The MIT License
*
* Copyright 2014, 2015, 2016 Rui Martinho (rmartinho@gmail.com), António Braz (antoniocbraz@gmail.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.poreid.cc;
import org.poreid.pcscforjava.CardException;
import org.poreid.pcscforjava.CommandAPDU;
import org.poreid.pcscforjava.ResponseAPDU;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.ResourceBundle;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.poreid.POReIDException;
import org.poreid.Pin;
import org.poreid.common.Util;
import org.poreid.crypto.CanContinue;
import org.poreid.crypto.POReIDSocketFactory;
import org.poreid.dialogs.dialog.DialogController;
import org.poreid.dialogs.pindialogs.otpfeedback.OTPFeedbackDialogController;
import org.poreid.json.JSONObject;
import org.poreid.json.JSONTokener;
/**
*
* @author POReID
*/
class OTP {
private final String OTP_CONNECT_URL = "https://otp.cartaodecidadao.pt/CAPPINChange/connect";
private final String OTP_SEND_PARAMETERS_URL = "https://otp.cartaodecidadao.pt/CAPPINChange/sendParameters";
private final String OTP_SEND_CHANGE_PIN_RESPONSE_URL = "https://otp.cartaodecidadao.pt/CAPPINChange/sendChangePINResponse";
private final String OTP_SCRIPT_COUNTER_PARAMETERS_URL = "https://otp.cartaodecidadao.pt/CAPPINChange/sendResetScriptCounterParameters";
private final String OTP_SCRIPT_COUNTER_RESPONSE_URL = "https://otp.cartaodecidadao.pt/CAPPINChange/resetScriptCounterResponse";
private final String OTP_TRUST_STORE = "/org/poreid/cc/keystores/poreid.cc.otp.ks";
private final String OTP_TRUST_STORE_PASSWORD = "";
private SSLSocketFactory sslSocketFactory;
private String cookie;
private final POReIDCard card;
private Pin pin;
private byte[] p;
private final byte[] GET_PROCESSING_OPTIONS = new byte[]{(byte) 0x80, (byte) 0xA8, 0x00, 0x00, 0x02, (byte) 0x83, 0x00};
private final byte[] GET_DATA__PIN_TRY_COUNTER = new byte[]{(byte) 0x80, (byte) 0xCA, (byte) 0x9F, 0x17, 0x04};
private final byte[] READ_RECORD = new byte[]{ 0x00, (byte) 0xB2, 0x01, 0x0C, 0x5F};
private final byte EMV_PLAIN_TEXT_PIN_PROP = (byte)0x80;
private final byte COUNTER = 0;
private final byte AAC = 0x00;
private final byte TC = 0x40;
private final byte ARQC = (byte)0x80;
private boolean errorExpected;
private OTPFeedbackDialogController otpDialogCtl;
private final ResourceBundle bundle;
private final Proxy proxy;
protected OTP(POReIDCard card, Pin pin, ByteBuffer pins[]) throws POReIDException{
try {
this.card = card;
this.pin = pin;
this.p = pins[1].array();
this.proxy = card.getCardSpecificReferences().getProxy();
this.bundle = CCConfig.getBundle(OTP.class.getSimpleName(),card.getCardSpecificReferences().getLocale());
sslSocketFactory = POReIDSocketFactory.getSSLSocketFactoryOTP(new CanContinue__(), card, OTP_TRUST_STORE, OTP_TRUST_STORE_PASSWORD, pins[0].array());
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | IOException | CertificateException | InvalidAlgorithmParameterException ex) {
throw new POReIDException("Não foi possivel iniciar o processo de alteração do pin OTP", ex);
}
}
protected void doOTPPinModify() throws POReIDException {
byte[] unlockRequestApdu;
byte[] cdol2;
otpDialogCtl = OTPFeedbackDialogController.getInstance(pin.getLabel(), card.getCardSpecificReferences().getLocale());
otpDialogCtl.displayOTPFeedbackDialog();
try {
httpPostDummyRequest();
oTPTransmit(Util.hexToBytes(card.getCardSpecificReferences().getEmvAID()));
unlockRequestApdu = httpPostNewPin(getOTPParameters(p));
otpDialogCtl.updateState();
ResponseAPDU response = oTPTransmit(unlockRequestApdu);
httpPostChangeUnlockPINResponse(new PinChangeUnlockResponse(response.getSW()));
cdol2 = httpPostResetScriptCounter(getOTPOnlineTransactionParameters(p));
httpPostResetScriptCounterResponse(new ResetScriptCounterResponse(oTPTransmitIgnoreErrors(generateAC(TC, cdol2)).getSW()));
} finally {
oTPTransmit(Util.hexToBytes(card.getCardSpecificReferences().getAID()));
}
}
protected void finish(){
otpDialogCtl.updateState();
}
private void httpPostDummyRequest() throws POReIDException{
try {
String post = new JSONObject().put("connect", "").toString();
HttpsURLConnection con = (HttpsURLConnection) new URL(OTP_CONNECT_URL).openConnection(this.proxy);
con.setSSLSocketFactory(sslSocketFactory);
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
con.setRequestProperty("charset", "utf-8");
con.setRequestProperty("Content-Length", String.valueOf(post.getBytes(StandardCharsets.UTF_8).length));
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) {
out.writeBytes(post);
out.flush();
}
otpDialogCtl.updateState();
if (HttpsURLConnection.HTTP_OK == con.getResponseCode()) {
JSONObject js = new JSONObject(new JSONTokener(con.getInputStream()));
if (js.has("connect")) {
cookie = con.getHeaderField("Set-Cookie");
return;
}
}
warnCitizen(new POReIDException("Não foi possivel iniciar o processo de alteração do pin OTP"));
} catch (IOException ex) {
warnCitizen(new POReIDException("Não foi possivel iniciar o processo de alteração do pin OTP", ex));
}
}
private byte[] httpPostNewPin(PinPafUpdate ppu) throws POReIDException{
try {
HttpsURLConnection con = (HttpsURLConnection) new URL(OTP_SEND_PARAMETERS_URL).openConnection(this.proxy);
con.setSSLSocketFactory(sslSocketFactory);
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
con.setRequestProperty("charset", "utf-8");
con.setRequestProperty("Content-Length", "" + String.valueOf(ppu.toString().getBytes(StandardCharsets.UTF_8).length));
con.setRequestProperty("Cookie", cookie);
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) {
out.writeBytes(ppu.toString());
out.flush();
}
otpDialogCtl.updateState();
if (HttpsURLConnection.HTTP_OK == con.getResponseCode()) {
JSONObject js = new JSONObject(new JSONTokener(con.getInputStream()));
if (js.has("PinChangeUnlockRequest") && js.getJSONObject("PinChangeUnlockRequest").has("apdu")) {
js.getJSONObject("PinChangeUnlockRequest").getString("apdu");
return Util.hexToBytes(js.getJSONObject("PinChangeUnlockRequest").getString("apdu"));
}
}
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP"));
} catch (IOException ex) {
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP", ex));
}
return null;
}
private void httpPostChangeUnlockPINResponse(PinChangeUnlockResponse pcur) throws POReIDException {
try {
HttpsURLConnection con = (HttpsURLConnection) new URL(OTP_SEND_CHANGE_PIN_RESPONSE_URL).openConnection(this.proxy);
con.setSSLSocketFactory(sslSocketFactory);
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
con.setRequestProperty("charset", "utf-8");
con.setRequestProperty("Content-Length", String.valueOf(pcur.toString().getBytes(StandardCharsets.UTF_8).length));
con.setRequestProperty("Cookie", cookie);
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) {
out.writeBytes(pcur.toString());
out.flush();
}
if (HttpsURLConnection.HTTP_OK == con.getResponseCode()) {
JSONObject js = new JSONObject(new JSONTokener(con.getInputStream()));
if (js.has("sendChangePINResponse")){
return;
}
}
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP"));
} catch (IOException ex) {
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP", ex));
}
}
private byte[] httpPostResetScriptCounter(OnlineTransactionParameters otp) throws POReIDException{
try {
HttpsURLConnection con = (HttpsURLConnection) new URL(OTP_SCRIPT_COUNTER_PARAMETERS_URL).openConnection(this.proxy);
con.setSSLSocketFactory(sslSocketFactory);
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
con.setRequestProperty("charset", "utf-8");
con.setRequestProperty("Content-Length", String.valueOf(otp.toString().getBytes(StandardCharsets.UTF_8).length));
con.setRequestProperty("Cookie", cookie);
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) {
out.writeBytes(otp.toString());
out.flush();
}
otpDialogCtl.updateState();
if (HttpsURLConnection.HTTP_OK == con.getResponseCode()) {
JSONObject js = new JSONObject(new JSONTokener(con.getInputStream()));
if (js.has("OnlineTransactionRequest") && js.getJSONObject("OnlineTransactionRequest").has("cdol2")) {
return Util.hexToBytes(js.getJSONObject("OnlineTransactionRequest").getString("cdol2"));
}
}
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP"));
} catch (IOException ex) {
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP", ex));
}
return null;
}
private void httpPostResetScriptCounterResponse(ResetScriptCounterResponse rscr) throws POReIDException{
try {
HttpsURLConnection con = (HttpsURLConnection) new URL(OTP_SCRIPT_COUNTER_RESPONSE_URL).openConnection(this.proxy);
con.setSSLSocketFactory(sslSocketFactory);
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
con.setRequestProperty("charset", "utf-8");
con.setRequestProperty("Content-Length", String.valueOf(rscr.toString().getBytes(StandardCharsets.UTF_8).length));
con.setRequestProperty("Cookie", cookie);
con.setUseCaches(false);
con.setDoInput(true);
con.setDoOutput(true);
try (DataOutputStream out = new DataOutputStream(con.getOutputStream())) {
out.writeBytes(rscr.toString());
out.flush();
}
if (HttpsURLConnection.HTTP_OK == con.getResponseCode() || errorExpected) {
otpDialogCtl.updateState();
return;
}
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP"));
} catch (IOException ex) {
warnCitizen(new POReIDException("Não foi possivel continuar o processo de alteração do pin OTP", ex));
}
}
private PinPafUpdate getOTPParameters(byte[] p) throws POReIDException {
byte[] retbuf;
PinPafUpdate ppu = new PinPafUpdate();
ppu.setPin(new String(p, StandardCharsets.UTF_8));
ppu.setCdol1(card.getCardSpecificReferences().getCDOL1());
ppu.setCounter(COUNTER);
oTPTransmit(GET_PROCESSING_OPTIONS);
retbuf = oTPTransmit(GET_DATA__PIN_TRY_COUNTER).getBytes();
ppu.setPinTryCounter(retbuf[3]);
retbuf = oTPTransmit(READ_RECORD).getBytes();
ppu.setPan(Util.extractFromASN1(retbuf, 21, 8));
ppu.setPanSeqNumber(String.format("%02x", retbuf[32]));
retbuf = oTPTransmit(generateAC(AAC, Util.hexToBytes(card.getCardSpecificReferences().getCDOL1()))).getBytes();
ppu.setArqc(Util.extractFromASN1(retbuf, 14, 8));
ppu.setAtc(Util.extractFromASN1(retbuf, 9, 2));
return ppu;
}
private OnlineTransactionParameters getOTPOnlineTransactionParameters(byte[] p) throws POReIDException {
byte[] retbuf;
byte[] otpVerifyPin = new CommandAPDU(0x00, 0x20, 0x00, EMV_PLAIN_TEXT_PIN_PROP, createOTPPinBlock(p)).getBytes();
OnlineTransactionParameters otp = new OnlineTransactionParameters();
oTPTransmit(Util.hexToBytes(card.getCardSpecificReferences().getEmvAID()));
otp.setCdol1(card.getCardSpecificReferences().getCDOL1());
otp.setCounter(COUNTER);
retbuf = oTPTransmit(READ_RECORD).getBytes();
otp.setPan(Util.extractFromASN1(retbuf, 21, 8));
otp.setPanSeqNumber(String.format("%02x", retbuf[32]));
oTPTransmit(GET_PROCESSING_OPTIONS);
oTPTransmit(GET_DATA__PIN_TRY_COUNTER).getBytes();
oTPTransmit(otpVerifyPin);
retbuf = oTPTransmit(generateAC(ARQC, Util.hexToBytes(card.getCardSpecificReferences().getCDOL1()))).getBytes();
otp.setArqc(Util.extractFromASN1(retbuf, 14, 8));
otp.setAtc(Util.extractFromASN1(retbuf, 9, 2));
return otp;
}
private byte[] createOTPPinBlock(byte[] p){
byte[] bcd = new byte[8];
Arrays.fill(bcd, (byte)0xff);
bcd[0] = (byte) (0x02 << 4 | (byte)(p.length));
for (int i=0; i<p.length/2+p.length%2; i++){
bcd[i+1] = (byte)((p[i*2]-0x30)<< 4 | (i*2+1 < p.length ? (p[i*2+1]-0x30) : 0x0f));
}
return bcd;
}
private byte[] generateAC(byte referenceControlParameter, byte[] cdol) {
byte[] apdu = null;
switch(referenceControlParameter){
case ARQC:
case AAC:
apdu = new byte[40];
apdu[0] = (byte) 0x80; // CLA
apdu[1] = (byte) 0xAE; // INS
apdu[2] = referenceControlParameter; // P1
apdu[3] = 0x00; // P2
apdu[4] = (byte)(cdol.length + 6);
System.arraycopy(cdol, 0,apdu, 5, cdol.length);
apdu[34] = 0x34;
apdu[35] = 0x00;
apdu[36] = 0x00;
apdu[37] = 0x01;
apdu[38] = 0x00;
apdu[39] = 0x01;
break;
case TC:
apdu = new byte[cdol.length+5];
apdu[0] = (byte) 0x80; // CLA
apdu[1] = (byte) 0xAE; // INS
apdu[2] = referenceControlParameter; // P1
apdu[3] = 0x00; // P2
apdu[4] = (byte) cdol.length;
System.arraycopy(cdol, 0,apdu, 5, cdol.length);
break;
}
return apdu;
}
private ResponseAPDU oTPTransmitIgnoreErrors(byte[] apdu) throws POReIDException{
ResponseAPDU response = transmit(apdu);
switch(response.getSW()){
case 0x9000:
return response;
case 0x6A80:
case 0x6A86:
errorExpected = true;
return response;
default:
warnCitizen(new POReIDException("Não foi possível modificar o" + pin.getLabel() + ". Código de estado: " + response.getSW()));
}
return null;
}
private ResponseAPDU oTPTransmit(byte[] apdu) throws POReIDException{
ResponseAPDU response = transmit(apdu);
if (0x9000 != response.getSW()) {
warnCitizen(new POReIDException("Não foi possível modificar o" + pin.getLabel() + ". Código de estado: " + response.getSW()));
}
return response;
}
private ResponseAPDU transmit(byte[] apdu) throws POReIDException{
try {
return card.getCardSpecificReferences().getCard().getBasicChannel().transmit(new CommandAPDU(apdu),true,true);
} catch (CardException ex) {
warnCitizen(new POReIDException("Não foi possível modificar o" + pin.getLabel() + ". Código de estado: ",ex));
}
return null;
}
private void warnCitizen(POReIDException ex) throws POReIDException{
otpDialogCtl.closeDialog();
DialogController.getInstance(MessageFormat.format(bundle.getString("dialog.otp.error.title"), pin.getLabel()), MessageFormat.format(bundle.getString("dialog.otp.error.message"),
pin.getLabel()), card.getCardSpecificReferences().getLocale(), true).displayDialog(card.getCardSpecificReferences().getStartTime());
throw ex;
}
private static class CanContinue__ implements CanContinue {
@Override
public boolean proceed() {
return Thread.currentThread().getStackTrace()[3].getClassName().equalsIgnoreCase(CCConfig.AUTHORIZED_INVOCATION);
}
}
}