Unified sdk (#12)

* unifying Android and Desktop SDK

* implement desktop SDK adapter

* updating declarations

* change include syntax

* following jitpack.io guide for Android libraries

* add install task to all artefacts, add javadoc generation

* fixing javadoc

* use explicit provider "SC" instead of relying on order

* move to BouncyCastle for desktop compatibility

* improve documentation
This commit is contained in:
Bitgamma
2018-12-11 11:50:53 +03:00
committed by GitHub
parent 07fc087cb3
commit b08b1dc7ce
47 changed files with 268 additions and 103 deletions
@@ -100,7 +100,7 @@ public class ApplicationInfo {
/**
* The number of remaining pairing slots. If zero is returned, no further pairing is possible.
* @return
* @return the number of remaining pairing slots
*/
public byte getFreePairingSlots() {
return freePairingSlots;
@@ -1,7 +1,7 @@
package im.status.keycard.applet;
import org.spongycastle.crypto.digests.KeccakDigest;
import org.spongycastle.math.ec.ECPoint;
import org.bouncycastle.crypto.digests.KeccakDigest;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -41,8 +41,8 @@ public class CardDuplicator {
* @param pin the card PIN
* @param deviceCount the number of devices which will be adding entropy for the key, including this one
*
* @throws IOException
* @throws APDUException
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void startDuplication(CardChannel channel, Pairing pairing, String pin, int deviceCount) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
@@ -56,8 +56,8 @@ public class CardDuplicator {
* @param pairing the pairing info
* @param pin the card PIN
*
* @throws IOException
* @throws APDUException
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] exportKey(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
@@ -66,13 +66,13 @@ public class CardDuplicator {
/**
* Imports key. Must be used on all cards designated as the target for the duplication.
* @param channel
* @param pairing
* @param pin
* @param key
* @return
* @throws IOException
* @throws APDUException
* @param channel the card channel
* @param pairing the pairing info
* @param pin the user PIN
* @param key the key to import
* @return the key UID
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] importKey(CardChannel channel, Pairing pairing, String pin, byte[] key) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
@@ -84,8 +84,8 @@ public class CardDuplicator {
* exactly once, except for the device which started the backup.
*
* @param channel
* @throws IOException
* @throws APDUException
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void addEntropy(CardChannel channel) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
@@ -1,6 +1,6 @@
package im.status.keycard.applet;
import org.spongycastle.util.encoders.Hex;
import org.bouncycastle.util.encoders.Hex;
public class Identifiers {
public static final byte[] PACKAGE_AID = Hex.decode("53746174757357616C6C6574");
@@ -4,9 +4,8 @@ import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.util.encoders.Hex;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
@@ -117,7 +116,6 @@ public class KeycardCommandSet {
/**
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @return the raw card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel() throws IOException {
@@ -145,7 +143,7 @@ public class KeycardCommandSet {
SecretKey key;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", "BC");
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), 50000, 32 * 8);
key = skf.generateSecret(spec);
} catch (Exception e) {
@@ -141,7 +141,7 @@ public class Mnemonic {
SecretKey key;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512", "BC");
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
key = skf.generateSecret(spec);
} catch (Exception e) {
@@ -1,6 +1,6 @@
package im.status.keycard.applet;
import org.spongycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Base64;
import java.util.Arrays;
@@ -1,13 +1,13 @@
package im.status.keycard.applet;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.asn1.x9.X9IntegerConverter;
import org.spongycastle.crypto.ec.CustomNamedCurves;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.math.ec.ECAlgorithms;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.math.ec.FixedPointUtil;
import org.spongycastle.math.ec.custom.sec.SecP256K1Curve;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointUtil;
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
import java.math.BigInteger;
import java.util.Arrays;
@@ -28,7 +28,7 @@ public class RecoverableSignature {
static final ECDomainParameters CURVE;
static {
FixedPointUtil.precompute(CURVE_PARAMS.getG(), 6);
FixedPointUtil.precompute(CURVE_PARAMS.getG());
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
}
@@ -4,13 +4,13 @@ import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.macs.CBCBlockCipherMac;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
@@ -68,17 +68,17 @@ public class SecureChannelSession {
public void generateSecret(byte[] keyData) {
try {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
g.initialize(ecSpec, random);
KeyPair keyPair = g.generateKeyPair();
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(keyPair.getPrivate());
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
keyAgreement.doPhase(cardKey, true);
secret = keyAgreement.generateSecret();
@@ -116,7 +116,6 @@ public class SecureChannelSession {
* Follows the specifications from the SECURE_CHANNEL.md document.
*
* @param apduChannel the apdu channel
* @return the card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
@@ -155,7 +154,7 @@ public class SecureChannelSession {
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SC_SECRET_LENGTH), "AES");
sessionMacKey = new KeyParameter(keyData, SC_SECRET_LENGTH, SC_SECRET_LENGTH);
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
open = true;
} catch(Exception e) {
@@ -196,7 +195,7 @@ public class SecureChannelSession {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
md = MessageDigest.getInstance("SHA256", "BC");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
@@ -309,7 +308,6 @@ public class SecureChannelSession {
* Unpair all other clients
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
@@ -441,7 +439,7 @@ public class SecureChannelSession {
random.nextBytes(iv);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionEncKey = new SecretKeySpec(secret, "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
initData = sessionCipher.doFinal(initData);
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
@@ -1,10 +1,8 @@
package im.status.keycard.globalplatform;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
@@ -23,6 +21,16 @@ public class Crypto {
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
private static boolean spongyCastleLoaded = false;
public static void addSpongyCastleProvider() {
if (!spongyCastleLoaded) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
spongyCastleLoaded = true;
}
}
/**
* Derives a session key for SCP02.
*
@@ -44,7 +52,7 @@ public class Crypto {
SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8));
return cipher.doFinal(derivationData);
@@ -52,6 +60,8 @@ public class Crypto {
throw new IllegalStateException("error generating session keys.", e);
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
} catch (NoSuchProviderException e) {
throw new RuntimeException("SpongyCastle not installed");
}
}
@@ -100,7 +110,7 @@ public class Crypto {
public static byte[] mac3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data, 0, 24);
byte[] tail = new byte[8];
@@ -122,11 +132,11 @@ public class Crypto {
public static byte[] macFull3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec keyDes = new SecretKeySpec(resizeKey8(keyData), "DES");
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding");
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding", "BC");
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding");
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
byte[] des3Iv = iv.clone();
if (data.length > 8) {
@@ -183,7 +193,7 @@ public class Crypto {
*/
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
try {
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding", "BC");
SecretKeySpec key = new SecretKeySpec(resizeKey8(macKeyData), "DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(mac);
@@ -1,7 +1,7 @@
package im.status.keycard.globalplatform;
import im.status.keycard.applet.Identifiers;
import org.spongycastle.util.encoders.Hex;
import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -48,8 +48,8 @@ public class SecureChannel {
* @param hostChallenge the host challenge
* @param cardKeys the SCP02 keys
* @param resp the response from the card to the INITIALIZE UPDATE oommand
* @return
* @throws APDUException
* @return the Session object built on succesful verification
* @throws APDUException communication error
*/
public static Session verifyChallenge(byte[] hostChallenge, SCP02Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
@@ -1,34 +0,0 @@
package im.status.keycard.io;
import android.nfc.tech.IsoDep;
import android.util.Log;
import java.io.IOException;
/**
* Implementation of the CardChannel interface using the Android NFC API.
*/
public class NFCCardChannel implements CardChannel {
private static final String TAG = "CardChannel";
private IsoDep isoDep;
public NFCCardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
@Override
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;
}
@Override
public boolean isConnected() {
return this.isoDep.isConnected();
}
}
@@ -1,116 +0,0 @@
package im.status.keycard.io;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.security.Security;
/**
* Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
* a runloop which monitors the connection and from which CardListener callbacks are called.
*/
public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback {
private static final String TAG = "NFCCardManager";
private static final int DEFAULT_LOOP_SLEEP_MS = 50;
private IsoDep isoDep;
private boolean isRunning;
private CardListener cardListener;
private int loopSleepMS;
/**
* Constructs an NFC Card Manager with default delay between loop iterations.
*/
public NFCCardManager() {
this(DEFAULT_LOOP_SLEEP_MS);
}
/**
* Constructs an NFC Card Manager with the given delay between loop iterations.
*
* @param loopSleepMS time to sleep between loops
*/
public NFCCardManager(int loopSleepMS) {
this.loopSleepMS = loopSleepMS;
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
/**
* True if connected, false otherwise.
* @return if connected, false otherwise
*/
public boolean isConnected() {
return isoDep != null && isoDep.isConnected();
}
@Override
public void onTagDiscovered(Tag tag) {
isoDep = IsoDep.get(tag);
try {
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException e) {
Log.e(TAG, "error connecting to tag");
}
}
/**
* Runloop. Do NOT invoke directly. Use start() instead.
*/
public void run() {
boolean connected = isConnected();
while (true) {
boolean newConnected = isConnected();
if (newConnected != connected) {
connected = newConnected;
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
if (connected && !isRunning) {
onCardConnected();
} else {
onCardDisconnected();
}
}
SystemClock.sleep(loopSleepMS);
}
}
/**
* Reacts on card connected by calling the callback of the registered listener.
*/
private void onCardConnected() {
isRunning = true;
if (cardListener != null) {
cardListener.onConnected(new NFCCardChannel(isoDep));
}
isRunning = false;
}
/**
* Reacts on card disconnected by calling the callback of the registered listener.
*/
private void onCardDisconnected() {
isRunning = false;
isoDep = null;
if (cardListener != null) {
cardListener.onDisconnected();
}
}
/**
* Sets the card listener.
*
* @param listener the new listener
*/
public void setCardListener(CardListener listener) {
cardListener = listener;
}
}