From 3656cbd539e22efc125b5be869f9029887aa52c4 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 22:43:38 +0600 Subject: [PATCH 01/28] Add new build type to mitigate debugging issues --- OpenKeychain/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 4d1b572aa..6abb92477 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -207,6 +207,12 @@ android { buildConfigField "String", "GITHUB_CLIENT_ID", "\"c942cd81844d94e7e41b\"" buildConfigField "String", "GITHUB_CLIENT_SECRET", "\"f1dd17e70a0614abbd9310b00a310e23c6c8edff\"" } + + // Workaround for http://stackoverflow.com/questions/27909613/cannot-see-parameter-value-in-android-studio-when-breakpoint-is-in-first-line-of + debugWithoutTestCoverage.initWith(debug) + debugWithoutTestCoverage { + testCoverageEnabled false + } } productFlavors { From 345178ccd1e790bf4ebfd846299613e77b6a451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 15 Apr 2016 18:35:41 +0200 Subject: [PATCH 02/28] Add Nitrokey device filter --- OpenKeychain/src/main/res/xml/usb_device_filter.xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/OpenKeychain/src/main/res/xml/usb_device_filter.xml b/OpenKeychain/src/main/res/xml/usb_device_filter.xml index 789e4ffb7..f335201dd 100644 --- a/OpenKeychain/src/main/res/xml/usb_device_filter.xml +++ b/OpenKeychain/src/main/res/xml/usb_device_filter.xml @@ -1,7 +1,10 @@ @@ -24,4 +27,12 @@ + + + + + + + + \ No newline at end of file From 597d6d5b5a650631ca0f1d2131c11d96a5cd6b6f Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 7 May 2016 15:54:04 +0600 Subject: [PATCH 03/28] wip: t1 tpdu loe --- .../keychain/securitytoken/UsbTransport.java | 41 +++-- .../securitytoken/t1/ChecksumType.java | 35 +++++ .../keychain/securitytoken/t1/Frame.java | 145 ++++++++++++++++++ .../keychain/securitytoken/t1/FrameType.java | 56 +++++++ .../keychain/securitytoken/t1/RBlock.java | 5 + .../securitytoken/t1/T1TPDUProtocol.java | 85 ++++++++++ 6 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java 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; + } + +} From c9c7feb5f7bebbe9b9ab151fd0c95d158254b641 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 8 May 2016 23:11:57 +0600 Subject: [PATCH 04/28] SecurityToken: add support for T=1 TPDU level of exchange --- .../keychain/securitytoken/t1/Frame.java | 145 --------------- .../keychain/securitytoken/t1/RBlock.java | 5 - .../securitytoken/t1/T1TPDUProtocol.java | 85 --------- .../securitytoken/usb/CcidTransceiver.java | 169 +++++++++++++++++ .../usb/CcidTransportProtocol.java | 24 +++ .../usb/T1ShortApduProtocol.java | 41 +++++ .../securitytoken/{ => usb}/UsbTransport.java | 172 ++++-------------- .../{ => usb}/UsbTransportException.java | 2 +- .../usb/tpdu/BlockChecksumType.java | 46 +++++ .../{t1 => usb/tpdu}/FrameType.java | 4 +- .../usb/tpdu/T1TpduProtocol.java | 148 +++++++++++++++ .../securitytoken/usb/tpdu/block/Block.java | 100 ++++++++++ .../usb/tpdu/block/BlockFactory.java | 51 ++++++ .../securitytoken/usb/tpdu/block/IBlock.java | 45 +++++ .../securitytoken/usb/tpdu/block/RBlock.java | 63 +++++++ .../tpdu/block/SBlock.java} | 19 +- .../ui/base/BaseSecurityTokenActivity.java | 2 +- 17 files changed, 735 insertions(+), 386 deletions(-) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransportProtocol.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/T1ShortApduProtocol.java rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{ => usb}/UsbTransport.java (58%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{ => usb}/UsbTransportException.java (95%) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/BlockChecksumType.java rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{t1 => usb/tpdu}/FrameType.java (92%) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{t1/ChecksumType.java => usb/tpdu/block/SBlock.java} (64%) 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 deleted file mode 100644 index b91efdcae..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/Frame.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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/RBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java deleted file mode 100644 index 842497585..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/RBlock.java +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 899758252..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/T1TPDUProtocol.java +++ /dev/null @@ -1,85 +0,0 @@ -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; - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java new file mode 100644 index 000000000..d43c373d7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java @@ -0,0 +1,169 @@ +/* + * 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.usb; + +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class CcidTransceiver { + private static final int TIMEOUT = 20 * 1000; // 20s + + private byte mCounter; + private UsbDeviceConnection mConnection; + private UsbEndpoint mBulkIn; + private UsbEndpoint mBulkOut; + + public CcidTransceiver(final UsbDeviceConnection connection, final UsbEndpoint bulkIn, + final UsbEndpoint bulkOut) { + + mConnection = connection; + mBulkIn = bulkIn; + mBulkOut = bulkOut; + } + + public byte[] receiveRaw() throws UsbTransportException { + byte[] bytes; + do { + bytes = receive(); + } while (isDataBlockNotReady(bytes)); + + checkDataBlockResponse(bytes); + + return Arrays.copyOfRange(bytes, 10, bytes.length); + } + + /** + * Power of ICC + * Spec: 6.1.1 PC_to_RDR_IccPowerOn + * + * @throws UsbTransportException + */ + @NonNull + public byte[] iccPowerOn() throws UsbTransportException { + final byte[] iccPowerCommand = { + 0x62, + 0x00, 0x00, 0x00, 0x00, + 0x00, + mCounter++, + 0x00, + 0x00, 0x00 + }; + + sendRaw(iccPowerCommand); + + long startTime = System.currentTimeMillis(); + byte[] atr = null; + while (true) { + try { + atr = receiveRaw(); + break; + } catch (UsbTransportException e) { + // Try more startTime + if (System.currentTimeMillis() - startTime > TIMEOUT) { + break; + } + } + SystemClock.sleep(100); + } + + if (atr == null) { + throw new UsbTransportException("Couldn't power up Security Token"); + } + + return atr; + } + + /** + * Transmits XfrBlock + * 6.1.4 PC_to_RDR_XfrBlock + * @param payload payload to transmit + * @throws UsbTransportException + */ + public void sendXfrBlock(byte[] payload) throws UsbTransportException { + int l = payload.length; + byte[] data = Arrays.concatenate(new byte[]{ + 0x6f, + (byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24), + 0x00, + mCounter++, + 0x00, + 0x00, 0x00}, + payload); + + int send = 0; + while (send < data.length) { + final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send); + sendRaw(Arrays.copyOfRange(data, send, send + len)); + send += len; + } + } + + public byte[] receive() throws UsbTransportException { + byte[] buffer = new byte[mBulkIn.getMaxPacketSize()]; + byte[] result = null; + int readBytes = 0, totalBytes = 0; + + do { + int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT); + if (res < 0) { + throw new UsbTransportException("USB error - failed to receive response " + res); + } + if (result == null) { + if (res < 10) { + throw new UsbTransportException("USB-CCID error - failed to receive CCID header"); + } + totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10; + result = new byte[totalBytes]; + } + System.arraycopy(buffer, 0, result, readBytes, res); + readBytes += res; + } while (readBytes < totalBytes); + + return result; + } + + private void sendRaw(final byte[] data) throws UsbTransportException { + final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT); + if (tr1 != data.length) { + throw new UsbTransportException("USB error - failed to transmit data " + tr1); + } + } + + private byte getStatus(byte[] bytes) { + return (byte) ((bytes[7] >> 6) & 0x03); + } + + private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException { + final byte status = getStatus(bytes); + if (status != 0) { + throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1)); + } + } + + private boolean isDataBlockNotReady(byte[] bytes) { + return getStatus(bytes) == 2; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransportProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransportProtocol.java new file mode 100644 index 000000000..5cfc3586d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransportProtocol.java @@ -0,0 +1,24 @@ +/* + * 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.usb; + +import android.support.annotation.NonNull; + +public interface CcidTransportProtocol { + byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/T1ShortApduProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/T1ShortApduProtocol.java new file mode 100644 index 000000000..883f11cef --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/T1ShortApduProtocol.java @@ -0,0 +1,41 @@ +/* + * 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.usb; + +import android.support.annotation.NonNull; + +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +public class T1ShortApduProtocol implements CcidTransportProtocol { + private CcidTransceiver mTransceiver; + + public T1ShortApduProtocol(CcidTransceiver transceiver) throws UsbTransportException { + mTransceiver = transceiver; + + byte[] atr = mTransceiver.iccPowerOn(); + Log.d(Constants.TAG, "Usb transport connected T1/Short APDU, ATR=" + Hex.toHexString(atr)); + } + + @Override + public byte[] transceive(@NonNull final byte[] apdu) throws UsbTransportException { + mTransceiver.sendXfrBlock(apdu); + return mTransceiver.receiveRaw(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java similarity index 58% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java index 94460fc10..037a23352 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java @@ -15,9 +15,8 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.usb; -import android.hardware.usb.UsbConfiguration; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; @@ -28,10 +27,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.securitytoken.Transport; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol; import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; @@ -45,7 +43,15 @@ import java.nio.ByteOrder; */ public class UsbTransport implements Transport { private static final int USB_CLASS_SMARTCARD = 11; - private static final int TIMEOUT = 20 * 1000; // 20s + private static final int PROTOCOLS_OFFSET = 6; + private static final int FEATURES_OFFSET = 40; + private static final int MASK_T1_PROTO = 2; + + // dwFeatures Masks + private static final int MASK_TPDU = 0x10000; + private static final int MASK_SHORT_APDU = 0x20000; + private static final int MASK_EXTENDED_APDU = 0x40000; + private final UsbManager mUsbManager; private final UsbDevice mUsbDevice; @@ -53,34 +59,14 @@ public class UsbTransport implements Transport { private UsbEndpoint mBulkIn; private UsbEndpoint mBulkOut; private UsbDeviceConnection mConnection; - private byte mCounter; + private CcidTransceiver mTransceiver; + private CcidTransportProtocol mProtocol; public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) { mUsbDevice = usbDevice; mUsbManager = usbManager; } - - /** - * Manage ICC power, Yubikey requires to power on ICC - * Spec: 6.1.1 PC_to_RDR_IccPowerOn; 6.1.2 PC_to_RDR_IccPowerOff - * - * @param on true to turn ICC on, false to turn it off - * @throws UsbTransportException - */ - private void setIccPower(boolean on) throws UsbTransportException { - final byte[] iccPowerCommand = { - (byte) (on ? 0x62 : 0x63), - 0x00, 0x00, 0x00, 0x00, - 0x00, - mCounter++, - 0x00, - 0x00, 0x00 - }; - - sendRaw(iccPowerCommand); - } - /** * Get first class 11 (Chip/Smartcard) interface of the device * @@ -162,7 +148,6 @@ public class UsbTransport implements Transport { */ @Override public void connect() throws IOException { - mCounter = 0; mUsbInterface = getSmartCardInterface(mUsbDevice); if (mUsbInterface == null) { // Shouldn't happen as we whitelist only class 11 devices @@ -186,18 +171,34 @@ public class UsbTransport implements Transport { throw new UsbTransportException("USB error - failed to claim interface"); } - setIccPower(true); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { + mTransceiver = new CcidTransceiver(mConnection, mBulkIn, mBulkOut); + + + configureProtocol(); + } + + private void configureProtocol() throws UsbTransportException { + byte[] desc = mConnection.getRawDescriptors(); + if (desc.length < FEATURES_OFFSET + 4) + throw new UsbTransportException("Can't access dwFeatures for protocol selection"); + + int dwProtocols = ByteBuffer.wrap(desc, FEATURES_OFFSET, 4).order(ByteOrder.LITTLE_ENDIAN) + .asIntBuffer().get(); + int dwFeatures = ByteBuffer.wrap(desc, FEATURES_OFFSET, 4).order(ByteOrder.LITTLE_ENDIAN) + .asIntBuffer().get(); + + if ((dwProtocols & MASK_T1_PROTO) == 0) { + throw new UsbTransportException("T=0 protocol is not supported"); } - byte[] atrPacket = receiveRaw(); - Log.d(Constants.TAG, "Usb transport connected, ATR=" - + Hex.toHexString(atrPacket)); - - new T1TPDUProtocol(this).pps(); + if ((dwFeatures & MASK_TPDU) != 0) { + mProtocol = new T1TpduProtocol(mTransceiver); + } else if (((dwFeatures & MASK_SHORT_APDU) != 0) || ((dwFeatures & MASK_EXTENDED_APDU) != 0)) { + mProtocol = new T1ShortApduProtocol(mTransceiver); + } else { + throw new UsbTransportException("Character level exchange is not supported"); + } } /** @@ -208,100 +209,7 @@ public class UsbTransport implements Transport { */ @Override public byte[] transceive(byte[] data) throws UsbTransportException { - 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); - - return Arrays.copyOfRange(bytes, 10, bytes.length); - } - - /** - * Transmits XfrBlock - * 6.1.4 PC_to_RDR_XfrBlock - * @param payload payload to transmit - * @throws UsbTransportException - */ - public void sendXfrBlock(byte[] payload) throws UsbTransportException { - int l = payload.length; - byte[] data = Arrays.concatenate(new byte[]{ - 0x6f, - (byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24), - 0x00, - mCounter++, - 0x00, - 0x00, 0x00}, - payload); - - int send = 0; - while (send < data.length) { - final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send); - sendRaw(Arrays.copyOfRange(data, send, send + len)); - send += len; - } - } - - 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) { - throw new UsbTransportException("USB error - failed to receive response " + res); - } - if (result == null) { - if (res < 10) { - throw new UsbTransportException("USB-CCID error - failed to receive CCID header"); - } - totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10; - result = new byte[totalBytes]; - } - System.arraycopy(buffer, 0, result, readBytes, res); - readBytes += res; - } while (readBytes < totalBytes); - - return result; - } - - private void sendRaw(final byte[] data) throws UsbTransportException { - final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT); - if (tr1 != data.length) { - throw new UsbTransportException("USB error - failed to transmit data " + tr1); - } - } - - private byte getStatus(byte[] bytes) { - return (byte) ((bytes[7] >> 6) & 0x03); - } - - private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException { - final byte status = getStatus(bytes); - if (status != 0) { - throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1)); - } - } - - private boolean isDataBlockNotReady(byte[] bytes) { - return getStatus(bytes) == 2; + return mProtocol.transceive(data); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java similarity index 95% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java index 6d9212d9f..13e86c723 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.usb; import java.io.IOException; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/BlockChecksumType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/BlockChecksumType.java new file mode 100644 index 000000000..4af6364e3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/BlockChecksumType.java @@ -0,0 +1,46 @@ +/* + * 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.usb.tpdu; + +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; + +public enum BlockChecksumType { + LRC(1), CRC(2); + + private int mLength; + + BlockChecksumType(int length) { + mLength = length; + } + + public byte[] computeChecksum(byte[] data, int offset, int len) throws UsbTransportException { + if (this == LRC) { + byte res = 0; + for (int i = offset; i < len; i++) { + res ^= data[i]; + } + return new byte[]{res}; + } else { + throw new UsbTransportException("CRC checksum is not implemented"); + } + } + + public int getLength() { + return mLength; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/FrameType.java similarity index 92% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/FrameType.java index 2a9fd60c6..03a8b109a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/FrameType.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/FrameType.java @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.t1; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; -import org.sufficientlysecure.keychain.securitytoken.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; public enum FrameType { I_BLOCK(0b00000000, 0b10000000, 6, true), // Information diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java new file mode 100644 index 000000000..59e651a3f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java @@ -0,0 +1,148 @@ +/* + * 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.usb.tpdu; + +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.usb.CcidTransceiver; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransportProtocol; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.Block; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.IBlock; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.RBlock; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.SBlock; +import org.sufficientlysecure.keychain.util.Log; + +public class T1TpduProtocol implements CcidTransportProtocol { + private final static int MAX_FRAME_LEN = 254; + + private byte mCounter = 0; + private CcidTransceiver mTransceiver; + private BlockChecksumType mChecksumType; + + public T1TpduProtocol(final CcidTransceiver transceiver) throws UsbTransportException { + mTransceiver = transceiver; + + // Connect + byte[] atr = mTransceiver.iccPowerOn(); + Log.d(Constants.TAG, "Usb transport connected T1/TPDU, ATR=" + Hex.toHexString(atr)); + // TODO: set checksum from atr + mChecksumType = BlockChecksumType.LRC; + + // PPS all auto + pps(); + } + + protected void pps() throws UsbTransportException { + byte[] pps = new byte[]{(byte) 0xFF, 1, (byte) (0xFF ^ 1)}; + + mTransceiver.sendXfrBlock(pps); + + byte[] ppsResponse = mTransceiver.receiveRaw(); + + Log.d(Constants.TAG, "PPS response " + Hex.toHexString(ppsResponse)); + } + + public byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException { + int start = 0; + + if (apdu.length == 0) { + throw new UsbTransportException("Cant transcive zero-length apdu(tpdu)"); + } + + Block responseBlock = null; + while (apdu.length - start > 0) { + boolean hasMore = start + MAX_FRAME_LEN < apdu.length; + int len = Math.min(MAX_FRAME_LEN, apdu.length - start); + + // Send next frame + Block block = newIBlock(mCounter++, hasMore, Arrays.copyOfRange(apdu, start, start + len)); + + mTransceiver.sendXfrBlock(block.getRawData()); + + // Receive I or R block + responseBlock = getBlockFromResponse(mTransceiver.receiveRaw()); + + start += len; + + if (responseBlock instanceof SBlock) { + Log.d(Constants.TAG, "S-Block received " + responseBlock.toString()); + // just ignore + } else if (responseBlock instanceof RBlock) { + Log.d(Constants.TAG, "R-Block received " + responseBlock.toString()); + if (((RBlock) responseBlock).getError() != RBlock.RError.NO_ERROR) { + throw new UsbTransportException("R-Block reports error " + + ((RBlock) responseBlock).getError()); + } + } else { // I block + if (start != apdu.length) { + throw new UsbTransportException("T1 frame response underflow"); + } + break; + } + } + + // Receive + if (responseBlock == null || !(responseBlock instanceof IBlock)) + throw new UsbTransportException("Invalid tpdu sequence state"); + + byte[] responseApdu = responseBlock.getApdu(); + + while (((IBlock) responseBlock).getChaining()) { + Block ackBlock = newRBlock(((IBlock) responseBlock).getSequence()); + mTransceiver.sendXfrBlock(ackBlock.getRawData()); + + responseBlock = getBlockFromResponse(mTransceiver.receive()); + + if (responseBlock instanceof IBlock) { + responseApdu = Arrays.concatenate(responseApdu, responseBlock.getApdu()); + } else { + Log.d(Constants.TAG, "Response block received " + responseBlock.toString()); + throw new UsbTransportException("Response: invalid state - invalid block received"); + } + } + + return responseApdu; + } + + // Factory methods + public Block getBlockFromResponse(byte[] data) throws UsbTransportException { + final Block baseBlock = new Block(mChecksumType, data); + + if ((baseBlock.getPcb() & 0b10000000) == 0b00000000) { + return new IBlock(baseBlock); + } else if ((baseBlock.getPcb() & 0b11000000) == 0b11000000) { + return new SBlock(baseBlock); + } else if ((baseBlock.getPcb() & 0b11000000) == 0b10000000) { + return new RBlock(baseBlock); + } + + throw new UsbTransportException("TPDU Unknown block type"); + } + + public IBlock newIBlock(byte sequence, boolean chaining, byte[] apdu) throws UsbTransportException { + return new IBlock(mChecksumType, (byte) 0, sequence, chaining, apdu); + } + + public RBlock newRBlock(byte sequence) throws UsbTransportException { + return new RBlock(mChecksumType, (byte) 0, sequence); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java new file mode 100644 index 000000000..bfebd1934 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java @@ -0,0 +1,100 @@ +/* + * 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.usb.tpdu.block; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; + +public class Block { + protected static final int MAX_PAYLOAD_LEN = 254; + protected static final int OFFSET_NAD = 0; + protected static final int OFFSET_PCB = 1; + protected static final int OFFSET_LEN = 2; + protected static final int OFFSET_DATA = 3; + + protected byte[] mData; + protected BlockChecksumType mChecksumType; + + public Block(BlockChecksumType checksumType, byte[] data) throws UsbTransportException { + this.mChecksumType = checksumType; + this.mData = data; + + int checksumOffset = this.mData.length - mChecksumType.getLength(); + byte[] checksum = mChecksumType.computeChecksum(data, 0, checksumOffset); + if (!Arrays.areEqual(checksum, getEdc())) { + throw new UsbTransportException("TPDU CRC doesn't match"); + } + } + + protected Block(BlockChecksumType checksumType, byte nad, byte pcb, byte[] apdu) + throws UsbTransportException { + this.mChecksumType = checksumType; + if (apdu.length > MAX_PAYLOAD_LEN) { + throw new UsbTransportException("APDU is too long; should be split"); + } + this.mData = Arrays.concatenate( + new byte[]{nad, pcb, (byte) apdu.length}, + apdu, + new byte[mChecksumType.getLength()]); + + int checksumOffset = this.mData.length - mChecksumType.getLength(); + byte[] checksum = mChecksumType.computeChecksum(this.mData, 0, checksumOffset); + + System.arraycopy(checksum, 0, this.mData, checksumOffset, mChecksumType.getLength()); + } + + protected Block(Block baseBlock) { + this.mChecksumType = baseBlock.getChecksumType(); + this.mData = baseBlock.getRawData(); + } + + public byte getNad() { + return mData[OFFSET_NAD]; + } + + public byte getPcb() { + return mData[OFFSET_PCB]; + } + + public byte getLen() { + return mData[OFFSET_LEN]; + } + + public byte[] getEdc() { + return Arrays.copyOfRange(mData, mData.length - mChecksumType.getLength(), mChecksumType.getLength()); + } + + public BlockChecksumType getChecksumType() { + return mChecksumType; + } + + public byte[] getApdu() { + return Arrays.copyOfRange(mData, OFFSET_DATA, mData.length - mChecksumType.getLength()); + } + + public byte[] getRawData() { + return mData; + } + + @Override + public String toString() { + return Hex.toHexString(mData); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java new file mode 100644 index 000000000..c64f2315c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java @@ -0,0 +1,51 @@ +/* + * 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.usb.tpdu.block; + +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; + +public class BlockFactory { + private BlockChecksumType mChecksumType = BlockChecksumType.LRC; + + public Block getBlockFromResponse(byte[] data) throws UsbTransportException { + final Block baseBlock = new Block(mChecksumType, data); + + if ((baseBlock.getPcb() & 0b10000000) == 0b00000000) { + return new IBlock(baseBlock); + } else if ((baseBlock.getPcb() & 0b11000000) == 0b11000000) { + return new SBlock(baseBlock); + } else if ((baseBlock.getPcb() & 0b11000000) == 0b10000000) { + return new RBlock(baseBlock); + } + + throw new UsbTransportException("TPDU Unknown block type"); + } + + public IBlock newIBlock(byte sequence, boolean chaining, byte[] apdu) throws UsbTransportException { + return new IBlock(mChecksumType, (byte) 0, sequence, chaining, apdu); + } + + public RBlock newRBlock(byte sequence) throws UsbTransportException { + return new RBlock(mChecksumType, (byte) 0, sequence); + } + + public void setChecksumType(final BlockChecksumType checksumType) { + mChecksumType = checksumType; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java new file mode 100644 index 000000000..e7e9f8b51 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java @@ -0,0 +1,45 @@ +/* + * 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.usb.tpdu.block; + +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; + +public class IBlock extends Block { + private static final byte BIT_SEQUENCE = 6; + private static final byte BIT_CHAINING = 5; + + public IBlock(final Block baseBlock) { + super(baseBlock); + } + + public IBlock(BlockChecksumType checksumType, byte nad, byte sequence, boolean chaining, + byte[] apdu) throws UsbTransportException { + super(checksumType, nad, + (byte) (((sequence & 1) << BIT_SEQUENCE) | (chaining ? 1 << BIT_CHAINING : 0)), + apdu); + } + + public byte getSequence() { + return (byte) ((getPcb() >> BIT_SEQUENCE) & 1); + } + + public boolean getChaining() { + return ((getPcb() >> BIT_CHAINING) & 1) != 0; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java new file mode 100644 index 000000000..f0262257e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java @@ -0,0 +1,63 @@ +/* + * 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.usb.tpdu.block; + +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; +import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; + +public class RBlock extends Block { + private static final byte BIT_SEQUENCE = 5; + + public RBlock(Block baseBlock) throws UsbTransportException { + super(baseBlock); + if (getApdu().length != 0) { + throw new UsbTransportException("Data in R-block"); + } + } + + public RBlock(BlockChecksumType checksumType, byte nad, byte sequence) + throws UsbTransportException { + super(checksumType, nad, (byte) (0b10000000 | ((sequence & 1) << BIT_SEQUENCE)), new byte[0]); + } + + public RError getError() throws UsbTransportException { + return RError.from(getPcb()); + } + + public enum RError { + NO_ERROR(0), EDC_ERROR(1), OTHER_ERROR(2); + + private byte mLowBits; + + RError(int lowBits) { + mLowBits = (byte) lowBits; + } + + @NonNull + public static RError from(byte 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/ChecksumType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java similarity index 64% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java index dbd8896fa..18a66f548 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/t1/ChecksumType.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java @@ -15,21 +15,10 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.t1; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block; -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; - } +public class SBlock extends Block { + public SBlock(Block baseBlock) { + super(baseBlock); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java index 680613596..72216bd52 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java @@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.securitytoken.NfcTransport; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper; import org.sufficientlysecure.keychain.securitytoken.Transport; import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher; -import org.sufficientlysecure.keychain.securitytoken.UsbTransport; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransport; import org.sufficientlysecure.keychain.ui.CreateKeyActivity; import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity; import org.sufficientlysecure.keychain.ui.ViewKeyActivity; From a017b9f25a1188eeff6f1b1f2da4111fbb147d16 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 9 May 2016 13:42:05 +0600 Subject: [PATCH 05/28] SecurityToken: fix ccid class header parsing --- .../securitytoken/usb/CcidTransceiver.java | 2 +- .../securitytoken/usb/UsbTransport.java | 40 +++++++++++---- .../usb/tpdu/{block => }/Block.java | 5 +- .../usb/tpdu/{block => }/IBlock.java | 3 +- .../usb/tpdu/{block => }/RBlock.java | 3 +- .../usb/tpdu/{block => }/SBlock.java | 2 +- .../usb/tpdu/T1TpduProtocol.java | 6 +-- .../usb/tpdu/block/BlockFactory.java | 51 ------------------- 8 files changed, 38 insertions(+), 74 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/{block => }/Block.java (94%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/{block => }/IBlock.java (90%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/{block => }/RBlock.java (93%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/{block => }/SBlock.java (92%) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java index d43c373d7..acb1ca9f4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java @@ -80,7 +80,7 @@ public class CcidTransceiver { try { atr = receiveRaw(); break; - } catch (UsbTransportException e) { + } catch (Exception e) { // Try more startTime if (System.currentTimeMillis() - startTime > TIMEOUT) { break; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java index 037a23352..e1409f39e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java @@ -30,6 +30,7 @@ import android.util.Pair; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.securitytoken.Transport; import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol; +import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; @@ -42,10 +43,9 @@ import java.nio.ByteOrder; * Implements small subset of these features */ public class UsbTransport implements Transport { - private static final int USB_CLASS_SMARTCARD = 11; private static final int PROTOCOLS_OFFSET = 6; private static final int FEATURES_OFFSET = 40; - private static final int MASK_T1_PROTO = 2; + private static final short MASK_T1_PROTO = 2; // dwFeatures Masks private static final int MASK_TPDU = 0x10000; @@ -77,7 +77,7 @@ public class UsbTransport implements Transport { private static UsbInterface getSmartCardInterface(UsbDevice device) { for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface anInterface = device.getInterface(i); - if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) { + if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CSCID) { return anInterface; } } @@ -180,13 +180,35 @@ public class UsbTransport implements Transport { private void configureProtocol() throws UsbTransportException { byte[] desc = mConnection.getRawDescriptors(); - if (desc.length < FEATURES_OFFSET + 4) - throw new UsbTransportException("Can't access dwFeatures for protocol selection"); + int dwProtocols = 0, dwFeatures = 0; + boolean hasCcidDescriptor = false; - int dwProtocols = ByteBuffer.wrap(desc, FEATURES_OFFSET, 4).order(ByteOrder.LITTLE_ENDIAN) - .asIntBuffer().get(); - int dwFeatures = ByteBuffer.wrap(desc, FEATURES_OFFSET, 4).order(ByteOrder.LITTLE_ENDIAN) - .asIntBuffer().get(); + ByteBuffer byteBuffer = ByteBuffer.wrap(desc).order(ByteOrder.LITTLE_ENDIAN); + + while (byteBuffer.hasRemaining()) { + byteBuffer.mark(); + byte len = byteBuffer.get(), type = byteBuffer.get(); + + if (type == 0x21 && len == 0x36) { + byteBuffer.reset(); + + byteBuffer.position(byteBuffer.position() + PROTOCOLS_OFFSET); + dwProtocols = byteBuffer.getInt(); + + byteBuffer.reset(); + + byteBuffer.position(byteBuffer.position() + FEATURES_OFFSET); + dwFeatures = byteBuffer.getInt(); + hasCcidDescriptor = true; + break; + } else { + byteBuffer.position(byteBuffer.position() + len - 2); + } + } + + if (!hasCcidDescriptor) { + throw new UsbTransportException("CCID descriptor not found"); + } if ((dwProtocols & MASK_T1_PROTO) == 0) { throw new UsbTransportException("T=0 protocol is not supported"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/Block.java similarity index 94% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/Block.java index bfebd1934..1fe29b2a2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/Block.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/Block.java @@ -15,12 +15,11 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; public class Block { protected static final int MAX_PAYLOAD_LEN = 254; @@ -78,7 +77,7 @@ public class Block { } public byte[] getEdc() { - return Arrays.copyOfRange(mData, mData.length - mChecksumType.getLength(), mChecksumType.getLength()); + return Arrays.copyOfRange(mData, mData.length - mChecksumType.getLength(), mData.length); } public BlockChecksumType getChecksumType() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java similarity index 90% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java index e7e9f8b51..8caa54ae8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/IBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java @@ -15,10 +15,9 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; public class IBlock extends Block { private static final byte BIT_SEQUENCE = 6; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java similarity index 93% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java index f0262257e..678b5e01e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/RBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java @@ -15,12 +15,11 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; public class RBlock extends Block { private static final byte BIT_SEQUENCE = 5; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java similarity index 92% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java index 18a66f548..9bf3fdc9d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/SBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block; +package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; public class SBlock extends Block { public SBlock(Block baseBlock) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java index 59e651a3f..fb86c8020 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java @@ -25,10 +25,6 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransportProtocol; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.Block; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.IBlock; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.RBlock; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.block.SBlock; import org.sufficientlysecure.keychain.util.Log; public class T1TpduProtocol implements CcidTransportProtocol { @@ -107,7 +103,7 @@ public class T1TpduProtocol implements CcidTransportProtocol { byte[] responseApdu = responseBlock.getApdu(); while (((IBlock) responseBlock).getChaining()) { - Block ackBlock = newRBlock(((IBlock) responseBlock).getSequence()); + Block ackBlock = newRBlock((byte) (1 - ((IBlock) responseBlock).getSequence())); mTransceiver.sendXfrBlock(ackBlock.getRawData()); responseBlock = getBlockFromResponse(mTransceiver.receive()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java deleted file mode 100644 index c64f2315c..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/block/BlockFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.usb.tpdu.block; - -import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; -import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.BlockChecksumType; - -public class BlockFactory { - private BlockChecksumType mChecksumType = BlockChecksumType.LRC; - - public Block getBlockFromResponse(byte[] data) throws UsbTransportException { - final Block baseBlock = new Block(mChecksumType, data); - - if ((baseBlock.getPcb() & 0b10000000) == 0b00000000) { - return new IBlock(baseBlock); - } else if ((baseBlock.getPcb() & 0b11000000) == 0b11000000) { - return new SBlock(baseBlock); - } else if ((baseBlock.getPcb() & 0b11000000) == 0b10000000) { - return new RBlock(baseBlock); - } - - throw new UsbTransportException("TPDU Unknown block type"); - } - - public IBlock newIBlock(byte sequence, boolean chaining, byte[] apdu) throws UsbTransportException { - return new IBlock(mChecksumType, (byte) 0, sequence, chaining, apdu); - } - - public RBlock newRBlock(byte sequence) throws UsbTransportException { - return new RBlock(mChecksumType, (byte) 0, sequence); - } - - public void setChecksumType(final BlockChecksumType checksumType) { - mChecksumType = checksumType; - } -} From 9f57a1312e8e81f7e0e42fc813e49342bfd7291c Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 9 May 2016 15:25:06 +0600 Subject: [PATCH 06/28] SecurityToken: fix T=1 TPDU chaining handling --- .../keychain/securitytoken/usb/tpdu/IBlock.java | 3 +++ .../keychain/securitytoken/usb/tpdu/RBlock.java | 7 +++++-- .../keychain/securitytoken/usb/tpdu/SBlock.java | 3 +++ .../securitytoken/usb/tpdu/T1TpduProtocol.java | 10 +++++----- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java index 8caa54ae8..3df4d9cdf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/IBlock.java @@ -20,6 +20,9 @@ package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; public class IBlock extends Block { + public static final byte MASK_RBLOCK = (byte) 0b10000000; + public static final byte MASK_VALUE_RBLOCK = (byte) 0b00000000; + private static final byte BIT_SEQUENCE = 6; private static final byte BIT_CHAINING = 5; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java index 678b5e01e..153758fb2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/RBlock.java @@ -22,7 +22,10 @@ import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; public class RBlock extends Block { - private static final byte BIT_SEQUENCE = 5; + public static final byte MASK_RBLOCK = (byte) 0b11000000; + public static final byte MASK_VALUE_RBLOCK = (byte) 0b10000000; + + private static final byte BIT_SEQUENCE = 4; public RBlock(Block baseBlock) throws UsbTransportException { super(baseBlock); @@ -33,7 +36,7 @@ public class RBlock extends Block { public RBlock(BlockChecksumType checksumType, byte nad, byte sequence) throws UsbTransportException { - super(checksumType, nad, (byte) (0b10000000 | ((sequence & 1) << BIT_SEQUENCE)), new byte[0]); + super(checksumType, nad, (byte) (MASK_VALUE_RBLOCK | ((sequence & 1) << BIT_SEQUENCE)), new byte[0]); } public RError getError() throws UsbTransportException { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java index 9bf3fdc9d..72302fc4b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/SBlock.java @@ -18,6 +18,9 @@ package org.sufficientlysecure.keychain.securitytoken.usb.tpdu; public class SBlock extends Block { + public static final byte MASK_SBLOCK = (byte) 0b11000000; + public static final byte MASK_VALUE_SBLOCK = (byte) 0b11000000; + public SBlock(Block baseBlock) { super(baseBlock); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java index fb86c8020..2d0b4c383 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/tpdu/T1TpduProtocol.java @@ -103,10 +103,10 @@ public class T1TpduProtocol implements CcidTransportProtocol { byte[] responseApdu = responseBlock.getApdu(); while (((IBlock) responseBlock).getChaining()) { - Block ackBlock = newRBlock((byte) (1 - ((IBlock) responseBlock).getSequence())); + Block ackBlock = newRBlock((byte) (((IBlock) responseBlock).getSequence() + 1)); mTransceiver.sendXfrBlock(ackBlock.getRawData()); - responseBlock = getBlockFromResponse(mTransceiver.receive()); + responseBlock = getBlockFromResponse(mTransceiver.receiveRaw()); if (responseBlock instanceof IBlock) { responseApdu = Arrays.concatenate(responseApdu, responseBlock.getApdu()); @@ -123,11 +123,11 @@ public class T1TpduProtocol implements CcidTransportProtocol { public Block getBlockFromResponse(byte[] data) throws UsbTransportException { final Block baseBlock = new Block(mChecksumType, data); - if ((baseBlock.getPcb() & 0b10000000) == 0b00000000) { + if ((baseBlock.getPcb() & IBlock.MASK_RBLOCK) == IBlock.MASK_VALUE_RBLOCK) { return new IBlock(baseBlock); - } else if ((baseBlock.getPcb() & 0b11000000) == 0b11000000) { + } else if ((baseBlock.getPcb() & SBlock.MASK_SBLOCK) == SBlock.MASK_VALUE_SBLOCK) { return new SBlock(baseBlock); - } else if ((baseBlock.getPcb() & 0b11000000) == 0b10000000) { + } else if ((baseBlock.getPcb() & RBlock.MASK_RBLOCK) == RBlock.MASK_VALUE_RBLOCK) { return new RBlock(baseBlock); } From aadc59691beea3c58af3d0edf15302f5a33265d6 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 9 May 2016 15:27:45 +0600 Subject: [PATCH 07/28] SecurityToken: fix TLV in GET DATA fingerprint DO handling --- .../securitytoken/SecurityTokenHelper.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index bc1c42f7f..a4f950254 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -477,10 +477,18 @@ public class SecurityTokenHelper { String data = "00CA006E00"; byte[] buf = mTransport.transceive(Hex.decode(data)); - Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true); - Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint()); + Iso7816TLV[] tlvs = Iso7816TLV.readList(buf, true); + Iso7816TLV fptlv = null; + + for (int i = 0; i < tlvs.length; i++) { + Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlvs[i].prettyPrint()); + + Iso7816TLV tlv = Iso7816TLV.findRecursive(tlvs[i], 0xc5); + if (tlv != null) { + fptlv = tlv; + } + } - Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5); if (fptlv == null) { return null; } From ee8cd3862f65de580ed949bc838628610e22cd98 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Thu, 12 May 2016 01:17:21 +0600 Subject: [PATCH 08/28] wip: Working nitrokey pro --- .../securitytoken/SecurityTokenHelper.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index a4f950254..0b81c9fbb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -47,7 +47,7 @@ import nordpol.Apdu; * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf */ public class SecurityTokenHelper { - private static final int MAX_APDU_DATAFIELD_SIZE = 254; + private static final int MAX_APDU_DATAFIELD_SIZE = 254 * 500; // Fidesmo constants private static final String FIDESMO_APPS_AID_PREFIX = "A000000617"; @@ -254,17 +254,22 @@ public class SecurityTokenHelper { verifyPin(0x82); // (Verify PW1 with mode 82 for decryption) } - int offset = 1; // Skip first byte + int offset = 2; // Skip first byte TODO: why? String response = "", status = ""; + boolean shouldPad = true; // Transmit while (offset < encryptedSessionKey.length) { - boolean isLastCommand = offset + MAX_APDU_DATAFIELD_SIZE < encryptedSessionKey.length; - String cla = isLastCommand ? "10" : "00"; + boolean isLastCommand = MAX_APDU_DATAFIELD_SIZE >= encryptedSessionKey.length - offset; + String cla = isLastCommand ? "00" : "10"; - int len = Math.min(MAX_APDU_DATAFIELD_SIZE, encryptedSessionKey.length - offset); - response = communicate(cla + "2a8086" + Hex.toHexString(new byte[]{(byte) len}) - + Hex.toHexString(encryptedSessionKey, offset, len)); + int len = Math.min(MAX_APDU_DATAFIELD_SIZE, encryptedSessionKey.length - offset + (shouldPad ? 1 : 0)); + String command = cla + "2a8086" + + Hex.toHexString(new byte[]{(byte) ((len >> 16) & 0xFF), (byte) ((len >> 8) & 0xFF), (byte) (len & 0xFF)}) + + (shouldPad ? "00": "") + + Hex.toHexString(encryptedSessionKey, offset, len - (shouldPad ? 1 : 0)) + "0000"; + shouldPad = false; + response = communicate(command); status = response.substring(response.length() - 4); if (!isLastCommand && !response.endsWith("9000")) { @@ -579,8 +584,9 @@ public class SecurityTokenHelper { // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) String apdu = "002A9E9A" // CLA, INS, P1, P2 + + "0000" + dsi // digital signature input - + "00"; // Le + + "0000"; // Le String response = communicate(apdu); From 131bf94b9bd68af421dc8b309fbb056d66906be3 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 13:14:31 +0600 Subject: [PATCH 09/28] SecurityToken: add useful classes from javax.smartcardio package --- .../securitytoken/smartcardio/ATR.java | 165 +++++ .../securitytoken/smartcardio/Card.java | 167 +++++ .../smartcardio/CardChannel.java | 184 ++++++ .../smartcardio/CardException.java | 68 ++ .../smartcardio/CardNotPresentException.java | 68 ++ .../smartcardio/CardPermission.java | 301 +++++++++ .../smartcardio/CommandAPDU.java | 606 ++++++++++++++++++ .../smartcardio/ResponseAPDU.java | 185 ++++++ 8 files changed, 1744 insertions(+) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java new file mode 100644 index 000000000..5d1581b19 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.util.*; + +/** + * A Smart Card's answer-to-reset bytes. A Card's ATR object can be obtained + * by calling {@linkplain Card#getATR}. + * This class does not attempt to verify that the ATR encodes a semantically + * valid structure. + * + *

