From 60faf16d61a621bf558cbc18c50438b0750fe062 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Mon, 12 Nov 2018 13:30:46 +0300 Subject: [PATCH] use the SDK instead of duplicating classes --- app/build.gradle | 1 + app/src/main/assets/wallet.cap | Bin 71367 -> 68344 bytes .../appletinstaller/APDUCommand.java | 67 --- .../appletinstaller/APDUException.java | 15 - .../appletinstaller/APDUResponse.java | 69 --- .../appletinstaller/APDUWrapper.java | 2 + .../{CardManager.java => ActionRunner.java} | 101 +--- .../appletinstaller/CardChannel.java | 23 - .../appletinstaller/Channel.java | 7 - .../appletinstaller/Installer.java | 37 +- .../appletinstaller/MainActivity.java | 20 +- .../appletinstaller/PerfTest.java | 135 +---- .../appletinstaller/SecureChannel.java | 10 +- .../appletinstaller/apducommands/Delete.java | 2 +- .../apducommands/ExternalAuthenticate.java | 4 +- .../apducommands/InitializeUpdate.java | 10 +- .../apducommands/InstallForInstall.java | 4 +- .../apducommands/InstallForLoad.java | 4 +- .../appletinstaller/apducommands/Load.java | 7 +- .../apducommands/SecureChannelSession.java | 474 ----------------- .../appletinstaller/apducommands/Select.java | 2 +- .../appletinstaller/apducommands/Status.java | 2 +- .../apducommands/WalletAppletCommandSet.java | 499 ------------------ .../apducommands/InitializeUpdateTest.java | 3 - .../apducommands/InstallForInstallTest.java | 2 - .../apducommands/InstallForLoadTest.java | 1 - .../apducommands/LoadTest.java | 7 - .../apducommands/SelectTest.java | 1 - .../apducommands/StatusTest.java | 2 - build.gradle | 1 + 30 files changed, 102 insertions(+), 1410 deletions(-) delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/APDUCommand.java delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/APDUException.java delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/APDUResponse.java rename app/src/main/java/im/status/applet_installer_test/appletinstaller/{CardManager.java => ActionRunner.java} (56%) delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/CardChannel.java delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/Channel.java delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/SecureChannelSession.java delete mode 100644 app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/WalletAppletCommandSet.java diff --git a/app/build.gradle b/app/build.gradle index eb6d673..6e55802 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation 'com.android.support.constraint:constraint-layout:1.1.2' implementation 'com.madgag.spongycastle:core:1.58.0.0' implementation 'com.madgag.spongycastle:prov:1.58.0.0' + implementation 'com.github.status-im:hardwallet-lite-android:059125a' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/app/src/main/assets/wallet.cap b/app/src/main/assets/wallet.cap index c0d73caaec6a07d02c02eb89812f888642cfa2e8..73bd2fab39eb442324bc937ed61cc6d58705caad 100644 GIT binary patch literal 68344 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 literal 71367 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 2000) { - this.perform(); - } - } - - try { - this.sleep(50); - } catch (InterruptedException e) { - Logger.e("error in TagManager thread: " + e.getMessage()); - this.interrupt(); - } - } - } - - private void onCardConnected() { - this.cardConnectedAt = System.currentTimeMillis(); - if (this.requestedAction != ACTION_NONE) { - Logger.i("waiting 2 seconds to start requested action"); - } else { - Logger.i("no action requested yet"); - } - } - - private void onCardDisconnected() { - this.cardConnectedAt = 0; - this.isoDep = null; - } - - private void perform() { + private void perform(CardChannel ch) { Logger.i("starting requested action"); - this.running = true; try { - CardChannel ch = new CardChannel(this.isoDep); + if (!ch.isConnected()) { + Logger.i("tag disconnected"); + return; + } switch (requestedAction) { case ACTION_INSTALL: @@ -141,9 +83,22 @@ public class CardManager extends Thread implements NfcAdapter.ReaderCallback { } catch (Exception e) { Logger.e("Other exception: " + e.getMessage()); } finally { - this.running = false; this.requestedAction = ACTION_NONE; - this.cardConnectedAt = 0; + } + } + + @Override + public void onConnected(final CardChannel channel) { + if (this.requestedAction != ACTION_NONE) { + Logger.i("waiting 2 seconds to start requested action"); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + perform(channel); + } + }, 2000); + } else { + Logger.i("no action requested yet"); } } } diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/CardChannel.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/CardChannel.java deleted file mode 100644 index d92915a..0000000 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/CardChannel.java +++ /dev/null @@ -1,23 +0,0 @@ -package im.status.applet_installer_test.appletinstaller; - -import android.nfc.tech.IsoDep; - -import java.io.IOException; - -import im.status.applet_installer_test.appletinstaller.CardManager; - -public class CardChannel implements Channel { - private IsoDep isoDep; - - public CardChannel(IsoDep isoDep) { - this.isoDep = isoDep; - } - - public APDUResponse send(APDUCommand cmd) throws IOException { - byte[] apdu = cmd.serialize(); - Logger.d(String.format("COMMAND %s", HexUtils.byteArrayToHexString(apdu))); - byte[] resp = this.isoDep.transceive(apdu); - Logger.d(String.format("RESPONSE %s %n-----------------------", HexUtils.byteArrayToHexString(resp))); - return new APDUResponse(resp); - } -} diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/Channel.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/Channel.java deleted file mode 100644 index 4caf7d5..0000000 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/Channel.java +++ /dev/null @@ -1,7 +0,0 @@ -package im.status.applet_installer_test.appletinstaller; - -import java.io.IOException; - -public interface Channel { - APDUResponse send(APDUCommand cmd) throws IOException; -} diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/Installer.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/Installer.java index f136c5e..341e6f7 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/Installer.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/Installer.java @@ -2,18 +2,21 @@ package im.status.applet_installer_test.appletinstaller; import android.content.res.AssetManager; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; import im.status.applet_installer_test.appletinstaller.apducommands.*; +import im.status.hardwallet_lite_android.io.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUException; +import im.status.hardwallet_lite_android.io.APDUResponse; +import im.status.hardwallet_lite_android.io.CardChannel; +import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet; public class Installer { - private Channel plainChannel; - private Channel channel; + private CardChannel plainChannel; + private SecureChannel channel; private Keys cardKeys; private AssetManager assets; private String capPath; @@ -21,9 +24,8 @@ public class Installer { static final byte[] cardKeyData = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f"); - public Installer(Channel channel, AssetManager assets, String capPath, boolean testSecrets) { + public Installer(CardChannel channel, AssetManager assets, String capPath, boolean testSecrets) { this.plainChannel = channel; - this.channel = channel; this.cardKeys = new Keys(cardKeyData, cardKeyData); this.assets = assets; this.capPath = capPath; @@ -47,7 +49,7 @@ public class Installer { Session session = init.verifyResponse(this.cardKeys, resp); Keys sessionKeys = session.getKeys(); - this.channel = new SecureChannel(this.channel, sessionKeys); + this.channel = new SecureChannel(this.plainChannel, sessionKeys); ExternalAuthenticate auth = new ExternalAuthenticate(sessionKeys.getEncKeyData(), session.getCardChallenge(), hostChallenge); resp = this.send("external auth", auth.getCommand()); @@ -87,7 +89,7 @@ public class Installer { APDUCommand loadCmd; while((loadCmd = load.getCommand()) != null) { - this.send("load " + load.getCount() + "/37", loadCmd); + this.send("load " + load.getCount() + "/35", loadCmd); } InstallForInstall installNDEF = new InstallForInstall(packageAID, ndefAppletAID, ndefInstanceAID, new byte[0]); @@ -96,27 +98,32 @@ public class Installer { InstallForInstall install = new InstallForInstall(packageAID, walletAID, walletAID, new byte[0]); this.send("perform and make selectable (wallet)", install.getCommand()); - installSecrets(); + personalizeApplet(); long duration = System.currentTimeMillis() - startTime; Logger.i(String.format("installation completed in %d seconds", duration / 1000)); } - private void installSecrets() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException { + private void personalizeApplet() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException { Secrets secrets = testSecrets ? Secrets.testSecrets() : Secrets.generate(); - WalletAppletCommandSet cmdSet = new WalletAppletCommandSet((CardChannel) this.plainChannel); - byte[] ecKey = cmdSet.select().checkOK().getData(); - SecureChannelSession secureChannel = new SecureChannelSession(Arrays.copyOfRange(ecKey, 2, ecKey.length)); - cmdSet.setSecureChannel(secureChannel); + WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(this.plainChannel); + cmdSet.select().checkOK(); cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingToken()).checkOK(); + cmdSet.select().checkOK(); + cmdSet.autoPair(secrets.getPairingPassword()); + cmdSet.autoOpenSecureChannel(); + cmdSet.verifyPIN(secrets.getPin()).checkOK(); + cmdSet.setNDEF(HexUtils.hexStringToByteArray("0024d40f12616e64726f69642e636f6d3a706b67696d2e7374617475732e657468657265756d")).checkOK(); + cmdSet.autoUnpair(); + Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s\nPairing token: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword(), HexUtils.byteArrayToHexString(secrets.getPairingToken()))); } private APDUResponse send(String description, APDUCommand cmd) throws IOException, APDUException { Logger.d("sending command " + description); - APDUResponse resp = this.channel.send(cmd); + APDUResponse resp = this.channel == null ? this.plainChannel.send(cmd) : this.channel.send(cmd); if(resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) { Logger.e("SW_SECURITY_CONDITION_NOT_SATISFIED: card might be blocked"); diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/MainActivity.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/MainActivity.java index 04d7d2b..173671f 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/MainActivity.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/MainActivity.java @@ -5,11 +5,12 @@ import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.nfc.NfcAdapter; import android.text.method.ScrollingMovementMethod; -import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ScrollView; import android.widget.TextView; +import im.status.hardwallet_lite_android.io.CardManager; + import java.security.Security; public class MainActivity extends AppCompatActivity implements UILogger { @@ -24,6 +25,7 @@ public class MainActivity extends AppCompatActivity implements UILogger { private Button buttonInstall; private Button buttonInstallTest; private Button buttonPerfTest; + private ActionRunner actionRunner; private CardManager cardManager; @Override @@ -34,8 +36,10 @@ public class MainActivity extends AppCompatActivity implements UILogger { Logger.setUILogger(this); AssetManager assets = this.getAssets(); - this.cardManager = new CardManager(nfcAdapter, assets, "wallet.cap"); - this.cardManager.start(); + this.actionRunner = new ActionRunner(assets, "wallet.cap"); + this.cardManager = new CardManager(); + this.cardManager.setOnCardConnectedListener(this.actionRunner); + cardManager.start(); textViewScroll = (ScrollView) findViewById(R.id.textViewScroll); @@ -46,21 +50,21 @@ public class MainActivity extends AppCompatActivity implements UILogger { buttonInstall.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - requestAction(CardManager.ACTION_INSTALL); + requestAction(ActionRunner.ACTION_INSTALL); } }); buttonInstallTest = (Button) findViewById(R.id.buttonInstallTest); buttonInstallTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - requestAction(CardManager.ACTION_INSTALL_TEST); + requestAction(ActionRunner.ACTION_INSTALL_TEST); } }); buttonPerfTest = (Button) findViewById(R.id.buttonPerfTest); buttonPerfTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - requestAction(CardManager.ACTION_PERFTEST); + requestAction(ActionRunner.ACTION_PERFTEST); } }); @@ -78,9 +82,9 @@ public class MainActivity extends AppCompatActivity implements UILogger { } private void requestAction(int action) { - if (this.cardManager != null) { + if (this.actionRunner != null) { clearTextView(); - this.cardManager.requestAction(action); + this.actionRunner.requestAction(action); } } diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/PerfTest.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/PerfTest.java index 95c51cb..ffbb6af 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/PerfTest.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/PerfTest.java @@ -2,33 +2,22 @@ package im.status.applet_installer_test.appletinstaller; import android.util.Log; -import im.status.applet_installer_test.appletinstaller.apducommands.SecureChannelSession; -import im.status.applet_installer_test.appletinstaller.apducommands.WalletAppletCommandSet; -import org.spongycastle.asn1.ASN1InputStream; -import org.spongycastle.asn1.ASN1Integer; -import org.spongycastle.asn1.DLSequence; +import im.status.hardwallet_lite_android.io.CardChannel; +import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet; import org.spongycastle.asn1.x9.X9ECParameters; import org.spongycastle.crypto.ec.CustomNamedCurves; import org.spongycastle.crypto.params.ECDomainParameters; -import org.spongycastle.crypto.params.ECPublicKeyParameters; -import org.spongycastle.crypto.signers.ECDSASigner; import org.spongycastle.jce.ECNamedCurveTable; import org.spongycastle.jce.spec.ECParameterSpec; -import org.spongycastle.math.ec.ECPoint; import org.spongycastle.math.ec.FixedPointUtil; -import java.io.IOException; -import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.MessageDigest; -import java.util.Arrays; import java.util.Random; public class PerfTest { private CardChannel cardChannel; private WalletAppletCommandSet cmdSet; - private SecureChannelSession secureChannel; private long openSecureChannelTime = 0; private long loadKeysTime = 0; @@ -42,42 +31,19 @@ public class PerfTest { static { FixedPointUtil.precompute(CURVE_PARAMS.getG(), 12); CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH()); - - byte[] tmp; - - try { - tmp = Crypto.generatePairingKey(new char[] {'W', 'a', 'l', 'l', 'e', 't', 'A','p', 'p', 'l', 'e', 't', 'T', 'e', 's', 't'}); - } catch (Exception e) { - tmp = null; - } - - SHARED_SECRET = tmp; } - static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00; - static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40; - static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80; - static final byte EXPORT_KEY_P1_HIGH = 0x01; - static final byte SIGN_P1_PRECOMPUTED_HASH = 0x01; - static final byte GET_STATUS_P1_APPLICATION = 0x00; - static final byte GET_STATUS_P1_KEY_PATH = 0x01; - // m/44'/60'/0'/0/0 static final byte[] BIP44_PATH = new byte[] { (byte) 0x80, 0x00, 0x00, 0x2c, (byte) 0x80, 0x00, 0x00, 0x3c, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}; - // TODO: Make this an input - public static final byte[] SHARED_SECRET; - public PerfTest(CardChannel cardChannel) { this.cardChannel = cardChannel; } public void test() throws Exception { cmdSet = new WalletAppletCommandSet(cardChannel); - byte[] keyData = extractPublicKeyFromSelect(cmdSet.select().getData()); - secureChannel = new SecureChannelSession(keyData); - cmdSet.setSecureChannel(secureChannel); - cmdSet.autoPair(SHARED_SECRET); + cmdSet.select().checkOK(); + cmdSet.autoPair(Secrets.testSecrets().getPairingToken()); openSecureChannelTime = System.currentTimeMillis(); cmdSet.autoOpenSecureChannel(); openSecureChannelTime = System.currentTimeMillis() - openSecureChannelTime; @@ -97,7 +63,7 @@ public class PerfTest { } Logger.i("Reenabling logging."); - cmdSet.select(); + cmdSet.select().checkOK(); cmdSet.autoOpenSecureChannel(); cmdSet.verifyPIN("000000").checkOK(); cmdSet.autoUnpair(); @@ -113,21 +79,21 @@ public class PerfTest { private void getStatus() throws Exception { long time = System.currentTimeMillis(); - cmdSet.select(); + cmdSet.select().checkOK(); cmdSet.autoOpenSecureChannel(); - cmdSet.getStatus(GET_STATUS_P1_APPLICATION).checkOK(); + cmdSet.getStatus(WalletAppletCommandSet.GET_STATUS_P1_APPLICATION).checkOK(); getStatusTime = System.currentTimeMillis() - time; } private void login() throws Exception { long time = System.currentTimeMillis(); - cmdSet.select(); + cmdSet.select().checkOK(); cmdSet.autoOpenSecureChannel(); cmdSet.verifyPIN("000000").checkOK(); - cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x00}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK(); - cmdSet.exportKey(EXPORT_KEY_P1_HIGH, false).checkOK(); - cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x01}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK(); - cmdSet.exportKey(EXPORT_KEY_P1_HIGH, false).checkOK(); + cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x00}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK(); + cmdSet.exportKey(WalletAppletCommandSet.EXPORT_KEY_P1_HIGH, false).checkOK(); + cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x01}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK(); + cmdSet.exportKey(WalletAppletCommandSet.EXPORT_KEY_P1_HIGH, false).checkOK(); loginTime = System.currentTimeMillis() - time; } @@ -140,19 +106,19 @@ public class PerfTest { cmdSet.loadKey(keyPair, false, chainCode).checkOK(); long time = System.currentTimeMillis(); - cmdSet.deriveKey(BIP44_PATH, DERIVE_P1_SOURCE_CURRENT, false, false).checkOK(); + cmdSet.deriveKey(BIP44_PATH, WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT).checkOK(); loadKeysTime = System.currentTimeMillis() - time; } private void signTransactions() throws Exception { long time = System.currentTimeMillis(); - cmdSet.select(); + cmdSet.select().checkOK(); cmdSet.autoOpenSecureChannel(); cmdSet.verifyPIN("000000").checkOK(); deriveKeyFromParent = System.currentTimeMillis(); - cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK(); + cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK(); deriveKeyFromParent = System.currentTimeMillis() - deriveKeyFromParent; - cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes(), SIGN_P1_PRECOMPUTED_HASH, true, true).checkOK(); + cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes()).checkOK(); signTime = System.currentTimeMillis() - time; } @@ -163,73 +129,4 @@ public class PerfTest { return g; } - - private byte[] extractPublicKeyFromSelect(byte[] select) { - return Arrays.copyOfRange(select, 22, 22 + select[21]); - } - - private byte[] derivePublicKey(byte[] data) throws Exception { - byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]); - byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length); - byte[] hash = MessageDigest.getInstance("SHA256").digest("STATUS KEY DERIVATION".getBytes()); - - pubKey[0] = 0x02; - - ECPoint candidate = CURVE.getCurve().decodePoint(pubKey); - if (!verifySig(hash, signature, candidate)) { - pubKey[0] = 0x03; - candidate = CURVE.getCurve().decodePoint(pubKey); - if (!verifySig(hash, signature, candidate)) { - throw new Exception("Public key is incorrect"); - } - } - - return candidate.getEncoded(false); - } - - private boolean verifySig(byte[] hash, byte[] signature, ECPoint pub) { - ECDSASigner signer = new ECDSASigner(); - ECPublicKeyParameters params = new ECPublicKeyParameters(pub, CURVE); - signer.init(false, params); - ECDSASignature sig = ECDSASignature.decodeFromDER(signature); - return signer.verifySignature(hash, sig.r, sig.s); - } - - static class ECDSASignature { - /** The two components of the signature. */ - public final BigInteger r, s; - - /** - * Constructs a signature with the given components. Does NOT automatically canonicalise the signature. - */ - public ECDSASignature(BigInteger r, BigInteger s) { - this.r = r; - this.s = s; - } - - public static ECDSASignature decodeFromDER(byte[] bytes) { - ASN1InputStream decoder = null; - try { - decoder = new ASN1InputStream(bytes); - DLSequence seq = (DLSequence) decoder.readObject(); - if (seq == null) - throw new RuntimeException("Reached past end of ASN.1 stream."); - ASN1Integer r, s; - try { - r = (ASN1Integer) seq.getObjectAt(0); - s = (ASN1Integer) seq.getObjectAt(1); - } catch (ClassCastException e) { - throw new IllegalArgumentException(e); - } - // OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be - // Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html - return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue()); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - if (decoder != null) - try { decoder.close(); } catch (IOException x) {} - } - } - } } diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/SecureChannel.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/SecureChannel.java index 84530f4..dc22c76 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/SecureChannel.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/SecureChannel.java @@ -1,14 +1,16 @@ package im.status.applet_installer_test.appletinstaller; -import android.provider.Settings; +import im.status.hardwallet_lite_android.io.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUResponse; +import im.status.hardwallet_lite_android.io.CardChannel; import java.io.IOException; -public class SecureChannel implements Channel { - private Channel channel; +public class SecureChannel { + private CardChannel channel; private APDUWrapper wrapper; - public SecureChannel(Channel channel, Keys keys) { + public SecureChannel(CardChannel channel, Keys keys) { this.channel = channel; this.wrapper = new APDUWrapper(keys.getMacKeyData()); } diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Delete.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Delete.java index 62117b6..397faa1 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Delete.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Delete.java @@ -1,6 +1,6 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import im.status.applet_installer_test.appletinstaller.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUCommand; public class Delete { private static final int CLA = 0x80; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/ExternalAuthenticate.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/ExternalAuthenticate.java index 1e4858c..c96df1a 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/ExternalAuthenticate.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/ExternalAuthenticate.java @@ -1,8 +1,8 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUResponse; import im.status.applet_installer_test.appletinstaller.Crypto; +import im.status.hardwallet_lite_android.io.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUResponse; public class ExternalAuthenticate { public static int CLA = 0x84; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdate.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdate.java index db75714..df5cd44 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdate.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdate.java @@ -1,15 +1,11 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import java.security.SecureRandom; - -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUException; -import im.status.applet_installer_test.appletinstaller.APDUResponse; import im.status.applet_installer_test.appletinstaller.Crypto; -import im.status.applet_installer_test.appletinstaller.HexUtils; import im.status.applet_installer_test.appletinstaller.Keys; -import im.status.applet_installer_test.appletinstaller.Logger; import im.status.applet_installer_test.appletinstaller.Session; +import im.status.hardwallet_lite_android.io.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUException; +import im.status.hardwallet_lite_android.io.APDUResponse; public class InitializeUpdate { public static final int CLA = 0x80; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstall.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstall.java index 88a7846..11f8882 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstall.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstall.java @@ -1,10 +1,10 @@ package im.status.applet_installer_test.appletinstaller.apducommands; +import im.status.hardwallet_lite_android.io.APDUCommand; + import java.io.ByteArrayOutputStream; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; - public class InstallForInstall { public static final int CLA = 0x80; public static final int INS = 0xE6; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoad.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoad.java index 7e13622..4da8a1e 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoad.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoad.java @@ -1,10 +1,10 @@ package im.status.applet_installer_test.appletinstaller.apducommands; +import im.status.hardwallet_lite_android.io.APDUCommand; + import java.io.ByteArrayOutputStream; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; - public class InstallForLoad { public static final int CLA = 0x80; public static final int INS = 0xE6; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Load.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Load.java index a14d85e..9168505 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Load.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Load.java @@ -1,8 +1,8 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import java.io.ByteArrayInputStream; +import im.status.hardwallet_lite_android.io.APDUCommand; + import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -11,9 +11,6 @@ import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.HexUtils; - public class Load { public static final int CLA = 0x80; public static final int INS = 0xE8; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/SecureChannelSession.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/SecureChannelSession.java deleted file mode 100644 index 15f984d..0000000 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/SecureChannelSession.java +++ /dev/null @@ -1,474 +0,0 @@ -package im.status.applet_installer_test.appletinstaller.apducommands; - -import im.status.applet_installer_test.appletinstaller.*; -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 javax.crypto.Cipher; -import javax.crypto.KeyAgreement; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import java.io.IOException; -import java.security.*; -import java.util.Arrays; - -/** - * Handles a SecureChannel session with the card. - */ -public class SecureChannelSession { - public static final short SC_SECRET_LENGTH = 32; - public static final short SC_BLOCK_SIZE = 16; - - public static final byte INS_OPEN_SECURE_CHANNEL = 0x10; - public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11; - public static final byte INS_PAIR = 0x12; - public static final byte INS_UNPAIR = 0x13; - - public static final byte PAIR_P1_FIRST_STEP = 0x00; - public static final byte PAIR_P1_LAST_STEP = 0x01; - - public static final int PAYLOAD_MAX_SIZE = 223; - - static final byte PAIRING_MAX_CLIENT_COUNT = 5; - - - private byte[] secret; - private byte[] publicKey; - private byte[] pairingKey; - private byte[] iv; - private byte pairingIndex; - private Cipher sessionCipher; - private CBCBlockCipherMac sessionMac; - private SecretKeySpec sessionEncKey; - private KeyParameter sessionMacKey; - private SecureRandom random; - private boolean open; - - /** - * Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session. - * The public key of the card is used as input for the EC-DH algorithm. The output is stored as the secret. - * - * @param keyData the public key returned by the applet as response to the SELECT command - */ - public SecureChannelSession(byte[] keyData) { - random = new SecureRandom(); - generateSecret(keyData); - open = false; - } - - public void generateSecret(byte[] keyData) { - try { - random = new SecureRandom(); - ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); - KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH"); - g.initialize(ecSpec, random); - - KeyPair keyPair = g.generateKeyPair(); - - publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false); - KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); - keyAgreement.init(keyPair.getPrivate()); - - ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec); - ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec); - - keyAgreement.doPhase(cardKey, true); - secret = keyAgreement.generateSecret(); - } catch (Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - - /** - * Returns the public key - * @return the public key - */ - public byte[] getPublicKey() { - return publicKey; - } - - /** - * Returns the pairing index - * @return the pairing index - */ - public byte getPairingIndex() { - return pairingIndex; - } - - /** - * Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step. - * 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 { - APDUResponse response = openSecureChannel(apduChannel, pairingIndex, publicKey); - - if (response.getSw() != 0x9000) { - throw new IOException("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"); - } - } - - /** - * Processes the response from OPEN SECURE CHANNEL. This initialize the session keys, Cipher and MAC internally. - * - * @param response the card response - */ - public void processOpenSecureChannelResponse(APDUResponse response) { - try { - MessageDigest md = MessageDigest.getInstance("SHA512"); - md.update(secret); - md.update(pairingKey); - byte[] data = response.getData(); - byte[] keyData = md.digest(Arrays.copyOf(data, SC_SECRET_LENGTH)); - iv = Arrays.copyOfRange(data, SC_SECRET_LENGTH, data.length); - - 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"); - sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null); - open = true; - } catch(Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - - /** - * Verify that the response from MUTUALLY AUTHENTICATE is correct. - * - * @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; - } - - /** - * Handles the entire pairing procedure in order to be able to use the secure channel - * - * @param apduChannel the apdu channel - * @throws IOException communication error - */ - public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException { - 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"); - } - - byte[] respData = resp.getData(); - byte[] cardCryptogram = Arrays.copyOf(respData, 32); - byte[] cardChallenge = Arrays.copyOfRange(respData, 32, respData.length); - byte[] checkCryptogram; - - MessageDigest md; - - try { - md = MessageDigest.getInstance("SHA256"); - } catch(Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - - md.update(sharedSecret); - checkCryptogram = md.digest(challenge); - - if (!Arrays.equals(checkCryptogram, cardCryptogram)) { - throw new IOException("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"); - } - - respData = resp.getData(); - md.update(sharedSecret); - pairingKey = md.digest(Arrays.copyOfRange(respData, 1, respData.length)); - pairingIndex = respData[0]; - } - - /** - * Unpairs the current paired key - * - * @param apduChannel the apdu channel - * @throws IOException communication error - */ - public void autoUnpair(CardChannel apduChannel) throws IOException { - APDUResponse resp = unpair(apduChannel, pairingIndex); - - if (resp.getSw() != 0x9000) { - throw new IOException("Unpairing failed"); - } - } - - /** - * Sends a OPEN SECURE CHANNEL APDU. - * - * @param apduChannel the apdu channel - * @param index the P1 parameter - * @param data the data - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws IOException { - open = false; - APDUCommand openSecureChannel = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, index, 0, data); - return apduChannel.send(openSecureChannel); - } - - /** - * Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically - * - * @param apduChannel the apdu channel - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException { - byte[] data = new byte[SC_SECRET_LENGTH]; - random.nextBytes(data); - - return mutuallyAuthenticate(apduChannel, data); - } - - /** - * Sends a MUTUALLY AUTHENTICATE APDU. - * - * @param apduChannel the apdu channel - * @param data the data - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws IOException { - APDUCommand mutuallyAuthenticate = protectedCommand(0x80, INS_MUTUALLY_AUTHENTICATE, 0, 0, data); - return transmit(apduChannel, mutuallyAuthenticate); - } - - /** - * Sends a PAIR APDU. - * - * @param apduChannel the apdu channel - * @param p1 the P1 parameter - * @param data the data - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse pair(CardChannel apduChannel, byte p1, byte[] data) throws IOException { - APDUCommand openSecureChannel = new APDUCommand(0x80, INS_PAIR, p1, 0, data); - return transmit(apduChannel, openSecureChannel); - } - - /** - * Sends a UNPAIR APDU. - * - * @param apduChannel the apdu channel - * @param p1 the P1 parameter - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException { - APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]); - return transmit(apduChannel, openSecureChannel); - } - - /** - * 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 { - for (int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) { - if (i != pairingIndex) { - APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]); - transmit(apduChannel, openSecureChannel).checkOK(); - } - } - } - - /** - * Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext - * already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array - * the returned data will still contain the IV and padding. - * - * @param data the plaintext data - * @return the encrypted data - */ - private byte[] encryptAPDU(byte[] data) { - assert data.length <= PAYLOAD_MAX_SIZE; - - try { - IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); - - sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec); - return sessionCipher.doFinal(data); - } catch(Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - - /** - * Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding - * and can be potentially empty. - * - * @param data the ciphetext - * @return the plaintext - */ - private byte[] decryptAPDU(byte[] data) { - try { - IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); - sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec); - return sessionCipher.doFinal(data); - } catch(Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - - /** - * Returns a command APDU with MAC and encrypted data. - * - * @param cla the CLA byte - * @param ins the INS byte - * @param p1 the P1 byte - * @param p2 the P2 byte - * @param data the data, can be an empty array but not null - * @return the command APDU - */ - public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) { - byte[] finalData; - - if (open) { - data = encryptAPDU(data); - byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - updateIV(meta, data); - - finalData = Arrays.copyOf(iv, iv.length + data.length); - System.arraycopy(data, 0, finalData, iv.length, data.length); - } else { - finalData = data; - } - - return new APDUCommand(cla, ins, p1, p2, finalData); - } - - /** - * Transmits a protected command APDU and unwraps the response data. The MAC is verified, the data decrypted and the - * SW read from the payload. - * - * @param apduChannel the APDU channel - * @param apdu the APDU to send - * @return the unwrapped response APDU - * @throws IOException transmission error - */ - public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException { - APDUResponse resp = apduChannel.send(apdu); - - if (resp.getSw() == 0x6982) { - open = false; - } - - if (open) { - byte[] data = resp.getData(); - byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - byte[] mac = Arrays.copyOf(data, iv.length); - data = Arrays.copyOfRange(data, iv.length, data.length); - - byte[] plainData = decryptAPDU(data); - - updateIV(meta, data); - - if (!Arrays.equals(iv, mac)) { - throw new IOException("Invalid MAC"); - } - - return new APDUResponse(plainData); - } else { - return resp; - } - } - - /** - * Marks the SecureChannel as closed - */ - public void reset() { - open = false; - } - - /** - * Encrypts the payload for the INIT command - * @param initData the payload for the INIT command - * - * @return the encrypted buffer - */ - public byte[] oneShotEncrypt(byte[] initData) { - try { - iv = new byte[SC_BLOCK_SIZE]; - random.nextBytes(iv); - IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); - sessionEncKey = new SecretKeySpec(secret, "AES"); - sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding"); - sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec); - initData = sessionCipher.doFinal(initData); - byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length]; - encrypted[0] = (byte) publicKey.length; - System.arraycopy(publicKey, 0, encrypted, 1, publicKey.length); - System.arraycopy(iv, 0, encrypted, (1 + publicKey.length), iv.length); - System.arraycopy(initData, 0, encrypted, (1 + publicKey.length + iv.length), initData.length); - return encrypted; - } catch (Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - - /** - * Marks the SecureChannel as open. Only to be used when writing tests for the SecureChannel, in normal operation this - * would only make things wrong. - * - */ - void setOpen() { - open = true; - } - - /** - * Calculates a CMAC from the metadata and data provided and sets it as the IV for the next message. - * - * @param meta metadata - * @param data data - */ - private void updateIV(byte[] meta, byte[] data) { - try { - sessionMac.init(sessionMacKey); - sessionMac.update(meta, 0, meta.length); - sessionMac.update(data, 0, data.length); - sessionMac.doFinal(iv, 0); - } catch (Exception e) { - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } -} diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Select.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Select.java index f977d04..7de4e23 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Select.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Select.java @@ -1,6 +1,6 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import im.status.applet_installer_test.appletinstaller.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUCommand; public class Select { private static final int CLA = 0x00; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Status.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Status.java index d2e59dc..09ced90 100644 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Status.java +++ b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/Status.java @@ -1,6 +1,6 @@ package im.status.applet_installer_test.appletinstaller.apducommands; -import im.status.applet_installer_test.appletinstaller.APDUCommand; +import im.status.hardwallet_lite_android.io.APDUCommand; public class Status { private static final int CLA = 0x80; diff --git a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/WalletAppletCommandSet.java b/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/WalletAppletCommandSet.java deleted file mode 100644 index 60874b4..0000000 --- a/app/src/main/java/im/status/applet_installer_test/appletinstaller/apducommands/WalletAppletCommandSet.java +++ /dev/null @@ -1,499 +0,0 @@ -package im.status.applet_installer_test.appletinstaller.apducommands; - -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUException; -import im.status.applet_installer_test.appletinstaller.APDUResponse; -import im.status.applet_installer_test.appletinstaller.CardChannel; -import org.spongycastle.jce.interfaces.ECPrivateKey; -import org.spongycastle.jce.interfaces.ECPublicKey; -import org.spongycastle.util.encoders.Hex; - -import java.io.IOException; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.util.Arrays; - -/** - * This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md - * file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some - * pre/post processing. - */ -public class WalletAppletCommandSet { - static final byte INS_INIT = (byte) 0xFE; - static final byte INS_GET_STATUS = (byte) 0xF2; - static final byte INS_VERIFY_PIN = (byte) 0x20; - static final byte INS_CHANGE_PIN = (byte) 0x21; - static final byte INS_UNBLOCK_PIN = (byte) 0x22; - static final byte INS_LOAD_KEY = (byte) 0xD0; - static final byte INS_DERIVE_KEY = (byte) 0xD1; - static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2; - static final byte INS_REMOVE_KEY = (byte) 0xD3; - static final byte INS_SIGN = (byte) 0xC0; - static final byte INS_SET_PINLESS_PATH = (byte) 0xC1; - static final byte INS_EXPORT_KEY = (byte) 0xC2; - - static final byte GET_STATUS_P1_APPLICATION = 0x00; - - static final byte LOAD_KEY_P1_EC = 0x01; - static final byte LOAD_KEY_P1_EXT_EC = 0x02; - static final byte LOAD_KEY_P1_SEED = 0x03; - - static final byte DERIVE_P1_ASSISTED_MASK = 0x01; - static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00; - - static final byte DERIVE_P2_KEY_PATH = 0x00; - static final byte DERIVE_P2_PUBLIC_KEY = 0x01; - - static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00; - static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01; - - static final byte TLV_PUB_KEY = (byte) 0x80; - static final byte TLV_PRIV_KEY = (byte) 0x81; - static final byte TLV_CHAIN_CODE = (byte) 0x82; - - public static final String APPLET_AID = "53746174757357616C6C6574417070"; - public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID); - - private final CardChannel apduChannel; - private SecureChannelSession secureChannel; - - public WalletAppletCommandSet(CardChannel apduChannel) { - this.apduChannel = apduChannel; - } - - public void setSecureChannel(SecureChannelSession secureChannel) { - this.secureChannel = secureChannel; - } - - /** - * Selects the applet. 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 { - if (secureChannel != null) { - secureChannel.reset(); - } - - APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES); - return apduChannel.send(selectApplet); - } - - /** - * 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 { - secureChannel.autoOpenSecureChannel(apduChannel); - } - - /** - * Automatically pairs. Calls the corresponding method of the SecureChannel class. - * - * @throws IOException communication error - */ - public void autoPair(byte[] sharedSecret) throws IOException { - secureChannel.autoPair(apduChannel, sharedSecret); - } - - /** - * Automatically unpairs. Calls the corresponding method of the SecureChannel class. - * - * @throws IOException communication error - */ - public void autoUnpair() throws IOException { - secureChannel.autoUnpair(apduChannel); - } - - /** - * Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class. - */ - public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException { - return secureChannel.openSecureChannel(apduChannel, index, data); - } - - /** - * Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class. - */ - public APDUResponse mutuallyAuthenticate() throws IOException { - return secureChannel.mutuallyAuthenticate(apduChannel); - } - - /** - * Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class. - */ - public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException { - return secureChannel.mutuallyAuthenticate(apduChannel, data); - } - - /** - * Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class. - */ - public APDUResponse pair(byte p1, byte[] data) throws IOException { - return secureChannel.pair(apduChannel, p1, data); - } - - /** - * Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class. - */ - public APDUResponse unpair(byte p1) throws IOException { - return secureChannel.unpair(apduChannel, p1); - } - - /** - * Unpair all other clients. - */ - public void unpairOthers() throws IOException, APDUException { - secureChannel.unpairOthers(apduChannel); - } - - /** - * Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet - * class itself. - * - * @param info the P1 of the APDU - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse getStatus(byte info) throws IOException { - APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]); - return secureChannel.transmit(apduChannel, getStatus); - } - - /** - * Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key - * derivation support. - * - * @return whether public key derivation is supported or not - * @throws IOException communication error - */ - public boolean getPublicKeyDerivationSupport() throws IOException { - APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION); - byte[] data = resp.getData(); - return data[data.length - 1] != 0x00; - } - - /** - * Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization - * status - * - * @return whether public key derivation is supported or not - * @throws IOException communication error - */ - public boolean getKeyInitializationStatus() throws IOException { - APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION); - byte[] data = resp.getData(); - return data[data.length - 4] != 0x00; - } - - /** - * Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU - * data. - * - * @param pin the pin - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse verifyPIN(String pin) throws IOException { - APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes()); - return secureChannel.transmit(apduChannel, verifyPIN); - } - - /** - * Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU - * data. - * - * @param pinType the PIN type - * @param pin the new PIN - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse changePIN(int pinType, String pin) throws IOException { - return changePIN(pinType, pin.getBytes()); - } - - /** - * Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU - * data. - * - * @param pinType the PIN type - * @param pin the new PIN - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse changePIN(int pinType, byte[] pin) throws IOException { - APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin); - return secureChannel.transmit(apduChannel, changePIN); - } - - /** - * Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure - * channel and used as APDU data. - * - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse unblockPIN(String puk, String newPin) throws IOException { - APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes()); - return secureChannel.transmit(apduChannel, unblockPIN); - } - - /** - * Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of - * the command is set to LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation. - * The loaded keyset is extended and support further key derivation. - * - * @param aPrivate a private key - * @param chainCode the chain code - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse loadKey(PrivateKey aPrivate, byte[] chainCode) throws IOException { - byte[] privateKey = ((ECPrivateKey) aPrivate).getD().toByteArray(); - - int privLen = privateKey.length; - int privOff = 0; - - if(privateKey[0] == 0x00) { - privOff++; - privLen--; - } - - byte[] data = new byte[chainCode.length + privLen]; - System.arraycopy(privateKey, privOff, data, 0, privLen); - System.arraycopy(chainCode, 0, data, privLen, chainCode.length); - - return loadKey(data, LOAD_KEY_P1_SEED); - } - - /** - * Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that - * the card will not be able to do further key derivation. - * - * @param ecKeyPair a key pair - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse loadKey(KeyPair ecKeyPair) throws IOException { - return loadKey(ecKeyPair, false, null); - } - - /** - * Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value - * of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically - * to either LOAD_KEY_P1_EC or LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode. - * - * @param keyPair a key pair - * @param omitPublicKey whether the public key is sent or not - * @param chainCode the chain code - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws IOException { - byte[] publicKey = omitPublicKey ? null : ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false); - byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(); - - return loadKey(publicKey, privateKey, chainCode); - } - - /** - * Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is - * included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or - * LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode. - * - * @param publicKey a raw public key - * @param privateKey a raw private key - * @param chainCode the chain code - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException { - int privLen = privateKey.length; - int privOff = 0; - - if(privateKey[0] == 0x00) { - privOff++; - privLen--; - } - - int off = 0; - int totalLength = publicKey == null ? 0 : (publicKey.length + 2); - totalLength += (privLen + 2); - totalLength += chainCode == null ? 0 : (chainCode.length + 2); - - if (totalLength > 127) { - totalLength += 3; - } else { - totalLength += 2; - } - - byte[] data = new byte[totalLength]; - data[off++] = (byte) 0xA1; - - if (totalLength > 127) { - data[off++] = (byte) 0x81; - data[off++] = (byte) (totalLength - 3); - } else { - data[off++] = (byte) (totalLength - 2); - } - - if (publicKey != null) { - data[off++] = TLV_PUB_KEY; - data[off++] = (byte) publicKey.length; - System.arraycopy(publicKey, 0, data, off, publicKey.length); - off += publicKey.length; - } - - data[off++] = TLV_PRIV_KEY; - data[off++] = (byte) privLen; - System.arraycopy(privateKey, privOff, data, off, privLen); - off += privLen; - - byte p1; - - if (chainCode != null) { - p1 = LOAD_KEY_P1_EXT_EC; - data[off++] = (byte) TLV_CHAIN_CODE; - data[off++] = (byte) chainCode.length; - System.arraycopy(chainCode, 0, data, off, chainCode.length); - } else { - p1 = LOAD_KEY_P1_EC; - } - - return loadKey(data, p1); - } - - /** - * Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1. - * - * @param data key data - * @param keyType the P1 parameter - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse loadKey(byte[] data, byte keyType) throws IOException { - APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data); - return secureChannel.transmit(apduChannel, loadKey); - } - - /** - * Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1. - * - * @param cs the P1 parameter - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse generateMnemonic(int cs) throws IOException { - APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]); - return secureChannel.transmit(apduChannel, generateMnemonic); - } - - /** - * Sends a REMOVE KEY APDU. - * - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse removeKey() throws IOException { - APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]); - return secureChannel.transmit(apduChannel, removeKey); - } - - /** - * Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form - * the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is - * generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and - * isLast will always be true at the same time. - * - * @param data the data to sign - * @param dataType the P1 parameter - * @param isFirst whether this is the first block of the command or not - * @param isLast whether this is the last block of the command or not - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws IOException { - byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00)); - APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, dataType, p2, data); - return secureChannel.transmit(apduChannel, sign); - } - - /** - * Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 and P2 parameters are forced to 0, meaning - * that the derivation starts from the master key and is non-assisted. - * - * @param data the raw key path - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse deriveKey(byte[] data) throws IOException { - return deriveKey(data, DERIVE_P1_SOURCE_MASTER, false, false); - } - - /** - * Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The reset and assisted parameters are combined to - * form P1. The isPublicKey parameter is used for P2. - * - * @param data the raw key path or a public key - * @param source the source to start derivation - * @param assisted whether we are doing assisted derivation or not - * @param isPublicKey whether we are sending a public key or a key path (only make sense during assisted derivation) - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse deriveKey(byte[] data, int source, boolean assisted, boolean isPublicKey) throws IOException { - byte p1 = assisted ? DERIVE_P1_ASSISTED_MASK : 0; - p1 |= source; - byte p2 = isPublicKey ? DERIVE_P2_PUBLIC_KEY : DERIVE_P2_KEY_PATH; - - APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, p1, p2, data); - return secureChannel.transmit(apduChannel, deriveKey); - } - - /** - * Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is. - * - * @param data the raw key path - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse setPinlessPath(byte [] data) throws IOException { - APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data); - return secureChannel.transmit(apduChannel, setPinlessPath); - } - - /** - * Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself - * - * @param keyPathIndex the P1 parameter - * @param publicOnly the P2 parameter - * @return the raw card response - * @throws IOException communication error - */ - public APDUResponse exportKey(byte keyPathIndex, boolean publicOnly) throws IOException { - byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC; - APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]); - return secureChannel.transmit(apduChannel, exportKey); - } - - /** - * Sends the INIT command to the card. - * - * @param pin the PIN - * @param puk the PUK - * @param sharedSecret the shared secret for pairing - * @return the raw card response - * @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); - System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length()); - System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length); - APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData)); - return apduChannel.send(init); - } -} diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdateTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdateTest.java index 8bf37d6..baf9707 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdateTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InitializeUpdateTest.java @@ -4,9 +4,6 @@ import org.junit.Test; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUException; -import im.status.applet_installer_test.appletinstaller.APDUResponse; import im.status.applet_installer_test.appletinstaller.HexUtils; import im.status.applet_installer_test.appletinstaller.Keys; diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstallTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstallTest.java index 47b0f02..68befd0 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstallTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForInstallTest.java @@ -4,8 +4,6 @@ import org.junit.Test; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUCommandTest; import im.status.applet_installer_test.appletinstaller.HexUtils; import static org.junit.Assert.*; diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoadTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoadTest.java index 46751a9..40cb2e9 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoadTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/InstallForLoadTest.java @@ -4,7 +4,6 @@ import org.junit.Test; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; import im.status.applet_installer_test.appletinstaller.HexUtils; import static org.junit.Assert.*; diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/LoadTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/LoadTest.java index 43a6605..ce1c573 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/LoadTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/LoadTest.java @@ -3,13 +3,6 @@ package im.status.applet_installer_test.appletinstaller.apducommands; import org.junit.Test; import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; - -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.HexUtils; - -import static org.junit.Assert.*; public class LoadTest { @Test diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/SelectTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/SelectTest.java index 479a053..9e29a72 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/SelectTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/SelectTest.java @@ -4,7 +4,6 @@ import org.junit.Test; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; import im.status.applet_installer_test.appletinstaller.HexUtils; import static org.junit.Assert.*; diff --git a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/StatusTest.java b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/StatusTest.java index d402123..a671104 100644 --- a/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/StatusTest.java +++ b/app/src/test/java/im/status/applet_installer_test/appletinstaller/apducommands/StatusTest.java @@ -4,8 +4,6 @@ import org.junit.Test; import java.io.IOException; -import im.status.applet_installer_test.appletinstaller.APDUCommand; -import im.status.applet_installer_test.appletinstaller.APDUWrapper; import im.status.applet_installer_test.appletinstaller.HexUtils; import static org.junit.Assert.*; diff --git a/build.gradle b/build.gradle index 077cb2f..bef99bb 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ allprojects { repositories { google() jcenter() + maven { url 'https://jitpack.io' } } }