Merge pull request #1868 from nmikhailov/nitrokey
WIP: Security token USB handling improvements
This commit is contained in:
605
OpenKeychain/src/main/java/javax/smartcardio/CommandAPDU.java
Normal file
605
OpenKeychain/src/main/java/javax/smartcardio/CommandAPDU.java
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
/*
|
||||||
|
* 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 javax.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.
|
||||||
|
*
|
||||||
|
* <p>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."
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>For the header bytes CLA, INS, P1, and P2 the Java type <code>int</code>
|
||||||
|
* is used to represent the 8 bit unsigned values. In the constructors, only
|
||||||
|
* the 8 lowest bits of the <code>int</code> value specified by the application
|
||||||
|
* are significant. The accessor methods always return the byte as an unsigned
|
||||||
|
* value between 0 and 255.
|
||||||
|
*
|
||||||
|
* <p>Instances of this class are immutable. Where data is passed in or out
|
||||||
|
* via byte arrays, defensive cloning is performed.
|
||||||
|
*
|
||||||
|
* @see ResponseAPDU
|
||||||
|
*
|
||||||
|
* @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).
|
||||||
|
*
|
||||||
|
* <p>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
|
||||||
|
* <code>apduOffset</code> in the byte array and is <code>apduLength</code>
|
||||||
|
* bytes long.
|
||||||
|
*
|
||||||
|
* <p>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 <code>position</code> must be set to the start of the APDU,
|
||||||
|
* its <code>limit</code> to the end of the APDU. Upon return, the buffer's
|
||||||
|
* <code>position</code> is equal to its limit; its limit remains unchanged.
|
||||||
|
*
|
||||||
|
* <p>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 <code>data</code> is null or
|
||||||
|
* its length is 0, the APDU is encoded as ISO 7816 case 1.
|
||||||
|
*
|
||||||
|
* <p>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 <code>dataLength</code>
|
||||||
|
* is 0, the APDU is encoded as ISO 7816 case 1.
|
||||||
|
*
|
||||||
|
* <p>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 <code>data</code> 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.
|
||||||
|
*
|
||||||
|
* <p>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
|
||||||
|
* <code>dataLength</code>.
|
||||||
|
* If Ne or Nc
|
||||||
|
* are zero, the APDU is encoded as case 1, 2, or 3 per ISO 7816.
|
||||||
|
*
|
||||||
|
* <p>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
|
||||||
|
* <code>getData().length</code>.
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
2
OpenKeychain/src/main/java/javax/smartcardio/README
Normal file
2
OpenKeychain/src/main/java/javax/smartcardio/README
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Copied 'as is' from OpenJDK:
|
||||||
|
http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/f51368baecd9/src/share/classes/javax/smartcardio
|
||||||
184
OpenKeychain/src/main/java/javax/smartcardio/ResponseAPDU.java
Normal file
184
OpenKeychain/src/main/java/javax/smartcardio/ResponseAPDU.java
Normal file
@@ -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 javax.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.
|
||||||
|
*
|
||||||
|
* <p>Instances of this class are immutable. Where data is passed in or out
|
||||||
|
* via byte arrays, defensive cloning is performed.
|
||||||
|
*
|
||||||
|
* @see CommandAPDU
|
||||||
|
*
|
||||||
|
* @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).
|
||||||
|
*
|
||||||
|
* <p>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
|
||||||
|
* <code>getData().length</code>.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <code>(getSW1() << 8) | getSW2()</code>.
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,15 @@ public class CardException extends IOException {
|
|||||||
mResponseCode = responseCode;
|
mResponseCode = responseCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CardException(String detailMessage, int responseCode) {
|
||||||
|
super(detailMessage);
|
||||||
|
mResponseCode = (short) responseCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardException(String s) {
|
||||||
|
this(s, -1);
|
||||||
|
}
|
||||||
|
|
||||||
public short getResponseCode() {
|
public short getResponseCode() {
|
||||||
return mResponseCode;
|
return mResponseCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public enum KeyType {
|
|||||||
return mIdx;
|
return mIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getmSlot() {
|
public int getSlot() {
|
||||||
return mSlot;
|
return mSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ public enum KeyType {
|
|||||||
return mTimestampObjectId;
|
return mTimestampObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getmFingerprintObjectId() {
|
public int getFingerprintObjectId() {
|
||||||
return mFingerprintObjectId;
|
return mFingerprintObjectId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package org.sufficientlysecure.keychain.securitytoken;
|
|||||||
|
|
||||||
import android.nfc.Tag;
|
import android.nfc.Tag;
|
||||||
|
|
||||||
|
import javax.smartcardio.CommandAPDU;
|
||||||
|
import javax.smartcardio.ResponseAPDU;
|
||||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -43,8 +45,8 @@ public class NfcTransport implements Transport {
|
|||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public byte[] transceive(final byte[] data) throws IOException {
|
public ResponseAPDU transceive(final CommandAPDU data) throws IOException {
|
||||||
return mIsoCard.transceive(data);
|
return new ResponseAPDU(mIsoCard.transceive(data.getBytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<KeyType, KeyFormat> mKeyFormats;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
case 0x4F:
|
||||||
|
mAid = tlv.mV;
|
||||||
|
break;
|
||||||
|
case 0x5F52:
|
||||||
|
mHistoricalBytes = tlv.mV;
|
||||||
|
break;
|
||||||
|
case 0x73:
|
||||||
|
parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv);
|
||||||
|
break;
|
||||||
|
case 0xC0:
|
||||||
|
parseExtendedCaps(tlv.mV);
|
||||||
|
break;
|
||||||
|
case 0xC1:
|
||||||
|
mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC2:
|
||||||
|
mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC3:
|
||||||
|
mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC4:
|
||||||
|
mPw1ValidForMultipleSignatures = tlv.mV[0] == 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseDdo(Iso7816TLV.Iso7816CompositeTLV tlvs) {
|
||||||
|
for (Iso7816TLV tlv : tlvs.mSubs) {
|
||||||
|
switch (tlv.mT) {
|
||||||
|
case 0xC0:
|
||||||
|
parseExtendedCaps(tlv.mV);
|
||||||
|
break;
|
||||||
|
case 0xC1:
|
||||||
|
mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC2:
|
||||||
|
mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC3:
|
||||||
|
mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV));
|
||||||
|
break;
|
||||||
|
case 0xC4:
|
||||||
|
mPw1ValidForMultipleSignatures = tlv.mV[0] == 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 KeyFormat getFormatForKeyType(KeyType keyType) {
|
||||||
|
return mKeyFormats.get(keyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.securitytoken;
|
package org.sufficientlysecure.keychain.securitytoken;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
@@ -30,33 +29,47 @@ import org.bouncycastle.util.encoders.Hex;
|
|||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||||
|
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.Iso7816TLV;
|
||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||||
|
import org.sufficientlysecure.keychain.util.SecurityTokenUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.interfaces.RSAPrivateCrtKey;
|
import java.security.interfaces.RSAPrivateCrtKey;
|
||||||
|
|
||||||
import nordpol.Apdu;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
|
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
|
||||||
* devices.
|
* devices.
|
||||||
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
|
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
|
||||||
*/
|
*/
|
||||||
public class SecurityTokenHelper {
|
public class SecurityTokenHelper {
|
||||||
private static final int MAX_APDU_DATAFIELD_SIZE = 254;
|
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;
|
||||||
|
private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61;
|
||||||
|
|
||||||
|
private static final int MASK_CLA_CHAINING = 1 << 4;
|
||||||
|
|
||||||
// Fidesmo constants
|
// Fidesmo constants
|
||||||
private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
|
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 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 Transport mTransport;
|
||||||
|
private CardCapabilities mCardCapabilities;
|
||||||
|
private OpenPgpCapabilities mOpenPgpCapabilities;
|
||||||
|
|
||||||
private Passphrase mPin;
|
private Passphrase mPin;
|
||||||
private Passphrase mAdminPin;
|
private Passphrase mAdminPin;
|
||||||
private boolean mPw1ValidForMultipleSignatures;
|
|
||||||
private boolean mPw1ValidatedForSignature;
|
private boolean mPw1ValidatedForSignature;
|
||||||
private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
|
private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
|
||||||
private boolean mPw3Validated;
|
private boolean mPw3Validated;
|
||||||
@@ -68,20 +81,9 @@ public class SecurityTokenHelper {
|
|||||||
return LazyHolder.SECURITY_TOKEN_HELPER;
|
return LazyHolder.SECURITY_TOKEN_HELPER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getHex(byte[] raw) {
|
private String getHolderName(byte[] name) {
|
||||||
return new String(Hex.encode(raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getHolderName(String name) {
|
|
||||||
try {
|
try {
|
||||||
String slength;
|
return (new String(name, 4, name[3])).replace('<', ' ');
|
||||||
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;
|
|
||||||
} catch (IndexOutOfBoundsException e) {
|
} catch (IndexOutOfBoundsException e) {
|
||||||
// try-catch for https://github.com/FluffyKaon/OpenPGP-Card
|
// try-catch for https://github.com/FluffyKaon/OpenPGP-Card
|
||||||
// Note: This should not happen, but happens with
|
// Note: This should not happen, but happens with
|
||||||
@@ -126,8 +128,8 @@ public class SecurityTokenHelper {
|
|||||||
keyType.toString()));
|
keyType.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
putKey(keyType.getmSlot(), secretKey, passphrase);
|
putKey(keyType, secretKey, passphrase);
|
||||||
putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint());
|
putData(keyType.getFingerprintObjectId(), secretKey.getFingerprint());
|
||||||
putData(keyType.getTimestampObjectId(), timestampBytes);
|
putData(keyType.getTimestampObjectId(), timestampBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,53 +152,27 @@ public class SecurityTokenHelper {
|
|||||||
*/
|
*/
|
||||||
public void connectToDevice() throws IOException {
|
public void connectToDevice() throws IOException {
|
||||||
// Connect on transport layer
|
// Connect on transport layer
|
||||||
|
mCardCapabilities = new CardCapabilities();
|
||||||
|
|
||||||
mTransport.connect();
|
mTransport.connect();
|
||||||
|
|
||||||
// Connect on smartcard layer
|
// 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)
|
// Command APDU (page 51) for SELECT FILE command (page 29)
|
||||||
String opening =
|
CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401"));
|
||||||
"00" // CLA
|
ResponseAPDU response = communicate(select); // activate connection
|
||||||
+ "A4" // INS
|
|
||||||
+ "04" // P1
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
+ "00" // P2
|
throw new CardException("Initialization failed!", response.getSW());
|
||||||
+ "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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] pwStatusBytes = getPwStatusBytes();
|
mOpenPgpCapabilities = new OpenPgpCapabilities(getData(0x00, 0x6E));
|
||||||
mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
|
mCardCapabilities = new CardCapabilities(mOpenPgpCapabilities.getHistoricalBytes());
|
||||||
|
|
||||||
mPw1ValidatedForSignature = false;
|
mPw1ValidatedForSignature = false;
|
||||||
mPw1ValidatedForDecrypt = false;
|
mPw1ValidatedForDecrypt = false;
|
||||||
mPw3Validated = false;
|
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
|
* 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.
|
* conformance to the token's requirements for key length.
|
||||||
@@ -230,16 +206,11 @@ public class SecurityTokenHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Command APDU for CHANGE REFERENCE DATA command (page 32)
|
// Command APDU for CHANGE REFERENCE DATA command (page 32)
|
||||||
String changeReferenceDataApdu = "00" // CLA
|
CommandAPDU changePin = new CommandAPDU(0x00, 0x24, 0x00, pw, Arrays.concatenate(pin, newPin));
|
||||||
+ "24" // INS
|
ResponseAPDU response = communicate(changePin);
|
||||||
+ "00" // P1
|
|
||||||
+ String.format("%02x", pw) // P2
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
+ String.format("%02x", pin.length + newPin.length) // Lc
|
throw new CardException("Failed to change PIN", response.getSW());
|
||||||
+ getHex(pin)
|
|
||||||
+ getHex(newPin);
|
|
||||||
String response = communicate(changeReferenceDataApdu); // change PIN
|
|
||||||
if (!response.equals("9000")) {
|
|
||||||
throw new CardException("Failed to change PIN", parseCardStatus(response));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,38 +225,20 @@ public class SecurityTokenHelper {
|
|||||||
verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
|
verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
|
||||||
}
|
}
|
||||||
|
|
||||||
int offset = 1; // Skip first byte
|
|
||||||
String response = "", status = "";
|
|
||||||
|
|
||||||
// Transmit
|
// Transmit
|
||||||
while (offset < encryptedSessionKey.length) {
|
byte[] data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length);
|
||||||
boolean isLastCommand = offset + MAX_APDU_DATAFIELD_SIZE < encryptedSessionKey.length;
|
if (data[0] != 0) {
|
||||||
String cla = isLastCommand ? "10" : "00";
|
data = Arrays.prepend(data, (byte) 0x00);
|
||||||
|
|
||||||
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));
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receive
|
CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x80, 0x86, data, MAX_APDU_NE_EXT);
|
||||||
String result = getDataField(response);
|
ResponseAPDU response = communicate(command);
|
||||||
while (response.endsWith("61")) {
|
|
||||||
response = communicate("00C00000" + status.substring(2));
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
status = response.substring(response.length() - 4);
|
throw new CardException("Deciphering with Security token failed on receive", response.getSW());
|
||||||
result += getDataField(response);
|
|
||||||
}
|
|
||||||
if (!status.equals("9000")) {
|
|
||||||
throw new CardException("Deciphering with Security token failed on receive", parseCardStatus(response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Hex.decode(result);
|
return response.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,12 +257,9 @@ public class SecurityTokenHelper {
|
|||||||
pin = mPin.toStringUnsafe().getBytes();
|
pin = mPin.toStringUnsafe().getBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
|
ResponseAPDU response = tryPin(mode, pin);// login
|
||||||
// See specification, page 51
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
String accepted = "9000";
|
throw new CardException("Bad PIN!", response.getSW());
|
||||||
String response = tryPin(mode, pin); // login
|
|
||||||
if (!response.equals(accepted)) {
|
|
||||||
throw new CardException("Bad PIN!", parseCardStatus(response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == 0x81) {
|
if (mode == 0x81) {
|
||||||
@@ -342,16 +292,11 @@ public class SecurityTokenHelper {
|
|||||||
verifyPin(0x83); // (Verify PW3)
|
verifyPin(0x83); // (Verify PW3)
|
||||||
}
|
}
|
||||||
|
|
||||||
String putDataApdu = "00" // CLA
|
CommandAPDU command = new CommandAPDU(0x00, 0xDA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data);
|
||||||
+ "DA" // INS
|
ResponseAPDU response = communicate(command); // put data
|
||||||
+ String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
|
|
||||||
+ String.format("%02x", dataObject & 0xFF) // P2
|
|
||||||
+ String.format("%02x", data.length) // Lc
|
|
||||||
+ getHex(data);
|
|
||||||
|
|
||||||
String response = communicate(putDataApdu); // put data
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
if (!response.equals("9000")) {
|
throw new CardException("Failed to put data.", response.getSW());
|
||||||
throw new CardException("Failed to put data.", parseCardStatus(response));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,12 +308,8 @@ public class SecurityTokenHelper {
|
|||||||
* 0xB8: Decipherment Key
|
* 0xB8: Decipherment Key
|
||||||
* 0xA4: Authentication Key
|
* 0xA4: Authentication Key
|
||||||
*/
|
*/
|
||||||
private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
|
private void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
|
|
||||||
throw new IOException("Invalid key slot");
|
|
||||||
}
|
|
||||||
|
|
||||||
RSAPrivateCrtKey crtSecretKey;
|
RSAPrivateCrtKey crtSecretKey;
|
||||||
try {
|
try {
|
||||||
secretKey.unlock(passphrase);
|
secretKey.unlock(passphrase);
|
||||||
@@ -391,80 +332,17 @@ public class SecurityTokenHelper {
|
|||||||
verifyPin(0x83); // (Verify PW3 with mode 83)
|
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
String putKeyCommand = "10DB3FFF";
|
|
||||||
String lastPutKeyCommand = "00DB3FFF";
|
|
||||||
|
|
||||||
// Now we're ready to communicate with the token.
|
// Now we're ready to communicate with the token.
|
||||||
offset = 0;
|
byte[] bytes = SecurityTokenUtils.createPrivKeyTemplate(crtSecretKey, slot,
|
||||||
String response;
|
mOpenPgpCapabilities.getFormatForKeyType(slot));
|
||||||
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")) {
|
CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, bytes);
|
||||||
throw new CardException("Key export to Security Token failed", parseCardStatus(response));
|
ResponseAPDU response = communicate(apdu);
|
||||||
}
|
|
||||||
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
|
throw new CardException("Key export to Security Token failed", response.getSW());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear array with secret data before we return.
|
|
||||||
Arrays.fill(dataToSend, (byte) 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -474,17 +352,29 @@ public class SecurityTokenHelper {
|
|||||||
* @return The fingerprints of all subkeys in a contiguous byte array.
|
* @return The fingerprints of all subkeys in a contiguous byte array.
|
||||||
*/
|
*/
|
||||||
public byte[] getFingerprints() throws IOException {
|
public byte[] getFingerprints() throws IOException {
|
||||||
String data = "00CA006E00";
|
CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E, MAX_APDU_NE_EXT);
|
||||||
byte[] buf = mTransport.transceive(Hex.decode(data));
|
ResponseAPDU response = communicate(apdu);
|
||||||
|
|
||||||
Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
|
throw new CardException("Failed to get fingerprints", response.getSW());
|
||||||
|
}
|
||||||
|
|
||||||
Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
|
Iso7816TLV[] tlvList = Iso7816TLV.readList(response.getData(), true);
|
||||||
if (fptlv == null) {
|
Iso7816TLV fingerPrintTlv = null;
|
||||||
|
|
||||||
|
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 (fingerPrintTlv == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return fptlv.mV;
|
return fingerPrintTlv.mV;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -493,18 +383,23 @@ public class SecurityTokenHelper {
|
|||||||
* @return Seven bytes in fixed format, plus 0x9000 status word at the end.
|
* @return Seven bytes in fixed format, plus 0x9000 status word at the end.
|
||||||
*/
|
*/
|
||||||
private byte[] getPwStatusBytes() throws IOException {
|
private byte[] getPwStatusBytes() throws IOException {
|
||||||
String data = "00CA00C400";
|
return getData(0x00, 0xC4);
|
||||||
return mTransport.transceive(Hex.decode(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getAid() throws IOException {
|
public byte[] getAid() throws IOException {
|
||||||
String info = "00CA004F00";
|
return getData(0x00, 0x4F);
|
||||||
return mTransport.transceive(Hex.decode(info));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserId() throws IOException {
|
public String getUserId() throws IOException {
|
||||||
String info = "00CA006500";
|
return getHolderName(getData(0x00, 0x65));
|
||||||
return getHolderName(communicate(info));
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new CardException("Failed to get pw status bytes", response.getSW());
|
||||||
|
}
|
||||||
|
return response.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -518,8 +413,7 @@ public class SecurityTokenHelper {
|
|||||||
verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
|
verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dsi, including Lc
|
byte[] dsi;
|
||||||
String dsi;
|
|
||||||
|
|
||||||
Log.i(Constants.TAG, "Hash: " + hashAlgo);
|
Log.i(Constants.TAG, "Hash: " + hashAlgo);
|
||||||
switch (hashAlgo) {
|
switch (hashAlgo) {
|
||||||
@@ -527,95 +421,127 @@ public class SecurityTokenHelper {
|
|||||||
if (hash.length != 20) {
|
if (hash.length != 20) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
|
||||||
}
|
}
|
||||||
dsi = "23" // Lc
|
dsi = Arrays.concatenate(Hex.decode(
|
||||||
+ "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
|
"3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
|
||||||
+ "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
|
+ "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
|
||||||
+ "0605" + "2B0E03021A" // OID of SHA1
|
+ "0605" + "2B0E03021A" // OID of SHA1
|
||||||
+ "0500" // TLV coding of ZERO
|
+ "0500" // TLV coding of ZERO
|
||||||
+ "0414" + getHex(hash); // 0x14 are 20 hash bytes
|
+ "0414"), hash); // 0x14 are 20 hash bytes
|
||||||
break;
|
break;
|
||||||
case HashAlgorithmTags.RIPEMD160:
|
case HashAlgorithmTags.RIPEMD160:
|
||||||
if (hash.length != 20) {
|
if (hash.length != 20) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
|
||||||
}
|
}
|
||||||
dsi = "233021300906052B2403020105000414" + getHex(hash);
|
dsi = Arrays.concatenate(Hex.decode("3021300906052B2403020105000414"), hash);
|
||||||
break;
|
break;
|
||||||
case HashAlgorithmTags.SHA224:
|
case HashAlgorithmTags.SHA224:
|
||||||
if (hash.length != 28) {
|
if (hash.length != 28) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
|
||||||
}
|
}
|
||||||
dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
|
dsi = Arrays.concatenate(Hex.decode("302D300D06096086480165030402040500041C"), hash);
|
||||||
break;
|
break;
|
||||||
case HashAlgorithmTags.SHA256:
|
case HashAlgorithmTags.SHA256:
|
||||||
if (hash.length != 32) {
|
if (hash.length != 32) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
|
||||||
}
|
}
|
||||||
dsi = "333031300D060960864801650304020105000420" + getHex(hash);
|
dsi = Arrays.concatenate(Hex.decode("3031300D060960864801650304020105000420"), hash);
|
||||||
break;
|
break;
|
||||||
case HashAlgorithmTags.SHA384:
|
case HashAlgorithmTags.SHA384:
|
||||||
if (hash.length != 48) {
|
if (hash.length != 48) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
|
||||||
}
|
}
|
||||||
dsi = "433041300D060960864801650304020205000430" + getHex(hash);
|
dsi = Arrays.concatenate(Hex.decode("3041300D060960864801650304020205000430"), hash);
|
||||||
break;
|
break;
|
||||||
case HashAlgorithmTags.SHA512:
|
case HashAlgorithmTags.SHA512:
|
||||||
if (hash.length != 64) {
|
if (hash.length != 64) {
|
||||||
throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
|
throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
|
||||||
}
|
}
|
||||||
dsi = "533051300D060960864801650304020305000440" + getHex(hash);
|
dsi = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IOException("Not supported hash algo!");
|
throw new IOException("Not supported hash algo!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
|
// Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
|
||||||
String apdu =
|
CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, dsi, MAX_APDU_NE_EXT);
|
||||||
"002A9E9A" // CLA, INS, P1, P2
|
ResponseAPDU response = communicate(command);
|
||||||
+ dsi // digital signature input
|
|
||||||
+ "00"; // Le
|
|
||||||
|
|
||||||
String response = communicate(apdu);
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
|
throw new CardException("Failed to sign", response.getSW());
|
||||||
if (response.length() < 4) {
|
|
||||||
throw new CardException("Bad response", (short) 0);
|
|
||||||
}
|
|
||||||
// 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 (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) {
|
||||||
|
|
||||||
if (!mPw1ValidForMultipleSignatures) {
|
|
||||||
mPw1ValidatedForSignature = false;
|
mPw1ValidatedForSignature = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"9000".equals(status)) {
|
byte[] signature = response.getData();
|
||||||
throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the signature we received is actually the expected number of bytes long!
|
// Make sure the signature we received is actually the expected number of bytes long!
|
||||||
if (signature.length() != 256 && signature.length() != 512
|
if (signature.length != 128 && signature.length != 256
|
||||||
&& signature.length() != 768 && signature.length() != 1024) {
|
&& signature.length != 384 && signature.length != 512) {
|
||||||
throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length() / 2);
|
throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Hex.decode(signature);
|
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 String communicate(String apdu) throws IOException {
|
private ResponseAPDU communicate(CommandAPDU apdu) throws IOException {
|
||||||
return getHex(mTransport.transceive(Hex.decode(apdu)));
|
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
ResponseAPDU lastResponse = null;
|
||||||
|
// Transmit
|
||||||
|
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(), 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(), 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) {
|
||||||
|
throw new UsbTransportException("Can't transmit command");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.write(lastResponse.getData());
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
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);
|
||||||
|
result.write(lastResponse.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.write(lastResponse.getSW1());
|
||||||
|
result.write(lastResponse.getSW2());
|
||||||
|
|
||||||
|
return new ResponseAPDU(result.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transport getTransport() {
|
public Transport getTransport() {
|
||||||
@@ -631,9 +557,8 @@ public class SecurityTokenHelper {
|
|||||||
try {
|
try {
|
||||||
// By trying to select any apps that have the Fidesmo AID prefix we can
|
// By trying to select any apps that have the Fidesmo AID prefix we can
|
||||||
// see if it is a Fidesmo device or not
|
// see if it is a Fidesmo device or not
|
||||||
byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
|
CommandAPDU apdu = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode(FIDESMO_APPS_AID_PREFIX));
|
||||||
// Compare the status returned by our select with the OK status code
|
return communicate(apdu).getSW() == APDU_SW_SUCCESS;
|
||||||
return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(Constants.TAG, "Card communication failed!", e);
|
Log.e(Constants.TAG, "Card communication failed!", e);
|
||||||
}
|
}
|
||||||
@@ -664,38 +589,19 @@ public class SecurityTokenHelper {
|
|||||||
verifyPin(0x83); // (Verify PW3 with mode 83)
|
verifyPin(0x83); // (Verify PW3 with mode 83)
|
||||||
}
|
}
|
||||||
|
|
||||||
String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
|
CommandAPDU apdu = new CommandAPDU(0x00, 0x47, 0x80, 0x00, new byte[]{(byte) slot, 0x00}, MAX_APDU_NE_EXT);
|
||||||
String getResponseApdu = "00C00000";
|
ResponseAPDU response = communicate(apdu);
|
||||||
|
|
||||||
String first = communicate(generateKeyApdu);
|
if (response.getSW() != APDU_SW_SUCCESS) {
|
||||||
String second = communicate(getResponseApdu);
|
|
||||||
|
|
||||||
if (!second.endsWith("9000")) {
|
|
||||||
throw new IOException("On-card key generation failed");
|
throw new IOException("On-card key generation failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
String publicKeyData = getDataField(first) + getDataField(second);
|
return response.getData();
|
||||||
|
|
||||||
Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
|
|
||||||
|
|
||||||
return Hex.decode(publicKeyData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getDataField(String output) {
|
private ResponseAPDU tryPin(int mode, byte[] pin) throws IOException {
|
||||||
return output.substring(0, output.length() - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String tryPin(int mode, byte[] pin) throws IOException {
|
|
||||||
// Command APDU for VERIFY command (page 32)
|
// Command APDU for VERIFY command (page 32)
|
||||||
String login =
|
return communicate(new CommandAPDU(0x00, 0x20, 0x00, mode, pin));
|
||||||
"00" // CLA
|
|
||||||
+ "20" // INS
|
|
||||||
+ "00" // P1
|
|
||||||
+ String.format("%02x", mode) // P2
|
|
||||||
+ String.format("%02x", pin.length) // Lc
|
|
||||||
+ Hex.toHexString(pin);
|
|
||||||
|
|
||||||
return communicate(login);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -704,35 +610,37 @@ public class SecurityTokenHelper {
|
|||||||
* Afterwards, the token is reactivated.
|
* Afterwards, the token is reactivated.
|
||||||
*/
|
*/
|
||||||
public void resetAndWipeToken() throws IOException {
|
public void resetAndWipeToken() throws IOException {
|
||||||
String accepted = "9000";
|
|
||||||
|
|
||||||
// try wrong PIN 4 times until counter goes to C0
|
// try wrong PIN 4 times until counter goes to C0
|
||||||
byte[] pin = "XXXXXX".getBytes();
|
byte[] pin = "XXXXXX".getBytes();
|
||||||
for (int i = 0; i <= 4; i++) {
|
for (int i = 0; i <= 4; i++) {
|
||||||
String response = tryPin(0x81, pin);
|
ResponseAPDU response = tryPin(0x81, pin);
|
||||||
if (response.equals(accepted)) { // Should NOT accept!
|
if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept!
|
||||||
throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
|
throw new CardException("Should never happen, XXXXXX has been accepted!", response.getSW());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try wrong Admin PIN 4 times until counter goes to C0
|
// try wrong Admin PIN 4 times until counter goes to C0
|
||||||
byte[] adminPin = "XXXXXXXX".getBytes();
|
byte[] adminPin = "XXXXXXXX".getBytes();
|
||||||
for (int i = 0; i <= 4; i++) {
|
for (int i = 0; i <= 4; i++) {
|
||||||
String response = tryPin(0x83, adminPin);
|
ResponseAPDU response = tryPin(0x83, adminPin);
|
||||||
if (response.equals(accepted)) { // Should NOT accept!
|
if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept!
|
||||||
throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
|
throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSW());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reactivate token!
|
// reactivate token!
|
||||||
String reactivate1 = "00" + "e6" + "00" + "00";
|
// NOTE: keep the order here! First execute _both_ reactivate commands. Before checking _both_ responses
|
||||||
String reactivate2 = "00" + "44" + "00" + "00";
|
// If a token is in a bad state and reactivate1 fails, it could still be reactivated with reactivate2
|
||||||
String response1 = communicate(reactivate1);
|
CommandAPDU reactivate1 = new CommandAPDU(0x00, 0xE6, 0x00, 0x00);
|
||||||
String response2 = communicate(reactivate2);
|
CommandAPDU reactivate2 = new CommandAPDU(0x00, 0x44, 0x00, 0x00);
|
||||||
if (!response1.equals(accepted) || !response2.equals(accepted)) {
|
ResponseAPDU response1 = communicate(reactivate1);
|
||||||
throw new CardException("Reactivating failed!", parseCardStatus(response1));
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
|
|
||||||
package org.sufficientlysecure.keychain.securitytoken;
|
package org.sufficientlysecure.keychain.securitytoken;
|
||||||
|
|
||||||
|
import javax.smartcardio.CommandAPDU;
|
||||||
|
import javax.smartcardio.ResponseAPDU;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +32,7 @@ public interface Transport {
|
|||||||
* @return received data
|
* @return received data
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
byte[] transceive(byte[] data) throws IOException;
|
ResponseAPDU transceive(CommandAPDU data) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect and release connection
|
* Disconnect and release connection
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 (Exception 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 static 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 static boolean isDataBlockNotReady(byte[] bytes) {
|
||||||
|
return getStatus(bytes) == 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.securitytoken.usb;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface CcidTransportProtocol {
|
||||||
|
byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.securitytoken;
|
package org.sufficientlysecure.keychain.securitytoken.usb;
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
import android.hardware.usb.UsbConstants;
|
||||||
import android.hardware.usb.UsbDevice;
|
import android.hardware.usb.UsbDevice;
|
||||||
@@ -27,9 +27,11 @@ import android.support.annotation.NonNull;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import org.bouncycastle.util.Arrays;
|
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
|
import org.sufficientlysecure.keychain.securitytoken.Transport;
|
||||||
|
import javax.smartcardio.CommandAPDU;
|
||||||
|
import javax.smartcardio.ResponseAPDU;
|
||||||
|
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol;
|
||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -42,8 +44,15 @@ import java.nio.ByteOrder;
|
|||||||
* Implements small subset of these features
|
* Implements small subset of these features
|
||||||
*/
|
*/
|
||||||
public class UsbTransport implements Transport {
|
public class UsbTransport implements Transport {
|
||||||
private static final int USB_CLASS_SMARTCARD = 11;
|
private static final int PROTOCOLS_OFFSET = 6;
|
||||||
private static final int TIMEOUT = 20 * 1000; // 20s
|
private static final int FEATURES_OFFSET = 40;
|
||||||
|
private static final short 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 UsbManager mUsbManager;
|
||||||
private final UsbDevice mUsbDevice;
|
private final UsbDevice mUsbDevice;
|
||||||
@@ -51,39 +60,14 @@ public class UsbTransport implements Transport {
|
|||||||
private UsbEndpoint mBulkIn;
|
private UsbEndpoint mBulkIn;
|
||||||
private UsbEndpoint mBulkOut;
|
private UsbEndpoint mBulkOut;
|
||||||
private UsbDeviceConnection mConnection;
|
private UsbDeviceConnection mConnection;
|
||||||
private byte mCounter;
|
private CcidTransceiver mTransceiver;
|
||||||
|
private CcidTransportProtocol mProtocol;
|
||||||
|
|
||||||
public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
|
public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
|
||||||
mUsbDevice = usbDevice;
|
mUsbDevice = usbDevice;
|
||||||
mUsbManager = usbManager;
|
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);
|
|
||||||
byte[] bytes;
|
|
||||||
do {
|
|
||||||
bytes = receive();
|
|
||||||
} while (isDataBlockNotReady(bytes));
|
|
||||||
checkDataBlockResponse(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get first class 11 (Chip/Smartcard) interface of the device
|
* Get first class 11 (Chip/Smartcard) interface of the device
|
||||||
*
|
*
|
||||||
@@ -94,7 +78,7 @@ public class UsbTransport implements Transport {
|
|||||||
private static UsbInterface getSmartCardInterface(UsbDevice device) {
|
private static UsbInterface getSmartCardInterface(UsbDevice device) {
|
||||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||||
UsbInterface anInterface = device.getInterface(i);
|
UsbInterface anInterface = device.getInterface(i);
|
||||||
if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) {
|
if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CSCID) {
|
||||||
return anInterface;
|
return anInterface;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +149,6 @@ public class UsbTransport implements Transport {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void connect() throws IOException {
|
public void connect() throws IOException {
|
||||||
mCounter = 0;
|
|
||||||
mUsbInterface = getSmartCardInterface(mUsbDevice);
|
mUsbInterface = getSmartCardInterface(mUsbDevice);
|
||||||
if (mUsbInterface == null) {
|
if (mUsbInterface == null) {
|
||||||
// Shouldn't happen as we whitelist only class 11 devices
|
// Shouldn't happen as we whitelist only class 11 devices
|
||||||
@@ -189,8 +172,56 @@ public class UsbTransport implements Transport {
|
|||||||
throw new UsbTransportException("USB error - failed to claim interface");
|
throw new UsbTransportException("USB error - failed to claim interface");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIccPower(true);
|
mTransceiver = new CcidTransceiver(mConnection, mBulkIn, mBulkOut);
|
||||||
Log.d(Constants.TAG, "Usb transport connected");
|
|
||||||
|
|
||||||
|
|
||||||
|
configureProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureProtocol() throws UsbTransportException {
|
||||||
|
byte[] desc = mConnection.getRawDescriptors();
|
||||||
|
int dwProtocols = 0, dwFeatures = 0;
|
||||||
|
boolean hasCcidDescriptor = false;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,87 +231,8 @@ public class UsbTransport implements Transport {
|
|||||||
* @throws UsbTransportException
|
* @throws UsbTransportException
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public byte[] transceive(byte[] data) throws UsbTransportException {
|
public ResponseAPDU transceive(CommandAPDU data) throws UsbTransportException {
|
||||||
sendXfrBlock(data);
|
return new ResponseAPDU(mProtocol.transceive(data.getBytes()));
|
||||||
byte[] bytes;
|
|
||||||
do {
|
|
||||||
bytes = receive();
|
|
||||||
} while (isDataBlockNotReady(bytes));
|
|
||||||
|
|
||||||
checkDataBlockResponse(bytes);
|
|
||||||
// Discard header
|
|
||||||
return Arrays.copyOfRange(bytes, 10, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transmits XfrBlock
|
|
||||||
* 6.1.4 PC_to_RDR_XfrBlock
|
|
||||||
* @param payload payload to transmit
|
|
||||||
* @throws UsbTransportException
|
|
||||||
*/
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.securitytoken;
|
package org.sufficientlysecure.keychain.securitytoken.usb;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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(), mData.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.securitytoken.usb.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
|
||||||
|
|
||||||
|
public class RBlock extends Block {
|
||||||
|
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);
|
||||||
|
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) (MASK_VALUE_RBLOCK | ((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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.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((byte) (((IBlock) responseBlock).getSequence() + 1));
|
||||||
|
mTransceiver.sendXfrBlock(ackBlock.getRawData());
|
||||||
|
|
||||||
|
responseBlock = getBlockFromResponse(mTransceiver.receiveRaw());
|
||||||
|
|
||||||
|
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() & IBlock.MASK_RBLOCK) == IBlock.MASK_VALUE_RBLOCK) {
|
||||||
|
return new IBlock(baseBlock);
|
||||||
|
} else if ((baseBlock.getPcb() & SBlock.MASK_SBLOCK) == SBlock.MASK_VALUE_SBLOCK) {
|
||||||
|
return new SBlock(baseBlock);
|
||||||
|
} else if ((baseBlock.getPcb() & RBlock.MASK_RBLOCK) == RBlock.MASK_VALUE_RBLOCK) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
|
|||||||
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
|
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
|
||||||
import org.sufficientlysecure.keychain.securitytoken.Transport;
|
import org.sufficientlysecure.keychain.securitytoken.Transport;
|
||||||
import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
|
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.CreateKeyActivity;
|
||||||
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
|
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
|
||||||
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
|
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Based on https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
|
Based on
|
||||||
|
https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
|
||||||
|
https://www.nitrokey.com/de/documentation/installation#p:nitrokey-pro&os:linux
|
||||||
|
|
||||||
Note that values are decimal.
|
Note that values are decimal.
|
||||||
-->
|
-->
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
@@ -24,4 +27,16 @@
|
|||||||
<!-- Yubikey 4 OTP + U2F + CCID-->
|
<!-- Yubikey 4 OTP + U2F + CCID-->
|
||||||
<usb-device class="11" vendor-id="4176" product-id="1031"/>
|
<usb-device class="11" vendor-id="4176" product-id="1031"/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Nitrokey Pro-->
|
||||||
|
<usb-device class="11" vendor-id="8352" product-id="16648"/>
|
||||||
|
<!-- Nitrokey Storage-->
|
||||||
|
<usb-device class="11" vendor-id="8352" product-id="16649"/>
|
||||||
|
|
||||||
|
<!--GNUK based device are not supported right now-->
|
||||||
|
|
||||||
|
<!-- Nitrokey Start-->
|
||||||
|
<!--<usb-device class="11" vendor-id="8352" product-id="16913"/>-->
|
||||||
|
<!-- Default GNUK vid/pid-->
|
||||||
|
<!--<usb-device class="11" vendor-id="9035" product-id="0"/>-->
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
@RunWith(RobolectricGradleTestRunner.class)
|
||||||
|
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||||
|
public class SecurityTokenUtilsTest extends Mockito {
|
||||||
|
@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"));
|
||||||
|
RSAPrivateCrtKey key2048 = mock(RSAPrivateCrtKey.class);
|
||||||
|
byte[] tmp = new byte[128];
|
||||||
|
Arrays.fill(tmp, (byte) 0x11);
|
||||||
|
when(key2048.getPrimeP()).thenReturn(new BigInteger(tmp));
|
||||||
|
|
||||||
|
Arrays.fill(tmp, (byte) 0x12);
|
||||||
|
when(key2048.getPrimeQ()).thenReturn(new BigInteger(tmp));
|
||||||
|
|
||||||
|
when(key2048.getPublicExponent()).thenReturn(new BigInteger("65537"));
|
||||||
|
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user