Instances of this class are immutable. Where data is passed in or out + * via byte arrays, defensive cloning is performed. + * + * @see Card#getATR + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public final class ATR implements java.io.Serializable { + + private static final long serialVersionUID = 6695383790847736493L; + + private byte[] atr; + + private transient int startHistorical, nHistorical; + + /** + * Constructs an ATR from a byte array. + * + * @param atr the byte array containing the answer-to-reset bytes + * @throws NullPointerException if atr is null + */ + public ATR(byte[] atr) { + this.atr = atr.clone(); + parse(); + } + + private void parse() { + if (atr.length < 2) { + return; + } + if ((atr[0] != 0x3b) && (atr[0] != 0x3f)) { + return; + } + int t0 = (atr[1] & 0xf0) >> 4; + int n = atr[1] & 0xf; + int i = 2; + while ((t0 != 0) && (i < atr.length)) { + if ((t0 & 1) != 0) { + i++; + } + if ((t0 & 2) != 0) { + i++; + } + if ((t0 & 4) != 0) { + i++; + } + if ((t0 & 8) != 0) { + if (i >= atr.length) { + return; + } + t0 = (atr[i++] & 0xf0) >> 4; + } else { + t0 = 0; + } + } + int k = i + n; + if ((k == atr.length) || (k == atr.length - 1)) { + startHistorical = i; + nHistorical = n; + } + } + + /** + * Returns a copy of the bytes in this ATR. + * + * @return a copy of the bytes in this ATR. + */ + public byte[] getBytes() { + return atr.clone(); + } + + /** + * Returns a copy of the historical bytes in this ATR. + * If this ATR does not contain historical bytes, an array of length + * zero is returned. + * + * @return a copy of the historical bytes in this ATR. + */ + public byte[] getHistoricalBytes() { + byte[] b = new byte[nHistorical]; + System.arraycopy(atr, startHistorical, b, 0, nHistorical); + return b; + } + + /** + * Returns a string representation of this ATR. + * + * @return a String representation of this ATR. + */ + public String toString() { + return "ATR: " + atr.length + " bytes"; + } + + /** + * Compares the specified object with this ATR for equality. + * Returns true if the given object is also an ATR and its bytes are + * identical to the bytes in this ATR. + * + * @param obj the object to be compared for equality with this ATR + * @return true if the specified object is equal to this ATR + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ATR == false) { + return false; + } + ATR other = (ATR)obj; + return Arrays.equals(this.atr, other.atr); + } + + /** + * Returns the hash code value for this ATR. + * + * @return the hash code value for this ATR. + */ + public int hashCode() { + return Arrays.hashCode(atr); + } + + private void readObject(java.io.ObjectInputStream in) + throws java.io.IOException, ClassNotFoundException { + atr = (byte[])in.readUnshared(); + parse(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java new file mode 100644 index 000000000..b3b7fdc4f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.nio.ByteBuffer; + +/** + * A Smart Card with which a connection has been established. Card objects + * are obtained by calling {@link CardTerminal#connect CardTerminal.connect()}. + * + * @see CardTerminal + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group +*/ +public abstract class Card { + + /** + * Constructs a new Card object. + * + *

