Compare commits

...
19 Commits
Author SHA1 Message Date
Michele Balistreri 9366028aaf add factory reset 2023-06-06 16:42:49 +02:00
Michele Balistreri 0d27ac445c treat malformed response as ioexception 2023-02-22 12:16:44 +01:00
Michele Balistreri ccb353ca82 handle securityexceptions 2023-02-06 13:01:59 +01:00
Michele Balistreri 78c6dfb6d6 support raw signature format 2022-12-09 12:31:56 +01:00
Michele Balistreri 15a61e16e7 Add init with alt PIN (#29)
* init with alt pin

* chain code in pubkeys
2022-11-21 08:43:55 +01:00
Michele Balistreri 7d968cf969 support export chain code (#28) 2022-11-10 08:11:03 +01:00
Michele Balistreri 9fac06b19d Add IDENTIFY CARD (#24)
* ident applet support

* add identify card command

* fix certificate class

* make sure private key is 32 bytes

* don't remove TLV header from signature

* fix typo

* use secure channel if open
2022-11-04 12:33:06 +01:00
Audrius Molis 6c965f726a test adding exception info to the RuntimeException in RuntimeException (#26) 2022-09-09 15:31:58 +02:00
Michele Balistreri bbcce01742 add metadata parser/encoder (#22) 2022-07-18 14:16:36 +02:00
Michele Balistreri a39924aba3 BLS support (#21)
add support for BLS
2022-07-18 14:05:43 +02:00
Michele Balistreri 22db82e8f2 add init with pin/puk retries 2021-12-23 09:29:21 +03:00
Michele Balistreri 9295aa6553 check OPEN SECURE CHANNEL response 2020-11-09 15:36:50 +01:00
Michele Balistreri 86e6cb60ec bc 1.60 2020-11-09 14:03:25 +01:00
Michele Balistreri aaf2c9d9e6 update README 2020-06-02 10:22:27 +03:00
Michele Balistreri 9431d7c497 differentiate between communication errors and unexpected APDU response in the "auto" methods of the SecureChannelSession 2020-06-02 09:44:11 +03:00
Michele Balistreri f97363704b Merge branch 'master' of github.com:status-im/status-keycard-java 2020-04-15 13:52:10 +03:00
Michele Balistreri 144474415d closes #20 2020-04-15 13:46:49 +03:00
ligi 3f8966f1a8 Make setNDEF backward compatible to the 2.x style (#19) 2019-10-23 14:21:12 +03:00
Michele Balistreri 3acea10750 hardcode english dictionary 2019-10-23 09:59:44 +03:00
23 changed files with 3701 additions and 141 deletions
+3
View File
@@ -1,8 +1,11 @@
*.iml
.gradle
.vscode
/local.properties
.idea
.DS_Store
/build
/captures
/desktop/bin
lib/bin
.externalNativeBuild
+2 -2
View File
@@ -15,7 +15,7 @@ You can import the SDK in your Gradle or Maven project using [Jitpack.io](https:
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:android:2.2.0'
implementation 'com.github.status-im.status-keycard-java:android:3.0.2'
}
```
@@ -23,6 +23,6 @@ dependencies {
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:2.2.0'
implementation 'com.github.status-im.status-keycard-java:desktop:3.0.2'
}
```
+2 -2
View File
@@ -8,8 +8,8 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 300
versionName "3.0.0"
versionCode 304
versionName "3.0.4"
}
compileOptions {
@@ -24,14 +24,25 @@ public class NFCCardChannel implements CardChannel {
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
byte[] resp = this.isoDep.transceive(apdu);
APDUResponse response = new APDUResponse(resp);
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
return response;
try {
byte[] resp = this.isoDep.transceive(apdu);
APDUResponse response = new APDUResponse(resp);
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
return response;
} catch(SecurityException e) {
throw new IOException("Tag disconnected", e);
} catch(IllegalArgumentException e) {
throw new IOException("Malformed card response", e);
}
}
@Override
public boolean isConnected() {
return this.isoDep.isConnected();
try {
return this.isoDep.isConnected();
} catch(SecurityException e) {
return false;
}
}
}
@@ -48,7 +48,11 @@ public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback
* @return if connected, false otherwise
*/
public boolean isConnected() {
return isoDep != null && isoDep.isConnected();
try {
return isoDep != null && isoDep.isConnected();
} catch (SecurityException e) {
return false;
}
}
@Override
@@ -58,7 +62,7 @@ public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException e) {
} catch (IOException | SecurityException e) {
Log.e(TAG, "error connecting to tag");
}
}
+14
View File
@@ -1,5 +1,19 @@
import org.gradle.plugins.ide.eclipse.model.AccessRule
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'eclipse'
eclipse {
classpath {
file {
whenMerged {
def jre = entries.find { it.path.contains 'org.eclipse.jdt.launching.JRE_CONTAINER' }
jre.accessRules.add(new AccessRule('0', 'javax/smartcardio/**'))
}
}
}
}
dependencies {
compile project(':lib')
@@ -23,8 +23,9 @@ public class ApplicationInfo {
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04;
static final byte CAPABILITY_NDEF = (byte) 0x08;
static final byte CAPABILITY_FACTORY_RESET = (byte) 0x10;
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF;
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF | CAPABILITY_FACTORY_RESET;
/**
* Constructs an object by parsing the TLV data.
@@ -191,4 +192,13 @@ public class ApplicationInfo {
public boolean hasNDEFCapability() {
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
}
/**
* Returns true if the device supports the Factory Reset capability.
*
* @return true or false
*/
public boolean hasFactoryResetCapability() {
return (capabilities & CAPABILITY_FACTORY_RESET) == CAPABILITY_FACTORY_RESET;
}
}
@@ -1,6 +1,5 @@
package im.status.keycard.applet;
import org.bouncycastle.crypto.digests.KeccakDigest;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Mac;
@@ -67,13 +66,13 @@ public class BIP32KeyPair {
tlv.unreadLastTag();
privKey = tlv.readPrimitive(TLV_PRIV_KEY);
tag = tlv.readTag();
if (tag == TLV_CHAIN_CODE) {
tlv.unreadLastTag();
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
}
}
if (tag == TLV_CHAIN_CODE) {
tlv.unreadLastTag();
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
}
return new BIP32KeyPair(privKey, chainCode, pubKey);
}
@@ -0,0 +1,863 @@
package im.status.keycard.applet;
import java.math.BigInteger;
import java.security.DigestException;
import java.security.MessageDigest;
import java.util.Arrays;
public class BLS {
public static byte[] hash(byte[] msg) {
Fp[][] u = hashToField(msg, 2);
PointG2 q0 = isogenyMapG2(mapToCurveSimpleSWU9mod16(new Fp2(u[0][0], u[0][1])));
PointG2 q1 = isogenyMapG2(mapToCurveSimpleSWU9mod16(new Fp2(u[1][0], u[1][1])));
PointG2 r = q0.add(q1).clearCofactor();
return r.toByteArray(false);
}
public static byte[] compress(byte[] g2) {
return new PointG2(g2).toByteArray(true);
}
private BLS() {}
final static byte DST[] = {
(byte) 0x42, (byte) 0x4C, (byte) 0x53, (byte) 0x5F, (byte) 0x53, (byte) 0x49, (byte) 0x47, (byte) 0x5F,
(byte) 0x42, (byte) 0x4C, (byte) 0x53, (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x38, (byte) 0x31,
(byte) 0x47, (byte) 0x32, (byte) 0x5F, (byte) 0x58, (byte) 0x4D, (byte) 0x44, (byte) 0x3A, (byte) 0x53,
(byte) 0x48, (byte) 0x41, (byte) 0x2D, (byte) 0x32, (byte) 0x35, (byte) 0x36, (byte) 0x5F, (byte) 0x53,
(byte) 0x53, (byte) 0x57, (byte) 0x55, (byte) 0x5F, (byte) 0x52, (byte) 0x4F, (byte) 0x5F, (byte) 0x4E,
(byte) 0x55, (byte) 0x4C, (byte) 0x5F, (byte) 0x2B,
};
final private static int L = 64;
final private static int M = 2;
final private static int SHA256_DIGEST_SIZE = 32;
final private static BigInteger P = new BigInteger("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16);
final private static BigInteger P_MINUS_9_DIV_16 = P.pow(2).subtract(BigInteger.valueOf(9)).divide(BigInteger.valueOf(16));
final private static BigInteger CURVE_X = new BigInteger("d201000000010000", 16);
final private static Fp rv1 = new Fp("6af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09");
final private static Fp ev1 = new Fp("699be3b8c6870965e5bf892ad5d2cc7b0e85a117402dfd83b7f4a947e02d978498255a2aaec0ac627b5afbdf1bf1c90");
final private static Fp ev2 = new Fp("8157cd83046453f5dd0972b6e3949e4288020b5b8a9cc99ca07e27089a2ce2436d965026adad3ef7baba37f2183e9b5");
final private static Fp ev3 = new Fp("ab1c2ffdd6c253ca155231eb3e71ba044fd562f6f72bc5bad5ec46a0b7a3b0247cf08ce6c6317f40edbc653a72dee17");
final private static Fp ev4 = new Fp("aa404866706722864480885d68ad0ccac1967c7544b447873cc37e0181271e006df72162a3d3e0287bf597fbf7f8fc1");
final private static Fp PSI2_C1 = new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac");
final private static Fp2[] xnum = new Fp2[] {
new Fp2(new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6"),
new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6")),
new Fp2(Fp.ZERO,
new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71a")),
new Fp2(new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71e"),
new Fp("8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38d")),
new Fp2(new Fp("171d6541fa38ccfaed6dea691f5fb614cb14b4e7f4e810aa22d6108f142b85757098e38d0f671c7188e2aaaaaaaa5ed1"),
Fp.ZERO),
};
final private static Fp2[] xden = new Fp2[] {
new Fp2(Fp.ZERO,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa63")),
new Fp2(new Fp(0xc),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa9f")),
Fp2.ONE,
Fp2.ZERO,
};
final private static Fp2[] ynum = new Fp2[] {
new Fp2(new Fp("1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706"),
new Fp("1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706")),
new Fp2(Fp.ZERO,
new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97be")),
new Fp2(new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71c"),
new Fp("8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38f")),
new Fp2(new Fp("124c9ad43b6cf79bfbf7043de3811ad0761b0f37a1e26286b0e977c69aa274524e79097a56dc4bd9e1b371c71c718b10"),
Fp.ZERO),
};
final private static Fp2[] yden = new Fp2[] {
new Fp2(new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb"),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb")),
new Fp2(Fp.ZERO,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa9d3")),
new Fp2(new Fp(0x12),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa99")),
new Fp2(Fp.ONE, Fp.ZERO),
};
final private static Fp2[][] ISOGENY_COEFFICIENTS = new Fp2[][] { xnum, xden, ynum, yden };
final private static Fp2[] FP2_ROOTS_OF_UNITY = new Fp2[] {
Fp2.ONE,
new Fp2(rv1, rv1.neg()),
new Fp2(Fp.ZERO, Fp.ONE),
new Fp2(rv1, rv1),
new Fp2(Fp.ONE.neg(), Fp.ZERO),
new Fp2(rv1.neg(), rv1),
new Fp2(Fp.ZERO, Fp.ONE.neg()),
new Fp2(rv1.neg(), rv1.neg()),
};
final private static Fp2[] FP2_ETAs = new Fp2[] {
new Fp2(ev1, ev2),
new Fp2(ev2.neg(), ev1),
new Fp2(ev3, ev4),
new Fp2(ev4.neg(), ev3),
};
private static byte[] strxor(byte[] b0, byte[] b1, int b1off) {
byte[] xored = new byte[b0.length];
for (int i = 0; i < xored.length; i++) {
xored[i] = (byte) (b0[i] ^ b1[i + b1off]);
}
return xored;
}
private static byte[] expandMessage(byte[] msg, byte[] DST, int len) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
} catch (Exception e) {
throw new RuntimeException("SHA256 missing");
}
int ell = (len + (SHA256_DIGEST_SIZE - 1)) / SHA256_DIGEST_SIZE;
md.update(new byte[SHA256_DIGEST_SIZE * 2]);
md.update(msg);
md.update(new byte[] { (byte) ((len >> 8) & 0xff), (byte) (len & 0xff), (byte) 0 });
md.update(DST);
byte[] b0 = md.digest();
byte[] b = new byte[ell * SHA256_DIGEST_SIZE];
for (int i = 0; i < ell; i++) {
if (i == 0) {
md.update(b0);
} else {
md.update(strxor(b0, b, ((i - 1) * SHA256_DIGEST_SIZE)));
}
md.update((byte) (i + 1));
md.update(DST);
try {
md.digest(b, (i * SHA256_DIGEST_SIZE), SHA256_DIGEST_SIZE);
} catch (DigestException e) {
throw new RuntimeException("SHA256 error");
}
}
return Arrays.copyOf(b, len);
}
private static Fp[][] hashToField(byte[] msg, int count) {
byte[] uniformBytes = expandMessage(msg, DST, count * M * L);
Fp[][] u = new Fp[count][M];
for (int i = 0; i < count; i++) {
for (int j = 0; j < M; j++) {
int off = (L * (j + (i * M)));
u[i][j] = new Fp(Arrays.copyOfRange(uniformBytes, off, off + L));
}
}
return u;
}
private static PointG2 isogenyMapG2(PointG2 point) {
Fp2[] zPowers = new Fp2[] {point.z, point.z.square(), point.z.pow(3)};
Fp2[] mapped = new Fp2[] {Fp2.ZERO, Fp2.ZERO, Fp2.ZERO, Fp2.ZERO};
for (int i = 0; i < ISOGENY_COEFFICIENTS.length; i++) {
Fp2[] kI = ISOGENY_COEFFICIENTS[i];
mapped[i] = kI[3];
Fp2[] arr = new Fp2[] { kI[2], kI[1], kI[0] };
for (int j = 0; j < arr.length; j++) {
Fp2 kIJ = arr[j];
mapped[i] = mapped[i].mul(point.x).add(zPowers[j].mul(kIJ));
}
}
mapped[2] = mapped[2].mul(point.y);
mapped[3] = mapped[3].mul(point.z);
Fp2 z2 = mapped[1].mul(mapped[3]);
Fp2 x2 = mapped[0].mul(mapped[3]);
Fp2 y2 = mapped[1].mul(mapped[2]);
return new PointG2(x2, y2, z2);
}
private static SqrtDivFp2Res sqrtDivFp2(Fp2 u, Fp2 v) {
Fp2 v7 = v.pow(7);
Fp2 uv7 = u.mul(v7);
Fp2 uv15 = uv7.mul(v7.mul(v));
Fp2 gamma = uv15.pow(P_MINUS_9_DIV_16).mul(uv7);
for (int i = 0; i < 4; i++) {
Fp2 candidate = FP2_ROOTS_OF_UNITY[i].mul(gamma);
if (candidate.square().mul(v).sub(u).isZero()) {
return new SqrtDivFp2Res(true, candidate);
}
}
return new SqrtDivFp2Res(false, gamma);
}
private static PointG2 mapToCurveSimpleSWU9mod16(Fp2 t) {
Fp2 iso3a = new Fp2(new Fp(0), new Fp(240));
Fp2 iso3b = new Fp2(new Fp(1012), new Fp(1012));
Fp2 iso3z = new Fp2(new Fp(-2), new Fp(-1));
Fp2 t2 = t.square();
Fp2 iso3zt2 = iso3z.mul(t2);
Fp2 ztzt = iso3zt2.add(iso3zt2.square());
Fp2 denominator = iso3a.mul(ztzt).neg();
Fp2 numerator = iso3b.mul(ztzt.add(Fp2.ONE));
if (denominator.isZero()) {
denominator = iso3z.mul(iso3a);
}
Fp2 v = denominator.pow(3);
Fp2 u = numerator.pow(3)
.add(iso3a.mul(numerator).mul(denominator.square()))
.add(iso3b.mul(v));
SqrtDivFp2Res sqrtCandidateOrGamma = sqrtDivFp2(u, v);
Fp2 y = null;
if (!sqrtCandidateOrGamma.success) {
u = iso3zt2.pow(3).mul(u);
Fp2 sqrtCandidateX1 = sqrtCandidateOrGamma.value.mul(t.pow(3));
for (int i = 0; i < FP2_ETAs.length; i++) {
Fp2 etaSqrtCanditate = FP2_ETAs[i].mul(sqrtCandidateX1);
if (etaSqrtCanditate.square().mul(v).sub(u).isZero()) {
y = etaSqrtCanditate;
numerator = numerator.mul(iso3zt2);
break;
}
}
} else {
y = sqrtCandidateOrGamma.value;
}
if (y == null) {
throw new RuntimeException("Hash to Curve - Optimized SWU failed");
}
if (t.sgn0() != y.sgn0()) {
y = y.neg();
}
y = y.mul(denominator);
return new PointG2(numerator, y, denominator);
}
static class Fp {
final static Fp ZERO = new Fp(BigInteger.ZERO);
final static Fp ONE = new Fp(BigInteger.ONE);
final static int SIZE = 48;
private BigInteger i;
Fp(byte[] b) {
this(new BigInteger(1, b));
}
Fp(long i) {
this(BigInteger.valueOf(i));
}
Fp(BigInteger i) {
this.i = i.mod(P);
}
Fp(String hex) {
this(new BigInteger(hex, 16));
}
Fp mul(Fp b) {
return new Fp(this.i.multiply(b.i));
}
Fp add(Fp b) {
return new Fp(this.i.add(b.i));
}
Fp sub(Fp b) {
return new Fp(this.i.subtract(b.i));
}
Fp neg() {
return new Fp(this.i.negate());
}
Fp square() {
return new Fp(this.i.pow(2));
}
Fp inv() {
return new Fp(i.modInverse(P));
}
boolean isZero() {
return this.i.signum() == 0;
}
void serialize(byte[] out, int off) {
byte[] encoded = i.toByteArray();
int padding = SIZE - encoded.length;
System.arraycopy(encoded, 0, out, off + padding, encoded.length);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Fp)) {
return false;
}
Fp b = (Fp) o;
return b.i.equals(this.i);
}
}
static class Fp2 {
final static Fp[] FROBENIUS_COEFFICIENTS = new Fp[] {
Fp.ONE,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa")
};
final static Fp2 ZERO = new Fp2(Fp.ZERO, Fp.ZERO);
final static Fp2 ONE = new Fp2(Fp.ONE, Fp.ZERO);
final static int SIZE = Fp.SIZE * 2;
private Fp re;
private Fp im;
Fp2(Fp re, Fp im) {
this.re = re;
this.im = im;
}
Fp2(byte[] buf, int off) {
this(new Fp(Arrays.copyOfRange(buf, off + Fp.SIZE, off + Fp2.SIZE)), new Fp(Arrays.copyOfRange(buf, off, off + Fp.SIZE)));
}
int sgn0() {
boolean sign0 = this.re.i.testBit(0);
return sign0 || (this.re.isZero() && this.im.i.testBit(0)) ? 1 : 0;
}
Fp2 square() {
Fp a = this.re.add(this.im);
Fp b = this.re.sub(this.im);
Fp c = this.re.add(this.re);
return new Fp2(a.mul(b), c.mul(this.im));
}
Fp2 pow(long n) {
return this.pow(BigInteger.valueOf(n));
}
Fp2 pow(BigInteger n) {
if (n.signum() == 0) return Fp2.ONE;
if (n.equals(BigInteger.ONE)) return this;
Fp2 p = Fp2.ONE;
Fp2 d = this;
int bitLength = n.bitLength();
for (int i = 0; i < bitLength; i++) {
if (n.testBit(i)) {
p = p.mul(d);
}
d = d.square();
}
return p;
}
boolean isZero() {
return this.re.isZero() && this.im.isZero();
}
Fp2 mul(Fp2 b) {
Fp t1 = this.re.mul(b.re);
Fp t2 = this.im.mul(b.im);
return new Fp2(t1.sub(t2), this.re.add(this.im).mul(b.re.add(b.im)).sub(t1.add(t2)));
}
Fp2 mul(long b) {
return mul(new Fp(b));
}
Fp2 mul(Fp b) {
return new Fp2(this.re.mul(b), this.im.mul(b));
}
Fp2 add(Fp2 b) {
return new Fp2(this.re.add(b.re), this.im.add(b.im));
}
Fp2 sub(Fp2 b) {
return new Fp2(this.re.sub(b.re), this.im.sub(b.im));
}
Fp2 neg() {
return new Fp2(this.re.neg(), this.im.neg());
}
Fp2 inv() {
Fp factor = this.re.square().add(this.im.square()).inv();
return new Fp2(factor.mul(this.re), factor.mul(this.im.neg()));
}
Fp2 mulByNonresidue() {
return new Fp2(this.re.sub(this.im), this.re.add(this.im));
}
Fp2 frobeniusMap(int power) {
return new Fp2(this.re, this.im.mul(FROBENIUS_COEFFICIENTS[power % 2]));
}
void serialize(byte[] out, int off) {
this.im.serialize(out, off);
this.re.serialize(out, Fp.SIZE + off);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Fp2)) {
return false;
}
Fp2 b = (Fp2) o;
return b.re.equals(this.re) && b.im.equals(this.im);
}
}
static class Fp6 {
final static Fp2[] FROBENIUS_COEFFICIENTS_1 = new Fp2[] {
Fp2.ONE,
new Fp2(
Fp.ZERO,
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(Fp.ZERO, Fp.ONE),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
Fp.ZERO,
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe")
),
};
final static Fp2[] FROBENIUS_COEFFICIENTS_2 = new Fp2[] {
Fp2.ONE,
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"),
Fp.ZERO
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa"),
Fp.ZERO
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff"),
Fp.ZERO
),
};
final static Fp6 ZERO = new Fp6(Fp2.ZERO, Fp2.ZERO, Fp2.ZERO);
final static Fp6 ONE = new Fp6(Fp2.ONE, Fp2.ZERO, Fp2.ZERO);
private Fp2 c0;
private Fp2 c1;
private Fp2 c2;
Fp6(Fp2 c0, Fp2 c1, Fp2 c2) {
this.c0 = c0;
this.c1 = c1;
this.c2 = c2;
}
Fp6 add(Fp6 b) {
return new Fp6(this.c0.add(b.c0), this.c1.add(b.c1), this.c2.add(b.c2));
}
Fp6 sub(Fp6 b) {
return new Fp6(this.c0.sub(b.c0), this.c1.sub(b.c1), this.c2.sub(b.c2));
}
Fp6 mul(Fp6 b) {
Fp2 t0 = this.c0.mul(b.c0);
Fp2 t1 = this.c1.mul(b.c1);
Fp2 t2 = this.c2.mul(b.c2);
return new Fp6(
t0.add(this.c1.add(this.c2).mul(b.c1.add(b.c2)).sub(t1.add(t2)).mulByNonresidue()),
c0.add(c1).mul(b.c0.add(b.c1)).sub(t0.add(t1)).add(t2.mulByNonresidue()),
t1.add(c0.add(c2).mul(b.c0.add(b.c2)).sub(t0.add(t2)))
);
}
Fp6 mulByNonresidue() {
return new Fp6(this.c2.mulByNonresidue(), this.c0, this.c1);
}
Fp6 mulByFp2(Fp2 b) {
return new Fp6(this.c0.mul(b), this.c1.mul(b), this.c2.mul(b));
}
Fp6 square() {
Fp2 t0 = this.c0.square();
Fp2 t1 = this.c0.mul(this.c1).mul(2);
Fp2 t3 = this.c1.mul(this.c2).mul(2);
Fp2 t4 = this.c2.square();
return new Fp6(
t3.mulByNonresidue().add(t0),
t4.mulByNonresidue().add(t1),
t1.add(this.c0.sub(this.c1).add(this.c2).square()).add(t3).sub(t0).sub(t4)
);
}
Fp6 neg() {
return new Fp6(this.c0.neg(), this.c1.neg(), this.c2.neg());
}
Fp6 inv() {
Fp2 t0 = this.c0.square().sub(this.c2.mul(this.c1).mulByNonresidue());
Fp2 t1 = this.c2.square().mulByNonresidue().sub(this.c0.mul(this.c1));
Fp2 t2 = this.c1.square().sub(this.c0.mul(this.c2));
Fp2 t4 = this.c2.mul(t1).add(this.c1.mul(t2)).mulByNonresidue().add(this.c0.mul(t0)).inv();
return new Fp6(t4.mul(t0), t4.mul(t1), t4.mul(t2));
}
Fp6 frobeniusMap(int power) {
return new Fp6(
this.c0.frobeniusMap(power),
this.c1.frobeniusMap(power).mul(FROBENIUS_COEFFICIENTS_1[power % 6]),
this.c2.frobeniusMap(power).mul(FROBENIUS_COEFFICIENTS_2[power % 6])
);
}
}
static class Fp12 {
final static Fp2[] FROBENIUS_COEFFICIENTS = new Fp2[] {
Fp2.ONE,
new Fp2(
new Fp("1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8"),
new Fp("00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff"),
Fp.ZERO
),
new Fp2(
new Fp("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2"),
new Fp("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(
new Fp("144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995"),
new Fp("05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116")
),
new Fp2(
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa"),
Fp.ZERO
),
new Fp2(
new Fp("00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3"),
new Fp("1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8")
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
new Fp("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09"),
new Fp("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2")
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"),
Fp.ZERO
),
new Fp2(
new Fp("05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116"),
new Fp("144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995")
),
};
final static Fp12 ZERO = new Fp12(Fp6.ZERO, Fp6.ZERO);
final static Fp12 ONE = new Fp12(Fp6.ONE, Fp6.ZERO);
private Fp6 c0;
private Fp6 c1;
Fp12(Fp6 c0, Fp6 c1) {
this.c0 = c0;
this.c1 = c1;
}
Fp12 add(Fp12 b) {
return new Fp12(this.c0.add(b.c0), this.c1.add(b.c1));
}
Fp12 sub(Fp12 b) {
return new Fp12(this.c0.sub(b.c0), this.c1.sub(b.c1));
}
Fp12 mul(Fp12 b) {
Fp6 t1 = this.c0.mul(b.c0);
Fp6 t2 = this.c1.mul(b.c1);
return new Fp12(
t1.add(t2.mulByNonresidue()),
this.c0.add(this.c1).mul(b.c0.add(b.c1)).sub(t1.add(t2))
);
}
Fp12 mulByFp2(Fp2 b) {
return new Fp12(this.c0.mulByFp2(b), this.c1.mulByFp2(b));
}
Fp12 square() {
Fp6 ab = this.c0.mul(this.c1);
return new Fp12(
this.c1.mulByNonresidue().add(this.c0).mul(this.c0.add(this.c1)).sub(ab).sub(ab.mulByNonresidue()),
ab.add(ab)
);
}
Fp12 inv() {
Fp6 t = this.c0.square().sub(this.c1.square().mulByNonresidue()).inv();
return new Fp12(this.c0.mul(t), this.c1.mul(t).neg());
}
Fp12 frobeniusMap(int power) {
Fp6 r0 = this.c0.frobeniusMap(power);
Fp6 r1 = this.c1.frobeniusMap(power);
Fp2 coeff = FROBENIUS_COEFFICIENTS[power % 12];
return new Fp12(
r0,
new Fp6(r1.c0.mul(coeff), r1.c1.mul(coeff), r1.c2.mul(coeff))
);
}
}
static class SqrtDivFp2Res {
private boolean success;
private Fp2 value;
SqrtDivFp2Res(boolean success, Fp2 value) {
this.success = success;
this.value = value;
}
}
static class PointG2 {
final static Fp6 UT_ROOT = new Fp6(Fp2.ZERO, Fp2.ONE, Fp2.ZERO);
final static Fp12 WSQ = new Fp12(UT_ROOT, Fp6.ZERO);
final static Fp12 WCU = new Fp12(Fp6.ZERO, UT_ROOT);
final static Fp12 WSQ_INV = WSQ.inv();
final static Fp12 WCU_INV = WCU.inv();
final static PointG2 ZERO = new PointG2(Fp2.ONE, Fp2.ONE, Fp2.ZERO);
private Fp2 x;
private Fp2 y;
private Fp2 z;
PointG2(Fp2 x, Fp2 y, Fp2 z) {
this.x = x;
this.y = y;
this.z = z;
}
PointG2(byte[] buf) {
this.x = new Fp2(buf, 0);
this.y = new Fp2(buf, Fp2.SIZE);
this.z = Fp2.ONE;
}
PointG2 add(PointG2 b) {
if (this.isZero()) {
return b;
} else if (b.isZero()) {
return this;
}
Fp2 x1 = this.x;
Fp2 y1 = this.y;
Fp2 z1 = this.z;
Fp2 x2 = b.x;
Fp2 y2 = b.y;
Fp2 z2 = b.z;
Fp2 u1 = y2.mul(z1);
Fp2 u2 = y1.mul(z2);
Fp2 v1 = x2.mul(z1);
Fp2 v2 = x1.mul(z2);
if (v1.equals(v2) && u1.equals(u2)) {
return this.doubleP();
}
if (v1.equals(v2)) {
return PointG2.ZERO;
}
Fp2 u = u1.sub(u2);
Fp2 v = v1.sub(v2);
Fp2 vv = v.square();
Fp2 vvv = vv.mul(v);
Fp2 v2vv = v2.mul(vv);
Fp2 w = z1.mul(z2);
Fp2 a = u.square().mul(w).sub(vvv).sub(v2vv.add(v2vv));
Fp2 x3 = v.mul(a);
Fp2 y3 = u.mul(v2vv.sub(a)).sub(vvv.mul(u2));
Fp2 z3 = vvv.mul(w);
return new PointG2(x3, y3, z3);
}
private PointG2 doubleP() {
Fp2 w = this.x.square().mul(3);
Fp2 s = this.y.mul(this.z);
Fp2 ss = s.square();
Fp2 sss = ss.mul(s);
Fp2 b = this.x.mul(this.y).mul(s);
Fp2 h = w.square().sub(b.mul(8));
Fp2 x3 = h.mul(s).mul(2);
Fp2 y3 = w.mul(b.mul(4).sub(h)).sub(
this.y.square().mul(8).mul(ss)
);
Fp2 z3 = sss.mul(8);
return new PointG2(x3, y3, z3);
}
private boolean isZero() {
return this.z.isZero();
}
PointG2 clearCofactor() {
PointG2 t1 = this.mulCurveX();
PointG2 t2 = this.psi();
PointG2 t3 = this.doubleP();
t3 = t3.psi2();
t3 = t3.sub(t2);
t2 = t1.add(t2);
t2 = t2.mulCurveX();
t3 = t3.add(t2);
t3 = t3.sub(t1);
PointG2 q = t3.sub(this);
return q;
}
private PointG2 sub(PointG2 p) {
return this.add(p.neg());
}
private PointG2 neg() {
return new PointG2(x, y.neg(), z);
}
private PointG2 psi2() {
PointG2 p = toAffine();
return new PointG2(p.x.mul(PSI2_C1), p.y.neg(), p.z);
}
private PointG2 psi() {
PointG2 p = toAffine();
Fp2 x2 = WSQ_INV.mulByFp2(p.x).frobeniusMap(1).mul(WSQ).c0.c0;
Fp2 y2 = WCU_INV.mulByFp2(p.y).frobeniusMap(1).mul(WCU).c0.c0;
return new PointG2(x2, y2, p.z);
}
private PointG2 mulCurveX() {
return this.mulUnsafe(CURVE_X).neg();
}
private PointG2 mulUnsafe(BigInteger n) {
PointG2 point = PointG2.ZERO;
PointG2 d = this;
int bitLength = n.bitLength();
for (int i = 0; i < bitLength; i++) {
if (n.testBit(i)) {
point = point.add(d);
}
d = d.doubleP();
}
return point;
}
PointG2 toAffine() {
Fp2 invZ = this.z.inv();
return new PointG2(this.x.mul(invZ), this.y.mul(invZ), Fp2.ONE);
}
byte[] toByteArray(boolean compressed) {
PointG2 p = this.toAffine();
byte[] result = new byte[Fp2.SIZE * (compressed ? 1 : 2)];
p.x.serialize(result, 0);
if (compressed) {
result[0] |= (byte) 0x80;
BigInteger tmp = p.y.im.isZero() ? p.y.re.i.shiftLeft(1) : p.y.im.i.shiftLeft(1);
if (tmp.compareTo(P) > 0) {
result[0] |= 0x20;
}
} else {
p.y.serialize(result, Fp2.SIZE);
}
return result;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof PointG2)) {
return false;
}
PointG2 p = (PointG2) o;
return p.x.equals(this.x) && p.y.equals(this.y) && p.z.equals(this.z);
}
}
}
@@ -33,15 +33,48 @@ public class CashCommandSet {
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
* Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long
*
* @param challenge the data of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse identifyCard(byte[] challenge) throws IOException {
APDUCommand identifyCard = new APDUCommand(0x80, KeycardCommandSet.INS_IDENTIFY_CARD, 0, 0, challenge);
return apduChannel.send(identifyCard);
}
/**
* Sends a SIGN APDU.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, byte p2) throws IOException {
APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_SIGN, 0x00, p2, data);
return apduChannel.send(sign);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash with ECDSA so the input must be exactly 32-bytes long.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data) throws IOException {
APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_SIGN, 0x00, 0x00, data);
return apduChannel.send(sign);
return sign(data, KeycardCommandSet.SIGN_P2_ECDSA);
}
/**
* Sends a SIGN APDU. The message can be any length, and it is mapped to a point on G2 internally.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signBLS(byte[] data) throws IOException {
return sign(BLS.hash(data), KeycardCommandSet.SIGN_P2_BLS12_381);
}
}
@@ -0,0 +1,141 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.util.Arrays;
public class Certificate extends RecoverableSignature {
public static final byte TLV_CERT = (byte) 0x8A;
private byte[] identPriv;
private byte[] identPub;
public Certificate(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) {
super(publicKey, compressed,r, s, recId);
}
public static KeyPair generateIdentKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256k1");
keyPairGenerator.initialize(spec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static Certificate createCertificate(KeyPair caPair, KeyPair identKeys) {
try {
byte[] pub = ((ECPublicKey) identKeys.getPublic()).getQ().getEncoded(true);
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
byte[] hash = md.digest(pub);
Signature signer = Signature.getInstance("NONEwithECDSA", "BC");
signer.initSign(caPair.getPrivate());
signer.update(hash);
byte[] sig = signer.sign();
TinyBERTLV tlv = new TinyBERTLV(sig);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
byte[] r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
byte[] s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
Certificate cert = new Certificate(((ECPublicKey)caPair.getPublic()).getQ().getEncoded(true), true, r, s, -1);
cert.calculateRecID(hash);
cert.identPriv = toUInt(((ECPrivateKey) identKeys.getPrivate()).getD().toByteArray());
cert.identPub = pub;
return cert;
} catch(IllegalArgumentException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static Certificate generateNewCertificate(KeyPair caPair) {
return createCertificate(caPair, generateIdentKeyPair());
}
public static Certificate fromTLV(byte[] certData) {
try {
byte[] pub = Arrays.copyOfRange(certData, 0, 33);
byte[] r = Arrays.copyOfRange(certData, 33, 65);
byte[] s = Arrays.copyOfRange(certData, 65, 97);
int recId = certData[97];
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
byte[] hash = md.digest(pub);
byte[] caPub = recoverFromSignature(recId, hash, r, s, true);
Certificate cert = new Certificate(caPub, true, r, s, recId);
cert.identPub = pub;
return cert;
} catch(IllegalArgumentException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static byte[] verifyIdentity(byte[] hash, byte[] tlvData) {
try {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
byte[] certData = tlv.readPrimitive(TLV_CERT);
Certificate cert = fromTLV(certData);
byte[] signature = tlv.peekUnread();
Signature verifier = Signature.getInstance("NONEWithECDSA", "BC");
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(cert.identPub), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
verifier.initVerify(cardKey);
verifier.update(hash);
if (!verifier.verify(signature)) {
return null;
}
return cert.getPublicKey();
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public byte[] toStoreData() {
if (identPriv == null) {
throw new IllegalStateException("The private key must be set.");
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
os.write(this.identPub);
os.write(this.getR());
os.write(this.getS());
os.write(this.getRecId());
os.write(this.identPriv);
} catch(IOException e) {
throw new RuntimeException(e);
}
return os.toByteArray();
}
}
@@ -0,0 +1,46 @@
package im.status.keycard.applet;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import java.io.IOException;
/**
* Command set for the Ident applet.
*/
public class IdentCommandSet {
private final CardChannel apduChannel;
/**
* Creates a IdentCommandSet using the given APDU Channel
* @param apduChannel APDU channel
*/
public IdentCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
}
/**
* Selects a Cash instance. The applet is assumed to have been installed with its default AID. The returned data is
* a public key which must be used to initialize the secure channel.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.IDENT_INSTANCE_AID);
return apduChannel.send(selectApplet);
}
/**
* Sends a STORE DATA APDU.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse storeData(byte[] data) throws IOException {
APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_STORE_DATA, 0x00, 0x00, data);
return apduChannel.send(sign);
}
}
@@ -16,6 +16,9 @@ public class Identifiers {
public static final byte[] CASH_AID = Hex.decode("A000000804000103");
public static final byte[] CASH_INSTANCE_AID = Hex.decode("A00000080400010301");
public static final byte[] IDENT_AID = Hex.decode("A000000804000104");
public static final byte[] IDENT_INSTANCE_AID = Hex.decode("A00000080400010401");
/**
* Gets the instance AID of the default instance of the Keycard applet.
*
@@ -18,7 +18,10 @@ import java.util.Arrays;
*/
public class KeycardCommandSet {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_FACTORY_RESET = (byte) 0xFD;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_SET_NDEF = (byte) 0xF3;
static final byte INS_IDENTIFY_CARD = (byte) 0x14;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
@@ -58,6 +61,9 @@ public class KeycardCommandSet {
static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte SIGN_P1_PINLESS = 0x03;
public static final byte SIGN_P2_ECDSA = 0x00;
public static final byte SIGN_P2_BLS12_381 = 0x01;
public static final byte STORE_DATA_P1_PUBLIC = 0x00;
public static final byte STORE_DATA_P1_NDEF = 0x01;
public static final byte STORE_DATA_P1_CASH = 0x02;
@@ -72,8 +78,12 @@ public class KeycardCommandSet {
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
public static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
public static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
public static final byte EXPORT_KEY_P2_EXTENDED_PUBLIC = 0x02;
static final byte FACTORY_RESET_P1_MAGIC = (byte) 0xAA;
static final byte FACTORY_RESET_P2_MAGIC = 0x55;
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
@@ -163,8 +173,9 @@ public class KeycardCommandSet {
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException secure channel error
*/
public void autoOpenSecureChannel() throws IOException {
public void autoOpenSecureChannel() throws IOException, APDUException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
@@ -172,8 +183,9 @@ public class KeycardCommandSet {
* Automatically pairs. Derives the secret from the given password.
*
* @throws IOException communication error
* @throws APDUException pairing error
*/
public void autoPair(String pairingPassword) throws IOException {
public void autoPair(String pairingPassword) throws IOException, APDUException {
byte[] secret = pairingPasswordToSecret(pairingPassword);
secureChannel.autoPair(apduChannel, secret);
@@ -202,8 +214,9 @@ public class KeycardCommandSet {
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException pairing error
*/
public void autoPair(byte[] sharedSecret) throws IOException {
public void autoPair(byte[] sharedSecret) throws IOException, APDUException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
@@ -211,8 +224,9 @@ public class KeycardCommandSet {
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void autoUnpair() throws IOException {
public void autoUnpair() throws IOException, APDUException {
secureChannel.autoUnpair(apduChannel);
}
@@ -253,11 +267,26 @@ public class KeycardCommandSet {
/**
* Unpair all other clients.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
}
/**
* Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long
*
* @param challenge the data of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse identifyCard(byte[] challenge) throws IOException {
APDUCommand identifyCard = secureChannel.protectedCommand(0x80, INS_IDENTIFY_CARD, 0, 0, challenge);
return secureChannel.transmit(apduChannel, identifyCard);
}
/**
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
* class itself.
@@ -529,7 +558,7 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, int p1) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x00, data);
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x01, data);
return secureChannel.transmit(apduChannel, sign);
}
@@ -609,6 +638,10 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, setPinlessPath);
}
private byte poToP2(boolean publicOnly) {
return publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
}
/**
* Sends an EXPORT KEY APDU to export the current key.
*
@@ -617,9 +650,20 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportCurrentKey(boolean publicOnly) throws IOException {
return exportKey(EXPORT_KEY_P1_CURRENT, publicOnly, new byte[0]);
return exportCurrentKey(poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU to export the current key.
*
* @param p2 the p2 parameter
* @return the raw card reponse
* @throws IOException communication error
*/
public APDUResponse exportCurrentKey(byte p2) throws IOException {
return exportKey(EXPORT_KEY_P1_CURRENT, p2, new byte[0]);
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
@@ -630,10 +674,23 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(String keyPath, boolean makeCurrent, boolean publicOnly) throws IOException {
KeyPath path = new KeyPath(keyPath);
return exportKey(path.getData(), path.getSource(), makeCurrent, publicOnly);
return exportKey(keyPath, makeCurrent, poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param p2 the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(String keyPath, boolean makeCurrent, byte p2) throws IOException {
KeyPath path = new KeyPath(keyPath);
return exportKey(path.getData(), path.getSource(), makeCurrent, p2);
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
@@ -644,10 +701,23 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, boolean publicOnly) throws IOException {
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
return exportKey(p1, publicOnly, keyPath);
return exportKey(keyPath, source, makeCurrent, poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param p2 the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, byte p2) throws IOException {
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
return exportKey(p1, p2, keyPath);
}
/**
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
*
@@ -658,10 +728,22 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(int derivationOptions, boolean publicOnly, byte[] keypath) throws IOException {
byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
return exportKey(derivationOptions, poToP2(publicOnly), keypath);
}
/**
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
*
* @param derivationOptions the P1 parameter
* @param p2 the P2 parameter
* @param keypath the data parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(int derivationOptions, byte p2, byte[] keypath) throws IOException {
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, derivationOptions, p2, keypath);
return secureChannel.transmit(apduChannel, exportKey);
}
}
/**
* Sends a GET DATA APDU.
@@ -683,15 +765,20 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse setNDEF(byte[] ndef) throws IOException {
if ((ndef.length - 2) != ((ndef[0] << 8) | ndef[1])) {
byte[] tmp = new byte[ndef.length + 2];
tmp[0] = (byte) (ndef.length >> 8);
tmp[1] = (byte) (ndef.length & 0xff);
System.arraycopy(ndef, 0, tmp, 2, ndef.length);
ndef = tmp;
}
if ((info.getAppVersion() >> 8) > 2) {
if ((ndef.length - 2) != ((ndef[0] << 8) | ndef[1])) {
byte[] tmp = new byte[ndef.length + 2];
tmp[0] = (byte) (ndef.length >> 8);
tmp[1] = (byte) (ndef.length & 0xff);
System.arraycopy(ndef, 0, tmp, 2, ndef.length);
ndef = tmp;
}
return storeData(ndef, STORE_DATA_P1_NDEF);
return storeData(ndef, STORE_DATA_P1_NDEF);
} else {
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
return secureChannel.transmit(apduChannel, setNDEF);
}
}
/**
@@ -708,7 +795,7 @@ public class KeycardCommandSet {
}
/**
* Sends the INIT command to the card.
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
* @param puk the PUK
@@ -717,9 +804,40 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
return this.init(pin, puk, pairingPasswordToSecret(pairingPassword));
return this.init(pin, puk, pairingPassword, (byte) 0, (byte) 0);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param puk the PUK
* @param pairingPassword pairing password
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
return this.init(pin, null, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param altPin the alternative PIN
* @param puk the PUK
* @param pairingPassword pairing password
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String altPin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
return this.init(pin, altPin, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
/**
* Sends the INIT command to the card.
*
@@ -730,10 +848,58 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length);
return init(pin, null, puk, sharedSecret, (byte) 0, (byte) 0);
}
/**
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
* @param pin the alternative
* @param puk the PUK
* @param sharedSecret the shared secret for pairing
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException {
int baselen = pin.length() + puk.length() + sharedSecret.length;
int extlen;
if (altPin != null) {
extlen = 2 + altPin.length();
} else if ((pinRetries != 0) || (pukRetries != 0)) {
extlen = 2;
} else {
extlen = 0;
}
byte[] initData = Arrays.copyOf(pin.getBytes(), baselen + extlen);
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
if (extlen > 0) {
initData[baselen] = pinRetries;
initData[baselen + 1] = pukRetries;
if (extlen > 2) {
System.arraycopy(altPin.getBytes(), 0, initData, baselen + 2, altPin.length());
}
}
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
/**
* Sends the FACTORY RESET command to the card.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse factoryReset() throws IOException {
APDUCommand factoryReset = new APDUCommand(0x80, INS_FACTORY_RESET, FACTORY_RESET_P1_MAGIC, FACTORY_RESET_P2_MAGIC, new byte[0]);
return apduChannel.send(factoryReset);
}
}
@@ -0,0 +1,106 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.SortedSet;
import java.util.TreeSet;
public class Metadata {
private String cardName;
private SortedSet<Long> wallets;
public static Metadata fromData(byte[] data) {
int version = (data[0] & 0xe0) >> 5;
if (version != 1) {
throw new RuntimeException("Invalid version");
}
int namelen = (data[0] & 0x1f);
int off = 1;
String cardName = new String(data, off, namelen, Charset.forName("US-ASCII"));
off += namelen;
SortedSet<Long> set = new TreeSet<>();
while(off < data.length) {
int[] start = TinyBERTLV.readNum(data, off);
int[] count = TinyBERTLV.readNum(data, start[1]);
off = count[1];
long s = start[0] & 0xffffffffl;
buildRange(set, s, (s + count[0]));
}
return new Metadata(cardName, set);
}
private static void buildRange(SortedSet<Long> set, long start, long end) {
for (long i = start; i <= end; i++) {
set.add(i);
}
}
Metadata(String cardName, SortedSet<Long> wallets) {
this.cardName = cardName;
this.wallets = wallets;
}
public Metadata(String cardName) {
this(cardName, new TreeSet<>());
}
public String getCardName() {
return cardName;
}
public void setCardName(String cardName) {
if (cardName.length() > 20) {
throw new IllegalArgumentException("card name too long");
}
this.cardName = cardName;
}
public SortedSet<Long> getWallets() {
return wallets;
}
public void addWallet(long w) {
this.wallets.add(w);
}
public void removeWallet(long w) {
this.wallets.remove(w);
}
public byte[] toByteArray() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] name = this.cardName.getBytes(Charset.forName("US-ASCII"));
os.write(0x20 | name.length);
os.write(name, 0, name.length);
if (wallets.isEmpty()) {
return os.toByteArray();
}
long start = wallets.first();
int len = 0;
for (Long w : wallets.tailSet(start + 1)) {
if (w == (start + len + 1)) {
len++;
} else {
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
len = 0;
start = w;
}
}
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
return os.toByteArray();
}
}
@@ -3,10 +3,6 @@ package im.status.keycard.applet;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Scanner;
public class Mnemonic {
private final static int WORDLIST_SIZE = 2048;
@@ -41,28 +37,11 @@ public class Mnemonic {
}
/**
* Retrieves the official BIP39 english wordlist from GitHub.
* Returns the official BIP39 english wordlist as fetched from https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt on 23 Oct 2019.
*
* @throws IOException network error
*/
public void fetchBIP39EnglishWordlist() throws IOException {
URL remoteList = new URL("https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt");
Scanner scanner = new Scanner(remoteList.openStream());
ArrayList<String> list = new ArrayList<>();
while(scanner.hasNextLine()) {
list.add(scanner.nextLine());
}
scanner.close();
if (list.size() != WORDLIST_SIZE) {
throw new IllegalArgumentException("The list must contain exactly 2048 entries");
}
this.wordlist = new String[WORDLIST_SIZE];
list.toArray(this.wordlist);
public void fetchBIP39EnglishWordlist() {
this.wordlist = MnemonicEnglishDictionary.words;
}
/**
@@ -145,7 +124,7 @@ public class Mnemonic {
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
throw new RuntimeException("Is Bouncycastle correctly initialized?", e);
}
return key.getEncoded();
File diff suppressed because it is too large Load Diff
@@ -20,8 +20,10 @@ public class RecoverableSignature {
private int recId;
private byte[] r;
private byte[] s;
private boolean compressed;
public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
public static final byte TLV_RAW_SIGNATURE = (byte) 0x80;
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
@@ -40,16 +42,50 @@ public class RecoverableSignature {
*/
public RecoverableSignature(byte[] hash, byte[] tlvData) {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
int tag = tlv.readTag();
tlv.unreadLastTag();
if (tag == TLV_RAW_SIGNATURE) {
initFromRawSignature(hash, tlv.readPrimitive(tag));
} else if (tag == TLV_SIGNATURE_TEMPLATE) {
initFromLegacy(hash, tlv);
} else {
throw new IllegalArgumentException("invalid tlv");
}
}
private void initFromLegacy(byte[] hash, TinyBERTLV tlv) {
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
this.publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
this.r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.compressed = false;
calculateRecID(hash);
}
private void initFromRawSignature(byte[] hash, byte[] signature) {
this.r = Arrays.copyOfRange(signature, 0, 32);
this.s = Arrays.copyOfRange(signature, 32, 64);
this.recId = signature[64];
this.compressed = false;
this.publicKey = recoverFromSignature(this.recId, hash, this.r, this.s, this.compressed);
}
public RecoverableSignature(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) {
this.publicKey = publicKey;
this.r = r;
this.s = s;
this.compressed = compressed;
this.recId = recId;
}
void calculateRecID(byte[] hash) {
recId = -1;
for (int i = 0; i < 4; i++) {
byte[] candidate = recoverFromSignature(i, new BigInteger(1, hash), new BigInteger(1, r), new BigInteger(1, s));
byte[] candidate = recoverFromSignature(i, hash, r, s, compressed);
if (Arrays.equals(candidate, publicKey)) {
recId = i;
@@ -62,7 +98,7 @@ public class RecoverableSignature {
}
}
private byte[] toUInt(byte[] signedInt) {
static byte[] toUInt(byte[] signedInt) {
if (signedInt[0] == 0) {
return Arrays.copyOfRange(signedInt, 1, signedInt.length);
} else {
@@ -114,7 +150,15 @@ public class RecoverableSignature {
return Ethereum.toEthereumAddress(publicKey);
}
private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) {
static byte[] recoverFromSignature(int recId, byte[] hash, byte[] r, byte[] s, boolean compressed) {
BigInteger h = new BigInteger(1, hash);
BigInteger br = new BigInteger(1, r);
BigInteger bs = new BigInteger(1, s);
return recoverFromSignature(recId, h, br, bs, compressed);
}
static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s, boolean compressed) {
BigInteger n = CURVE.getN();
BigInteger i = BigInteger.valueOf((long) recId / 2);
BigInteger x = r.add(i.multiply(n));
@@ -135,7 +179,7 @@ public class RecoverableSignature {
BigInteger srInv = rInv.multiply(s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
return q.getEncoded(false);
return q.getEncoded(compressed);
}
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
@@ -118,24 +118,14 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException, APDUException {
APDUResponse response = openSecureChannel(apduChannel, pairing.getPairingIndex(), publicKey);
if (response.getSw() != 0x9000) {
throw new IOException("OPEN SECURE CHANNEL failed");
}
response.checkOK("OPEN SECURE CHANNEL failed");
processOpenSecureChannelResponse(response);
response = mutuallyAuthenticate(apduChannel);
if (response.getSw() != 0x9000) {
throw new IOException("MUTUALLY AUTHENTICATE failed");
}
if(!verifyMutuallyAuthenticateResponse(response)) {
throw new IOException("Invalid authentication data from the card");
}
response.checkOK("MUTUALLY AUTHENTICATE failed");
verifyMutuallyAuthenticateResponse(response);
}
/**
@@ -168,8 +158,10 @@ public class SecureChannelSession {
* @param response the card response
* @return true if response is correct, false otherwise
*/
public boolean verifyMutuallyAuthenticateResponse(APDUResponse response) {
return response.getData().length == SC_SECRET_LENGTH;
public void verifyMutuallyAuthenticateResponse(APDUResponse response) throws APDUException {
if (response.getData().length != SC_SECRET_LENGTH) {
throw new APDUException("Invalid authentication data from the card");
}
}
/**
@@ -178,14 +170,10 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException {
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException, APDUException {
byte[] challenge = new byte[32];
random.nextBytes(challenge);
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 1");
}
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge).checkOK("Pairing failed on step 1");
byte[] respData = resp.getData();
byte[] cardCryptogram = Arrays.copyOf(respData, 32);
@@ -204,18 +192,13 @@ public class SecureChannelSession {
checkCryptogram = md.digest(challenge);
if (!Arrays.equals(checkCryptogram, cardCryptogram)) {
throw new IOException("Invalid card cryptogram");
throw new APDUException("Invalid card cryptogram");
}
md.update(sharedSecret);
checkCryptogram = md.digest(cardChallenge);
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 2");
}
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram).checkOK("Pairing failed on step 2");
respData = resp.getData();
md.update(sharedSecret);
pairing = new Pairing(md.digest(Arrays.copyOfRange(respData, 1, respData.length)), respData[0]);
@@ -227,12 +210,8 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoUnpair(CardChannel apduChannel) throws IOException {
APDUResponse resp = unpair(apduChannel, pairing.getPairingIndex());
if (resp.getSw() != 0x9000) {
throw new IOException("Unpairing failed");
}
public void autoUnpair(CardChannel apduChannel) throws IOException, APDUException {
unpair(apduChannel, pairing.getPairingIndex()).checkOK("Unpairing failed");
}
/**
@@ -1,5 +1,6 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/**
@@ -14,6 +15,57 @@ public class TinyBERTLV {
private byte[] buffer;
private int pos;
public static int[] readNum(byte[] buf, int off) {
int len = buf[off++] & 0xff;
int lenlen = 0;
if ((len & 0x80) == 0x80) {
lenlen = len & 0x7f;
len = readVal(buf, off, lenlen);
}
return new int[] {len, off + lenlen};
}
public static int readVal(byte[] val, int off, int len) {
switch (len) {
case 1:
return val[off] & 0xff;
case 2:
return ((val[off] & 0xff) << 8) | (val[off+1] & 0xff);
case 3:
return ((val[off] & 0xff) << 16) | ((val[off+1] & 0xff) << 8) | (val[off+2] & 0xff);
case 4:
return ((val[off] & 0xff) << 24) | ((val[off+1] & 0xff) << 16) | ((val[off+2] & 0xff) << 8) | (val[off+3] & 0xff);
default:
throw new IllegalArgumentException("Integers of length " + len + " are unsupported");
}
}
public static void writeNum(ByteArrayOutputStream os, int len) {
if ((len & 0xff000000) != 0) {
os.write(0x84);
os.write((len & 0xff000000) >> 24);
os.write((len & 0x00ff0000) >> 16);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x00ff0000) != 0) {
os.write(0x83);
os.write((len & 0x00ff0000) >> 16);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x0000ff00) != 0) {
os.write(0x82);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x00000080) != 0) {
os.write(0x81);
os.write(len & 0x000000ff);
} else {
os.write(len);
}
}
public TinyBERTLV(byte[] buffer) {
this.buffer = buffer;
this.pos = 0;
@@ -64,19 +116,16 @@ public class TinyBERTLV {
*/
public int readInt() throws IllegalArgumentException {
byte[] val = readPrimitive(TLV_INT);
return TinyBERTLV.readVal(val, 0, val.length);
}
switch (val.length) {
case 1:
return val[0] & 0xff;
case 2:
return ((val[0] & 0xff) << 8) | (val[1] & 0xff);
case 3:
return ((val[0] & 0xff) << 16) | ((val[1] & 0xff) << 8) | (val[2] & 0xff);
case 4:
return ((val[0] & 0xff) << 24) | ((val[1] & 0xff) << 16) | ((val[2] & 0xff) << 8) | (val[3] & 0xff);
default:
throw new IllegalArgumentException("Integers of length " + val.length + " are unsupported");
}
/**
* Returns all unread bytes in the TLV.
*
* @return all unread bytes
*/
byte[] peekUnread() {
return Arrays.copyOfRange(buffer, pos, buffer.length);
}
/**
@@ -104,13 +153,9 @@ public class TinyBERTLV {
* @return the tag
*/
public int readLength() {
int len = buffer[pos++] & 0xff;
if (len == 0x81) {
len = buffer[pos++] & 0xff;
}
return len;
int[] len = TinyBERTLV.readNum(buffer, pos);
pos = len[1];
return len[0];
}
private void checkTag(int expected, int actual) throws IllegalArgumentException {
@@ -61,7 +61,7 @@ public class Crypto {
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
} catch (NoSuchProviderException e) {
throw new RuntimeException("SpongyCastle not installed");
throw new RuntimeException("BouncyCastle not installed");
}
}
@@ -250,6 +250,16 @@ public class GlobalPlatformCommandSet {
return delete(Identifiers.NDEF_INSTANCE_AID);
}
/**
* Deletes the Ident applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteIdentInstance() throws IOException {
return delete(Identifiers.IDENT_INSTANCE_AID);
}
/**
* Deletes the Keycard package.
*
@@ -268,10 +278,7 @@ public class GlobalPlatformCommandSet {
* @throws IOException communication error
*/
public void deleteKeycardInstancesAndPackage() throws IOException, APDUException {
deleteNDEFInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
deleteKeycardInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
deleteCashInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
delete(Identifiers.PACKAGE_AID, (byte) 0x80).checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
}
/**
@@ -282,15 +289,27 @@ public class GlobalPlatformCommandSet {
* @throws IOException communication error.
*/
public APDUResponse delete(byte[] aid) throws IOException {
return delete(aid, (byte) 0);
}
/**
* Sends a DELETE APDU with the given AID
* @param aid the AID to the delete
* @param p2 the P2 value
* @return the raw card response
*
* @throws IOException communication error.
*/
public APDUResponse delete(byte[] aid, byte p2) throws IOException {
byte[] data = new byte[aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) aid.length;
System.arraycopy(aid, 0, data, 2, aid.length);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, 0, data);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, p2, data);
return this.secureChannel.send(cmd);
}
}
/**
* Loads the Keycard package.
@@ -448,4 +467,14 @@ public class GlobalPlatformCommandSet {
public APDUResponse installCashApplet() throws IOException {
return installCashApplet(new byte[0]);
}
/**
* Installs the Ident applet.
*
* @return the card response
* @throws IOException communication error.
*/
public APDUResponse installIdentApplet() throws IOException {
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.IDENT_AID, Identifiers.IDENT_INSTANCE_AID, new byte[0]);
}
}
@@ -88,6 +88,35 @@ public class APDUResponse {
}
}
/**
* Asserts that the SW is 0x9000. Throws an exception with the given message if it isn't
*
* @param message the error message
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkOK(String message) throws APDUException {
return checkSW(message, SW_OK);
}
/**
* Asserts that the SW is contained in the given list. Throws an exception with the given message if it isn't.
*
* @param message the error message
* @param codes the list of SWs to match.
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkSW(String message, int... codes) throws APDUException {
for (int code : codes) {
if (this.sw == code) {
return this;
}
}
throw new APDUException(this.sw, message);
}
/**
* Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
*