open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActiv...

393 lines
16 KiB
Java

/*
* Copyright (C) 2017 Schürmann & Breitmoser GbR
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.ViewAnimator;
import nordpol.android.NfcGuideView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.daos.KeyRepository;
import org.sufficientlysecure.keychain.securitytoken.operations.ModifyPinTokenOp;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
import org.sufficientlysecure.keychain.securitytoken.operations.PsoDecryptTokenOp;
import org.sufficientlysecure.keychain.securitytoken.operations.SecurityTokenPsoSignTokenOp;
import org.sufficientlysecure.keychain.securitytoken.operations.SecurityTokenChangeKeyTokenOp;
import org.sufficientlysecure.keychain.securitytoken.operations.ResetAndWipeTokenOp;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.OrientationUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
import timber.log.Timber;
/**
* 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
*/
public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity {
public static final String TAG = "SecurityTokenOperationActivity";
public static final String EXTRA_REQUIRED_INPUT = "required_input";
public static final String EXTRA_CRYPTO_INPUT = "crypto_input";
public static final String RESULT_CRYPTO_INPUT = "result_data";
public static final String RESULT_TOKEN_INFO = "token_info";
public ViewAnimator vAnimator;
public TextView vErrorText;
public Button vErrorTryAgainButton;
public NfcGuideView nfcGuideView;
private RequiredInputParcel mRequiredInput;
private CryptoInputParcel mInputParcel;
private SecurityTokenInfo mResultTokenInfo;
@Override
protected void initTheme() {
mThemeChanger = new ThemeChanger(this);
mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog,
R.style.Theme_Keychain_Dark_Dialog);
mThemeChanger.changeTheme();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Timber.d("NfcOperationActivity.onCreate");
nfcGuideView = findViewById(R.id.nfc_guide_view);
// prevent annoying orientation changes while fumbling with the device
OrientationUtils.lockCurrentOrientation(this);
// prevent close when touching outside of the dialog (happens easily when fumbling with the device)
setFinishOnTouchOutside(false);
// keep screen on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT);
setTitle(R.string.security_token_nfc_text);
vAnimator = findViewById(R.id.view_animator);
vAnimator.setDisplayedChild(0);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.STARTING_POSITION);
vErrorText = findViewById(R.id.security_token_activity_3_error_text);
vErrorTryAgainButton = findViewById(R.id.security_token_activity_3_error_try_again);
vErrorTryAgainButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resumeTagHandling();
obtainPassphraseIfRequired();
vAnimator.setDisplayedChild(0);
nfcGuideView.setVisibility(View.VISIBLE);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.STARTING_POSITION);
}
});
Button vCancel = findViewById(R.id.security_token_activity_0_cancel);
vCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setResult(RESULT_CANCELED);
finish();
}
});
Intent intent = getIntent();
Bundle data = intent.getExtras();
mRequiredInput = data.getParcelable(EXTRA_REQUIRED_INPUT);
obtainPassphraseIfRequired();
}
private void obtainPassphraseIfRequired() {
// obtain passphrase for this subkey
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
obtainSecurityTokenPin(mRequiredInput);
checkPinAvailability();
} else {
checkDeviceConnection();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (REQUEST_CODE_PIN == requestCode) {
checkPinAvailability();
}
}
private void checkPinAvailability() {
try {
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
if (passphrase != null) {
checkDeviceConnection();
}
} catch (PassphraseCacheService.KeyNotFoundException e) {
throw new AssertionError(
"tried to find passphrase for non-existing key. this is a programming error!");
}
}
@Override
protected void initLayout() {
setContentView(R.layout.security_token_operation_activity);
}
@Override
public void onSecurityTokenPreExecute() {
// start with indeterminate progress
vAnimator.setDisplayedChild(1);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
}
@Override
protected void doSecurityTokenInBackground(SecurityTokenConnection stConnection) throws IOException {
switch (mRequiredInput.mType) {
case SECURITY_TOKEN_DECRYPT: {
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
stConnection.getOpenPgpCapabilities().getFingerprintEncrypt());
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
throw new IOException(getString(R.string.error_wrong_security_token));
}
KeyRepository keyRepository = KeyRepository.create(this);
CanonicalizedPublicKeyRing publicKeyRing;
try {
Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(mRequiredInput.getMasterKeyId());
publicKeyRing = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId);
} catch (KeyRepository.NotFoundException e) {
throw new IOException("Couldn't find subkey for key to token operation.");
}
PsoDecryptTokenOp psoDecryptTokenOp = PsoDecryptTokenOp.create(stConnection);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
byte[] decryptedSessionKey = psoDecryptTokenOp
.verifyAndDecryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId));
mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey);
}
break;
}
case SECURITY_TOKEN_SIGN: {
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
stConnection.getOpenPgpCapabilities().getFingerprintSign());
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
throw new IOException(getString(R.string.error_wrong_security_token));
}
mInputParcel = mInputParcel.withSignatureTime(mRequiredInput.mSignatureTime);
SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.create(stConnection);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
byte[] signedHash = psoSignUseCase.calculateSignature(hash, algo);
mInputParcel = mInputParcel.withCryptoData(hash, signedHash);
}
break;
}
case SECURITY_TOKEN_AUTH: {
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
stConnection.getOpenPgpCapabilities().getFingerprintAuth());
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
throw new IOException(getString(R.string.error_wrong_security_token));
}
SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.create(stConnection);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
byte[] signedHash = psoSignUseCase.calculateAuthenticationSignature(hash, algo);
mInputParcel = mInputParcel.withCryptoData(hash, signedHash);
}
break;
}
case SECURITY_TOKEN_MOVE_KEY_TO_CARD: {
Passphrase adminPin = new Passphrase("12345678");
KeyRepository keyRepository =
KeyRepository.create(this);
CanonicalizedSecretKeyRing secretKeyRing;
try {
Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(mRequiredInput.getMasterKeyId());
secretKeyRing = keyRepository.getCanonicalizedSecretKeyRing(masterKeyId);
} catch (KeyRepository.NotFoundException e) {
throw new IOException("Couldn't find subkey for key to token operation.");
}
byte[] newPin = mRequiredInput.mInputData[0];
byte[] newAdminPin = mRequiredInput.mInputData[1];
for (int i = 2; i < mRequiredInput.mInputData.length; i++) {
byte[] subkeyBytes = mRequiredInput.mInputData[i];
ByteBuffer buf = ByteBuffer.wrap(subkeyBytes);
long subkeyId = buf.getLong();
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
byte[] tokenSerialNumber = Arrays.copyOf(stConnection.getOpenPgpCapabilities().getAid(), 16);
Passphrase passphrase;
try {
passphrase = PassphraseCacheService.getCachedPassphrase(this,
mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
} catch (PassphraseCacheService.KeyNotFoundException e) {
throw new IOException("Unable to get cached passphrase!");
}
SecurityTokenChangeKeyTokenOp putKeyUseCase = SecurityTokenChangeKeyTokenOp.create(stConnection);
putKeyUseCase.changeKey(key, passphrase, adminPin);
// TODO: Is this really used anywhere?
mInputParcel = mInputParcel.withCryptoData(subkeyBytes, tokenSerialNumber);
}
ModifyPinTokenOp.create(stConnection, adminPin).modifyPw1andPw3Pins(newPin, newAdminPin);
SecurityTokenConnection.clearCachedConnections();
break;
}
case SECURITY_TOKEN_RESET_CARD: {
ResetAndWipeTokenOp.create(stConnection).resetAndWipeToken();
mResultTokenInfo = stConnection.readTokenInfo();
break;
}
default: {
throw new AssertionError("Unhandled mRequiredInput.mType");
}
}
}
@Override
protected final void onSecurityTokenPostExecute(final SecurityTokenConnection stConnection) {
handleResult(mInputParcel);
// show finish
vAnimator.setDisplayedChild(2);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
if (stConnection.isPersistentConnectionAllowed()) {
// Just close
finish();
} else {
stConnection.clearSecureMessaging();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// check all 200ms if Security Token has been taken away
while (true) {
try {
if (stConnection.isConnected()) {
Thread.sleep(200);
} else {
return null;
}
} catch (InterruptedException ignored) {
// Sleep interrupted; ignore
} catch (SecurityException ignored) {
// In newer version of Android, isConnected() can throw an SecurityException
// when the Tag object becomes "stale"; this simply means the tag has been removed
// (and possibly rediscovered), so we can safely break from here.
return null;
}
}
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
finish();
}
}.execute();
}
}
/**
* Defines how the result of this activity is returned.
* Is overwritten in RemoteSecurityTokenOperationActivity
*/
protected void handleResult(CryptoInputParcel inputParcel) {
Intent result = new Intent();
// send back the CryptoInputParcel we received
result.putExtra(RESULT_CRYPTO_INPUT, inputParcel);
if (mResultTokenInfo != null) {
result.putExtra(RESULT_TOKEN_INFO, mResultTokenInfo);
}
setResult(RESULT_OK, result);
}
@Override
protected void onSecurityTokenError(String error) {
pauseTagHandling();
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
vAnimator.setDisplayedChild(3);
nfcGuideView.setVisibility(View.GONE);
}
@Override
public void onSecurityTokenPinError(String error, SecurityTokenInfo tokeninfo) {
onSecurityTokenError(error);
// clear (invalid) passphrase
PassphraseCacheService.clearCachedPassphrase(
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
}
}