This constructor is called by subclasses only. Application should + * call the {@linkplain CardTerminal#connect CardTerminal.connect()} + * method to obtain a Card + * object. + */ + protected Card() { + // empty + } + + /** + * Returns the ATR of this card. + * + * @return the ATR of this card. + */ + public abstract ATR getATR(); + + /** + * Returns the protocol in use for this card. + * + * @return the protocol in use for this card, for example "T=0" or "T=1" + */ + public abstract String getProtocol(); + + /** + * Returns the CardChannel for the basic logical channel. The basic + * logical channel has a channel number of 0. + * + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + * @throws IllegalStateException if this card object has been disposed of + * via the {@linkplain #disconnect disconnect()} method + */ + public abstract CardChannel getBasicChannel(); + + /** + * Opens a new logical channel to the card and returns it. The channel is + * opened by issuing a MANAGE CHANNEL command that should use + * the format [00 70 00 00 01]. + * + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + * @throws CardException is a new logical channel could not be opened + * @throws IllegalStateException if this card object has been disposed of + * via the {@linkplain #disconnect disconnect()} method + */ + public abstract CardChannel openLogicalChannel() throws CardException; + + /** + * Requests exclusive access to this card. + * + *

Once a thread has invoked beginExclusive, only this + * thread is allowed to communicate with this card until it calls + * endExclusive. Other threads attempting communication + * will receive a CardException. + * + *

Applications have to ensure that exclusive access is correctly + * released. This can be achieved by executing + * the beginExclusive() and endExclusive calls + * in a try ... finally block. + * + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + * @throws CardException if exclusive access has already been set + * or if exclusive access could not be established + * @throws IllegalStateException if this card object has been disposed of + * via the {@linkplain #disconnect disconnect()} method + */ + public abstract void beginExclusive() throws CardException; + + /** + * Releases the exclusive access previously established using + * beginExclusive. + * + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + * @throws IllegalStateException if the active Thread does not currently have + * exclusive access to this card or + * if this card object has been disposed of + * via the {@linkplain #disconnect disconnect()} method + * @throws CardException if the operation failed + */ + public abstract void endExclusive() throws CardException; + + /** + * Transmits a control command to the terminal device. + * + *

This can be used to, for example, control terminal functions like + * a built-in PIN pad or biometrics. + * + * @param controlCode the control code of the command + * @param command the command data + * + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + * @throws NullPointerException if command is null + * @throws CardException if the card operation failed + * @throws IllegalStateException if this card object has been disposed of + * via the {@linkplain #disconnect disconnect()} method + */ + public abstract byte[] transmitControlCommand(int controlCode, + byte[] command) throws CardException; + + /** + * Disconnects the connection with this card. After this method returns, + * calling methods on this object or in CardChannels associated with this + * object that require interaction with the card will raise an + * IllegalStateException. + * + * @param reset whether to reset the card after disconnecting. + * + * @throws CardException if the card operation failed + * @throws SecurityException if a SecurityManager exists and the + * caller does not have the required + * {@linkplain CardPermission permission} + */ + public abstract void disconnect(boolean reset) throws CardException; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java new file mode 100644 index 000000000..243c171bd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.nio.*; + +/** + * A logical channel connection to a Smart Card. It is used to exchange APDUs + * with a Smart Card. + * A CardChannel object can be obtained by calling the method + * {@linkplain Card#getBasicChannel} or {@linkplain Card#openLogicalChannel}. + * + * @see Card + * @see CommandAPDU + * @see ResponseAPDU + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public abstract class CardChannel { + + /** + * Constructs a new CardChannel object. + * + *

This constructor is called by subclasses only. Application should + * call the {@linkplain Card#getBasicChannel} and + * {@linkplain Card#openLogicalChannel} methods to obtain a CardChannel + * object. + */ + protected CardChannel() { + // empty + } + + /** + * Returns the Card this channel is associated with. + * + * @return the Card this channel is associated with + */ + public abstract Card getCard(); + + /** + * Returns the channel number of this CardChannel. A channel number of + * 0 indicates the basic logical channel. + * + * @return the channel number of this CardChannel. + * + * @throws IllegalStateException if this channel has been + * {@linkplain #close closed} or if the corresponding Card has been + * {@linkplain Card#disconnect disconnected}. + */ + public abstract int getChannelNumber(); + + /** + * Transmits the specified command APDU to the Smart Card and returns the + * response APDU. + * + *

The CLA byte of the command APDU is automatically adjusted to + * match the channel number of this CardChannel. + * + *

Note that this method cannot be used to transmit + * MANAGE CHANNEL APDUs. Logical channels should be managed + * using the {@linkplain Card#openLogicalChannel} and {@linkplain + * CardChannel#close CardChannel.close()} methods. + * + *

Implementations should transparently handle artifacts + * of the transmission protocol. + * For example, when using the T=0 protocol, the following processing + * should occur as described in ISO/IEC 7816-4: + * + *

    + *
  • if the response APDU has an SW1 of 61, the + * implementation should issue a GET RESPONSE command + * using SW2 as the Lefield. + * This process is repeated as long as an SW1 of 61 is + * received. The response body of these exchanges is concatenated + * to form the final response body. + * + *

  • if the response APDU is 6C XX, the implementation + * should reissue the command using XX as the + * Le field. + *

+ * + *

The ResponseAPDU returned by this method is the result + * after this processing has been performed. + * + * @param command the command APDU + * @return the response APDU received from the card + * + * @throws IllegalStateException if this channel has been + * {@linkplain #close closed} or if the corresponding Card has been + * {@linkplain Card#disconnect disconnected}. + * @throws IllegalArgumentException if the APDU encodes a + * MANAGE CHANNEL command + * @throws NullPointerException if command is null + * @throws CardException if the card operation failed + */ + public abstract ResponseAPDU transmit(CommandAPDU command) throws CardException; + + /** + * Transmits the command APDU stored in the command ByteBuffer and receives + * the reponse APDU in the response ByteBuffer. + * + *

The command buffer must contain valid command APDU data starting + * at command.position() and the APDU must be + * command.remaining() bytes long. + * Upon return, the command buffer's position will be equal + * to its limit; its limit will not have changed. The output buffer + * will have received the response APDU bytes. Its position will have + * advanced by the number of bytes received, which is also the return + * value of this method. + * + *

The CLA byte of the command APDU is automatically adjusted to + * match the channel number of this CardChannel. + * + *

Note that this method cannot be used to transmit + * MANAGE CHANNEL APDUs. Logical channels should be managed + * using the {@linkplain Card#openLogicalChannel} and {@linkplain + * CardChannel#close CardChannel.close()} methods. + * + *

See {@linkplain #transmit transmit()} for a discussion of the handling + * of response APDUs with the SW1 values 61 or 6C. + * + * @param command the buffer containing the command APDU + * @param response the buffer that shall receive the response APDU from + * the card + * @return the length of the received response APDU + * + * @throws IllegalStateException if this channel has been + * {@linkplain #close closed} or if the corresponding Card has been + * {@linkplain Card#disconnect disconnected}. + * @throws NullPointerException if command or response is null + * @throws ReadOnlyBufferException if the response buffer is read-only + * @throws IllegalArgumentException if command and response are the + * same object, if response may not have + * sufficient space to receive the response APDU + * or if the APDU encodes a MANAGE CHANNEL command + * @throws CardException if the card operation failed + */ + public abstract int transmit(ByteBuffer command, ByteBuffer response) + throws CardException; + + /** + * Closes this CardChannel. The logical channel is closed by issuing + * a MANAGE CHANNEL command that should use the format + * [xx 70 80 0n] where n is the channel number + * of this channel and xx is the CLA + * byte that encodes this logical channel and has all other bits set to 0. + * After this method returns, calling other + * methods in this class will raise an IllegalStateException. + * + *

Note that the basic logical channel cannot be closed using this + * method. It can be closed by calling {@link Card#disconnect}. + * + * @throws CardException if the card operation failed + * @throws IllegalStateException if this CardChannel represents a + * connection the basic logical channel + */ + public abstract void close() throws CardException; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java new file mode 100644 index 000000000..23d02e24e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +/** + * Exception for errors that occur during communication with the + * Smart Card stack or the card itself. + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public class CardException extends Exception { + + private static final long serialVersionUID = 7787607144922050628L; + + /** + * Constructs a new CardException with the specified detail message. + * + * @param message the detail message + */ + public CardException(String message) { + super(message); + } + + /** + * Constructs a new CardException with the specified cause and a detail message + * of (cause==null ? null : cause.toString()). + * + * @param cause the cause of this exception or null + */ + public CardException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new CardException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of this exception or null + */ + public CardException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java new file mode 100644 index 000000000..3b8289b75 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +/** + * Exception thrown when an application tries to establish a connection with a + * terminal that has no card present. + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public class CardNotPresentException extends CardException { + + private final static long serialVersionUID = 1346879911706545215L; + + /** + * Constructs a new CardNotPresentException with the specified detail message. + * + * @param message the detail message + */ + public CardNotPresentException(String message) { + super(message); + } + + /** + * Constructs a new CardNotPresentException with the specified cause and a detail message + * of (cause==null ? null : cause.toString()). + * + * @param cause the cause of this exception or null + */ + public CardNotPresentException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new CardNotPresentException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of this exception or null + */ + public CardNotPresentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java new file mode 100644 index 000000000..8731fa467 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.io.*; + +import java.security.Permission; + +/** + * A permission for Smart Card operations. A CardPermission consists of the + * name of the card terminal the permission applies to and a set of actions + * that are valid for that terminal. + * + *

A CardPermission with a name of * applies to all + * card terminals. The actions string is a comma separated list of the actions + * listed below, or * to signify "all actions." + * + *

Individual actions are: + *

+ *
connect + *
connect to a card using + * {@linkplain CardTerminal#connect CardTerminal.connect()} + * + *
reset + *
reset the card using {@linkplain Card#disconnect Card.disconnect(true)} + * + *
exclusive + *
establish exclusive access to a card using + * {@linkplain Card#beginExclusive} and {@linkplain Card#endExclusive + * endExclusive()} + * + *
transmitControl + *
transmit a control command using + * {@linkplain Card#transmitControlCommand Card.transmitControlCommand()} + * + *
getBasicChannel + *
obtain the basic logical channel using + * {@linkplain Card#getBasicChannel} + * + *
openLogicalChannel + *
open a new logical channel using + * {@linkplain Card#openLogicalChannel} + * + *
+ * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public class CardPermission extends Permission { + + private static final long serialVersionUID = 7146787880530705613L; + + private final static int A_CONNECT = 0x01; + private final static int A_EXCLUSIVE = 0x02; + private final static int A_GET_BASIC_CHANNEL = 0x04; + private final static int A_OPEN_LOGICAL_CHANNEL = 0x08; + private final static int A_RESET = 0x10; + private final static int A_TRANSMIT_CONTROL = 0x20; + + // sum of all the actions above + private final static int A_ALL = 0x3f; + + private final static int[] ARRAY_MASKS = { + A_ALL, + A_CONNECT, + A_EXCLUSIVE, + A_GET_BASIC_CHANNEL, + A_OPEN_LOGICAL_CHANNEL, + A_RESET, + A_TRANSMIT_CONTROL, + }; + + private final static String S_CONNECT = "connect"; + private final static String S_EXCLUSIVE = "exclusive"; + private final static String S_GET_BASIC_CHANNEL = "getBasicChannel"; + private final static String S_OPEN_LOGICAL_CHANNEL = "openLogicalChannel"; + private final static String S_RESET = "reset"; + private final static String S_TRANSMIT_CONTROL = "transmitControl"; + + private final static String S_ALL = "*"; + + private final static String[] ARRAY_STRINGS = { + S_ALL, + S_CONNECT, + S_EXCLUSIVE, + S_GET_BASIC_CHANNEL, + S_OPEN_LOGICAL_CHANNEL, + S_RESET, + S_TRANSMIT_CONTROL, + }; + + private transient int mask; + + /** + * @serial + */ + private volatile String actions; + + /** + * Constructs a new CardPermission with the specified actions. + * terminalName is the name of a CardTerminal or * + * if this permission applies to all terminals. actions + * contains a comma-separated list of the individual actions + * or * to signify all actions. For more information, + * see the documentation at the top of this {@linkplain CardPermission + * class}. + * + * @param terminalName the name of the card terminal, or * + * @param actions the action string (or null if the set of permitted + * actions is empty) + * + * @throws NullPointerException if terminalName is null + * @throws IllegalArgumentException if actions is an invalid actions + * specification + */ + public CardPermission(String terminalName, String actions) { + super(terminalName); + if (terminalName == null) { + throw new NullPointerException(); + } + mask = getMask(actions); + } + + private static int getMask(String actions) { + if ((actions == null) || (actions.length() == 0)) { + throw new IllegalArgumentException("actions must not be empty"); + } + + // try exact matches for simple actions first + for (int i = 0; i < ARRAY_STRINGS.length; i++) { + if (actions == ARRAY_STRINGS[i]) { + return ARRAY_MASKS[i]; + } + } + + if (actions.endsWith(",")) { + throw new IllegalArgumentException("Invalid actions: '" + actions + "'"); + } + int mask = 0; + String[] split = actions.split(","); + outer: + for (String s : split) { + for (int i = 0; i < ARRAY_STRINGS.length; i++) { + if (ARRAY_STRINGS[i].equalsIgnoreCase(s)) { + mask |= ARRAY_MASKS[i]; + continue outer; + } + } + throw new IllegalArgumentException("Invalid action: '" + s + "'"); + } + + return mask; + } + + private static String getActions(int mask) { + if (mask == A_ALL) { + return S_ALL; + } + boolean first = true; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ARRAY_MASKS.length; i++) { + int action = ARRAY_MASKS[i]; + if ((mask & action) == action) { + if (first == false) { + sb.append(","); + } else { + first = false; + } + sb.append(ARRAY_STRINGS[i]); + } + } + return sb.toString(); + } + + + /** + * Returns the canonical string representation of the actions. + * It is * to signify all actions defined by this class or + * the string concatenation of the comma-separated, + * lexicographically sorted list of individual actions. + * + * @return the canonical string representation of the actions. + */ + public String getActions() { + if (actions == null) { + actions = getActions(mask); + } + return actions; + } + + /** + * Checks if this CardPermission object implies the specified permission. + * That is the case, if and only if + *
    + *
  • permission is an instance of CardPermission,

    + *
  • permission's actions are a proper subset of this + * object's actions, and

    + *
  • this object's getName() method is either + * * or equal to permission's name. + *

    + *
+ * + * @param permission the permission to check against + * @return true if and only if this CardPermission object implies the + * specified permission. + */ + public boolean implies(Permission permission) { + if (permission instanceof CardPermission == false) { + return false; + } + CardPermission other = (CardPermission)permission; + if ((this.mask & other.mask) != other.mask) { + return false; + } + String thisName = getName(); + if (thisName.equals("*")) { + return true; + } + if (thisName.equals(other.getName())) { + return true; + } + return false; + } + + /** + * Compares the specified object with this CardPermission for equality. + * This CardPermission is equal to another Object object, if + * and only if + *
    + *
  • object is an instance of CardPermission,

    + *
  • this.getName() is equal to + * ((CardPermission)object).getName(), and

    + *
  • this.getActions() is equal to + * ((CardPermission)object).getActions().

    + *
+ * + * @param obj the object to be compared for equality with this CardPermission + * @return true if and only if the specified object is equal to this + * CardPermission + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof CardPermission == false) { + return false; + } + CardPermission other = (CardPermission)obj; + return this.getName().equals(other.getName()) && (this.mask == other.mask); + } + + /** + * Returns the hash code value for this CardPermission object. + * + * @return the hash code value for this CardPermission object. + */ + public int hashCode() { + return getName().hashCode() + 31 * mask; + } + + private void writeObject(ObjectOutputStream s) throws IOException { + // Write out the actions. The superclass takes care of the name. + // Call getActions to make sure actions field is initialized + if (actions == null) { + getActions(); + } + s.defaultWriteObject(); + } + + private void readObject(ObjectInputStream s) + throws IOException, ClassNotFoundException { + // Read in the actions, then restore the mask. + s.defaultReadObject(); + mask = getMask(actions); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java new file mode 100644 index 000000000..137f307f2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.util.Arrays; + +import java.nio.ByteBuffer; + +/** + * A command APDU following the structure defined in ISO/IEC 7816-4. + * It consists of a four byte header and a conditional body of variable length. + * This class does not attempt to verify that the APDU encodes a semantically + * valid command. + * + *

Note that when the expected length of the response APDU is specified + * in the {@linkplain #CommandAPDU(int,int,int,int,int) constructors}, + * the actual length (Ne) must be specified, not its + * encoded form (Le). Similarly, {@linkplain #getNe} returns the actual + * value Ne. In other words, a value of 0 means "no data in the response APDU" + * rather than "maximum length." + * + *

This class supports both the short and extended forms of length + * encoding for Ne and Nc. However, note that not all terminals and Smart Cards + * are capable of accepting APDUs that use the extended form. + * + *

For the header bytes CLA, INS, P1, and P2 the Java type int + * is used to represent the 8 bit unsigned values. In the constructors, only + * the 8 lowest bits of the int value specified by the application + * are significant. The accessor methods always return the byte as an unsigned + * value between 0 and 255. + * + *

Instances of this class are immutable. Where data is passed in or out + * via byte arrays, defensive cloning is performed. + * + * @see ResponseAPDU + * @see CardChannel#transmit CardChannel.transmit + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public final class CommandAPDU implements java.io.Serializable { + + private static final long serialVersionUID = 398698301286670877L; + + private static final int MAX_APDU_SIZE = 65544; + + /** @serial */ + private byte[] apdu; + + // value of nc + private transient int nc; + + // value of ne + private transient int ne; + + // index of start of data within the apdu array + private transient int dataOffset; + + /** + * Constructs a CommandAPDU from a byte array containing the complete + * APDU contents (header and body). + * + *

Note that the apdu bytes are copied to protect against + * subsequent modification. + * + * @param apdu the complete command APDU + * + * @throws NullPointerException if apdu is null + * @throws IllegalArgumentException if apdu does not contain a valid + * command APDU + */ + public CommandAPDU(byte[] apdu) { + this.apdu = apdu.clone(); + parse(); + } + + /** + * Constructs a CommandAPDU from a byte array containing the complete + * APDU contents (header and body). The APDU starts at the index + * apduOffset in the byte array and is apduLength + * bytes long. + * + *

Note that the apdu bytes are copied to protect against + * subsequent modification. + * + * @param apdu the complete command APDU + * @param apduOffset the offset in the byte array at which the apdu + * data begins + * @param apduLength the length of the APDU + * + * @throws NullPointerException if apdu is null + * @throws IllegalArgumentException if apduOffset or apduLength are + * negative or if apduOffset + apduLength are greater than apdu.length, + * or if the specified bytes are not a valid APDU + */ + public CommandAPDU(byte[] apdu, int apduOffset, int apduLength) { + checkArrayBounds(apdu, apduOffset, apduLength); + this.apdu = new byte[apduLength]; + System.arraycopy(apdu, apduOffset, this.apdu, 0, apduLength); + parse(); + } + + private void checkArrayBounds(byte[] b, int ofs, int len) { + if ((ofs < 0) || (len < 0)) { + throw new IllegalArgumentException + ("Offset and length must not be negative"); + } + if (b == null) { + if ((ofs != 0) && (len != 0)) { + throw new IllegalArgumentException + ("offset and length must be 0 if array is null"); + } + } else { + if (ofs > b.length - len) { + throw new IllegalArgumentException + ("Offset plus length exceed array size"); + } + } + } + + /** + * Creates a CommandAPDU from the ByteBuffer containing the complete APDU + * contents (header and body). + * The buffer's position must be set to the start of the APDU, + * its limit to the end of the APDU. Upon return, the buffer's + * position is equal to its limit; its limit remains unchanged. + * + *

Note that the data in the ByteBuffer is copied to protect against + * subsequent modification. + * + * @param apdu the ByteBuffer containing the complete APDU + * + * @throws NullPointerException if apdu is null + * @throws IllegalArgumentException if apdu does not contain a valid + * command APDU + */ + public CommandAPDU(ByteBuffer apdu) { + this.apdu = new byte[apdu.remaining()]; + apdu.get(this.apdu); + parse(); + } + + /** + * Constructs a CommandAPDU from the four header bytes. This is case 1 + * in ISO 7816, no command body. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + */ + public CommandAPDU(int cla, int ins, int p1, int p2) { + this(cla, ins, p1, p2, null, 0, 0, 0); + } + + /** + * Constructs a CommandAPDU from the four header bytes and the expected + * response data length. This is case 2 in ISO 7816, empty command data + * field with Ne specified. If Ne is 0, the APDU is encoded as ISO 7816 + * case 1. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + * @param ne the maximum number of expected data bytes in a response APDU + * + * @throws IllegalArgumentException if ne is negative or greater than + * 65536 + */ + public CommandAPDU(int cla, int ins, int p1, int p2, int ne) { + this(cla, ins, p1, p2, null, 0, 0, ne); + } + + /** + * Constructs a CommandAPDU from the four header bytes and command data. + * This is case 3 in ISO 7816, command data present and Ne absent. The + * value Nc is taken as data.length. If data is null or + * its length is 0, the APDU is encoded as ISO 7816 case 1. + * + *

Note that the data bytes are copied to protect against + * subsequent modification. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + * @param data the byte array containing the data bytes of the command body + * + * @throws IllegalArgumentException if data.length is greater than 65535 + */ + public CommandAPDU(int cla, int ins, int p1, int p2, byte[] data) { + this(cla, ins, p1, p2, data, 0, arrayLength(data), 0); + } + + /** + * Constructs a CommandAPDU from the four header bytes and command data. + * This is case 3 in ISO 7816, command data present and Ne absent. The + * value Nc is taken as dataLength. If dataLength + * is 0, the APDU is encoded as ISO 7816 case 1. + * + *

Note that the data bytes are copied to protect against + * subsequent modification. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + * @param data the byte array containing the data bytes of the command body + * @param dataOffset the offset in the byte array at which the data + * bytes of the command body begin + * @param dataLength the number of the data bytes in the command body + * + * @throws NullPointerException if data is null and dataLength is not 0 + * @throws IllegalArgumentException if dataOffset or dataLength are + * negative or if dataOffset + dataLength are greater than data.length + * or if dataLength is greater than 65535 + */ + public CommandAPDU(int cla, int ins, int p1, int p2, byte[] data, + int dataOffset, int dataLength) { + this(cla, ins, p1, p2, data, dataOffset, dataLength, 0); + } + + /** + * Constructs a CommandAPDU from the four header bytes, command data, + * and expected response data length. This is case 4 in ISO 7816, + * command data and Ne present. The value Nc is taken as data.length + * if data is non-null and as 0 otherwise. If Ne or Nc + * are zero, the APDU is encoded as case 1, 2, or 3 per ISO 7816. + * + *

Note that the data bytes are copied to protect against + * subsequent modification. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + * @param data the byte array containing the data bytes of the command body + * @param ne the maximum number of expected data bytes in a response APDU + * + * @throws IllegalArgumentException if data.length is greater than 65535 + * or if ne is negative or greater than 65536 + */ + public CommandAPDU(int cla, int ins, int p1, int p2, byte[] data, int ne) { + this(cla, ins, p1, p2, data, 0, arrayLength(data), ne); + } + + private static int arrayLength(byte[] b) { + return (b != null) ? b.length : 0; + } + + /** + * Command APDU encoding options: + * + * case 1: |CLA|INS|P1 |P2 | len = 4 + * case 2s: |CLA|INS|P1 |P2 |LE | len = 5 + * case 3s: |CLA|INS|P1 |P2 |LC |...BODY...| len = 6..260 + * case 4s: |CLA|INS|P1 |P2 |LC |...BODY...|LE | len = 7..261 + * case 2e: |CLA|INS|P1 |P2 |00 |LE1|LE2| len = 7 + * case 3e: |CLA|INS|P1 |P2 |00 |LC1|LC2|...BODY...| len = 8..65542 + * case 4e: |CLA|INS|P1 |P2 |00 |LC1|LC2|...BODY...|LE1|LE2| len =10..65544 + * + * LE, LE1, LE2 may be 0x00. + * LC must not be 0x00 and LC1|LC2 must not be 0x00|0x00 + */ + private void parse() { + if (apdu.length < 4) { + throw new IllegalArgumentException("apdu must be at least 4 bytes long"); + } + if (apdu.length == 4) { + // case 1 + return; + } + int l1 = apdu[4] & 0xff; + if (apdu.length == 5) { + // case 2s + this.ne = (l1 == 0) ? 256 : l1; + return; + } + if (l1 != 0) { + if (apdu.length == 4 + 1 + l1) { + // case 3s + this.nc = l1; + this.dataOffset = 5; + return; + } else if (apdu.length == 4 + 2 + l1) { + // case 4s + this.nc = l1; + this.dataOffset = 5; + int l2 = apdu[apdu.length - 1] & 0xff; + this.ne = (l2 == 0) ? 256 : l2; + return; + } else { + throw new IllegalArgumentException + ("Invalid APDU: length=" + apdu.length + ", b1=" + l1); + } + } + if (apdu.length < 7) { + throw new IllegalArgumentException + ("Invalid APDU: length=" + apdu.length + ", b1=" + l1); + } + int l2 = ((apdu[5] & 0xff) << 8) | (apdu[6] & 0xff); + if (apdu.length == 7) { + // case 2e + this.ne = (l2 == 0) ? 65536 : l2; + return; + } + if (l2 == 0) { + throw new IllegalArgumentException("Invalid APDU: length=" + + apdu.length + ", b1=" + l1 + ", b2||b3=" + l2); + } + if (apdu.length == 4 + 3 + l2) { + // case 3e + this.nc = l2; + this.dataOffset = 7; + return; + } else if (apdu.length == 4 + 5 + l2) { + // case 4e + this.nc = l2; + this.dataOffset = 7; + int leOfs = apdu.length - 2; + int l3 = ((apdu[leOfs] & 0xff) << 8) | (apdu[leOfs + 1] & 0xff); + this.ne = (l3 == 0) ? 65536 : l3; + } else { + throw new IllegalArgumentException("Invalid APDU: length=" + + apdu.length + ", b1=" + l1 + ", b2||b3=" + l2); + } + } + + /** + * Constructs a CommandAPDU from the four header bytes, command data, + * and expected response data length. This is case 4 in ISO 7816, + * command data and Le present. The value Nc is taken as + * dataLength. + * If Ne or Nc + * are zero, the APDU is encoded as case 1, 2, or 3 per ISO 7816. + * + *

Note that the data bytes are copied to protect against + * subsequent modification. + * + * @param cla the class byte CLA + * @param ins the instruction byte INS + * @param p1 the parameter byte P1 + * @param p2 the parameter byte P2 + * @param data the byte array containing the data bytes of the command body + * @param dataOffset the offset in the byte array at which the data + * bytes of the command body begin + * @param dataLength the number of the data bytes in the command body + * @param ne the maximum number of expected data bytes in a response APDU + * + * @throws NullPointerException if data is null and dataLength is not 0 + * @throws IllegalArgumentException if dataOffset or dataLength are + * negative or if dataOffset + dataLength are greater than data.length, + * or if ne is negative or greater than 65536, + * or if dataLength is greater than 65535 + */ + public CommandAPDU(int cla, int ins, int p1, int p2, byte[] data, + int dataOffset, int dataLength, int ne) { + checkArrayBounds(data, dataOffset, dataLength); + if (dataLength > 65535) { + throw new IllegalArgumentException("dataLength is too large"); + } + if (ne < 0) { + throw new IllegalArgumentException("ne must not be negative"); + } + if (ne > 65536) { + throw new IllegalArgumentException("ne is too large"); + } + this.ne = ne; + this.nc = dataLength; + if (dataLength == 0) { + if (ne == 0) { + // case 1 + this.apdu = new byte[4]; + setHeader(cla, ins, p1, p2); + } else { + // case 2s or 2e + if (ne <= 256) { + // case 2s + // 256 is encoded as 0x00 + byte len = (ne != 256) ? (byte)ne : 0; + this.apdu = new byte[5]; + setHeader(cla, ins, p1, p2); + this.apdu[4] = len; + } else { + // case 2e + byte l1, l2; + // 65536 is encoded as 0x00 0x00 + if (ne == 65536) { + l1 = 0; + l2 = 0; + } else { + l1 = (byte)(ne >> 8); + l2 = (byte)ne; + } + this.apdu = new byte[7]; + setHeader(cla, ins, p1, p2); + this.apdu[5] = l1; + this.apdu[6] = l2; + } + } + } else { + if (ne == 0) { + // case 3s or 3e + if (dataLength <= 255) { + // case 3s + apdu = new byte[4 + 1 + dataLength]; + setHeader(cla, ins, p1, p2); + apdu[4] = (byte)dataLength; + this.dataOffset = 5; + System.arraycopy(data, dataOffset, apdu, 5, dataLength); + } else { + // case 3e + apdu = new byte[4 + 3 + dataLength]; + setHeader(cla, ins, p1, p2); + apdu[4] = 0; + apdu[5] = (byte)(dataLength >> 8); + apdu[6] = (byte)dataLength; + this.dataOffset = 7; + System.arraycopy(data, dataOffset, apdu, 7, dataLength); + } + } else { + // case 4s or 4e + if ((dataLength <= 255) && (ne <= 256)) { + // case 4s + apdu = new byte[4 + 2 + dataLength]; + setHeader(cla, ins, p1, p2); + apdu[4] = (byte)dataLength; + this.dataOffset = 5; + System.arraycopy(data, dataOffset, apdu, 5, dataLength); + apdu[apdu.length - 1] = (ne != 256) ? (byte)ne : 0; + } else { + // case 4e + apdu = new byte[4 + 5 + dataLength]; + setHeader(cla, ins, p1, p2); + apdu[4] = 0; + apdu[5] = (byte)(dataLength >> 8); + apdu[6] = (byte)dataLength; + this.dataOffset = 7; + System.arraycopy(data, dataOffset, apdu, 7, dataLength); + if (ne != 65536) { + int leOfs = apdu.length - 2; + apdu[leOfs] = (byte)(ne >> 8); + apdu[leOfs + 1] = (byte)ne; + } // else le == 65536: no need to fill in, encoded as 0 + } + } + } + } + + private void setHeader(int cla, int ins, int p1, int p2) { + apdu[0] = (byte)cla; + apdu[1] = (byte)ins; + apdu[2] = (byte)p1; + apdu[3] = (byte)p2; + } + + /** + * Returns the value of the class byte CLA. + * + * @return the value of the class byte CLA. + */ + public int getCLA() { + return apdu[0] & 0xff; + } + + /** + * Returns the value of the instruction byte INS. + * + * @return the value of the instruction byte INS. + */ + public int getINS() { + return apdu[1] & 0xff; + } + + /** + * Returns the value of the parameter byte P1. + * + * @return the value of the parameter byte P1. + */ + public int getP1() { + return apdu[2] & 0xff; + } + + /** + * Returns the value of the parameter byte P2. + * + * @return the value of the parameter byte P2. + */ + public int getP2() { + return apdu[3] & 0xff; + } + + /** + * Returns the number of data bytes in the command body (Nc) or 0 if this + * APDU has no body. This call is equivalent to + * getData().length. + * + * @return the number of data bytes in the command body or 0 if this APDU + * has no body. + */ + public int getNc() { + return nc; + } + + /** + * Returns a copy of the data bytes in the command body. If this APDU as + * no body, this method returns a byte array with length zero. + * + * @return a copy of the data bytes in the command body or the empty + * byte array if this APDU has no body. + */ + public byte[] getData() { + byte[] data = new byte[nc]; + System.arraycopy(apdu, dataOffset, data, 0, nc); + return data; + } + + /** + * Returns the maximum number of expected data bytes in a response + * APDU (Ne). + * + * @return the maximum number of expected data bytes in a response APDU. + */ + public int getNe() { + return ne; + } + + /** + * Returns a copy of the bytes in this APDU. + * + * @return a copy of the bytes in this APDU. + */ + public byte[] getBytes() { + return apdu.clone(); + } + + /** + * Returns a string representation of this command APDU. + * + * @return a String representation of this command APDU. + */ + public String toString() { + return "CommmandAPDU: " + apdu.length + " bytes, nc=" + nc + ", ne=" + ne; + } + + /** + * Compares the specified object with this command APDU for equality. + * Returns true if the given object is also a CommandAPDU and its bytes are + * identical to the bytes in this CommandAPDU. + * + * @param obj the object to be compared for equality with this command APDU + * @return true if the specified object is equal to this command APDU + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof CommandAPDU == false) { + return false; + } + CommandAPDU other = (CommandAPDU)obj; + return Arrays.equals(this.apdu, other.apdu); + } + + /** + * Returns the hash code value for this command APDU. + * + * @return the hash code value for this command APDU. + */ + public int hashCode() { + return Arrays.hashCode(apdu); + } + + private void readObject(java.io.ObjectInputStream in) + throws java.io.IOException, ClassNotFoundException { + apdu = (byte[])in.readUnshared(); + // initialize transient fields + parse(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java new file mode 100644 index 000000000..c822dc07c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.sufficientlysecure.keychain.securitytoken.smartcardio; + +import java.util.Arrays; + +/** + * A response APDU as defined in ISO/IEC 7816-4. It consists of a conditional + * body and a two byte trailer. + * This class does not attempt to verify that the APDU encodes a semantically + * valid response. + * + *

Instances of this class are immutable. Where data is passed in or out + * via byte arrays, defensive cloning is performed. + * + * @see CommandAPDU + * @see CardChannel#transmit CardChannel.transmit + * + * @since 1.6 + * @author Andreas Sterbenz + * @author JSR 268 Expert Group + */ +public final class ResponseAPDU implements java.io.Serializable { + + private static final long serialVersionUID = 6962744978375594225L; + + /** @serial */ + private byte[] apdu; + + /** + * Constructs a ResponseAPDU from a byte array containing the complete + * APDU contents (conditional body and trailed). + * + *

Note that the byte array is cloned to protect against subsequent + * modification. + * + * @param apdu the complete response APDU + * + * @throws NullPointerException if apdu is null + * @throws IllegalArgumentException if apdu.length is less than 2 + */ + public ResponseAPDU(byte[] apdu) { + apdu = apdu.clone(); + check(apdu); + this.apdu = apdu; + } + + private static void check(byte[] apdu) { + if (apdu.length < 2) { + throw new IllegalArgumentException("apdu must be at least 2 bytes long"); + } + } + + /** + * Returns the number of data bytes in the response body (Nr) or 0 if this + * APDU has no body. This call is equivalent to + * getData().length. + * + * @return the number of data bytes in the response body or 0 if this APDU + * has no body. + */ + public int getNr() { + return apdu.length - 2; + } + + /** + * Returns a copy of the data bytes in the response body. If this APDU as + * no body, this method returns a byte array with a length of zero. + * + * @return a copy of the data bytes in the response body or the empty + * byte array if this APDU has no body. + */ + public byte[] getData() { + byte[] data = new byte[apdu.length - 2]; + System.arraycopy(apdu, 0, data, 0, data.length); + return data; + } + + /** + * Returns the value of the status byte SW1 as a value between 0 and 255. + * + * @return the value of the status byte SW1 as a value between 0 and 255. + */ + public int getSW1() { + return apdu[apdu.length - 2] & 0xff; + } + + /** + * Returns the value of the status byte SW2 as a value between 0 and 255. + * + * @return the value of the status byte SW2 as a value between 0 and 255. + */ + public int getSW2() { + return apdu[apdu.length - 1] & 0xff; + } + + /** + * Returns the value of the status bytes SW1 and SW2 as a single + * status word SW. + * It is defined as + * (getSW1() << 8) | getSW2(). + * + * @return the value of the status word SW. + */ + public int getSW() { + return (getSW1() << 8) | getSW2(); + } + + /** + * Returns a copy of the bytes in this APDU. + * + * @return a copy of the bytes in this APDU. + */ + public byte[] getBytes() { + return apdu.clone(); + } + + /** + * Returns a string representation of this response APDU. + * + * @return a String representation of this response APDU. + */ + public String toString() { + return "ResponseAPDU: " + apdu.length + " bytes, SW=" + + Integer.toHexString(getSW()); + } + + /** + * Compares the specified object with this response APDU for equality. + * Returns true if the given object is also a ResponseAPDU and its bytes are + * identical to the bytes in this ResponseAPDU. + * + * @param obj the object to be compared for equality with this response APDU + * @return true if the specified object is equal to this response APDU + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ResponseAPDU == false) { + return false; + } + ResponseAPDU other = (ResponseAPDU)obj; + return Arrays.equals(this.apdu, other.apdu); + } + + /** + * Returns the hash code value for this response APDU. + * + * @return the hash code value for this response APDU. + */ + public int hashCode() { + return Arrays.hashCode(apdu); + } + + private void readObject(java.io.ObjectInputStream in) + throws java.io.IOException, ClassNotFoundException { + apdu = (byte[])in.readUnshared(); + check(apdu); + } + +} From c2ca440c8837456af9c077a218796684183822fc Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 15:35:36 +0600 Subject: [PATCH 10/28] SecurityToken: use CommandAPDU instead of raw strings --- .../keychain/securitytoken/CardException.java | 9 + .../keychain/securitytoken/NfcTransport.java | 6 +- .../securitytoken/SecurityTokenHelper.java | 336 ++++++------------ .../keychain/securitytoken/Transport.java | 5 +- .../securitytoken/usb/UsbTransport.java | 7 +- 5 files changed, 135 insertions(+), 228 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java index 905deca90..6a011ff0b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java @@ -27,6 +27,15 @@ public class CardException extends IOException { mResponseCode = responseCode; } + public CardException(String detailMessage, int responseCode) { + super(detailMessage); + mResponseCode = (short) responseCode; + } + + public CardException(String s) { + this(s, -1); + } + public short getResponseCode() { return mResponseCode; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java index ba36ebdf0..61cfd1d24 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java @@ -19,6 +19,8 @@ package org.sufficientlysecure.keychain.securitytoken; import android.nfc.Tag; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; import java.io.IOException; @@ -43,8 +45,8 @@ public class NfcTransport implements Transport { * @throws IOException */ @Override - public byte[] transceive(final byte[] data) throws IOException { - return mIsoCard.transceive(data); + public ResponseAPDU transceive(final CommandAPDU data) throws IOException { + return new ResponseAPDU(mIsoCard.transceive(data.getBytes())); } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 0b81c9fbb..0232c36a8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -30,6 +30,8 @@ import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; @@ -47,7 +49,14 @@ import nordpol.Apdu; * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf */ public class SecurityTokenHelper { - private static final int MAX_APDU_DATAFIELD_SIZE = 254 * 500; + private static final int MAX_APDU_NC = 255; + private static final int MAX_APDU_NC_EXT = 65535; + + private static final int MAX_APDU_NE = 256; + private static final int MAX_APDU_NE_EXT = 65536; + + private static final int APDU_SW_SUCCESS = 0x9000; + // Fidesmo constants private static final String FIDESMO_APPS_AID_PREFIX = "A000000617"; @@ -72,16 +81,9 @@ public class SecurityTokenHelper { return new String(Hex.encode(raw)); } - private String getHolderName(String name) { + private String getHolderName(byte[] name) { try { - String slength; - int ilength; - name = name.substring(6); - slength = name.substring(0, 2); - ilength = Integer.parseInt(slength, 16) * 2; - name = name.substring(2, ilength + 2); - name = (new String(Hex.decode(name))).replace('<', ' '); - return name; + return (new String(name, 4, name[3])).replace('<', ' '); } catch (IndexOutOfBoundsException e) { // try-catch for https://github.com/FluffyKaon/OpenPGP-Card // Note: This should not happen, but happens with @@ -153,23 +155,12 @@ public class SecurityTokenHelper { mTransport.connect(); // Connect on smartcard layer - - // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time. - // See specification, page 51 - String accepted = "9000"; - // Command APDU (page 51) for SELECT FILE command (page 29) - String opening = - "00" // CLA - + "A4" // INS - + "04" // P1 - + "00" // P2 - + "06" // Lc (number of bytes) - + "D27600012401" // Data (6 bytes) - + "00"; // Le - String response = communicate(opening); // activate connection - if (!response.endsWith(accepted)) { - throw new CardException("Initialization failed!", parseCardStatus(response)); + CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401")); + ResponseAPDU response = communicate(select); // activate connection + + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Initialization failed!", response.getSW()); } byte[] pwStatusBytes = getPwStatusBytes(); @@ -179,24 +170,6 @@ public class SecurityTokenHelper { mPw3Validated = false; } - /** - * Parses out the status word from a JavaCard response string. - * - * @param response A hex string with the response from the card - * @return A short indicating the SW1/SW2, or 0 if a status could not be determined. - */ - private short parseCardStatus(String response) { - if (response.length() < 4) { - return 0; // invalid input - } - - try { - return Short.parseShort(response.substring(response.length() - 4), 16); - } catch (NumberFormatException e) { - return 0; - } - } - /** * Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for * conformance to the token's requirements for key length. @@ -230,16 +203,11 @@ public class SecurityTokenHelper { } // Command APDU for CHANGE REFERENCE DATA command (page 32) - String changeReferenceDataApdu = "00" // CLA - + "24" // INS - + "00" // P1 - + String.format("%02x", pw) // P2 - + String.format("%02x", pin.length + newPin.length) // Lc - + getHex(pin) - + getHex(newPin); - String response = communicate(changeReferenceDataApdu); // change PIN - if (!response.equals("9000")) { - throw new CardException("Failed to change PIN", parseCardStatus(response)); + CommandAPDU changePin = new CommandAPDU(0x00, 0x24, 0x00, pw, Arrays.concatenate(pin, newPin)); + ResponseAPDU response = communicate(changePin); + + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Failed to change PIN", response.getSW()); } } @@ -254,43 +222,20 @@ public class SecurityTokenHelper { verifyPin(0x82); // (Verify PW1 with mode 82 for decryption) } - int offset = 2; // Skip first byte TODO: why? - String response = "", status = ""; - boolean shouldPad = true; - // Transmit - while (offset < encryptedSessionKey.length) { - boolean isLastCommand = MAX_APDU_DATAFIELD_SIZE >= encryptedSessionKey.length - offset; - String cla = isLastCommand ? "00" : "10"; - - int len = Math.min(MAX_APDU_DATAFIELD_SIZE, encryptedSessionKey.length - offset + (shouldPad ? 1 : 0)); - String command = cla + "2a8086" - + Hex.toHexString(new byte[]{(byte) ((len >> 16) & 0xFF), (byte) ((len >> 8) & 0xFF), (byte) (len & 0xFF)}) - + (shouldPad ? "00": "") - + Hex.toHexString(encryptedSessionKey, offset, len - (shouldPad ? 1 : 0)) + "0000"; - shouldPad = false; - response = communicate(command); - status = response.substring(response.length() - 4); - - if (!isLastCommand && !response.endsWith("9000")) { - throw new CardException("Deciphering with Security token failed on transmit", parseCardStatus(response)); - } - - offset += MAX_APDU_DATAFIELD_SIZE; + byte[] data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length); + if (data[0] != 0) { + data = Arrays.prepend(data, (byte) 0x00); } - // Receive - String result = getDataField(response); - while (response.endsWith("61")) { - response = communicate("00C00000" + status.substring(2)); - status = response.substring(response.length() - 4); - result += getDataField(response); - } - if (!status.equals("9000")) { - throw new CardException("Deciphering with Security token failed on receive", parseCardStatus(response)); + CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x80, 0x86, data, MAX_APDU_NE_EXT); + ResponseAPDU response = communicate(command); + + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Deciphering with Security token failed on receive", response.getSW()); } - return Hex.decode(result); + return response.getData(); } /** @@ -309,12 +254,9 @@ public class SecurityTokenHelper { pin = mPin.toStringUnsafe().getBytes(); } - // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time. - // See specification, page 51 - String accepted = "9000"; - String response = tryPin(mode, pin); // login - if (!response.equals(accepted)) { - throw new CardException("Bad PIN!", parseCardStatus(response)); + ResponseAPDU response = tryPin(mode, pin);// login + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Bad PIN!", response.getSW()); } if (mode == 0x81) { @@ -347,16 +289,11 @@ public class SecurityTokenHelper { verifyPin(0x83); // (Verify PW3) } - String putDataApdu = "00" // CLA - + "DA" // INS - + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1 - + String.format("%02x", dataObject & 0xFF) // P2 - + String.format("%02x", data.length) // Lc - + getHex(data); + CommandAPDU command = new CommandAPDU(0x00, 0xDA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data); + ResponseAPDU response = communicate(command); // put data - String response = communicate(putDataApdu); // put data - if (!response.equals("9000")) { - throw new CardException("Failed to put data.", parseCardStatus(response)); + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Failed to put data.", response.getSW()); } } @@ -442,34 +379,18 @@ public class SecurityTokenHelper { currentKeyObject = crtSecretKey.getModulus().toByteArray(); System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256); - String putKeyCommand = "10DB3FFF"; - String lastPutKeyCommand = "00DB3FFF"; - // Now we're ready to communicate with the token. - offset = 0; - String response; - while (offset < dataToSend.length) { - int dataRemaining = dataToSend.length - offset; - if (dataRemaining > 254) { - response = communicate( - putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254) - ); - offset += 254; - } else { - int length = dataToSend.length - offset; - response = communicate( - lastPutKeyCommand + String.format("%02x", length) - + Hex.toHexString(dataToSend, offset, length)); - offset += length; - } - if (!response.endsWith("9000")) { - throw new CardException("Key export to Security Token failed", parseCardStatus(response)); - } - } + CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, dataToSend); // Clear array with secret data before we return. Arrays.fill(dataToSend, (byte) 0); + + ResponseAPDU response = communicate(apdu); + + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Key export to Security Token failed", response.getSW()); + } } /** @@ -479,25 +400,29 @@ public class SecurityTokenHelper { * @return The fingerprints of all subkeys in a contiguous byte array. */ public byte[] getFingerprints() throws IOException { - String data = "00CA006E00"; - byte[] buf = mTransport.transceive(Hex.decode(data)); + CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E); + ResponseAPDU response = communicate(apdu); - Iso7816TLV[] tlvs = Iso7816TLV.readList(buf, true); - Iso7816TLV fptlv = null; + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Failed to get fingerprints", response.getSW()); + } - for (int i = 0; i < tlvs.length; i++) { - Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlvs[i].prettyPrint()); + Iso7816TLV[] tlvList = Iso7816TLV.readList(response.getData(), true); + Iso7816TLV fingerPrintTlv = null; - Iso7816TLV tlv = Iso7816TLV.findRecursive(tlvs[i], 0xc5); - if (tlv != null) { - fptlv = tlv; + for (Iso7816TLV tlv : tlvList) { + Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint()); + + Iso7816TLV matchingTlv = Iso7816TLV.findRecursive(tlv, 0xc5); + if (matchingTlv != null) { + fingerPrintTlv = matchingTlv; } } - if (fptlv == null) { + if (fingerPrintTlv == null) { return null; } - return fptlv.mV; + return fingerPrintTlv.mV; } /** @@ -506,18 +431,23 @@ public class SecurityTokenHelper { * @return Seven bytes in fixed format, plus 0x9000 status word at the end. */ private byte[] getPwStatusBytes() throws IOException { - String data = "00CA00C400"; - return mTransport.transceive(Hex.decode(data)); + return getData(0x00, 0xC4); } public byte[] getAid() throws IOException { - String info = "00CA004F00"; - return mTransport.transceive(Hex.decode(info)); + return getData(0x00, 0x4F); } public String getUserId() throws IOException { - String info = "00CA006500"; - return getHolderName(communicate(info)); + return getHolderName(getData(0x00, 0x65)); + } + + private byte[] getData(int p1, int p2) throws IOException { + ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2)); + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Failed to get pw status bytes", response.getSW()); + } + return response.getData(); } /** @@ -532,7 +462,7 @@ public class SecurityTokenHelper { } // dsi, including Lc - String dsi; + byte[] dsi; Log.i(Constants.TAG, "Hash: " + hashAlgo); switch (hashAlgo) { @@ -540,86 +470,65 @@ public class SecurityTokenHelper { if (hash.length != 20) { throw new IOException("Bad hash length (" + hash.length + ", expected 10!"); } - dsi = "23" // Lc - + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes + dsi = Arrays.concatenate(Hex.decode( + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes + "0605" + "2B0E03021A" // OID of SHA1 + "0500" // TLV coding of ZERO - + "0414" + getHex(hash); // 0x14 are 20 hash bytes + + "0414"), hash); // 0x14 are 20 hash bytes break; case HashAlgorithmTags.RIPEMD160: if (hash.length != 20) { throw new IOException("Bad hash length (" + hash.length + ", expected 20!"); } - dsi = "233021300906052B2403020105000414" + getHex(hash); + dsi = Arrays.concatenate(Hex.decode("3021300906052B2403020105000414"), hash); break; case HashAlgorithmTags.SHA224: if (hash.length != 28) { throw new IOException("Bad hash length (" + hash.length + ", expected 28!"); } - dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash); + dsi = Arrays.concatenate(Hex.decode("302D300D06096086480165030402040500041C"), hash); break; case HashAlgorithmTags.SHA256: if (hash.length != 32) { throw new IOException("Bad hash length (" + hash.length + ", expected 32!"); } - dsi = "333031300D060960864801650304020105000420" + getHex(hash); + dsi = Arrays.concatenate(Hex.decode("3031300D060960864801650304020105000420"), hash); break; case HashAlgorithmTags.SHA384: if (hash.length != 48) { throw new IOException("Bad hash length (" + hash.length + ", expected 48!"); } - dsi = "433041300D060960864801650304020205000430" + getHex(hash); + dsi = Arrays.concatenate(Hex.decode("3041300D060960864801650304020205000430"), hash); break; case HashAlgorithmTags.SHA512: if (hash.length != 64) { throw new IOException("Bad hash length (" + hash.length + ", expected 64!"); } - dsi = "533051300D060960864801650304020305000440" + getHex(hash); + dsi = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash); break; default: throw new IOException("Not supported hash algo!"); } // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) - String apdu = - "002A9E9A" // CLA, INS, P1, P2 - + "0000" - + dsi // digital signature input - + "0000"; // Le + CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, dsi, MAX_APDU_NE_EXT); + ResponseAPDU response = communicate(command); - String response = communicate(apdu); - - if (response.length() < 4) { - throw new CardException("Bad response", (short) 0); + if (response.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Failed to sign", response.getSW()); } - // split up response into signature and status - String status = response.substring(response.length() - 4); - String signature = response.substring(0, response.length() - 4); - - // while we are getting 0x61 status codes, retrieve more data - while (status.substring(0, 2).equals("61")) { - Log.d(Constants.TAG, "requesting more data, status " + status); - // Send GET RESPONSE command - response = communicate("00C00000" + status.substring(2)); - status = response.substring(response.length() - 4); - signature += response.substring(0, response.length() - 4); - } - - Log.d(Constants.TAG, "final response:" + status); if (!mPw1ValidForMultipleSignatures) { mPw1ValidatedForSignature = false; } - if (!"9000".equals(status)) { - throw new CardException("Bad NFC response code: " + status, parseCardStatus(response)); - } + byte[] signature = response.getData(); // Make sure the signature we received is actually the expected number of bytes long! - if (signature.length() != 256 && signature.length() != 512 - && signature.length() != 768 && signature.length() != 1024) { - throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length() / 2); + if (signature.length != 128 && signature.length != 256 + && signature.length != 384 && signature.length != 512) { + throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length); } return Hex.decode(signature); @@ -628,8 +537,8 @@ public class SecurityTokenHelper { /** * Transceive data via NFC encoded as Hex */ - private String communicate(String apdu) throws IOException { - return getHex(mTransport.transceive(Hex.decode(apdu))); + private ResponseAPDU communicate(CommandAPDU apdu) throws IOException { + return mTransport.transceive(apdu); } public Transport getTransport() { @@ -645,9 +554,8 @@ public class SecurityTokenHelper { try { // By trying to select any apps that have the Fidesmo AID prefix we can // see if it is a Fidesmo device or not - byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX)); - // Compare the status returned by our select with the OK status code - return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU); + CommandAPDU apdu = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode(FIDESMO_APPS_AID_PREFIX)); + return communicate(apdu).getSW() == APDU_SW_SUCCESS; } catch (IOException e) { Log.e(Constants.TAG, "Card communication failed!", e); } @@ -678,38 +586,19 @@ public class SecurityTokenHelper { verifyPin(0x83); // (Verify PW3 with mode 83) } - String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000"; - String getResponseApdu = "00C00000"; + CommandAPDU apdu = new CommandAPDU(0x00, 0x47, 0x80, 0x00, new byte[]{(byte) slot, 0x00}, MAX_APDU_NE_EXT); + ResponseAPDU response = communicate(apdu); - String first = communicate(generateKeyApdu); - String second = communicate(getResponseApdu); - - if (!second.endsWith("9000")) { + if (response.getSW() != APDU_SW_SUCCESS) { throw new IOException("On-card key generation failed"); } - String publicKeyData = getDataField(first) + getDataField(second); - - Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData); - - return Hex.decode(publicKeyData); + return response.getData(); } - private String getDataField(String output) { - return output.substring(0, output.length() - 4); - } - - private String tryPin(int mode, byte[] pin) throws IOException { + private ResponseAPDU tryPin(int mode, byte[] pin) throws IOException { // Command APDU for VERIFY command (page 32) - String login = - "00" // CLA - + "20" // INS - + "00" // P1 - + String.format("%02x", mode) // P2 - + String.format("%02x", pin.length) // Lc - + Hex.toHexString(pin); - - return communicate(login); + return communicate(new CommandAPDU(0x00, 0x20, 0x00, mode, pin)); } /** @@ -723,30 +612,33 @@ public class SecurityTokenHelper { // try wrong PIN 4 times until counter goes to C0 byte[] pin = "XXXXXX".getBytes(); for (int i = 0; i <= 4; i++) { - String response = tryPin(0x81, pin); - if (response.equals(accepted)) { // Should NOT accept! - throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response)); + ResponseAPDU response = tryPin(0x81, pin); + if (response.getSW() != APDU_SW_SUCCESS) { // Should NOT accept! + throw new CardException("Should never happen, XXXXXX has been accepted!", response.getSW()); } } // try wrong Admin PIN 4 times until counter goes to C0 byte[] adminPin = "XXXXXXXX".getBytes(); for (int i = 0; i <= 4; i++) { - String response = tryPin(0x83, adminPin); - if (response.equals(accepted)) { // Should NOT accept! - throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response)); + ResponseAPDU response = tryPin(0x83, adminPin); + if (response.getSW() != APDU_SW_SUCCESS) { // Should NOT accept! + throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSW()); } } // reactivate token! - String reactivate1 = "00" + "e6" + "00" + "00"; - String reactivate2 = "00" + "44" + "00" + "00"; - String response1 = communicate(reactivate1); - String response2 = communicate(reactivate2); - if (!response1.equals(accepted) || !response2.equals(accepted)) { - throw new CardException("Reactivating failed!", parseCardStatus(response1)); - } + CommandAPDU reactivate1 = new CommandAPDU(0x00, 0xE6, 0x00, 0x00); + CommandAPDU reactivate2 = new CommandAPDU(0x00, 0x44, 0x00, 0x00); + ResponseAPDU response1 = communicate(reactivate1); + ResponseAPDU response2 = communicate(reactivate2); + if (response1.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Reactivating failed!", response1.getSW()); + } + if (response2.getSW() != APDU_SW_SUCCESS) { + throw new CardException("Reactivating failed!", response2.getSW()); + } } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java index 294eaa9ee..7c48b0c3a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java @@ -17,6 +17,9 @@ package org.sufficientlysecure.keychain.securitytoken; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; + import java.io.IOException; /** @@ -29,7 +32,7 @@ public interface Transport { * @return received data * @throws IOException */ - byte[] transceive(byte[] data) throws IOException; + ResponseAPDU transceive(CommandAPDU data) throws IOException; /** * Disconnect and release connection diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java index e1409f39e..d65bbedd3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java @@ -29,8 +29,9 @@ import android.util.Pair; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.securitytoken.Transport; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; +import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol; -import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; @@ -230,8 +231,8 @@ public class UsbTransport implements Transport { * @throws UsbTransportException */ @Override - public byte[] transceive(byte[] data) throws UsbTransportException { - return mProtocol.transceive(data); + public ResponseAPDU transceive(CommandAPDU data) throws UsbTransportException { + return new ResponseAPDU(mProtocol.transceive(data.getBytes())); } @Override From b247de783ccd073ca965abf16fa2cf2e10f37113 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 16:38:08 +0600 Subject: [PATCH 11/28] SecurityToken: support extended apdu and chaining based on capbilities --- .../securitytoken/CardCapabilities.java | 65 +++++++++++++++++++ .../securitytoken/SecurityTokenHelper.java | 56 +++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardCapabilities.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardCapabilities.java new file mode 100644 index 000000000..8133f985d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardCapabilities.java @@ -0,0 +1,65 @@ +/* + * 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; + +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; + +import java.nio.ByteBuffer; + +public class CardCapabilities { + private static final int MASK_CHAINING = 1 << 7; + private static final int MASK_EXTENDED = 1 << 6; + + private byte[] mCapabilityBytes; + + public CardCapabilities(byte[] historicalBytes) throws UsbTransportException { + if (historicalBytes[0] != 0x00) { + throw new UsbTransportException("Invalid historical bytes category indicator byte"); + } + + mCapabilityBytes = getCapabilitiesBytes(historicalBytes); + } + + public CardCapabilities() { + mCapabilityBytes = null; + } + + private static byte[] getCapabilitiesBytes(byte[] historicalBytes) { + // Compact TLV + ByteBuffer byteBuffer = ByteBuffer.wrap(historicalBytes, 1, historicalBytes.length - 2); + while (byteBuffer.hasRemaining()) { + byte tl = byteBuffer.get(); + if (tl == 0x73) { // Capabilities TL + byte[] val = new byte[3]; + byteBuffer.get(val); + return val; + } + byteBuffer.position(byteBuffer.position() + (tl & 0xF)); + } + + return null; + } + + public boolean hasChaining() { + return mCapabilityBytes != null && (mCapabilityBytes[2] & MASK_CHAINING) != 0; + } + + public boolean hasExtended() { + return mCapabilityBytes != null && (mCapabilityBytes[2] & MASK_EXTENDED) != 0; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 0232c36a8..034d4b1ac 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -32,10 +32,12 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -57,11 +59,14 @@ public class SecurityTokenHelper { private static final int APDU_SW_SUCCESS = 0x9000; + private static final int MASK_CLA_CHAINING = 1 << 4; + // Fidesmo constants private static final String FIDESMO_APPS_AID_PREFIX = "A000000617"; private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; private Transport mTransport; + private CardCapabilities mCardCapabilities; private Passphrase mPin; private Passphrase mAdminPin; @@ -152,6 +157,8 @@ public class SecurityTokenHelper { */ public void connectToDevice() throws IOException { // Connect on transport layer + mCardCapabilities = null; + mTransport.connect(); // Connect on smartcard layer @@ -163,6 +170,8 @@ public class SecurityTokenHelper { throw new CardException("Initialization failed!", response.getSW()); } + mCardCapabilities = new CardCapabilities(getHistoricalBytes()); + byte[] pwStatusBytes = getPwStatusBytes(); mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1); mPw1ValidatedForSignature = false; @@ -442,6 +451,10 @@ public class SecurityTokenHelper { return getHolderName(getData(0x00, 0x65)); } + private byte[] getHistoricalBytes() throws IOException { + return getData(0x5F, 0x52); + } + private byte[] getData(int p1, int p2) throws IOException { ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2)); if (response.getSW() != APDU_SW_SUCCESS) { @@ -538,7 +551,48 @@ public class SecurityTokenHelper { * Transceive data via NFC encoded as Hex */ private ResponseAPDU communicate(CommandAPDU apdu) throws IOException { - return mTransport.transceive(apdu); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + + ResponseAPDU lastResponse = null; + // Transmit + if (mCardCapabilities.hasExtended()) { + lastResponse = mTransport.transceive(apdu); + } else if (apdu.getData().length <= MAX_APDU_NC) { + lastResponse = mTransport.transceive(new CommandAPDU(apdu.getCLA(), apdu.getINS(), + apdu.getP1(), apdu.getP2(), apdu.getData(), MAX_APDU_NE)); + } else if (apdu.getData().length > MAX_APDU_NC && mCardCapabilities.hasChaining()) { + int offset = 0; + byte[] data = apdu.getData(); + while (offset < data.length) { + int curLen = Math.min(MAX_APDU_NC, data.length - offset); + boolean last = offset + curLen >= data.length; + int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); + + lastResponse = mTransport.transceive(new CommandAPDU(cla, apdu.getINS(), apdu.getP1(), + apdu.getP2(), apdu.getData(), MAX_APDU_NE)); + + if (!last && lastResponse.getSW() != APDU_SW_SUCCESS) { + throw new UsbTransportException("Failed to chain apdu"); + } + } + } + if (lastResponse == null) { + throw new UsbTransportException("Can't transmit command"); + } + + result.write(lastResponse.getData()); + + // Receive + while (lastResponse.getSW1() == 0x61) { + CommandAPDU getResponse = new CommandAPDU(0x00, 0xC0, 0x00, 0x00, lastResponse.getSW2()); + lastResponse = mTransport.transceive(getResponse); + result.write(lastResponse.getData()); + } + + result.write(lastResponse.getSW1()); + result.write(lastResponse.getSW2()); + + return new ResponseAPDU(result.toByteArray()); } public Transport getTransport() { From b71f9f4fd7b3bf172951ab57d0a59bc82139e4c9 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 20:13:37 +0600 Subject: [PATCH 12/28] SecurityToken: fix APDU ne lengths --- .../securitytoken/SecurityTokenHelper.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 034d4b1ac..dae273b91 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -157,13 +157,13 @@ public class SecurityTokenHelper { */ public void connectToDevice() throws IOException { // Connect on transport layer - mCardCapabilities = null; + mCardCapabilities = new CardCapabilities(); mTransport.connect(); // Connect on smartcard layer // Command APDU (page 51) for SELECT FILE command (page 29) - CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401")); + CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401"), MAX_APDU_NE_EXT); ResponseAPDU response = communicate(select); // activate connection if (response.getSW() != APDU_SW_SUCCESS) { @@ -409,7 +409,7 @@ public class SecurityTokenHelper { * @return The fingerprints of all subkeys in a contiguous byte array. */ public byte[] getFingerprints() throws IOException { - CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E); + CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E, MAX_APDU_NE_EXT); ResponseAPDU response = communicate(apdu); if (response.getSW() != APDU_SW_SUCCESS) { @@ -456,7 +456,7 @@ public class SecurityTokenHelper { } private byte[] getData(int p1, int p2) throws IOException { - ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2)); + ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2, MAX_APDU_NE_EXT)); if (response.getSW() != APDU_SW_SUCCESS) { throw new CardException("Failed to get pw status bytes", response.getSW()); } @@ -544,7 +544,7 @@ public class SecurityTokenHelper { throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length); } - return Hex.decode(signature); + return signature; } /** @@ -558,22 +558,26 @@ public class SecurityTokenHelper { if (mCardCapabilities.hasExtended()) { lastResponse = mTransport.transceive(apdu); } else if (apdu.getData().length <= MAX_APDU_NC) { + int ne = Math.min(apdu.getNe(), MAX_APDU_NE); lastResponse = mTransport.transceive(new CommandAPDU(apdu.getCLA(), apdu.getINS(), - apdu.getP1(), apdu.getP2(), apdu.getData(), MAX_APDU_NE)); + apdu.getP1(), apdu.getP2(), apdu.getData(), ne)); } else if (apdu.getData().length > MAX_APDU_NC && mCardCapabilities.hasChaining()) { int offset = 0; byte[] data = apdu.getData(); + int ne = Math.min(apdu.getNe(), MAX_APDU_NE); while (offset < data.length) { int curLen = Math.min(MAX_APDU_NC, data.length - offset); boolean last = offset + curLen >= data.length; int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); lastResponse = mTransport.transceive(new CommandAPDU(cla, apdu.getINS(), apdu.getP1(), - apdu.getP2(), apdu.getData(), MAX_APDU_NE)); + apdu.getP2(), Arrays.copyOfRange(data, offset, offset + curLen), ne)); if (!last && lastResponse.getSW() != APDU_SW_SUCCESS) { throw new UsbTransportException("Failed to chain apdu"); } + + offset += curLen; } } if (lastResponse == null) { @@ -604,7 +608,8 @@ public class SecurityTokenHelper { } public boolean isFidesmoToken() { - if (isConnected()) { // Check if we can still talk to the card + return false; // TODO + /*if (isConnected()) { // Check if we can still talk to the card try { // By trying to select any apps that have the Fidesmo AID prefix we can // see if it is a Fidesmo device or not @@ -614,7 +619,7 @@ public class SecurityTokenHelper { Log.e(Constants.TAG, "Card communication failed!", e); } } - return false; + return false;*/ } /** From 87743ec8ad0ff7d76302dff57354ac6158b1bc53 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 20:16:23 +0600 Subject: [PATCH 13/28] SecurityToken: fix reset&wipe token method --- .../keychain/securitytoken/SecurityTokenHelper.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index dae273b91..24e841bcf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -666,13 +666,11 @@ public class SecurityTokenHelper { * Afterwards, the token is reactivated. */ public void resetAndWipeToken() throws IOException { - String accepted = "9000"; - // try wrong PIN 4 times until counter goes to C0 byte[] pin = "XXXXXX".getBytes(); for (int i = 0; i <= 4; i++) { ResponseAPDU response = tryPin(0x81, pin); - if (response.getSW() != APDU_SW_SUCCESS) { // Should NOT accept! + if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept! throw new CardException("Should never happen, XXXXXX has been accepted!", response.getSW()); } } @@ -681,7 +679,7 @@ public class SecurityTokenHelper { byte[] adminPin = "XXXXXXXX".getBytes(); for (int i = 0; i <= 4; i++) { ResponseAPDU response = tryPin(0x83, adminPin); - if (response.getSW() != APDU_SW_SUCCESS) { // Should NOT accept! + if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept! throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSW()); } } From 0954684441a0d96d65876e6524cbba7253fb9587 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 21:03:05 +0600 Subject: [PATCH 14/28] SecurityToken: remove unused smartcadio stuff --- .../securitytoken/smartcardio/ATR.java | 165 ---------- .../securitytoken/smartcardio/Card.java | 167 ---------- .../smartcardio/CardChannel.java | 184 ----------- .../smartcardio/CardException.java | 68 ---- .../smartcardio/CardNotPresentException.java | 68 ---- .../smartcardio/CardPermission.java | 301 ------------------ .../smartcardio/CommandAPDU.java | 1 - .../smartcardio/ResponseAPDU.java | 1 - 8 files changed, 955 deletions(-) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java deleted file mode 100644 index 5d1581b19..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ATR.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -import java.util.*; - -/** - * A Smart Card's answer-to-reset bytes. A Card's ATR object can be obtained - * by calling {@linkplain Card#getATR}. - * This class does not attempt to verify that the ATR encodes a semantically - * valid structure. - * - *

Instances of this class are immutable. Where data is passed in or out - * via byte arrays, defensive cloning is performed. - * - * @see Card#getATR - * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group - */ -public final class ATR implements java.io.Serializable { - - private static final long serialVersionUID = 6695383790847736493L; - - private byte[] atr; - - private transient int startHistorical, nHistorical; - - /** - * Constructs an ATR from a byte array. - * - * @param atr the byte array containing the answer-to-reset bytes - * @throws NullPointerException if atr is null - */ - public ATR(byte[] atr) { - this.atr = atr.clone(); - parse(); - } - - private void parse() { - if (atr.length < 2) { - return; - } - if ((atr[0] != 0x3b) && (atr[0] != 0x3f)) { - return; - } - int t0 = (atr[1] & 0xf0) >> 4; - int n = atr[1] & 0xf; - int i = 2; - while ((t0 != 0) && (i < atr.length)) { - if ((t0 & 1) != 0) { - i++; - } - if ((t0 & 2) != 0) { - i++; - } - if ((t0 & 4) != 0) { - i++; - } - if ((t0 & 8) != 0) { - if (i >= atr.length) { - return; - } - t0 = (atr[i++] & 0xf0) >> 4; - } else { - t0 = 0; - } - } - int k = i + n; - if ((k == atr.length) || (k == atr.length - 1)) { - startHistorical = i; - nHistorical = n; - } - } - - /** - * Returns a copy of the bytes in this ATR. - * - * @return a copy of the bytes in this ATR. - */ - public byte[] getBytes() { - return atr.clone(); - } - - /** - * Returns a copy of the historical bytes in this ATR. - * If this ATR does not contain historical bytes, an array of length - * zero is returned. - * - * @return a copy of the historical bytes in this ATR. - */ - public byte[] getHistoricalBytes() { - byte[] b = new byte[nHistorical]; - System.arraycopy(atr, startHistorical, b, 0, nHistorical); - return b; - } - - /** - * Returns a string representation of this ATR. - * - * @return a String representation of this ATR. - */ - public String toString() { - return "ATR: " + atr.length + " bytes"; - } - - /** - * Compares the specified object with this ATR for equality. - * Returns true if the given object is also an ATR and its bytes are - * identical to the bytes in this ATR. - * - * @param obj the object to be compared for equality with this ATR - * @return true if the specified object is equal to this ATR - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof ATR == false) { - return false; - } - ATR other = (ATR)obj; - return Arrays.equals(this.atr, other.atr); - } - - /** - * Returns the hash code value for this ATR. - * - * @return the hash code value for this ATR. - */ - public int hashCode() { - return Arrays.hashCode(atr); - } - - private void readObject(java.io.ObjectInputStream in) - throws java.io.IOException, ClassNotFoundException { - atr = (byte[])in.readUnshared(); - parse(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java deleted file mode 100644 index b3b7fdc4f..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/Card.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -import java.nio.ByteBuffer; - -/** - * A Smart Card with which a connection has been established. Card objects - * are obtained by calling {@link CardTerminal#connect CardTerminal.connect()}. - * - * @see CardTerminal - * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group -*/ -public abstract class Card { - - /** - * Constructs a new Card object. - * - *

This constructor is called by subclasses only. Application should - * call the {@linkplain CardTerminal#connect CardTerminal.connect()} - * method to obtain a Card - * object. - */ - protected Card() { - // empty - } - - /** - * Returns the ATR of this card. - * - * @return the ATR of this card. - */ - public abstract ATR getATR(); - - /** - * Returns the protocol in use for this card. - * - * @return the protocol in use for this card, for example "T=0" or "T=1" - */ - public abstract String getProtocol(); - - /** - * Returns the CardChannel for the basic logical channel. The basic - * logical channel has a channel number of 0. - * - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - * @throws IllegalStateException if this card object has been disposed of - * via the {@linkplain #disconnect disconnect()} method - */ - public abstract CardChannel getBasicChannel(); - - /** - * Opens a new logical channel to the card and returns it. The channel is - * opened by issuing a MANAGE CHANNEL command that should use - * the format [00 70 00 00 01]. - * - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - * @throws CardException is a new logical channel could not be opened - * @throws IllegalStateException if this card object has been disposed of - * via the {@linkplain #disconnect disconnect()} method - */ - public abstract CardChannel openLogicalChannel() throws CardException; - - /** - * Requests exclusive access to this card. - * - *

Once a thread has invoked beginExclusive, only this - * thread is allowed to communicate with this card until it calls - * endExclusive. Other threads attempting communication - * will receive a CardException. - * - *

Applications have to ensure that exclusive access is correctly - * released. This can be achieved by executing - * the beginExclusive() and endExclusive calls - * in a try ... finally block. - * - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - * @throws CardException if exclusive access has already been set - * or if exclusive access could not be established - * @throws IllegalStateException if this card object has been disposed of - * via the {@linkplain #disconnect disconnect()} method - */ - public abstract void beginExclusive() throws CardException; - - /** - * Releases the exclusive access previously established using - * beginExclusive. - * - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - * @throws IllegalStateException if the active Thread does not currently have - * exclusive access to this card or - * if this card object has been disposed of - * via the {@linkplain #disconnect disconnect()} method - * @throws CardException if the operation failed - */ - public abstract void endExclusive() throws CardException; - - /** - * Transmits a control command to the terminal device. - * - *

This can be used to, for example, control terminal functions like - * a built-in PIN pad or biometrics. - * - * @param controlCode the control code of the command - * @param command the command data - * - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - * @throws NullPointerException if command is null - * @throws CardException if the card operation failed - * @throws IllegalStateException if this card object has been disposed of - * via the {@linkplain #disconnect disconnect()} method - */ - public abstract byte[] transmitControlCommand(int controlCode, - byte[] command) throws CardException; - - /** - * Disconnects the connection with this card. After this method returns, - * calling methods on this object or in CardChannels associated with this - * object that require interaction with the card will raise an - * IllegalStateException. - * - * @param reset whether to reset the card after disconnecting. - * - * @throws CardException if the card operation failed - * @throws SecurityException if a SecurityManager exists and the - * caller does not have the required - * {@linkplain CardPermission permission} - */ - public abstract void disconnect(boolean reset) throws CardException; - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java deleted file mode 100644 index 243c171bd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardChannel.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -import java.nio.*; - -/** - * A logical channel connection to a Smart Card. It is used to exchange APDUs - * with a Smart Card. - * A CardChannel object can be obtained by calling the method - * {@linkplain Card#getBasicChannel} or {@linkplain Card#openLogicalChannel}. - * - * @see Card - * @see CommandAPDU - * @see ResponseAPDU - * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group - */ -public abstract class CardChannel { - - /** - * Constructs a new CardChannel object. - * - *

This constructor is called by subclasses only. Application should - * call the {@linkplain Card#getBasicChannel} and - * {@linkplain Card#openLogicalChannel} methods to obtain a CardChannel - * object. - */ - protected CardChannel() { - // empty - } - - /** - * Returns the Card this channel is associated with. - * - * @return the Card this channel is associated with - */ - public abstract Card getCard(); - - /** - * Returns the channel number of this CardChannel. A channel number of - * 0 indicates the basic logical channel. - * - * @return the channel number of this CardChannel. - * - * @throws IllegalStateException if this channel has been - * {@linkplain #close closed} or if the corresponding Card has been - * {@linkplain Card#disconnect disconnected}. - */ - public abstract int getChannelNumber(); - - /** - * Transmits the specified command APDU to the Smart Card and returns the - * response APDU. - * - *

The CLA byte of the command APDU is automatically adjusted to - * match the channel number of this CardChannel. - * - *

Note that this method cannot be used to transmit - * MANAGE CHANNEL APDUs. Logical channels should be managed - * using the {@linkplain Card#openLogicalChannel} and {@linkplain - * CardChannel#close CardChannel.close()} methods. - * - *

Implementations should transparently handle artifacts - * of the transmission protocol. - * For example, when using the T=0 protocol, the following processing - * should occur as described in ISO/IEC 7816-4: - * - *

    - *
  • if the response APDU has an SW1 of 61, the - * implementation should issue a GET RESPONSE command - * using SW2 as the Lefield. - * This process is repeated as long as an SW1 of 61 is - * received. The response body of these exchanges is concatenated - * to form the final response body. - * - *

  • if the response APDU is 6C XX, the implementation - * should reissue the command using XX as the - * Le field. - *

- * - *

The ResponseAPDU returned by this method is the result - * after this processing has been performed. - * - * @param command the command APDU - * @return the response APDU received from the card - * - * @throws IllegalStateException if this channel has been - * {@linkplain #close closed} or if the corresponding Card has been - * {@linkplain Card#disconnect disconnected}. - * @throws IllegalArgumentException if the APDU encodes a - * MANAGE CHANNEL command - * @throws NullPointerException if command is null - * @throws CardException if the card operation failed - */ - public abstract ResponseAPDU transmit(CommandAPDU command) throws CardException; - - /** - * Transmits the command APDU stored in the command ByteBuffer and receives - * the reponse APDU in the response ByteBuffer. - * - *

The command buffer must contain valid command APDU data starting - * at command.position() and the APDU must be - * command.remaining() bytes long. - * Upon return, the command buffer's position will be equal - * to its limit; its limit will not have changed. The output buffer - * will have received the response APDU bytes. Its position will have - * advanced by the number of bytes received, which is also the return - * value of this method. - * - *

The CLA byte of the command APDU is automatically adjusted to - * match the channel number of this CardChannel. - * - *

Note that this method cannot be used to transmit - * MANAGE CHANNEL APDUs. Logical channels should be managed - * using the {@linkplain Card#openLogicalChannel} and {@linkplain - * CardChannel#close CardChannel.close()} methods. - * - *

See {@linkplain #transmit transmit()} for a discussion of the handling - * of response APDUs with the SW1 values 61 or 6C. - * - * @param command the buffer containing the command APDU - * @param response the buffer that shall receive the response APDU from - * the card - * @return the length of the received response APDU - * - * @throws IllegalStateException if this channel has been - * {@linkplain #close closed} or if the corresponding Card has been - * {@linkplain Card#disconnect disconnected}. - * @throws NullPointerException if command or response is null - * @throws ReadOnlyBufferException if the response buffer is read-only - * @throws IllegalArgumentException if command and response are the - * same object, if response may not have - * sufficient space to receive the response APDU - * or if the APDU encodes a MANAGE CHANNEL command - * @throws CardException if the card operation failed - */ - public abstract int transmit(ByteBuffer command, ByteBuffer response) - throws CardException; - - /** - * Closes this CardChannel. The logical channel is closed by issuing - * a MANAGE CHANNEL command that should use the format - * [xx 70 80 0n] where n is the channel number - * of this channel and xx is the CLA - * byte that encodes this logical channel and has all other bits set to 0. - * After this method returns, calling other - * methods in this class will raise an IllegalStateException. - * - *

Note that the basic logical channel cannot be closed using this - * method. It can be closed by calling {@link Card#disconnect}. - * - * @throws CardException if the card operation failed - * @throws IllegalStateException if this CardChannel represents a - * connection the basic logical channel - */ - public abstract void close() throws CardException; - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java deleted file mode 100644 index 23d02e24e..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardException.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -/** - * Exception for errors that occur during communication with the - * Smart Card stack or the card itself. - * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group - */ -public class CardException extends Exception { - - private static final long serialVersionUID = 7787607144922050628L; - - /** - * Constructs a new CardException with the specified detail message. - * - * @param message the detail message - */ - public CardException(String message) { - super(message); - } - - /** - * Constructs a new CardException with the specified cause and a detail message - * of (cause==null ? null : cause.toString()). - * - * @param cause the cause of this exception or null - */ - public CardException(Throwable cause) { - super(cause); - } - - /** - * Constructs a new CardException with the specified detail message and cause. - * - * @param message the detail message - * @param cause the cause of this exception or null - */ - public CardException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java deleted file mode 100644 index 3b8289b75..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardNotPresentException.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -/** - * Exception thrown when an application tries to establish a connection with a - * terminal that has no card present. - * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group - */ -public class CardNotPresentException extends CardException { - - private final static long serialVersionUID = 1346879911706545215L; - - /** - * Constructs a new CardNotPresentException with the specified detail message. - * - * @param message the detail message - */ - public CardNotPresentException(String message) { - super(message); - } - - /** - * Constructs a new CardNotPresentException with the specified cause and a detail message - * of (cause==null ? null : cause.toString()). - * - * @param cause the cause of this exception or null - */ - public CardNotPresentException(Throwable cause) { - super(cause); - } - - /** - * Constructs a new CardNotPresentException with the specified detail message and cause. - * - * @param message the detail message - * @param cause the cause of this exception or null - */ - public CardNotPresentException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java deleted file mode 100644 index 8731fa467..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CardPermission.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.sufficientlysecure.keychain.securitytoken.smartcardio; - -import java.io.*; - -import java.security.Permission; - -/** - * A permission for Smart Card operations. A CardPermission consists of the - * name of the card terminal the permission applies to and a set of actions - * that are valid for that terminal. - * - *

A CardPermission with a name of * applies to all - * card terminals. The actions string is a comma separated list of the actions - * listed below, or * to signify "all actions." - * - *

Individual actions are: - *

- *
connect - *
connect to a card using - * {@linkplain CardTerminal#connect CardTerminal.connect()} - * - *
reset - *
reset the card using {@linkplain Card#disconnect Card.disconnect(true)} - * - *
exclusive - *
establish exclusive access to a card using - * {@linkplain Card#beginExclusive} and {@linkplain Card#endExclusive - * endExclusive()} - * - *
transmitControl - *
transmit a control command using - * {@linkplain Card#transmitControlCommand Card.transmitControlCommand()} - * - *
getBasicChannel - *
obtain the basic logical channel using - * {@linkplain Card#getBasicChannel} - * - *
openLogicalChannel - *
open a new logical channel using - * {@linkplain Card#openLogicalChannel} - * - *
- * - * @since 1.6 - * @author Andreas Sterbenz - * @author JSR 268 Expert Group - */ -public class CardPermission extends Permission { - - private static final long serialVersionUID = 7146787880530705613L; - - private final static int A_CONNECT = 0x01; - private final static int A_EXCLUSIVE = 0x02; - private final static int A_GET_BASIC_CHANNEL = 0x04; - private final static int A_OPEN_LOGICAL_CHANNEL = 0x08; - private final static int A_RESET = 0x10; - private final static int A_TRANSMIT_CONTROL = 0x20; - - // sum of all the actions above - private final static int A_ALL = 0x3f; - - private final static int[] ARRAY_MASKS = { - A_ALL, - A_CONNECT, - A_EXCLUSIVE, - A_GET_BASIC_CHANNEL, - A_OPEN_LOGICAL_CHANNEL, - A_RESET, - A_TRANSMIT_CONTROL, - }; - - private final static String S_CONNECT = "connect"; - private final static String S_EXCLUSIVE = "exclusive"; - private final static String S_GET_BASIC_CHANNEL = "getBasicChannel"; - private final static String S_OPEN_LOGICAL_CHANNEL = "openLogicalChannel"; - private final static String S_RESET = "reset"; - private final static String S_TRANSMIT_CONTROL = "transmitControl"; - - private final static String S_ALL = "*"; - - private final static String[] ARRAY_STRINGS = { - S_ALL, - S_CONNECT, - S_EXCLUSIVE, - S_GET_BASIC_CHANNEL, - S_OPEN_LOGICAL_CHANNEL, - S_RESET, - S_TRANSMIT_CONTROL, - }; - - private transient int mask; - - /** - * @serial - */ - private volatile String actions; - - /** - * Constructs a new CardPermission with the specified actions. - * terminalName is the name of a CardTerminal or * - * if this permission applies to all terminals. actions - * contains a comma-separated list of the individual actions - * or * to signify all actions. For more information, - * see the documentation at the top of this {@linkplain CardPermission - * class}. - * - * @param terminalName the name of the card terminal, or * - * @param actions the action string (or null if the set of permitted - * actions is empty) - * - * @throws NullPointerException if terminalName is null - * @throws IllegalArgumentException if actions is an invalid actions - * specification - */ - public CardPermission(String terminalName, String actions) { - super(terminalName); - if (terminalName == null) { - throw new NullPointerException(); - } - mask = getMask(actions); - } - - private static int getMask(String actions) { - if ((actions == null) || (actions.length() == 0)) { - throw new IllegalArgumentException("actions must not be empty"); - } - - // try exact matches for simple actions first - for (int i = 0; i < ARRAY_STRINGS.length; i++) { - if (actions == ARRAY_STRINGS[i]) { - return ARRAY_MASKS[i]; - } - } - - if (actions.endsWith(",")) { - throw new IllegalArgumentException("Invalid actions: '" + actions + "'"); - } - int mask = 0; - String[] split = actions.split(","); - outer: - for (String s : split) { - for (int i = 0; i < ARRAY_STRINGS.length; i++) { - if (ARRAY_STRINGS[i].equalsIgnoreCase(s)) { - mask |= ARRAY_MASKS[i]; - continue outer; - } - } - throw new IllegalArgumentException("Invalid action: '" + s + "'"); - } - - return mask; - } - - private static String getActions(int mask) { - if (mask == A_ALL) { - return S_ALL; - } - boolean first = true; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < ARRAY_MASKS.length; i++) { - int action = ARRAY_MASKS[i]; - if ((mask & action) == action) { - if (first == false) { - sb.append(","); - } else { - first = false; - } - sb.append(ARRAY_STRINGS[i]); - } - } - return sb.toString(); - } - - - /** - * Returns the canonical string representation of the actions. - * It is * to signify all actions defined by this class or - * the string concatenation of the comma-separated, - * lexicographically sorted list of individual actions. - * - * @return the canonical string representation of the actions. - */ - public String getActions() { - if (actions == null) { - actions = getActions(mask); - } - return actions; - } - - /** - * Checks if this CardPermission object implies the specified permission. - * That is the case, if and only if - *
    - *
  • permission is an instance of CardPermission,

    - *
  • permission's actions are a proper subset of this - * object's actions, and

    - *
  • this object's getName() method is either - * * or equal to permission's name. - *

    - *
- * - * @param permission the permission to check against - * @return true if and only if this CardPermission object implies the - * specified permission. - */ - public boolean implies(Permission permission) { - if (permission instanceof CardPermission == false) { - return false; - } - CardPermission other = (CardPermission)permission; - if ((this.mask & other.mask) != other.mask) { - return false; - } - String thisName = getName(); - if (thisName.equals("*")) { - return true; - } - if (thisName.equals(other.getName())) { - return true; - } - return false; - } - - /** - * Compares the specified object with this CardPermission for equality. - * This CardPermission is equal to another Object object, if - * and only if - *
    - *
  • object is an instance of CardPermission,

    - *
  • this.getName() is equal to - * ((CardPermission)object).getName(), and

    - *
  • this.getActions() is equal to - * ((CardPermission)object).getActions().

    - *
- * - * @param obj the object to be compared for equality with this CardPermission - * @return true if and only if the specified object is equal to this - * CardPermission - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof CardPermission == false) { - return false; - } - CardPermission other = (CardPermission)obj; - return this.getName().equals(other.getName()) && (this.mask == other.mask); - } - - /** - * Returns the hash code value for this CardPermission object. - * - * @return the hash code value for this CardPermission object. - */ - public int hashCode() { - return getName().hashCode() + 31 * mask; - } - - private void writeObject(ObjectOutputStream s) throws IOException { - // Write out the actions. The superclass takes care of the name. - // Call getActions to make sure actions field is initialized - if (actions == null) { - getActions(); - } - s.defaultWriteObject(); - } - - private void readObject(ObjectInputStream s) - throws IOException, ClassNotFoundException { - // Read in the actions, then restore the mask. - s.defaultReadObject(); - mask = getMask(actions); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java index 137f307f2..1dabbba2c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java @@ -56,7 +56,6 @@ import java.nio.ByteBuffer; * via byte arrays, defensive cloning is performed. * * @see ResponseAPDU - * @see CardChannel#transmit CardChannel.transmit * * @since 1.6 * @author Andreas Sterbenz diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java index c822dc07c..728d8eb67 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java @@ -37,7 +37,6 @@ import java.util.Arrays; * via byte arrays, defensive cloning is performed. * * @see CommandAPDU - * @see CardChannel#transmit CardChannel.transmit * * @since 1.6 * @author Andreas Sterbenz From 16b47acee8edbb5813b10a6d3dd32b08b519aaf9 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 21:22:26 +0600 Subject: [PATCH 15/28] SecurityToken: reenable fidesmo checks --- .../keychain/securitytoken/SecurityTokenHelper.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 24e841bcf..f90e2463b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -163,7 +163,7 @@ public class SecurityTokenHelper { // Connect on smartcard layer // Command APDU (page 51) for SELECT FILE command (page 29) - CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401"), MAX_APDU_NE_EXT); + CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401")); ResponseAPDU response = communicate(select); // activate connection if (response.getSW() != APDU_SW_SUCCESS) { @@ -608,8 +608,7 @@ public class SecurityTokenHelper { } public boolean isFidesmoToken() { - return false; // TODO - /*if (isConnected()) { // Check if we can still talk to the card + if (isConnected()) { // Check if we can still talk to the card try { // By trying to select any apps that have the Fidesmo AID prefix we can // see if it is a Fidesmo device or not @@ -619,7 +618,7 @@ public class SecurityTokenHelper { Log.e(Constants.TAG, "Card communication failed!", e); } } - return false;*/ + return false; } /** From 6855417b5b7388ae8c62173840d08d8734052cd2 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sat, 14 May 2016 21:24:32 +0600 Subject: [PATCH 16/28] SecurityToken: disable Nitrokey Storage & Start untill checked --- OpenKeychain/src/main/res/xml/usb_device_filter.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/res/xml/usb_device_filter.xml b/OpenKeychain/src/main/res/xml/usb_device_filter.xml index f335201dd..7650105a4 100644 --- a/OpenKeychain/src/main/res/xml/usb_device_filter.xml +++ b/OpenKeychain/src/main/res/xml/usb_device_filter.xml @@ -31,8 +31,8 @@ - + - + \ No newline at end of file From 3d827d5a6b671449441420d923f6b2340be375f3 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 15 May 2016 23:22:49 +0600 Subject: [PATCH 17/28] SecurityToken: rename methods, fix comments --- .../keychain/securitytoken/KeyType.java | 4 ++-- .../keychain/securitytoken/SecurityTokenHelper.java | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java index 0e28f022b..59c0dc89e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java @@ -51,7 +51,7 @@ public enum KeyType { return mIdx; } - public int getmSlot() { + public int getSlot() { return mSlot; } @@ -59,7 +59,7 @@ public enum KeyType { return mTimestampObjectId; } - public int getmFingerprintObjectId() { + public int getFingerprintObjectId() { return mFingerprintObjectId; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index f90e2463b..3d484ad48 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -43,8 +43,6 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.interfaces.RSAPrivateCrtKey; -import nordpol.Apdu; - /** * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant * devices. @@ -82,10 +80,6 @@ public class SecurityTokenHelper { return LazyHolder.SECURITY_TOKEN_HELPER; } - private static String getHex(byte[] raw) { - return new String(Hex.encode(raw)); - } - private String getHolderName(byte[] name) { try { return (new String(name, 4, name[3])).replace('<', ' '); @@ -133,8 +127,8 @@ public class SecurityTokenHelper { keyType.toString())); } - putKey(keyType.getmSlot(), secretKey, passphrase); - putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint()); + putKey(keyType.getSlot(), secretKey, passphrase); + putData(keyType.getFingerprintObjectId(), secretKey.getFingerprint()); putData(keyType.getTimestampObjectId(), timestampBytes); } @@ -474,7 +468,6 @@ public class SecurityTokenHelper { verifyPin(0x81); // (Verify PW1 with mode 81 for signing) } - // dsi, including Lc byte[] dsi; Log.i(Constants.TAG, "Hash: " + hashAlgo); From 55e14cb45bc17c36cbdbaf80fbb8221069d01e3a Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 16 May 2016 00:19:54 +0600 Subject: [PATCH 18/28] SecurityToken: add openpgp card capabilities handling --- .../securitytoken/OpenPGPCapabilities.java | 174 ++++++++++++++++++ .../securitytoken/SecurityTokenHelper.java | 13 +- 2 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java new file mode 100644 index 000000000..5465b2f8a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java @@ -0,0 +1,174 @@ +/* + * 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; + +import org.sufficientlysecure.keychain.util.Iso7816TLV; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class OpenPGPCapabilities { + private final static int MASK_SM = 1 << 7; + private final static int MASK_KEY_IMPORT = 1 << 5; + private final static int MASK_ATTRIBUTES_CHANGABLE = 1 << 2; + + private boolean mPw1ValidForMultipleSignatures; + private byte[] mAid; + private byte[] mHistoricalBytes; + + private boolean mHasSM; + private boolean mAttriburesChangable; + private boolean mHasKeyImport; + + private byte mSMAlgo; + private int mMaxCmdLen; + private int mMaxRspLen; + + private Map mKeyFormats; + + public OpenPGPCapabilities(byte[] data) throws IOException { + Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); + mKeyFormats = new HashMap<>(); + + for (Iso7816TLV tlv : tlvs) { + switch (tlv.mT) { + case 0x4F: + mAid = tlv.mV; + break; + case 0x5F52: + mHistoricalBytes = tlv.mV; + break; + case 0x73: + parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv); + break; + } + } + } + + private void parseDdo(Iso7816TLV.Iso7816CompositeTLV tlvs) { + for (Iso7816TLV tlv : tlvs.mSubs) { + switch (tlv.mT) { + case 0xC0: + parseExtendedCaps(tlv.mV); + break; + case 0xC1: + parseAlgoCaps(KeyType.SIGN, tlv.mV); + break; + case 0xC2: + parseAlgoCaps(KeyType.ENCRYPT, tlv.mV); + break; + case 0xC3: + parseAlgoCaps(KeyType.AUTH, tlv.mV); + break; + case 0xC4: + mPw1ValidForMultipleSignatures = tlv.mV[0] == 1; + break; + } + } + } + + private void parseAlgoCaps(KeyType keyType, byte[] data) { + mKeyFormats.put(keyType, AlgorithmFormat.from(data[5])); + } + + private void parseExtendedCaps(byte[] v) { + mHasSM = (v[0] & MASK_SM) != 0; + mHasKeyImport = (v[0] & MASK_KEY_IMPORT) != 0; + mAttriburesChangable =(v[0] & MASK_ATTRIBUTES_CHANGABLE) != 0; + + mSMAlgo = v[1]; + + mMaxCmdLen = (v[6] << 8) + v[7]; + mMaxRspLen = (v[8] << 8) + v[9]; + } + + public boolean isPw1ValidForMultipleSignatures() { + return mPw1ValidForMultipleSignatures; + } + + public byte[] getAid() { + return mAid; + } + + public byte[] getHistoricalBytes() { + return mHistoricalBytes; + } + + public boolean isHasSM() { + return mHasSM; + } + + public boolean isAttriburesChangable() { + return mAttriburesChangable; + } + + public boolean isHasKeyImport() { + return mHasKeyImport; + } + + public byte getSMAlgo() { + return mSMAlgo; + } + + public int getMaxCmdLen() { + return mMaxCmdLen; + } + + public int getMaxRspLen() { + return mMaxRspLen; + } + + public AlgorithmFormat getFormatForKeyType(KeyType keyType) { + return mKeyFormats.get(keyType); + } + + public enum AlgorithmFormat { + STANDARD(0, false, false), + STANDARD_WITH_MODULUS(1, false, true), + CRT(2, true, false), + CRT_WITH_MODULUS(3, true, true); + + private int mValue; + private boolean mHasModulus; + private boolean mHasExtra; + + AlgorithmFormat(int value, boolean hasExtra, boolean hasModulus) { + mValue = value; + mHasModulus = hasModulus; + mHasExtra = hasExtra; + } + + public boolean isHasModulus() { + return mHasModulus; + } + + public boolean isHasExtra() { + return mHasExtra; + } + + public static AlgorithmFormat from(byte b) { + for (AlgorithmFormat format : values()) { + if (format.mValue == b) { + return format; + } + } + return null; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 3d484ad48..018b393b8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -65,10 +65,10 @@ public class SecurityTokenHelper { private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; private Transport mTransport; private CardCapabilities mCardCapabilities; + private OpenPGPCapabilities mOpenPGPCapabilities; private Passphrase mPin; private Passphrase mAdminPin; - private boolean mPw1ValidForMultipleSignatures; private boolean mPw1ValidatedForSignature; private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming? private boolean mPw3Validated; @@ -164,10 +164,9 @@ public class SecurityTokenHelper { throw new CardException("Initialization failed!", response.getSW()); } - mCardCapabilities = new CardCapabilities(getHistoricalBytes()); + mOpenPGPCapabilities = new OpenPGPCapabilities(getData(0x00, 0x65)); + mCardCapabilities = new CardCapabilities(mOpenPGPCapabilities.getHistoricalBytes()); - byte[] pwStatusBytes = getPwStatusBytes(); - mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1); mPw1ValidatedForSignature = false; mPw1ValidatedForDecrypt = false; mPw3Validated = false; @@ -445,10 +444,6 @@ public class SecurityTokenHelper { return getHolderName(getData(0x00, 0x65)); } - private byte[] getHistoricalBytes() throws IOException { - return getData(0x5F, 0x52); - } - private byte[] getData(int p1, int p2) throws IOException { ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2, MAX_APDU_NE_EXT)); if (response.getSW() != APDU_SW_SUCCESS) { @@ -525,7 +520,7 @@ public class SecurityTokenHelper { throw new CardException("Failed to sign", response.getSW()); } - if (!mPw1ValidForMultipleSignatures) { + if (!mOpenPGPCapabilities.isPw1ValidForMultipleSignatures()) { mPw1ValidatedForSignature = false; } From 231e3fb3d52ab84d74c8e062f3694c2d28d0f620 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Tue, 17 May 2016 00:35:12 +0600 Subject: [PATCH 19/28] SecurityToken: update put key to support different private key template --- .../securitytoken/OpenPGPCapabilities.java | 18 ++ .../securitytoken/SecurityTokenHelper.java | 161 +++++++++++------- 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java index 5465b2f8a..347386fd6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java @@ -45,6 +45,9 @@ public class OpenPGPCapabilities { public OpenPGPCapabilities(byte[] data) throws IOException { Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); mKeyFormats = new HashMap<>(); + if (tlvs.length == 1 && tlvs[0].mT == 0x6E) { + tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs; + } for (Iso7816TLV tlv : tlvs) { switch (tlv.mT) { @@ -57,6 +60,21 @@ public class OpenPGPCapabilities { case 0x73: parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv); break; + case 0xC0: + parseExtendedCaps(tlv.mV); + break; + case 0xC1: + parseAlgoCaps(KeyType.SIGN, tlv.mV); + break; + case 0xC2: + parseAlgoCaps(KeyType.ENCRYPT, tlv.mV); + break; + case 0xC3: + parseAlgoCaps(KeyType.AUTH, tlv.mV); + break; + case 0xC4: + mPw1ValidForMultipleSignatures = tlv.mV[0] == 1; + break; } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 018b393b8..fd9b95af6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -127,7 +127,7 @@ public class SecurityTokenHelper { keyType.toString())); } - putKey(keyType.getSlot(), secretKey, passphrase); + putKey(keyType, secretKey, passphrase); putData(keyType.getFingerprintObjectId(), secretKey.getFingerprint()); putData(keyType.getTimestampObjectId(), timestampBytes); } @@ -164,7 +164,7 @@ public class SecurityTokenHelper { throw new CardException("Initialization failed!", response.getSW()); } - mOpenPGPCapabilities = new OpenPGPCapabilities(getData(0x00, 0x65)); + mOpenPGPCapabilities = new OpenPGPCapabilities(getData(0x00, 0x6E)); mCardCapabilities = new CardCapabilities(mOpenPGPCapabilities.getHistoricalBytes()); mPw1ValidatedForSignature = false; @@ -307,12 +307,8 @@ public class SecurityTokenHelper { * 0xB8: Decipherment Key * 0xA4: Authentication Key */ - private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase) + private void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase) throws IOException { - if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) { - throw new IOException("Invalid key slot"); - } - RSAPrivateCrtKey crtSecretKey; try { secretKey.unlock(passphrase); @@ -335,59 +331,70 @@ public class SecurityTokenHelper { verifyPin(0x83); // (Verify PW3 with mode 83) } - byte[] header = Hex.decode( - "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23) - + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length - + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex) - + "9103" // Public modulus, length 3 - + "928180" // Prime P, length 128 - + "938180" // Prime Q, length 128 - + "948180" // Coefficient (1/q mod p), length 128 - + "958180" // Prime exponent P (d mod (p - 1)), length 128 - + "968180" // Prime exponent Q (d mod (1 - 1)), length 128 - + "97820100" // Modulus, length 256, last item in private key template - + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow - byte[] dataToSend = new byte[934]; - byte[] currentKeyObject; - int offset = 0; + ByteArrayOutputStream stream = new ByteArrayOutputStream(), + template = new ByteArrayOutputStream(), + data = new ByteArrayOutputStream(), + res = new ByteArrayOutputStream(); - System.arraycopy(header, 0, dataToSend, offset, header.length); - offset += header.length; - currentKeyObject = crtSecretKey.getPublicExponent().toByteArray(); - System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3); - offset += 3; - // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0 - // in the array to represent sign, so we take care to set the offset to 1 if necessary. - currentKeyObject = crtSecretKey.getPrimeP().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); - Arrays.fill(currentKeyObject, (byte) 0); - offset += 128; - currentKeyObject = crtSecretKey.getPrimeQ().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); - Arrays.fill(currentKeyObject, (byte) 0); - offset += 128; - currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); - Arrays.fill(currentKeyObject, (byte) 0); - offset += 128; - currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); - Arrays.fill(currentKeyObject, (byte) 0); - offset += 128; - currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); - Arrays.fill(currentKeyObject, (byte) 0); - offset += 128; - currentKeyObject = crtSecretKey.getModulus().toByteArray(); - System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256); + // Public modulus, length 3 + template.write(Hex.decode("9104")); + writeBits(data, crtSecretKey.getPublicExponent(), 4); + // Prime P, length 128 + template.write(Hex.decode("928180")); + writeBits(data, crtSecretKey.getPrimeP(), 128); + + // Prime Q, length 128 + template.write(Hex.decode("938180")); + writeBits(data, crtSecretKey.getPrimeQ(), 128); + + OpenPGPCapabilities.AlgorithmFormat format = mOpenPGPCapabilities.getFormatForKeyType(slot); + if (format.isHasExtra()) { + // Coefficient (1/q mod p), length 128 + template.write(Hex.decode("948180")); + writeBits(data, crtSecretKey.getCrtCoefficient(), 128); + + // Prime exponent P (d mod (p - 1)), length 128 + template.write(Hex.decode("958180")); + writeBits(data, crtSecretKey.getPrimeExponentP(), 128); + + // Prime exponent Q (d mod (1 - 1)), length 128 + template.write(Hex.decode("968180")); + writeBits(data, crtSecretKey.getPrimeExponentQ(), 128); + } + + if (format.isHasModulus()) { + // Modulus, length 256, last item in private key template + template.write(Hex.decode("97820100")); + writeBits(data, crtSecretKey.getModulus(), 256); + } + + // Bundle up + + // Ext header list data + // Control Reference Template to indicate the private key + stream.write(slot.getSlot()); + stream.write(0); + + // Cardholder private key template + stream.write(Hex.decode("7F48")); + stream.write(encodeLength(template.size())); + stream.write(template.toByteArray()); + + // Concatenation of key data as defined in DO 7F48 + stream.write(Hex.decode("5F48")); + stream.write(encodeLength(data.size())); + stream.write(data.toByteArray()); + + + // Result tlv + res.write(Hex.decode("4D")); + res.write(encodeLength(stream.size())); + res.write(stream.toByteArray()); + + byte[] bytes = res.toByteArray(); // Now we're ready to communicate with the token. - - CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, dataToSend); - - // Clear array with secret data before we return. - Arrays.fill(dataToSend, (byte) 0); - + CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, bytes); ResponseAPDU response = communicate(apdu); if (response.getSW() != APDU_SW_SUCCESS) { @@ -395,6 +402,46 @@ public class SecurityTokenHelper { } } + private byte[] encodeLength(int len) { + byte[] res; + if (len < 128) { + res = new byte[1]; + res[0] = (byte) len; + } else if (len < 256) { + res = new byte[2]; + res[0] = -127; + res[1] = (byte) len; + } else if (len < 65536) { + res = new byte[3]; + res[0] = -126; + res[1] = (byte) (len / 256); + res[2] = (byte) (len % 256); + } else { + res = new byte[4]; + if (len >= 16777216) { + throw new IllegalStateException("length [" + len + "] out of range (0x1000000)"); + } + + res[0] = -125; + res[1] = (byte) (len / 65536); + res[2] = (byte) (len / 256); + res[3] = (byte) (len % 256); + } + return res; + } + + private void writeBits(ByteArrayOutputStream stream, BigInteger value, int bits) { + byte[] prime = value.toByteArray(); + byte[] res = new byte[bits]; + int empty = bits - prime.length + 1; + + System.arraycopy(prime, 1, res, Math.max(0, empty), Math.min(prime.length - 1, bits)); + + stream.write(res, 0, bits); + Arrays.fill(res, (byte) 0); + Arrays.fill(prime, (byte) 0); + } + /** * Return fingerprints of all keys from application specific data stored * on tag, or null if data not available. From 12785480ef02bfcec22d26e44b949ebb711405bd Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 22 May 2016 23:17:01 +0600 Subject: [PATCH 20/28] SecurityToken: fix put key, add key format handling --- .../keychain/securitytoken/KeyFormat.java | 87 ++++++++++ .../securitytoken/OpenPGPCapabilities.java | 54 +----- .../securitytoken/SecurityTokenHelper.java | 106 +----------- .../keychain/util/SecurityTokenUtils.java | 154 ++++++++++++++++++ 4 files changed, 253 insertions(+), 148 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SecurityTokenUtils.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java new file mode 100644 index 000000000..3b2e93f0a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java @@ -0,0 +1,87 @@ +/* + * 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; + +// 4.3.3.6 Algorithm Attributes +public class KeyFormat { + private int mAlgorithmId; + private int mModulusLength; + private int mExponentLength; + private AlgorithmFormat mAlgorithmFormat; + + public KeyFormat(byte[] bytes) { + mAlgorithmId = bytes[0]; + mModulusLength = bytes[1] << 8 | bytes[2]; + mExponentLength = bytes[3] << 8 | bytes[4]; + mAlgorithmFormat = AlgorithmFormat.from(bytes[5]); + + if (mAlgorithmId != 1) { // RSA + throw new IllegalArgumentException("Unsupported Algorithm id " + mAlgorithmId); + } + } + + public int getAlgorithmId() { + return mAlgorithmId; + } + + public int getModulusLength() { + return mModulusLength; + } + + public int getExponentLength() { + return mExponentLength; + } + + public AlgorithmFormat getAlgorithmFormat() { + return mAlgorithmFormat; + } + + public enum AlgorithmFormat { + STANDARD(0, false, false), + STANDARD_WITH_MODULUS(1, false, true), + CRT(2, true, false), + CRT_WITH_MODULUS(3, true, true); + + private int mValue; + private boolean mIncludeModulus; + private boolean mIncludeCrt; + + AlgorithmFormat(int value, boolean includeCrt, boolean includeModulus) { + mValue = value; + mIncludeModulus = includeModulus; + mIncludeCrt = includeCrt; + } + + public static AlgorithmFormat from(byte b) { + for (AlgorithmFormat format : values()) { + if (format.mValue == b) { + return format; + } + } + return null; + } + + public boolean isIncludeModulus() { + return mIncludeModulus; + } + + public boolean isIncludeCrt() { + return mIncludeCrt; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java index 347386fd6..8aa86db02 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java @@ -40,7 +40,7 @@ public class OpenPGPCapabilities { private int mMaxCmdLen; private int mMaxRspLen; - private Map mKeyFormats; + private Map mKeyFormats; public OpenPGPCapabilities(byte[] data) throws IOException { Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); @@ -64,13 +64,13 @@ public class OpenPGPCapabilities { parseExtendedCaps(tlv.mV); break; case 0xC1: - parseAlgoCaps(KeyType.SIGN, tlv.mV); + mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV)); break; case 0xC2: - parseAlgoCaps(KeyType.ENCRYPT, tlv.mV); + mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV)); break; case 0xC3: - parseAlgoCaps(KeyType.AUTH, tlv.mV); + mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV)); break; case 0xC4: mPw1ValidForMultipleSignatures = tlv.mV[0] == 1; @@ -86,13 +86,13 @@ public class OpenPGPCapabilities { parseExtendedCaps(tlv.mV); break; case 0xC1: - parseAlgoCaps(KeyType.SIGN, tlv.mV); + mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV)); break; case 0xC2: - parseAlgoCaps(KeyType.ENCRYPT, tlv.mV); + mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV)); break; case 0xC3: - parseAlgoCaps(KeyType.AUTH, tlv.mV); + mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV)); break; case 0xC4: mPw1ValidForMultipleSignatures = tlv.mV[0] == 1; @@ -101,10 +101,6 @@ public class OpenPGPCapabilities { } } - private void parseAlgoCaps(KeyType keyType, byte[] data) { - mKeyFormats.put(keyType, AlgorithmFormat.from(data[5])); - } - private void parseExtendedCaps(byte[] v) { mHasSM = (v[0] & MASK_SM) != 0; mHasKeyImport = (v[0] & MASK_KEY_IMPORT) != 0; @@ -152,41 +148,7 @@ public class OpenPGPCapabilities { return mMaxRspLen; } - public AlgorithmFormat getFormatForKeyType(KeyType keyType) { + public KeyFormat getFormatForKeyType(KeyType keyType) { return mKeyFormats.get(keyType); } - - public enum AlgorithmFormat { - STANDARD(0, false, false), - STANDARD_WITH_MODULUS(1, false, true), - CRT(2, true, false), - CRT_WITH_MODULUS(3, true, true); - - private int mValue; - private boolean mHasModulus; - private boolean mHasExtra; - - AlgorithmFormat(int value, boolean hasExtra, boolean hasModulus) { - mValue = value; - mHasModulus = hasModulus; - mHasExtra = hasExtra; - } - - public boolean isHasModulus() { - return mHasModulus; - } - - public boolean isHasExtra() { - return mHasExtra; - } - - public static AlgorithmFormat from(byte b) { - for (AlgorithmFormat format : values()) { - if (format.mValue == b) { - return format; - } - } - return null; - } - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index fd9b95af6..71777143d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -19,7 +19,6 @@ * along with this program. If not, see . */ - package org.sufficientlysecure.keychain.securitytoken; import android.support.annotation.NonNull; @@ -36,6 +35,7 @@ import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.SecurityTokenUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -331,69 +331,11 @@ public class SecurityTokenHelper { verifyPin(0x83); // (Verify PW3 with mode 83) } - ByteArrayOutputStream stream = new ByteArrayOutputStream(), - template = new ByteArrayOutputStream(), - data = new ByteArrayOutputStream(), - res = new ByteArrayOutputStream(); - // Public modulus, length 3 - template.write(Hex.decode("9104")); - writeBits(data, crtSecretKey.getPublicExponent(), 4); - - // Prime P, length 128 - template.write(Hex.decode("928180")); - writeBits(data, crtSecretKey.getPrimeP(), 128); - - // Prime Q, length 128 - template.write(Hex.decode("938180")); - writeBits(data, crtSecretKey.getPrimeQ(), 128); - - OpenPGPCapabilities.AlgorithmFormat format = mOpenPGPCapabilities.getFormatForKeyType(slot); - if (format.isHasExtra()) { - // Coefficient (1/q mod p), length 128 - template.write(Hex.decode("948180")); - writeBits(data, crtSecretKey.getCrtCoefficient(), 128); - - // Prime exponent P (d mod (p - 1)), length 128 - template.write(Hex.decode("958180")); - writeBits(data, crtSecretKey.getPrimeExponentP(), 128); - - // Prime exponent Q (d mod (1 - 1)), length 128 - template.write(Hex.decode("968180")); - writeBits(data, crtSecretKey.getPrimeExponentQ(), 128); - } - - if (format.isHasModulus()) { - // Modulus, length 256, last item in private key template - template.write(Hex.decode("97820100")); - writeBits(data, crtSecretKey.getModulus(), 256); - } - - // Bundle up - - // Ext header list data - // Control Reference Template to indicate the private key - stream.write(slot.getSlot()); - stream.write(0); - - // Cardholder private key template - stream.write(Hex.decode("7F48")); - stream.write(encodeLength(template.size())); - stream.write(template.toByteArray()); - - // Concatenation of key data as defined in DO 7F48 - stream.write(Hex.decode("5F48")); - stream.write(encodeLength(data.size())); - stream.write(data.toByteArray()); - - - // Result tlv - res.write(Hex.decode("4D")); - res.write(encodeLength(stream.size())); - res.write(stream.toByteArray()); - - byte[] bytes = res.toByteArray(); // Now we're ready to communicate with the token. + byte[] bytes = SecurityTokenUtils.createPrivKeyTemplate(crtSecretKey, slot, + mOpenPGPCapabilities.getFormatForKeyType(slot)); + CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, bytes); ResponseAPDU response = communicate(apdu); @@ -402,46 +344,6 @@ public class SecurityTokenHelper { } } - private byte[] encodeLength(int len) { - byte[] res; - if (len < 128) { - res = new byte[1]; - res[0] = (byte) len; - } else if (len < 256) { - res = new byte[2]; - res[0] = -127; - res[1] = (byte) len; - } else if (len < 65536) { - res = new byte[3]; - res[0] = -126; - res[1] = (byte) (len / 256); - res[2] = (byte) (len % 256); - } else { - res = new byte[4]; - if (len >= 16777216) { - throw new IllegalStateException("length [" + len + "] out of range (0x1000000)"); - } - - res[0] = -125; - res[1] = (byte) (len / 65536); - res[2] = (byte) (len / 256); - res[3] = (byte) (len % 256); - } - return res; - } - - private void writeBits(ByteArrayOutputStream stream, BigInteger value, int bits) { - byte[] prime = value.toByteArray(); - byte[] res = new byte[bits]; - int empty = bits - prime.length + 1; - - System.arraycopy(prime, 1, res, Math.max(0, empty), Math.min(prime.length - 1, bits)); - - stream.write(res, 0, bits); - Arrays.fill(res, (byte) 0); - Arrays.fill(prime, (byte) 0); - } - /** * Return fingerprints of all keys from application specific data stored * on tag, or null if data not available. diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SecurityTokenUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SecurityTokenUtils.java new file mode 100644 index 000000000..2f959f912 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SecurityTokenUtils.java @@ -0,0 +1,154 @@ +/* + * 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.util; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.securitytoken.KeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyType; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.interfaces.RSAPrivateCrtKey; + +public class SecurityTokenUtils { + public static byte[] createPrivKeyTemplate(RSAPrivateCrtKey secretKey, KeyType slot, + KeyFormat format) throws IOException { + ByteArrayOutputStream stream = new ByteArrayOutputStream(), + template = new ByteArrayOutputStream(), + data = new ByteArrayOutputStream(), + res = new ByteArrayOutputStream(); + + int expLengthBytes = (format.getExponentLength() + 7) / 8; + // Public exponent + template.write(new byte[]{(byte) 0x91, (byte) expLengthBytes}); + writeBits(data, secretKey.getPublicExponent(), expLengthBytes); + + // Prime P, length 128 + template.write(Hex.decode("928180")); + writeBits(data, secretKey.getPrimeP(), 128); + + // Prime Q, length 128 + template.write(Hex.decode("938180")); + writeBits(data, secretKey.getPrimeQ(), 128); + + + if (format.getAlgorithmFormat().isIncludeCrt()) { + // Coefficient (1/q mod p), length 128 + template.write(Hex.decode("948180")); + writeBits(data, secretKey.getCrtCoefficient(), 128); + + // Prime exponent P (d mod (p - 1)), length 128 + template.write(Hex.decode("958180")); + writeBits(data, secretKey.getPrimeExponentP(), 128); + + // Prime exponent Q (d mod (1 - 1)), length 128 + template.write(Hex.decode("968180")); + writeBits(data, secretKey.getPrimeExponentQ(), 128); + } + + if (format.getAlgorithmFormat().isIncludeModulus()) { + // Modulus, length 256, last item in private key template + template.write(Hex.decode("97820100")); + writeBits(data, secretKey.getModulus(), 256); + } + + // Bundle up + + // Ext header list data + // Control Reference Template to indicate the private key + stream.write(slot.getSlot()); + stream.write(0); + + // Cardholder private key template + stream.write(Hex.decode("7F48")); + stream.write(encodeLength(template.size())); + stream.write(template.toByteArray()); + + // Concatenation of key data as defined in DO 7F48 + stream.write(Hex.decode("5F48")); + stream.write(encodeLength(data.size())); + stream.write(data.toByteArray()); + + + // Result tlv + res.write(Hex.decode("4D")); + res.write(encodeLength(stream.size())); + res.write(stream.toByteArray()); + + return res.toByteArray(); + } + + public static byte[] encodeLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("length is negative"); + } else if (len >= 16777216) { + throw new IllegalArgumentException("length is too big: " + len); + } + byte[] res; + if (len < 128) { + res = new byte[1]; + res[0] = (byte) len; + } else if (len < 256) { + res = new byte[2]; + res[0] = -127; + res[1] = (byte) len; + } else if (len < 65536) { + res = new byte[3]; + res[0] = -126; + res[1] = (byte) (len / 256); + res[2] = (byte) (len % 256); + } else { + res = new byte[4]; + + res[0] = -125; + res[1] = (byte) (len / 65536); + res[2] = (byte) (len / 256); + res[3] = (byte) (len % 256); + } + return res; + } + + public static void writeBits(ByteArrayOutputStream stream, BigInteger value, int width) { + if (value.signum() == -1) { + throw new IllegalArgumentException("value is negative"); + } else if (width <= 0) { + throw new IllegalArgumentException("width <= 0"); + } + + byte[] prime = value.toByteArray(); + int stripIdx = 0; + while (prime[stripIdx] == 0 && stripIdx + 1 < prime.length) { + stripIdx++; + } + + if (prime.length - stripIdx > width) { + throw new IllegalArgumentException("not enough width to fit value: " + + prime.length + "/" + width); + } + byte[] res = new byte[width]; + int empty = width - (prime.length - stripIdx); + + System.arraycopy(prime, stripIdx, res, Math.max(0, empty), Math.min(prime.length, width)); + + stream.write(res, 0, width); + Arrays.fill(res, (byte) 0); + Arrays.fill(prime, (byte) 0); + } +} From 6e2162202ddb5de0f07066a6fbe43747e7166cc4 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 22 May 2016 23:18:37 +0600 Subject: [PATCH 21/28] Add tests for SecurityTokenUtils --- .../securitytoken/SecurityTokenUtilsTest.java | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java new file mode 100644 index 000000000..4e719e338 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java @@ -0,0 +1,227 @@ +/* + * 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; + + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; +import org.sufficientlysecure.keychain.WorkaroundBuildConfig; +import org.sufficientlysecure.keychain.util.SecurityTokenUtils; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.interfaces.RSAPrivateCrtKey; + + +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml") +public class SecurityTokenUtilsTest { + private static RSAPrivateCrtKey mKey2048; + + @BeforeClass + public static void setUpOnce() throws Exception { + mKey2048 = new RSAPrivateCrtKey() { + @Override + public BigInteger getCrtCoefficient() { + byte[] bytes = new byte[128]; + Arrays.fill(bytes, (byte) 0x10); + return new BigInteger(bytes); + } + + @Override + public BigInteger getPrimeP() { + byte[] bytes = new byte[128]; + Arrays.fill(bytes, (byte) 0x11); + return new BigInteger(bytes); + } + + @Override + public BigInteger getPrimeQ() { + byte[] bytes = new byte[128]; + Arrays.fill(bytes, (byte) 0x12); + return new BigInteger(bytes); + } + + @Override + public BigInteger getPrimeExponentP() { + byte[] bytes = new byte[128]; + Arrays.fill(bytes, (byte) 0x13); + return new BigInteger(bytes); + } + + @Override + public BigInteger getPrimeExponentQ() { + byte[] bytes = new byte[128]; + Arrays.fill(bytes, (byte) 0x14); + return new BigInteger(bytes); + } + + @Override + public BigInteger getPublicExponent() { + return new BigInteger("10001", 16); + } + + @Override + public BigInteger getPrivateExponent() { + return null; + } + + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + + @Override + public BigInteger getModulus() { + byte[] bytes = new byte[256]; + Arrays.fill(bytes, (byte) 0x17); + return new BigInteger(bytes); + } + }; + } + + @Before + public void setUp() { + ShadowLog.stream = System.out; + } + + @Test + public void testEncodeLength() throws Exception { + // One byte + Assert.assertArrayEquals(new byte[]{0x00}, SecurityTokenUtils.encodeLength(0)); + Assert.assertArrayEquals(new byte[]{0x01}, SecurityTokenUtils.encodeLength(1)); + Assert.assertArrayEquals(new byte[]{0x7f}, SecurityTokenUtils.encodeLength(127)); + + // Two bytes + Assert.assertArrayEquals(new byte[]{(byte) 0x81, (byte) 0x80}, + SecurityTokenUtils.encodeLength(128)); + Assert.assertArrayEquals(new byte[]{(byte) 0x81, (byte) 0xFF}, + SecurityTokenUtils.encodeLength(255)); + + // Three bytes + Assert.assertArrayEquals(new byte[]{(byte) 0x82, (byte) 0x01, 0x00}, + SecurityTokenUtils.encodeLength(256)); + Assert.assertArrayEquals(new byte[]{(byte) 0x82, (byte) 0xFF, (byte) 0xFF}, + SecurityTokenUtils.encodeLength(65535)); + + // Four bytes + Assert.assertArrayEquals(new byte[]{(byte) 0x83, (byte) 0x01, (byte) 0x00, (byte) 0x00}, + SecurityTokenUtils.encodeLength(65536)); + Assert.assertArrayEquals(new byte[]{(byte) 0x83, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}, + SecurityTokenUtils.encodeLength(16777215)); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeLengthNegative() throws Exception { + SecurityTokenUtils.encodeLength(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeLengthTooBig() throws Exception { + SecurityTokenUtils.encodeLength(256 * 256 * 256); + } + + @Test + public void testWriteBits() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + SecurityTokenUtils.writeBits(stream, new BigInteger("0"), 10); + Assert.assertArrayEquals(new byte[10], stream.toByteArray()); + + stream.reset(); + SecurityTokenUtils.writeBits(stream, new BigInteger("0"), 1); + Assert.assertArrayEquals(new byte[1], stream.toByteArray()); + + stream.reset(); + SecurityTokenUtils.writeBits(stream, new BigInteger("65537"), 3); + Assert.assertArrayEquals(new byte[]{1, 0, 1}, stream.toByteArray()); + stream.reset(); + SecurityTokenUtils.writeBits(stream, new BigInteger("128"), 1); + Assert.assertArrayEquals(new byte[]{(byte) 128}, stream.toByteArray()); + + stream.reset(); + SecurityTokenUtils.writeBits(stream, new BigInteger("65537"), 4); + Assert.assertArrayEquals(new byte[]{0, 1, 0, 1}, stream.toByteArray()); + + stream.reset(); + SecurityTokenUtils.writeBits(stream, new BigInteger("65537"), 11); + Assert.assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1}, stream.toByteArray()); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteBitsInvalidValue() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + SecurityTokenUtils.writeBits(stream, new BigInteger("-1"), 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteBitsInvalidBits() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + SecurityTokenUtils.writeBits(stream, new BigInteger("1"), 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteBitsDoesntFit() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + SecurityTokenUtils.writeBits(stream, new BigInteger("65537"), 2); + } + + @Test + public void testPrivateKeyTemplateSimple2048() throws Exception { + KeyFormat format = new KeyFormat(Hex.decode("010000001800")); + byte[] res = SecurityTokenUtils.createPrivKeyTemplate(mKey2048, KeyType.AUTH, format); + + Assert.assertArrayEquals(Hex.decode("4d820115" + // Header TL + "a400" + // CRT + "7f4808" + // 8 bytes + "9103" + // e + "928180" + // p + "938180" + // q + "5f48820103" + + + "010001" + + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + ), res); + } +} From 2591d5dcb3c6d80316b52b82323b4c4a2095e608 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 22 May 2016 23:25:58 +0600 Subject: [PATCH 22/28] Move smartcardio stuff to original javax package --- .../securitytoken => javax}/smartcardio/CommandAPDU.java | 2 +- OpenKeychain/src/main/java/javax/smartcardio/README | 2 ++ .../securitytoken => javax}/smartcardio/ResponseAPDU.java | 2 +- .../keychain/securitytoken/NfcTransport.java | 4 ++-- .../keychain/securitytoken/SecurityTokenHelper.java | 4 ++-- .../sufficientlysecure/keychain/securitytoken/Transport.java | 4 ++-- .../keychain/securitytoken/usb/UsbTransport.java | 4 ++-- 7 files changed, 12 insertions(+), 10 deletions(-) rename OpenKeychain/src/main/java/{org/sufficientlysecure/keychain/securitytoken => javax}/smartcardio/CommandAPDU.java (99%) create mode 100644 OpenKeychain/src/main/java/javax/smartcardio/README rename OpenKeychain/src/main/java/{org/sufficientlysecure/keychain/securitytoken => javax}/smartcardio/ResponseAPDU.java (98%) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java b/OpenKeychain/src/main/java/javax/smartcardio/CommandAPDU.java similarity index 99% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java rename to OpenKeychain/src/main/java/javax/smartcardio/CommandAPDU.java index 1dabbba2c..936d450d0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/CommandAPDU.java +++ b/OpenKeychain/src/main/java/javax/smartcardio/CommandAPDU.java @@ -23,7 +23,7 @@ * questions. */ -package org.sufficientlysecure.keychain.securitytoken.smartcardio; +package javax.smartcardio; import java.util.Arrays; diff --git a/OpenKeychain/src/main/java/javax/smartcardio/README b/OpenKeychain/src/main/java/javax/smartcardio/README new file mode 100644 index 000000000..64cf4a096 --- /dev/null +++ b/OpenKeychain/src/main/java/javax/smartcardio/README @@ -0,0 +1,2 @@ +Copied 'as is' from OpenJDK: +http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/f51368baecd9/src/share/classes/javax/smartcardio \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java b/OpenKeychain/src/main/java/javax/smartcardio/ResponseAPDU.java similarity index 98% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java rename to OpenKeychain/src/main/java/javax/smartcardio/ResponseAPDU.java index 728d8eb67..b135a2e82 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/smartcardio/ResponseAPDU.java +++ b/OpenKeychain/src/main/java/javax/smartcardio/ResponseAPDU.java @@ -23,7 +23,7 @@ * questions. */ -package org.sufficientlysecure.keychain.securitytoken.smartcardio; +package javax.smartcardio; import java.util.Arrays; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java index 61cfd1d24..1485754ef 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java @@ -19,8 +19,8 @@ package org.sufficientlysecure.keychain.securitytoken; import android.nfc.Tag; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; import java.io.IOException; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 71777143d..7ab81d908 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -29,8 +29,8 @@ import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.util.Iso7816TLV; import org.sufficientlysecure.keychain.util.Log; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java index 7c48b0c3a..00fb4db07 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java @@ -17,8 +17,8 @@ package org.sufficientlysecure.keychain.securitytoken; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; import java.io.IOException; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java index d65bbedd3..ed42b9987 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java @@ -29,8 +29,8 @@ import android.util.Pair; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.securitytoken.Transport; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU; -import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU; +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol; import org.sufficientlysecure.keychain.util.Log; From c6ea5a3c65288af8feafae581b424e7a5aaa17c2 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 22 May 2016 23:28:48 +0600 Subject: [PATCH 23/28] Rename OpenPGPCapabilities according to styleguide --- ...enPGPCapabilities.java => OpenPgpCapabilities.java} | 4 ++-- .../keychain/securitytoken/SecurityTokenHelper.java | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{OpenPGPCapabilities.java => OpenPgpCapabilities.java} (97%) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java similarity index 97% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java index 8aa86db02..7a87a0a97 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPGPCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java @@ -23,7 +23,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class OpenPGPCapabilities { +public class OpenPgpCapabilities { private final static int MASK_SM = 1 << 7; private final static int MASK_KEY_IMPORT = 1 << 5; private final static int MASK_ATTRIBUTES_CHANGABLE = 1 << 2; @@ -42,7 +42,7 @@ public class OpenPGPCapabilities { private Map mKeyFormats; - public OpenPGPCapabilities(byte[] data) throws IOException { + public OpenPgpCapabilities(byte[] data) throws IOException { Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); mKeyFormats = new HashMap<>(); if (tlvs.length == 1 && tlvs[0].mT == 0x6E) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 7ab81d908..43ac3866a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -65,7 +65,7 @@ public class SecurityTokenHelper { private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; private Transport mTransport; private CardCapabilities mCardCapabilities; - private OpenPGPCapabilities mOpenPGPCapabilities; + private OpenPgpCapabilities mOpenPgpCapabilities; private Passphrase mPin; private Passphrase mAdminPin; @@ -164,8 +164,8 @@ public class SecurityTokenHelper { throw new CardException("Initialization failed!", response.getSW()); } - mOpenPGPCapabilities = new OpenPGPCapabilities(getData(0x00, 0x6E)); - mCardCapabilities = new CardCapabilities(mOpenPGPCapabilities.getHistoricalBytes()); + mOpenPgpCapabilities = new OpenPgpCapabilities(getData(0x00, 0x6E)); + mCardCapabilities = new CardCapabilities(mOpenPgpCapabilities.getHistoricalBytes()); mPw1ValidatedForSignature = false; mPw1ValidatedForDecrypt = false; @@ -334,7 +334,7 @@ public class SecurityTokenHelper { // Now we're ready to communicate with the token. byte[] bytes = SecurityTokenUtils.createPrivKeyTemplate(crtSecretKey, slot, - mOpenPGPCapabilities.getFormatForKeyType(slot)); + mOpenPgpCapabilities.getFormatForKeyType(slot)); CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, bytes); ResponseAPDU response = communicate(apdu); @@ -469,7 +469,7 @@ public class SecurityTokenHelper { throw new CardException("Failed to sign", response.getSW()); } - if (!mOpenPGPCapabilities.isPw1ValidForMultipleSignatures()) { + if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { mPw1ValidatedForSignature = false; } From e9bad47132e8d02a6346465da74d618f6326cedd Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 23 May 2016 00:12:20 +0600 Subject: [PATCH 24/28] SecurityToken: add comments --- .../keychain/securitytoken/SecurityTokenHelper.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 43ac3866a..cedfc9514 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -484,8 +484,14 @@ public class SecurityTokenHelper { return signature; } + /** - * Transceive data via NFC encoded as Hex + * Transceives APDU + * Splits extended APDU into short APDUs and chains them if necessary + * Performs GET RESPONSE command(ISO/IEC 7816-4 par.7.6.1) on retrieving if necessary + * @param apdu short or extended APDU to transceive + * @return response from the card + * @throws IOException */ private ResponseAPDU communicate(CommandAPDU apdu) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); @@ -525,6 +531,7 @@ public class SecurityTokenHelper { // Receive while (lastResponse.getSW1() == 0x61) { + // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 CommandAPDU getResponse = new CommandAPDU(0x00, 0xC0, 0x00, 0x00, lastResponse.getSW2()); lastResponse = mTransport.transceive(getResponse); result.write(lastResponse.getData()); @@ -621,7 +628,8 @@ public class SecurityTokenHelper { } // reactivate token! - + // NOTE: keep the order here! First execute _both_ reactivate commands. Before checking _both_ responses + // If a token is in a bad state and reactivate1 fails, it could still be reactivated with reactivate2 CommandAPDU reactivate1 = new CommandAPDU(0x00, 0xE6, 0x00, 0x00); CommandAPDU reactivate2 = new CommandAPDU(0x00, 0x44, 0x00, 0x00); ResponseAPDU response1 = communicate(reactivate1); From a4ab303ca99b4dc7d559c202b7c925ada71f2ff1 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Tue, 24 May 2016 22:27:34 +0600 Subject: [PATCH 25/28] Use mockito in SecurityTokenUtilsTest --- .../securitytoken/SecurityTokenUtilsTest.java | 124 +++++------------- 1 file changed, 30 insertions(+), 94 deletions(-) diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java index 4e719e338..f7da7cf6e 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java @@ -22,9 +22,9 @@ import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; @@ -38,81 +38,7 @@ import java.security.interfaces.RSAPrivateCrtKey; @RunWith(RobolectricGradleTestRunner.class) @Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml") -public class SecurityTokenUtilsTest { - private static RSAPrivateCrtKey mKey2048; - - @BeforeClass - public static void setUpOnce() throws Exception { - mKey2048 = new RSAPrivateCrtKey() { - @Override - public BigInteger getCrtCoefficient() { - byte[] bytes = new byte[128]; - Arrays.fill(bytes, (byte) 0x10); - return new BigInteger(bytes); - } - - @Override - public BigInteger getPrimeP() { - byte[] bytes = new byte[128]; - Arrays.fill(bytes, (byte) 0x11); - return new BigInteger(bytes); - } - - @Override - public BigInteger getPrimeQ() { - byte[] bytes = new byte[128]; - Arrays.fill(bytes, (byte) 0x12); - return new BigInteger(bytes); - } - - @Override - public BigInteger getPrimeExponentP() { - byte[] bytes = new byte[128]; - Arrays.fill(bytes, (byte) 0x13); - return new BigInteger(bytes); - } - - @Override - public BigInteger getPrimeExponentQ() { - byte[] bytes = new byte[128]; - Arrays.fill(bytes, (byte) 0x14); - return new BigInteger(bytes); - } - - @Override - public BigInteger getPublicExponent() { - return new BigInteger("10001", 16); - } - - @Override - public BigInteger getPrivateExponent() { - return null; - } - - @Override - public String getAlgorithm() { - return null; - } - - @Override - public String getFormat() { - return null; - } - - @Override - public byte[] getEncoded() { - return new byte[0]; - } - - @Override - public BigInteger getModulus() { - byte[] bytes = new byte[256]; - Arrays.fill(bytes, (byte) 0x17); - return new BigInteger(bytes); - } - }; - } - +public class SecurityTokenUtilsTest extends Mockito { @Before public void setUp() { ShadowLog.stream = System.out; @@ -201,27 +127,37 @@ public class SecurityTokenUtilsTest { @Test public void testPrivateKeyTemplateSimple2048() throws Exception { KeyFormat format = new KeyFormat(Hex.decode("010000001800")); - byte[] res = SecurityTokenUtils.createPrivKeyTemplate(mKey2048, KeyType.AUTH, format); + RSAPrivateCrtKey key2048 = mock(RSAPrivateCrtKey.class); + byte[] tmp = new byte[128]; + Arrays.fill(tmp, (byte) 0x11); + when(key2048.getPrimeP()).thenReturn(new BigInteger(tmp)); - Assert.assertArrayEquals(Hex.decode("4d820115" + // Header TL - "a400" + // CRT - "7f4808" + // 8 bytes - "9103" + // e - "928180" + // p - "938180" + // q - "5f48820103" + + Arrays.fill(tmp, (byte) 0x12); + when(key2048.getPrimeQ()).thenReturn(new BigInteger(tmp)); - "010001" + + when(key2048.getPublicExponent()).thenReturn(new BigInteger("65537")); - "1111111111111111111111111111111111111111111111111111111111111111" + - "1111111111111111111111111111111111111111111111111111111111111111" + - "1111111111111111111111111111111111111111111111111111111111111111" + - "1111111111111111111111111111111111111111111111111111111111111111" + + Assert.assertArrayEquals( + Hex.decode("4d820115" + // Header TL + "a400" + // CRT + "7f4808" + // 8 bytes + "9103" + // e + "928180" + // p + "938180" + // q + "5f48820103" + - "1212121212121212121212121212121212121212121212121212121212121212" + - "1212121212121212121212121212121212121212121212121212121212121212" + - "1212121212121212121212121212121212121212121212121212121212121212" + - "1212121212121212121212121212121212121212121212121212121212121212" - ), res); + "010001" + + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111111111111111111111" + + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + + "1212121212121212121212121212121212121212121212121212121212121212" + ), + SecurityTokenUtils.createPrivKeyTemplate(key2048, KeyType.AUTH, format)); } } From 762b40df008139f5c1ebf992ce98447af470e94a Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Thu, 9 Jun 2016 22:44:47 +0600 Subject: [PATCH 26/28] SecurityToken: improve unit tests --- .../securitytoken/SecurityTokenUtilsTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java index f7da7cf6e..8ec86b02b 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java @@ -29,9 +29,11 @@ import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.WorkaroundBuildConfig; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.util.SecurityTokenUtils; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; import java.security.interfaces.RSAPrivateCrtKey; @@ -160,4 +162,73 @@ public class SecurityTokenUtilsTest extends Mockito { ), SecurityTokenUtils.createPrivKeyTemplate(key2048, KeyType.AUTH, format)); } + + @Test + public void testCardCapabilities() throws UsbTransportException { + CardCapabilities capabilities; + + // Yk neo + capabilities = new CardCapabilities(Hex.decode("007300008000000000000000000000")); + Assert.assertEquals(capabilities.hasChaining(), true); + Assert.assertEquals(capabilities.hasExtended(), false); + + // Yk 4 + capabilities = new CardCapabilities(Hex.decode("0073000080059000")); + Assert.assertEquals(capabilities.hasChaining(), true); + Assert.assertEquals(capabilities.hasExtended(), false); + + // Nitrokey pro + capabilities = new CardCapabilities(Hex.decode("0031c573c00140059000")); + Assert.assertEquals(capabilities.hasChaining(), false); + Assert.assertEquals(capabilities.hasExtended(), true); + } + + @Test + public void testOpenPgpCapabilities() throws IOException { + byte[] data; + + // Yk-neo applet + data = Hex.decode("6e81de4f10d27600012401020000000000000100005f520f0073000080000000" + + "000000000000007381b7c00af00000ff04c000ff00ffc106010800001103c206" + + "010800001103c306010800001103c407007f7f7f030303c53cce1d5a2158a4f1" + + "8a7d853394e9e4c9efb468055fae77ab8ea3c68f053930e35f658fb62176e901" + + "df03d249cac1e82f8289c7ffabe1a1af868620fa56c63c000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000cd0c5741e8695741e8695741e8" + + "69"); + + OpenPgpCapabilities caps = new OpenPgpCapabilities(data); + + Assert.assertEquals(caps.isHasSM(), true); + } + /* + +yk-neo + +6e81de4f10d27600012401020000000000000100005f520f0073000080000000000000000000007381b7c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53cce1d5a2158a4f18a7d853394e9e4c9efb468055fae77ab8ea3c68f053930e35f658fb62176e901df03d249cac1e82f8289c7ffabe1a1af868620fa56c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5741e8695741e8695741e869 + +6e81de4f10d27600012401020000000000000100005f520f0073000080000000000000000000007381b7c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53c1a2f6436e422cd4f37f9e95775195c4984609678fbc5dd767789f1b304c9fba6f68a68ac563f71ae0000000000000000000000000000000000000000c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5744793c5744793c00000000 + + +007300008000000000000000000000 + + +yk neo ng +6e81de4f10d27600012401020000060364703400005f520f0073000080000000000000000000007381b7c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030302c53c7fdb876b9ebc534d674e63b7400207a0f9c34a50413b851137ced1b45d1b66526b4e93a9d5c4eed3585c34e7d38ec07d50f26e0554baa1867a038ed2c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5711219f5711219f5711219f + + + +6e81de4f10d27600012401020000060364703400005f520f0073000080000000000000000000007381b7c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030302c53c7fdb876b9ebc534d674e63b7400207a0f9c34a50413b851137ced1b45d1b66526b4e93a9d5c4eed3585c34e7d38ec07d50f26e0554baa1867a038ed2c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5711219f5711219f5711219f + +6e81dd4f10d27600012401020000060301402200005f520f0073000080000000000000000000007300c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53c98b23401029d2666e129e0b97e2f7e6d1026a77c198dd46ed68a107aada3a267a82e3157a024f7b4be59004e379abaf8fe7abf28deacdd05542a5acac63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5741f0165741f0165741f016 + +yk4 +6e81dd4f10d27600012401020100060417430400005f520800730000800590007f74038101207381b7c00a3c00000004c000ff00ffc106010800001100c206010800001100c306010800001100c407ff7f7f7f030003c53c3a9150768282a1ccf13ee80c4ef0217876ba06001ee6fc5c633b764583cc2d7144b76c0c83f3bdf6671d04d2b6ca89fbbd3940ea7203396c7b1ff1bac63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5741ef475741ef475741ef47 +0073000080059000 + +nitrokey pro + +4f10d2760001240102010005000038a100005f520a0031c573c001400590007381b7c00a7c000800080008000800c106010800002000c206010800002000c306010800002000c40700202020030003c53c5dcf5fc947201ef20636c448131e71ffecf7cbf7d3ed4826257b1340c5aedd2b15530eecf173fa890859306d64641ab01be054c3d6795052cedc3c38c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5741eec65741eec65741eec6 +0031c573c00140059000 + */ } From f79f1609c892f0b53a13bde8361233d3d49e39e6 Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Sun, 14 Aug 2016 22:21:38 +0700 Subject: [PATCH 27/28] Add default GNUK vid:pid to devices list --- OpenKeychain/src/main/res/xml/usb_device_filter.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/res/xml/usb_device_filter.xml b/OpenKeychain/src/main/res/xml/usb_device_filter.xml index 7650105a4..4532b928b 100644 --- a/OpenKeychain/src/main/res/xml/usb_device_filter.xml +++ b/OpenKeychain/src/main/res/xml/usb_device_filter.xml @@ -31,8 +31,9 @@ - + - + + \ No newline at end of file From c12d62f309b2240a9b8b586bc9b5864ce36bccfb Mon Sep 17 00:00:00 2001 From: Nikita Mikhailov Date: Mon, 15 Aug 2016 00:27:07 +0700 Subject: [PATCH 28/28] Apply some CR suggestions --- .../keychain/securitytoken/SecurityTokenHelper.java | 3 ++- .../keychain/securitytoken/usb/CcidTransceiver.java | 4 ++-- OpenKeychain/src/main/res/xml/usb_device_filter.xml | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index cedfc9514..878bf9df3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -56,6 +56,7 @@ public class SecurityTokenHelper { private static final int MAX_APDU_NE_EXT = 65536; private static final int APDU_SW_SUCCESS = 0x9000; + private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61; private static final int MASK_CLA_CHAINING = 1 << 4; @@ -530,7 +531,7 @@ public class SecurityTokenHelper { result.write(lastResponse.getData()); // Receive - while (lastResponse.getSW1() == 0x61) { + while (lastResponse.getSW1() == APDU_SW1_RESPONSE_AVAILABLE) { // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 CommandAPDU getResponse = new CommandAPDU(0x00, 0xC0, 0x00, 0x00, lastResponse.getSW2()); lastResponse = mTransport.transceive(getResponse); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java index acb1ca9f4..bf6711474 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java @@ -152,7 +152,7 @@ public class CcidTransceiver { } } - private byte getStatus(byte[] bytes) { + private static byte getStatus(byte[] bytes) { return (byte) ((bytes[7] >> 6) & 0x03); } @@ -163,7 +163,7 @@ public class CcidTransceiver { } } - private boolean isDataBlockNotReady(byte[] bytes) { + private static boolean isDataBlockNotReady(byte[] bytes) { return getStatus(bytes) == 2; } } diff --git a/OpenKeychain/src/main/res/xml/usb_device_filter.xml b/OpenKeychain/src/main/res/xml/usb_device_filter.xml index 4532b928b..5db692bf6 100644 --- a/OpenKeychain/src/main/res/xml/usb_device_filter.xml +++ b/OpenKeychain/src/main/res/xml/usb_device_filter.xml @@ -32,6 +32,9 @@ + + +