aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java448
1 files changed, 448 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java
new file mode 100644
index 000000000..0a7b7611b
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java
@@ -0,0 +1,448 @@
+package org.sufficientlysecure.keychain.ui.base;
+
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
+import org.sufficientlysecure.keychain.ui.NfcOperationActivity;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.util.Iso7816TLV;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Passphrase;
+import org.sufficientlysecure.keychain.util.Preferences;
+
+
+public abstract class BaseNfcActivity extends BaseActivity {
+
+ public static final int REQUEST_CODE_PASSPHRASE = 1;
+
+ protected Passphrase mPin;
+ private NfcAdapter mNfcAdapter;
+ private IsoDep mIsoDep;
+
+ private static final int TIMEOUT = 100000;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
+ throw new AssertionError("should not happen: NfcOperationActivity.onCreate is called instead of onNewIntent!");
+ }
+
+ }
+
+ /**
+ * This activity is started as a singleTop activity.
+ * All new NFC Intents which are delivered to this activity are handled here
+ */
+ @Override
+ public void onNewIntent(Intent intent) {
+ if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
+ try {
+ handleNdefDiscoveredIntent(intent);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Connection error!", e);
+ toast("Connection Error: " + e.getMessage());
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+ }
+
+ /**
+ * Called when the system is about to start resuming a previous activity,
+ * disables NFC Foreground Dispatch
+ */
+ public void onPause() {
+ super.onPause();
+ Log.d(Constants.TAG, "NfcOperationActivity.onPause");
+
+ disableNfcForegroundDispatch();
+ }
+
+ /**
+ * Called when the activity will start interacting with the user,
+ * enables NFC Foreground Dispatch
+ */
+ public void onResume() {
+ super.onResume();
+ Log.d(Constants.TAG, "NfcOperationActivity.onResume");
+
+ enableNfcForegroundDispatch();
+ }
+
+ protected void obtainYubikeyPin(RequiredInputParcel requiredInput) {
+
+ Preferences prefs = Preferences.getPreferences(this);
+ if (prefs.useDefaultYubikeyPin()) {
+ mPin = new Passphrase("123456");
+ return;
+ }
+
+ Intent intent = new Intent(this, PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
+ RequiredInputParcel.createRequiredPassphrase(requiredInput));
+ startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
+
+ }
+
+ protected void setYubikeyPin(Passphrase pin) {
+ mPin = pin;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PASSPHRASE:
+ CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_DATA);
+ mPin = input.getPassphrase();
+ break;
+
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /** Handle NFC communication and return a result.
+ *
+ * This method is called by onNewIntent above upon discovery of an NFC tag.
+ * It handles initialization and login to the application, subsequently
+ * calls either nfcCalculateSignature() or nfcDecryptSessionKey(), then
+ * finishes the activity with an appropiate result.
+ *
+ * On general communication, see also
+ * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
+ *
+ * References to pages are generally related to the OpenPGP Application
+ * on ISO SmartCard Systems specification.
+ *
+ */
+ protected void handleNdefDiscoveredIntent(Intent intent) throws IOException {
+
+ Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+
+ // Connect to the detected tag, setting a couple of settings
+ mIsoDep = IsoDep.get(detectedTag);
+ mIsoDep.setTimeout(TIMEOUT); // timeout is set to 100 seconds to avoid cancellation during calculation
+ mIsoDep.connect();
+
+ // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
+ // See specification, page 51
+ String accepted = "9000";
+
+ // Command APDU (page 51) for SELECT FILE command (page 29)
+ String opening =
+ "00" // CLA
+ + "A4" // INS
+ + "04" // P1
+ + "00" // P2
+ + "06" // Lc (number of bytes)
+ + "D27600012401" // Data (6 bytes)
+ + "00"; // Le
+ if ( ! nfcCommunicate(opening).equals(accepted)) { // activate connection
+ toast("Opening Error!");
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ if (mPin != null) {
+
+ byte[] pin = new String(mPin.getCharArray()).getBytes();
+
+ // Command APDU for VERIFY command (page 32)
+ String login =
+ "00" // CLA
+ + "20" // INS
+ + "00" // P1
+ + "82" // P2 (PW1)
+ + String.format("%02x", pin.length) // Lc
+ + Hex.toHexString(pin);
+ if (!nfcCommunicate(login).equals(accepted)) { // login
+ toast("Wrong PIN!");
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ }
+
+ onNfcPerform();
+
+ mIsoDep.close();
+ mIsoDep = null;
+
+ }
+
+ protected abstract void onNfcPerform() throws IOException;
+
+ /** Return the key id from application specific data stored on tag, or null
+ * if it doesn't exist.
+ *
+ * @param idx Index of the key to return the fingerprint from.
+ * @return The long key id of the requested key, or null if not found.
+ */
+ public Long nfcGetKeyId(int idx) throws IOException {
+ byte[] fp = nfcGetFingerprint(idx);
+ if (fp == null) {
+ return null;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(fp);
+ // skip first 12 bytes of the fingerprint
+ buf.position(12);
+ // the last eight bytes are the key id (big endian, which is default order in ByteBuffer)
+ return buf.getLong();
+ }
+
+ /** Return fingerprints of all keys from application specific data stored
+ * on tag, or null if data not available.
+ *
+ * @return The fingerprints of all subkeys in a contiguous byte array.
+ */
+ public byte[] nfcGetFingerprints() throws IOException {
+ String data = "00CA006E00";
+ byte[] buf = mIsoDep.transceive(Hex.decode(data));
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
+ Log.d(Constants.TAG, "nfc tlv data:\n" + tlv.prettyPrint());
+
+ Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
+ if (fptlv == null) {
+ return null;
+ }
+
+ return fptlv.mV;
+ }
+
+ /** Return the fingerprint from application specific data stored on tag, or
+ * null if it doesn't exist.
+ *
+ * @param idx Index of the key to return the fingerprint from.
+ * @return The fingerprint of the requested key, or null if not found.
+ */
+ public byte[] nfcGetFingerprint(int idx) throws IOException {
+ byte[] data = nfcGetFingerprints();
+
+ // return the master key fingerprint
+ ByteBuffer fpbuf = ByteBuffer.wrap(data);
+ byte[] fp = new byte[20];
+ fpbuf.position(idx*20);
+ fpbuf.get(fp, 0, 20);
+
+ return fp;
+ }
+
+
+ public String nfcGetUserId() throws IOException {
+ String info = "00CA006500";
+ String data = "00CA005E00";
+ return nfcGetHolderName(nfcCommunicate(info)) + " <" + (new String(Hex.decode(nfcGetDataField(
+ nfcCommunicate(data))))) + ">";
+ }
+
+ /**
+ * Calls to calculate the signature and returns the MPI value
+ *
+ * @param hash the hash for signing
+ * @return a big integer representing the MPI for the given hash
+ */
+ public byte[] nfcCalculateSignature(byte[] hash, int hashAlgo) throws IOException {
+
+ // dsi, including Lc
+ String dsi;
+
+ Log.i(Constants.TAG, "Hash: " + hashAlgo);
+ switch (hashAlgo) {
+ case HashAlgorithmTags.SHA1:
+ if (hash.length != 20) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 10!");
+ }
+ dsi = "23" // Lc
+ + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
+ + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
+ + "0605" + "2B0E03021A" // OID of SHA1
+ + "0500" // TLV coding of ZERO
+ + "0414" + getHex(hash); // 0x14 are 20 hash bytes
+ break;
+ case HashAlgorithmTags.RIPEMD160:
+ if (hash.length != 20) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 20!");
+ }
+ dsi = "233021300906052B2403020105000414" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA224:
+ if (hash.length != 28) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 28!");
+ }
+ dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA256:
+ if (hash.length != 32) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 32!");
+ }
+ dsi = "333031300D060960864801650304020105000420" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA384:
+ if (hash.length != 48) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 48!");
+ }
+ dsi = "433041300D060960864801650304020205000430" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA512:
+ if (hash.length != 64) {
+ throw new RuntimeException("Bad hash length (" + hash.length + ", expected 64!");
+ }
+ dsi = "533051300D060960864801650304020305000440" + getHex(hash);
+ break;
+ default:
+ throw new RuntimeException("Not supported hash algo!");
+ }
+
+ // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
+ String apdu =
+ "002A9E9A" // CLA, INS, P1, P2
+ + dsi // digital signature input
+ + "00"; // Le
+
+ String response = nfcCommunicate(apdu);
+
+ // split up response into signature and status
+ String status = response.substring(response.length()-4);
+ String signature = response.substring(0, response.length() - 4);
+
+ // while we are getting 0x61 status codes, retrieve more data
+ while (status.substring(0, 2).equals("61")) {
+ Log.d(Constants.TAG, "requesting more data, status " + status);
+ // Send GET RESPONSE command
+ response = nfcCommunicate("00C00000" + status.substring(2));
+ status = response.substring(response.length()-4);
+ signature += response.substring(0, response.length()-4);
+ }
+
+ Log.d(Constants.TAG, "final response:" + status);
+
+ if ( ! "9000".equals(status)) {
+ toast("Bad NFC response code: " + status);
+ return null;
+ }
+
+ // Make sure the signature we received is actually the expected number of bytes long!
+ if (signature.length() != 256 && signature.length() != 512) {
+ toast("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
+ return null;
+ }
+
+ return Hex.decode(signature);
+ }
+
+ /**
+ * Calls to calculate the signature and returns the MPI value
+ *
+ * @param encryptedSessionKey the encoded session key
+ * @return the decoded session key
+ */
+ public byte[] nfcDecryptSessionKey(byte[] encryptedSessionKey) throws IOException {
+ String firstApdu = "102a8086fe";
+ String secondApdu = "002a808603";
+ String le = "00";
+
+ byte[] one = new byte[254];
+ // leave out first byte:
+ System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
+
+ byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
+ for (int i = 0; i < two.length; i++) {
+ two[i] = encryptedSessionKey[i + one.length + 1];
+ }
+
+ String first = nfcCommunicate(firstApdu + getHex(one));
+ String second = nfcCommunicate(secondApdu + getHex(two) + le);
+
+ String decryptedSessionKey = nfcGetDataField(second);
+
+ Log.d(Constants.TAG, "decryptedSessionKey: " + decryptedSessionKey);
+
+ return Hex.decode(decryptedSessionKey);
+ }
+
+ /**
+ * Prints a message to the screen
+ *
+ * @param text the text which should be contained within the toast
+ */
+ protected void toast(String text) {
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show();
+ }
+
+ /**
+ * Receive new NFC Intents to this activity only by enabling foreground dispatch.
+ * This can only be done in onResume!
+ */
+ public void enableNfcForegroundDispatch() {
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ Intent nfcI = new Intent(this, NfcOperationActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent nfcPendingIntent = PendingIntent.getActivity(this, 0, nfcI, PendingIntent.FLAG_CANCEL_CURRENT);
+ IntentFilter[] writeTagFilters = new IntentFilter[]{
+ new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
+ };
+
+ // https://code.google.com/p/android/issues/detail?id=62918
+ // maybe mNfcAdapter.enableReaderMode(); ?
+ try {
+ mNfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, writeTagFilters, null);
+ } catch (IllegalStateException e) {
+ Log.i(Constants.TAG, "NfcForegroundDispatch Error!", e);
+ }
+ Log.d(Constants.TAG, "NfcForegroundDispatch has been enabled!");
+ }
+
+ /**
+ * Disable foreground dispatch in onPause!
+ */
+ public void disableNfcForegroundDispatch() {
+ mNfcAdapter.disableForegroundDispatch(this);
+ Log.d(Constants.TAG, "NfcForegroundDispatch has been disabled!");
+ }
+
+ public String nfcGetHolderName(String name) {
+ String slength;
+ int ilength;
+ name = name.substring(6);
+ slength = name.substring(0, 2);
+ ilength = Integer.parseInt(slength, 16) * 2;
+ name = name.substring(2, ilength + 2);
+ name = (new String(Hex.decode(name))).replace('<', ' ');
+ return (name);
+ }
+
+ private String nfcGetDataField(String output) {
+ return output.substring(0, output.length() - 4);
+ }
+
+ public String nfcCommunicate(String apdu) throws IOException {
+ return getHex(mIsoDep.transceive(Hex.decode(apdu)));
+ }
+
+ public static String getHex(byte[] raw) {
+ return new String(Hex.encode(raw));
+ }
+
+}