diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index ec6d2c35d..6b957b622 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -19,7 +19,6 @@ dependencies { compile project(':extern:minidns') compile project(':extern:KeybaseLib:Lib') compile project(':extern:TokenAutoComplete:library') - compile project(':extern:openpgp-card-nfc-lib:library') compile project(':extern:safeslinger-exchange') } diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index c78e80c19..a3aa0d2a4 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -619,6 +619,9 @@ + diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPGPContentSignerBuilder.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPGPContentSignerBuilder.java new file mode 100644 index 000000000..e0286ec15 --- /dev/null +++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPGPContentSignerBuilder.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann + * Copyright (c) 2000-2013 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) + * + * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. + */ + +package org.spongycastle.openpgp.operator.jcajce; + +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.operator.PGPContentSigner; +import org.spongycastle.openpgp.operator.PGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.PGPDigestCalculator; + +import java.io.OutputStream; +import java.security.Provider; +import java.util.Date; + +/** + * This class is based on JcaPGPContentSignerBuilder. + * + * Instead of using a Signature object based on a privateKey, this class only calculates the digest + * of the output stream and gives the result back using a RuntimeException. + */ +public class NfcSyncPGPContentSignerBuilder + implements PGPContentSignerBuilder +{ + private JcaPGPDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaPGPDigestCalculatorProviderBuilder(); + private int hashAlgorithm; + private int keyAlgorithm; + private long keyID; + + private byte[] signedHash; + private Date creationTimestamp; + + public static class NfcInteractionNeeded extends RuntimeException + { + public byte[] hashToSign; + public Date creationTimestamp; + public int hashAlgo; + + public NfcInteractionNeeded(byte[] hashToSign, int hashAlgo, Date creationTimestamp) + { + super("NFC interaction required!"); + this.hashToSign = hashToSign; + this.hashAlgo = hashAlgo; + this.creationTimestamp = creationTimestamp; + } + } + + public NfcSyncPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm, long keyID, byte[] signedHash, Date creationTimestamp) + { + this.keyAlgorithm = keyAlgorithm; + this.hashAlgorithm = hashAlgorithm; + this.keyID = keyID; + this.signedHash = signedHash; + this.creationTimestamp = creationTimestamp; + } + + public NfcSyncPGPContentSignerBuilder setProvider(Provider provider) + { + digestCalculatorProviderBuilder.setProvider(provider); + + return this; + } + + public NfcSyncPGPContentSignerBuilder setProvider(String providerName) + { + digestCalculatorProviderBuilder.setProvider(providerName); + + return this; + } + + public NfcSyncPGPContentSignerBuilder setDigestProvider(Provider provider) + { + digestCalculatorProviderBuilder.setProvider(provider); + + return this; + } + + public NfcSyncPGPContentSignerBuilder setDigestProvider(String providerName) + { + digestCalculatorProviderBuilder.setProvider(providerName); + + return this; + } + + public PGPContentSigner build(final int signatureType, PGPPrivateKey privateKey) + throws PGPException { + // NOTE: privateKey is null in this case! + return build(signatureType, keyID); + } + + public PGPContentSigner build(final int signatureType, final long keyID) + throws PGPException + { + final PGPDigestCalculator digestCalculator = digestCalculatorProviderBuilder.build().get(hashAlgorithm); + + return new PGPContentSigner() + { + public int getType() + { + return signatureType; + } + + public int getHashAlgorithm() + { + return hashAlgorithm; + } + + public int getKeyAlgorithm() + { + return keyAlgorithm; + } + + public long getKeyID() + { + return keyID; + } + + public OutputStream getOutputStream() + { + return digestCalculator.getOutputStream(); + } + + public byte[] getSignature() { + if (signedHash != null) { + // we already have the signed hash from a previous execution, return this! + return signedHash; + } else { + // catch this when signatureGenerator.generate() is executed and divert digest to card, + // when doing the operation again reuse creationTimestamp (this will be hashed) + throw new NfcInteractionNeeded(digestCalculator.getDigest(), getHashAlgorithm(), creationTimestamp); + } + } + + public byte[] getDigest() + { + return digestCalculator.getDigest(); + } + }; + } +} diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java new file mode 100644 index 000000000..ffa154876 --- /dev/null +++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java @@ -0,0 +1,275 @@ +/** + * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann + * + * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. + */ + +package org.spongycastle.openpgp.operator.jcajce; + +import org.spongycastle.bcpg.PublicKeyAlgorithmTags; +import org.spongycastle.jcajce.util.DefaultJcaJceHelper; +import org.spongycastle.jcajce.util.NamedJcaJceHelper; +import org.spongycastle.jcajce.util.ProviderJcaJceHelper; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.operator.PGPDataDecryptor; +import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; + +import java.security.Provider; + +/** + * This class is based on JcePublicKeyDataDecryptorFactoryBuilder + * + */ +public class NfcSyncPublicKeyDataDecryptorFactoryBuilder +{ + private OperatorHelper helper = new OperatorHelper(new DefaultJcaJceHelper()); + private OperatorHelper contentHelper = new OperatorHelper(new DefaultJcaJceHelper()); + private JcaPGPKeyConverter keyConverter = new JcaPGPKeyConverter(); +// private JcaPGPDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaPGPDigestCalculatorProviderBuilder(); +// private JcaKeyFingerprintCalculator fingerprintCalculator = new JcaKeyFingerprintCalculator(); + + public static class NfcInteractionNeeded extends RuntimeException + { + public byte[] encryptedSessionKey; + + public NfcInteractionNeeded(byte[] encryptedSessionKey) + { + super("NFC interaction required!"); + this.encryptedSessionKey = encryptedSessionKey; + } + } + + public NfcSyncPublicKeyDataDecryptorFactoryBuilder() + { + } + + /** + * Set the provider object to use for creating cryptographic primitives in the resulting factory the builder produces. + * + * @param provider provider object for cryptographic primitives. + * @return the current builder. + */ + public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(Provider provider) + { + this.helper = new OperatorHelper(new ProviderJcaJceHelper(provider)); + keyConverter.setProvider(provider); + this.contentHelper = helper; + + return this; + } + + /** + * Set the provider name to use for creating cryptographic primitives in the resulting factory the builder produces. + * + * @param providerName the name of the provider to reference for cryptographic primitives. + * @return the current builder. + */ + public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(String providerName) + { + this.helper = new OperatorHelper(new NamedJcaJceHelper(providerName)); + keyConverter.setProvider(providerName); + this.contentHelper = helper; + + return this; + } + + public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(Provider provider) + { + this.contentHelper = new OperatorHelper(new ProviderJcaJceHelper(provider)); + + return this; + } + + public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(String providerName) + { + this.contentHelper = new OperatorHelper(new NamedJcaJceHelper(providerName)); + + return this; + } + + public PublicKeyDataDecryptorFactory build(final byte[] nfcDecrypted) { + return new PublicKeyDataDecryptorFactory() + { + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) + throws PGPException + { + if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) + { + throw new PGPException("ECDH not supported!"); + } + + return decryptSessionData(keyAlgorithm, secKeyData, nfcDecrypted); + } + + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException + { + return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + }; + } + +// public PublicKeyDataDecryptorFactory build(final PrivateKey privKey) +// { +// return new PublicKeyDataDecryptorFactory() +// { +// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) +// throws PGPException +// { +// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) +// { +// throw new PGPException("ECDH requires use of PGPPrivateKey for decryption"); +// } +// return decryptSessionData(keyAlgorithm, privKey, secKeyData); +// } +// +// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) +// throws PGPException +// { +// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); +// } +// }; +// } + +// public PublicKeyDataDecryptorFactory build(final PGPPrivateKey privKey, final byte[] nfcDecrypted) +// { +// return new PublicKeyDataDecryptorFactory() +// { +// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) +// throws PGPException +// { +// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) +// { +// return decryptSessionData(privKey.getPrivateKeyDataPacket(), privKey.getPublicKeyPacket(), secKeyData); +// } +// +// return decryptSessionData(keyAlgorithm, keyConverter.getPrivateKey(privKey), secKeyData, nfcDecrypted); +// } +// +// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) +// throws PGPException +// { +// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); +// } +// }; +// } + +// private byte[] decryptSessionData(BCPGKey privateKeyPacket, PublicKeyPacket pubKeyData, byte[][] secKeyData) +// throws PGPException +// { +// ECDHPublicBCPGKey ecKey = (ECDHPublicBCPGKey)pubKeyData.getKey(); +// X9ECParameters x9Params = NISTNamedCurves.getByOID(ecKey.getCurveOID()); +// +// byte[] enc = secKeyData[0]; +// +// int pLen = ((((enc[0] & 0xff) << 8) + (enc[1] & 0xff)) + 7) / 8; +// byte[] pEnc = new byte[pLen]; +// +// System.arraycopy(enc, 2, pEnc, 0, pLen); +// +// byte[] keyEnc = new byte[enc[pLen + 2]]; +// +// System.arraycopy(enc, 2 + pLen + 1, keyEnc, 0, keyEnc.length); +// +// Cipher c = helper.createKeyWrapper(ecKey.getSymmetricKeyAlgorithm()); +// +// ECPoint S = x9Params.getCurve().decodePoint(pEnc).multiply(((ECSecretBCPGKey)privateKeyPacket).getX()).normalize(); +// +// RFC6637KDFCalculator rfc6637KDFCalculator = new RFC6637KDFCalculator(digestCalculatorProviderBuilder.build().get(ecKey.getHashAlgorithm()), ecKey.getSymmetricKeyAlgorithm()); +// Key key = new SecretKeySpec(rfc6637KDFCalculator.createKey(ecKey.getCurveOID(), S, fingerprintCalculator.calculateFingerprint(pubKeyData)), "AESWrap"); +// +// try +// { +// c.init(Cipher.UNWRAP_MODE, key); +// +// Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); +// +// return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); +// } +// catch (InvalidKeyException e) +// { +// throw new PGPException("error setting asymmetric cipher", e); +// } +// catch (NoSuchAlgorithmException e) +// { +// throw new PGPException("error setting asymmetric cipher", e); +// } +// } + + private byte[] decryptSessionData(int keyAlgorithm, byte[][] secKeyData, byte[] nfcDecrypted) + throws PGPException + { +// Cipher c1 = helper.createPublicKeyCipher(keyAlgorithm); +// +// try +// { +// c1.init(Cipher.DECRYPT_MODE, privKey); +// } +// catch (InvalidKeyException e) +// { +// throw new PGPException("error setting asymmetric cipher", e); +// } + + if (keyAlgorithm == PGPPublicKey.RSA_ENCRYPT + || keyAlgorithm == PGPPublicKey.RSA_GENERAL) + { + byte[] bi = secKeyData[0]; // encoded MPI + + if (nfcDecrypted != null) { + // we already have the decrypted bytes from a previous execution, return this! + return nfcDecrypted; + } else { + // catch this when decryptSessionData() is executed and divert digest to card, + // when doing the operation again reuse nfcDecrypted + throw new NfcInteractionNeeded(bi); + } + +// c1.update(bi, 2, bi.length - 2); + } + else + { + throw new PGPException("ElGamal not supported!"); + +// ElGamalKey k = (ElGamalKey)privKey; +// int size = (k.getParameters().getP().bitLength() + 7) / 8; +// byte[] tmp = new byte[size]; +// +// byte[] bi = secKeyData[0]; // encoded MPI +// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but... +// { +// c1.update(bi, 3, bi.length - 3); +// } +// else +// { +// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2); +// c1.update(tmp); +// } +// +// bi = secKeyData[1]; // encoded MPI +// for (int i = 0; i != tmp.length; i++) +// { +// tmp[i] = 0; +// } +// +// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but... +// { +// c1.update(bi, 3, bi.length - 3); +// } +// else +// { +// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2); +// c1.update(tmp); +// } + } + +// try +// { +// return c1.doFinal(); +// } +// catch (Exception e) +// { +// throw new PGPException("exception decrypting session data", e); +// } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 1b4ad1fc1..8a115b245 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -31,10 +31,9 @@ import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpApi; import org.spongycastle.util.encoders.Hex; -import org.sufficientlysecure.keychain.nfc.NfcActivity; +import org.sufficientlysecure.keychain.ui.NfcActivity; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; -import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface.NoSecretKeyException; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java index c9cfb9e0c..dd035c78f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -30,7 +30,6 @@ import android.widget.TextView; import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.nfc.NfcActivity; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.service.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java index 17d4865fd..c1c3409f8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java @@ -2,10 +2,6 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; -import org.sufficientlysecure.keychain.nfc.NfcActivity; - -import java.util.Date; - public class EncryptActivity extends DrawerActivity { public static final int REQUEST_CODE_PASSPHRASE = 0x00008001; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcActivity.java new file mode 100644 index 000000000..f466a7f1d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcActivity.java @@ -0,0 +1,498 @@ +/** + * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann + * + * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. + */ + +package org.sufficientlysecure.keychain.ui; + +import android.annotation.TargetApi; +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.Build; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.WindowManager; +import android.widget.Toast; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.R; + +import java.io.IOException; +import java.util.Locale; + +/** + * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant + * NFC devices. + * + * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf + */ +@TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) +public class NfcActivity extends ActionBarActivity { + private static final String TAG = "Keychain"; + + // actions + public static final String ACTION_SIGN_HASH = "sign_hash"; + public static final String ACTION_DECRYPT_SESSION_KEY = "decrypt_session_key"; + + // always + public static final String EXTRA_PIN = "pin"; + // special extra for OpenPgpService + public static final String EXTRA_DATA = "data"; + + // sign + public static final String EXTRA_NFC_HASH_TO_SIGN = "nfc_hash"; + public static final String EXTRA_NFC_HASH_ALGO = "nfc_hash_algo"; + + // decrypt + public static final String EXTRA_NFC_ENC_SESSION_KEY = "encrypted_session_key"; + + private Intent mServiceIntent; + + private static final int TIMEOUT = 100000; + + private NfcAdapter mNfcAdapter; + private IsoDep mIsoDep; + private String mAction; + + private String mPin; + + // sign + private byte[] mHashToSign; + private int mHashAlgo; + + // decrypt + private byte[] mEncryptedSessionKey; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "NfcActivity.onCreate"); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + setContentView(R.layout.nfc_activity); + + Intent intent = getIntent(); + Bundle data = intent.getExtras(); + String action = intent.getAction(); + + if (ACTION_SIGN_HASH.equals(action)) { + mAction = action; + mPin = data.getString(EXTRA_PIN); + mHashToSign = data.getByteArray(EXTRA_NFC_HASH_TO_SIGN); + mHashAlgo = data.getInt(EXTRA_NFC_HASH_ALGO); + mServiceIntent = data.getParcelable(EXTRA_DATA); + + Log.d(TAG, "NfcActivity mAction: " + mAction); + Log.d(TAG, "NfcActivity mPin: " + mPin); + Log.d(TAG, "NfcActivity mHashToSign as hex: " + getHex(mHashToSign)); + Log.d(TAG, "NfcActivity mServiceIntent: " + mServiceIntent.toString()); + } else if (ACTION_DECRYPT_SESSION_KEY.equals(action)) { + mAction = action; + mPin = data.getString(EXTRA_PIN); + mEncryptedSessionKey = data.getByteArray(EXTRA_NFC_ENC_SESSION_KEY); + mServiceIntent = data.getParcelable(EXTRA_DATA); + + Log.d(TAG, "NfcActivity mAction: " + mAction); + Log.d(TAG, "NfcActivity mPin: " + mPin); + Log.d(TAG, "NfcActivity mEncryptedSessionKey as hex: " + getHex(mEncryptedSessionKey)); + Log.d(TAG, "NfcActivity mServiceIntent: " + mServiceIntent.toString()); + } else if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) { + Log.e(TAG, "This should not happen, but there is a bug in Android!"); + + toast("This should not happen, but there is a bug in Android! Clear all app tasks and start app from launcher again!"); + finish(); + } else { + Log.d(TAG, "Action not supported: " + action); + } + } + + /** + * Called when the system is about to start resuming a previous activity, + * disables NFC Foreground Dispatch + */ + public void onPause() { + super.onPause(); + Log.d(TAG, "NfcActivity.onPause"); + + disableNfcForegroundDispatch(); + } + + /** + * Called when the activity will start interacting with the user, + * enables NFC Foreground Dispatch + */ + public void onResume() { + super.onResume(); + Log.d(TAG, "NfcActivity.onResume"); + + enableNfcForegroundDispatch(); + } + + /** + * This activity is started as a singleTop activity. + * All new NFC Intents which are delivered to this activity are handled here + */ + public void onNewIntent(Intent intent) { + if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) { + try { + handleNdefDiscoveredIntent(intent); + } catch (IOException e) { + Log.e(TAG, "Connection error!", e); + toast("Connection Error: " + e.getMessage()); + setResult(RESULT_CANCELED, mServiceIntent); + finish(); + } + } + } + + /** 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. + * + */ + private 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 ( ! card(opening).equals(accepted)) { // activate connection + toast("Opening Error!"); + setResult(RESULT_CANCELED, mServiceIntent); + finish(); + return; + } + + // Command APDU for VERIFY command (page 32) + String login = + "00" // CLA + + "20" // INS + + "00" // P1 + + "82" // P2 (PW1) + + "06" // Lc TODO should be length of PIN? + + Hex.toHexString(mPin.getBytes()); + if ( ! card(login).equals(accepted)) { // login + toast("Wrong PIN!"); + setResult(RESULT_CANCELED, mServiceIntent); + finish(); + return; + } + + if (ACTION_SIGN_HASH.equals(mAction)) { + // returns signed hash + byte[] signedHash = nfcCalculateSignature(mHashToSign, mHashAlgo); + + if (signedHash == null) { + setResult(RESULT_CANCELED, mServiceIntent); + finish(); + return; + } + + Log.d(TAG, "NfcActivity signedHash as hex: " + getHex(signedHash)); + + // give data through for new service call + // OpenPgpApi.EXTRA_NFC_SIGNED_HASH + mServiceIntent.putExtra("nfc_signed_hash", signedHash); + setResult(RESULT_OK, mServiceIntent); + finish(); + + } else if (ACTION_DECRYPT_SESSION_KEY.equals(mAction)) { + byte[] decryptedSessionKey = nfcDecryptSessionKey(mEncryptedSessionKey); + + // give data through for new service call + // OpenPgpApi.EXTRA_NFC_DECRYPTED_SESSION_KEY + mServiceIntent.putExtra("nfc_decrypted_session_key", decryptedSessionKey); + setResult(RESULT_OK, mServiceIntent); + finish(); + } + } + + /** + * Gets the user ID + * + * @return the user id as "name " + * @throws java.io.IOException + */ + public String getUserId() throws IOException { + String info = "00CA006500"; + String data = "00CA005E00"; + return getName(card(info)) + " <" + (new String(Hex.decode(getDataField(card(data))))) + ">"; + } + + /** + * Gets the long key ID + * + * @return the long key id (last 16 bytes of the fingerprint) + * @throws java.io.IOException + */ + public long getKeyId() throws IOException { + String keyId = getFingerprint().substring(24); + Log.d(TAG, "keyId: " + keyId); + return Long.parseLong(keyId, 16); + } + + /** + * Gets the fingerprint of the signature key + * + * @return the fingerprint + * @throws java.io.IOException + */ + public String getFingerprint() throws IOException { + String data = "00CA006E00"; + String fingerprint = card(data); + fingerprint = fingerprint.toLowerCase(Locale.ENGLISH); // better enforce lower case + fingerprint = fingerprint.substring(fingerprint.indexOf("c5") + 4, fingerprint.indexOf("c5") + 44); + + Log.d(TAG, "fingerprint: " + fingerprint); + + return fingerprint; + } + + /** + * 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 + * @throws java.io.IOException + */ + public byte[] nfcCalculateSignature(byte[] hash, int hashAlgo) throws IOException { + + // dsi, including Lc + String dsi; + + Log.i(TAG, "Hash: " + hashAlgo); + switch (hashAlgo) { + case HashAlgorithmTags.SHA1: + if (hash.length != 20) { + throw new RuntimeException("Bad hash length (" + hash.length + ", expected 10!"); + } + dsi = "30" // Lc TODO 0x30 == 48. shouldn't this be 35, for 15 header bytes + 20 hash? + + "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 = "303021300906052B2403020105000414" + getHex(hash); + break; + case HashAlgorithmTags.SHA224: + if (hash.length != 28) { + throw new RuntimeException("Bad hash length (" + hash.length + ", expected 28!"); + } + dsi = "34302D300D06096086480165030402040500041C" + 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 = "343041300D060960864801650304020205000430" + getHex(hash); + break; + case HashAlgorithmTags.SHA512: + if (hash.length != 64) { + throw new RuntimeException("Bad hash length (" + hash.length + ", expected 64!"); + } + dsi = "343051300D060960864801650304020305000440" + 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 = card(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(TAG, "requesting more data, status " + status); + // Send GET RESPONSE command + response = card("00C00000" + status.substring(2)); + status = response.substring(response.length()-4); + signature += response.substring(0, response.length()-4); + } + + Log.d(TAG, "final response:" + status); + + if ( ! status.equals("9000")) { + 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() != 512) { + toast("Bad signature length! Expected 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 + * @throws java.io.IOException + */ + 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 = card(firstApdu + getHex(one)); + String second = card(secondApdu + getHex(two) + le); + + String decryptedSessionKey = getDataField(second); + + Log.d(TAG, "decryptedSessionKey: " + decryptedSessionKey); + + return Hex.decode(decryptedSessionKey); + } + + /** + * Prints a message to the screen + * + * @param text the text which should be contained within the toast + */ + private 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, NfcActivity.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(TAG, "NfcForegroundDispatch Error!", e); + } + Log.d(TAG, "NfcForegroundDispatch has been enabled!"); + } + + /** + * Disable foreground dispatch in onPause! + */ + public void disableNfcForegroundDispatch() { + mNfcAdapter.disableForegroundDispatch(this); + Log.d(TAG, "NfcForegroundDispatch has been disabled!"); + } + + /** + * Gets the name of the user out of the raw card output regarding card holder related data + * + * @param name the raw card holder related data from the card + * @return the name given in this data + */ + public String getName(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); + } + + /** + * Reduces the raw data from the card by four characters + * + * @param output the raw data from the card + * @return the data field of that data + */ + private String getDataField(String output) { + return output.substring(0, output.length() - 4); + } + + /** + * Communicates with the OpenPgpCard via the APDU + * + * @param hex the hexadecimal APDU + * @return The answer from the card + * @throws java.io.IOException throws an exception if something goes wrong + */ + public String card(String hex) throws IOException { + return getHex(mIsoDep.transceive(Hex.decode(hex))); + } + + /** + * Converts a byte array into an hex string + * + * @param raw the byte array representation + * @return the hexadecimal string representation + */ + public static String getHex(byte[] raw) { + return new String(Hex.encode(raw)); + } +} diff --git a/OpenKeychain/src/main/res/drawable/yubikey_neo.png b/OpenKeychain/src/main/res/drawable/yubikey_neo.png new file mode 100644 index 000000000..d4156eb5e Binary files /dev/null and b/OpenKeychain/src/main/res/drawable/yubikey_neo.png differ diff --git a/OpenKeychain/src/main/res/layout/nfc_activity.xml b/OpenKeychain/src/main/res/layout/nfc_activity.xml new file mode 100644 index 000000000..e78fa7c87 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/nfc_activity.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/extern/openpgp-card-nfc-lib b/extern/openpgp-card-nfc-lib index 6bc1f1240..dc2701e41 160000 --- a/extern/openpgp-card-nfc-lib +++ b/extern/openpgp-card-nfc-lib @@ -1 +1 @@ -Subproject commit 6bc1f1240f9700e5bd378f02540360ef20dea629 +Subproject commit dc2701e415aa01c811b093f152409a231475e506 diff --git a/settings.gradle b/settings.gradle index 038e9da23..b51bdc94d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,5 +13,4 @@ include ':extern:SuperToasts:supertoasts' include ':extern:minidns' include ':extern:KeybaseLib:Lib' include ':extern:TokenAutoComplete:library' -include ':extern:openpgp-card-nfc-lib:library' include ':extern:safeslinger-exchange'