diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java index dfe91427e..94460fc10 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.securitytoken; +import android.hardware.usb.UsbConfiguration; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; @@ -30,6 +31,7 @@ import android.util.Pair; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.securitytoken.t1.T1TPDUProtocol; import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; @@ -77,11 +79,6 @@ public class UsbTransport implements Transport { }; sendRaw(iccPowerCommand); - byte[] bytes; - do { - bytes = receive(); - } while (isDataBlockNotReady(bytes)); - checkDataBlockResponse(bytes); } /** @@ -190,7 +187,17 @@ public class UsbTransport implements Transport { } setIccPower(true); - Log.d(Constants.TAG, "Usb transport connected"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + + } + byte[] atrPacket = receiveRaw(); + + Log.d(Constants.TAG, "Usb transport connected, ATR=" + + Hex.toHexString(atrPacket)); + + new T1TPDUProtocol(this).pps(); } /** @@ -201,14 +208,23 @@ public class UsbTransport implements Transport { */ @Override public byte[] transceive(byte[] data) throws UsbTransportException { - sendXfrBlock(data); + final T1TPDUProtocol t1TPDUProtocol = new T1TPDUProtocol(this); + return t1TPDUProtocol.transceive(data); + /*sendXfrBlock(data); + byte[] bytes = receiveRaw(); + + // Discard header + return Arrays.copyOfRange(bytes, 10, bytes.length);*/ + } + + public byte[] receiveRaw() throws UsbTransportException { byte[] bytes; do { bytes = receive(); } while (isDataBlockNotReady(bytes)); checkDataBlockResponse(bytes); - // Discard header + return Arrays.copyOfRange(bytes, 10, bytes.length); } @@ -218,7 +234,7 @@ public class UsbTransport implements Transport { * @param payload payload to transmit * @throws UsbTransportException */ - private void sendXfrBlock(byte[] payload) throws UsbTransportException { + public void sendXfrBlock(byte[] payload) throws UsbTransportException { int l = payload.length; byte[] data = Arrays.concatenate(new byte[]{ 0x6f, @@ -237,11 +253,16 @@ public class UsbTransport implements Transport { } } - private byte[] receive() throws UsbTransportException { + public byte[] receive() throws UsbTransportException { byte[] buffer = new byte[mBulkIn.getMaxPacketSize()]; byte[] result = null; int readBytes = 0, totalBytes = 0; + /*try { + Thread.sleep(1000); + } catch (InterruptedException e) { + + }*/ do { int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT); if (res < 0) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java new file mode 100644 index 000000000..dbd8896fa --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 Nikita Mikhailov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.securitytoken.t1; + +public enum ChecksumType { + LRC, CRC; + + public byte[] computeChecksum(byte[] data) { + if (this == LRC) { + byte res = 0; + for (byte b : data) { + res ^= b; + } + return new byte[]{res}; + } else { + // Not needed for Nitrokey + return null; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java new file mode 100644 index 000000000..b91efdcae --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2016 Nikita Mikhailov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.securitytoken.t1; + +import android.support.annotation.NonNull; + +import org.bouncycastle.util.Arrays; +import org.sufficientlysecure.keychain.securitytoken.UsbTransportException; + +public class Frame { + private static final byte IBLOCK_PCB_M_BIT_MASK = 1 << 5; + private ChecksumType mChecksumType; + private FrameType mFrameType; + + // Prologue + private byte mNAD; + private byte mPCB; + private byte mLEN; + // Information field + private byte[] mAPDU; // 0..254 + // Epilogue + private byte[] mEDC; // 1..2 + + @NonNull + public static Frame newFrame(@NonNull ChecksumType checksumType, @NonNull FrameType frameType, + boolean hasMore, byte sequenceCounter, @NonNull byte[] apdu) throws UsbTransportException { + final Frame res = new Frame(); + + res.mNAD = 0; + res.mPCB = (byte) (frameType.getSequenceBit() == -1 ? 0 : ((1 & sequenceCounter) << frameType.getSequenceBit())); + if (hasMore && !frameType.isChainingSupported()) { + throw new UsbTransportException("Invalid arguments"); + } + if (hasMore) { + res.mPCB |= 1 << 5; + } + + if (apdu.length == 0) { + throw new UsbTransportException("APDU is too short"); + } + if (apdu.length > 254) { + throw new UsbTransportException("APDU is too long; should be split"); + } + res.mLEN = (byte) apdu.length; + res.mAPDU = apdu; + res.mChecksumType = checksumType; + res.mFrameType = frameType; + + return res; + } + + @NonNull + public static Frame fromData(@NonNull ChecksumType checksumProtocol, @NonNull byte[] data) + throws UsbTransportException { + + final Frame res = new Frame(); + res.mChecksumType = checksumProtocol; + + res.mNAD = data[0]; + res.mPCB = data[1]; + res.mLEN = data[2]; + + int epilogueOffset = 3 + res.mLEN; + res.mAPDU = Arrays.copyOfRange(data, 3, epilogueOffset); + res.mEDC = Arrays.copyOfRange(data, epilogueOffset, data.length); + + if (!Arrays.areEqual(res.mEDC, res.mChecksumType.computeChecksum(Arrays.concatenate( + new byte[]{res.mNAD, res.mPCB, res.mLEN}, + res.mAPDU)))) { + throw new UsbTransportException("T=1 CRC doesnt match"); + } + + return res; + } + + @NonNull + public byte[] getBytes() { + byte[] res = Arrays.concatenate(new byte[]{ + mNAD, mPCB, mLEN + }, mAPDU); + + return Arrays.concatenate(res, mChecksumType.computeChecksum(res)); + } + + @NonNull + public RError getRError() throws UsbTransportException { + if (mFrameType != FrameType.R_BLOCK) { + throw new UsbTransportException("getRerror called for non R block"); + } + return RError.from(mPCB & 0x3); + } + + public byte[] getAPDU() { + return mAPDU; + } + + @NonNull + public FrameType getBlockType() throws UsbTransportException { + return FrameType.fromPCB(mPCB); + } + + public boolean hasMore() { + return mFrameType.isChainingSupported() && (mPCB & IBLOCK_PCB_M_BIT_MASK) != 0; + } + + public enum RError { + NO_ERROR(0), EDC_ERROR(1), OTHER_ERROR(2); + + private int mLowBits; + + RError(int lowBits) { + mLowBits = lowBits; + } + + public int getLowBits() { + return mLowBits; + } + + @NonNull + public static RError from(int i) throws UsbTransportException { + for (final RError error : values()) { + if (error.mLowBits == (i & 0x3)) { + return error; + } + } + throw new UsbTransportException("Invalid R block error bits"); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java new file mode 100644 index 000000000..2a9fd60c6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 Nikita Mikhailov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.securitytoken.t1; + +import org.sufficientlysecure.keychain.securitytoken.UsbTransportException; + +public enum FrameType { + I_BLOCK(0b00000000, 0b10000000, 6, true), // Information + R_BLOCK(0b10000000, 0b11000000, 4, false), // Receipt ack + S_BLOCK(0b11000000, 0b11000000, -1, false); // System + + private byte mValue; + private byte mMask; + private int mSequenceBit; + private boolean mChainingSupported; + + FrameType(int value, int mask, int sequenceBit, boolean chaining) { + // Accept ints just to avoid cast in creation + this.mValue = (byte) value; + this.mMask = (byte) mask; + this.mSequenceBit = sequenceBit; + this.mChainingSupported = chaining; + } + + public static FrameType fromPCB(byte pcb) throws UsbTransportException { + for (final FrameType frameType : values()) { + if ((frameType.mMask & pcb) == frameType.mValue) { + return frameType; + } + } + throw new UsbTransportException("Invalid PCB byte"); + } + + public int getSequenceBit() { + return mSequenceBit; + } + + public boolean isChainingSupported() { + return mChainingSupported; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java new file mode 100644 index 000000000..842497585 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java @@ -0,0 +1,5 @@ +package org.sufficientlysecure.keychain.securitytoken.t1; + +public class RBlock extends Frame { + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java new file mode 100644 index 000000000..899758252 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java @@ -0,0 +1,85 @@ +package org.sufficientlysecure.keychain.securitytoken.t1; + +import android.os.Debug; +import android.support.annotation.NonNull; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.securitytoken.UsbTransport; +import org.sufficientlysecure.keychain.securitytoken.UsbTransportException; +import org.sufficientlysecure.keychain.util.Log; + +public class T1TPDUProtocol { + private final static int MAX_FRAME_LEN = 254; + private byte mCounter = 0; + private UsbTransport mTransport; + + public T1TPDUProtocol(final UsbTransport transport) { + mTransport = transport; + } + + + public void pps() throws UsbTransportException { + byte[] pps = new byte[]{ + (byte) 0xFF, + 1, + (byte) (0xFF ^ 1) + }; + + mTransport.sendXfrBlock(pps); + + byte[] ppsResponse = mTransport.receiveRaw(); + + Log.d(Constants.TAG, "PPS response " + Hex.toHexString(ppsResponse)); + } + + public byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException { + int start = 0; + + Frame responseFrame = null; + while (apdu.length - start > 0) { + boolean hasMore = start + MAX_FRAME_LEN < apdu.length; + int len = Math.min(MAX_FRAME_LEN, apdu.length - start); + + final Frame frame = Frame.newFrame(ChecksumType.LRC, FrameType.I_BLOCK, hasMore, mCounter ^= 1, + Arrays.copyOfRange(apdu, start, start + len)); + + mTransport.sendXfrBlock(frame.getBytes()); + + // Receive R + byte[] response = mTransport.receiveRaw(); + responseFrame = Frame.fromData(ChecksumType.LRC, response); + + start += len; + + if (responseFrame.getBlockType() == FrameType.S_BLOCK) { + Log.d(Constants.TAG, "S block received " + Hex.toHexString(response)); + } else if (responseFrame.getBlockType() == FrameType.R_BLOCK) { + Log.d(Constants.TAG, "R block received " + Hex.toHexString(response)); + if (responseFrame.getRError() != Frame.RError.NO_ERROR) { + throw new UsbTransportException("R block reports error"); + } + } else { + // I block + if (start == apdu.length) { + throw new UsbTransportException("T1 frame response underflow"); + } + break; + } + } + + if (responseFrame == null || responseFrame.getBlockType() != FrameType.I_BLOCK) + throw new UsbTransportException("Invalid frame state"); + + byte[] responseApdu = responseFrame.getAPDU(); + + while (responseFrame.hasMore()) { + responseFrame = Frame.fromData(ChecksumType.LRC, mTransport.receive()); + responseApdu = Arrays.concatenate(responseApdu, responseFrame.getAPDU()); + } + + return responseApdu; + } + +}