diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index bcb74c5d0..f818b3c77 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -55,6 +55,7 @@ dependencies { // libs as submodules compile project(':libkeychain') compile project(':openpgp-api-lib') + compile project(':sshauthentication-api') compile project(':extern:bouncycastle:core') compile project(':extern:bouncycastle:pg') compile project(':extern:bouncycastle:prov') diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index a6b60b343..d3bd05044 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -767,7 +767,6 @@ - + + + + + + + + signedHashes) + throws PgpGeneralException { + if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) { + throw new PrivateKeyNotUnlockedException(); + } + + PGPContentSignerBuilder contentSignerBuilder = getContentSignerBuilder(hashAlgorithm, signedHashes); + + try { + PGPAuthenticationSignatureGenerator signatureGenerator = new PGPAuthenticationSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, mPrivateKey); + + return signatureGenerator; + } catch (PGPException e) { + // TODO: simply throw PGPException! + throw new PgpGeneralException("Error initializing signature!", e); + } + } + public PGPSignatureGenerator getDataSignatureGenerator(int hashAlgo, boolean cleartext, Map signedHashes, Date creationTimestamp) throws PgpGeneralException { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SshPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SshPublicKey.java new file mode 100644 index 000000000..94a20ba21 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SshPublicKey.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.pgp; + +import org.bouncycastle.bcpg.DSAPublicBCPGKey; +import org.bouncycastle.bcpg.ECPublicBCPGKey; +import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; +import org.bouncycastle.bcpg.RSAPublicBCPGKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.ssh.key.SshDSAPublicKey; +import org.sufficientlysecure.keychain.ssh.key.SshECDSAPublicKey; +import org.sufficientlysecure.keychain.ssh.key.SshEd25519PublicKey; +import org.sufficientlysecure.keychain.ssh.key.SshRSAPublicKey; + +public class SshPublicKey { + private final static String TAG = "SshPublicKey"; + + private CanonicalizedPublicKey mPublicKey; + + public SshPublicKey(CanonicalizedPublicKey publicKey) { + mPublicKey = publicKey; + } + + public String getEncodedKey() throws PgpGeneralException { + PGPPublicKey key = mPublicKey.getPublicKey(); + + switch (key.getAlgorithm()) { + case PGPPublicKey.RSA_GENERAL: + return encodeRSAKey(key); + case PGPPublicKey.ECDSA: + return encodeECKey(key); + case PGPPublicKey.EDDSA: + return encodeEdDSAKey(key); + case PGPPublicKey.DSA: + return encodeDSAKey(key); + default: + break; + } + throw new PgpGeneralException("Unknown algorithm"); + } + + private String encodeRSAKey(PGPPublicKey publicKey) { + RSAPublicBCPGKey publicBCPGKey = (RSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); + + SshRSAPublicKey pubkey = new SshRSAPublicKey(publicBCPGKey.getPublicExponent(), publicBCPGKey.getModulus()); + + return pubkey.getPublicKeyBlob(); + } + + private String encodeECKey(PGPPublicKey publicKey) { + ECPublicBCPGKey publicBCPGKey = (ECPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); + + String curveName = getCurveName(publicBCPGKey); + SshECDSAPublicKey sshECDSAPublicKey = new SshECDSAPublicKey(curveName, publicBCPGKey.getEncodedPoint()); + + return sshECDSAPublicKey.getPublicKeyBlob(); + } + + private String getCurveName(ECPublicBCPGKey publicBCPGKey) { + String curveOid = publicBCPGKey.getCurveOID().getId(); + // see RFC5656 section 10.{1,2} + switch (curveOid) { + // REQUIRED curves + case "1.2.840.10045.3.1.7": + return "nistp256"; + case "1.3.132.0.34": + return "nistp384"; + case "1.3.132.0.35": + return "nistp521"; + + // RECOMMENDED curves + case "1.3.132.0.1": + return "1.3.132.0.1"; + case "1.2.840.10045.3.1.1": + return "1.2.840.10045.3.1.1"; + case "1.3.132.0.33": + return "1.3.132.0.33"; + case "1.3.132.0.26": + return "1.3.132.0.26"; + case "1.3.132.0.27": + return "1.3.132.0.27"; + case "1.3.132.0.16": + return "1.3.132.0.16"; + case "1.3.132.0.36": + return "1.3.132.0.36"; + case "1.3.132.0.37": + return "1.3.132.0.37"; + case "1.3.132.0.38": + return "1.3.132.0.38"; + + default: + return null; + } + } + + private String encodeEdDSAKey(PGPPublicKey publicKey) { + EdDSAPublicBCPGKey publicBCPGKey = (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); + + SshEd25519PublicKey pubkey = new SshEd25519PublicKey(publicBCPGKey.getEdDSAEncodedPoint()); + + return pubkey.getPublicKeyBlob(); + } + + private String encodeDSAKey(PGPPublicKey publicKey) { + DSAPublicBCPGKey publicBCPGKey = (DSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); + + SshDSAPublicKey sshDSAPublicKey = new SshDSAPublicKey(publicBCPGKey.getP(), + publicBCPGKey.getQ(), + publicBCPGKey.getG(), + publicBCPGKey.getY()); + + return sshDSAPublicKey.getPublicKeyBlob(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java index 81b775b5f..da2b666a6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java @@ -201,6 +201,27 @@ public class CachedPublicKeyRing extends KeyRing { } } + /** Returns the key id which should be used for authentication. + * + * This method returns keys which are actually available (ie. secret available, and not stripped, + * revoked, or expired), hence only works on keyrings where a secret key is available! + * + */ + public long getSecretAuthenticationId() throws PgpKeyNotFoundException { + try { + Object data = mKeyRepository.getGenericData(mUri, + KeyRings.HAS_AUTHENTICATE, + KeyRepository.FIELD_TYPE_INTEGER); + return (Long) data; + } catch(KeyWritableRepository.NotFoundException e) { + throw new PgpKeyNotFoundException(e); + } + } + + public boolean hasAuthentication() throws PgpKeyNotFoundException { + return getSecretAuthenticationId() != 0; + } + @Override public int getVerified() throws PgpKeyNotFoundException { try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 795ac7487..d59586345 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -355,6 +355,8 @@ public class KeychainProvider extends ContentProvider { "kE." + Keys.KEY_ID + " AS " + KeyRings.HAS_ENCRYPT); projectionMap.put(KeyRings.HAS_SIGN, "kS." + Keys.KEY_ID + " AS " + KeyRings.HAS_SIGN); + projectionMap.put(KeyRings.HAS_AUTHENTICATE, + "kA." + Keys.KEY_ID + " AS " + KeyRings.HAS_AUTHENTICATE); projectionMap.put(KeyRings.HAS_CERTIFY, "kC." + Keys.KEY_ID + " AS " + KeyRings.HAS_CERTIFY); projectionMap.put(KeyRings.IS_EXPIRED, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index ff5351b2b..b855653aa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java @@ -17,7 +17,6 @@ package org.sufficientlysecure.keychain.remote; - import java.util.ArrayList; import android.app.PendingIntent; @@ -28,7 +27,6 @@ import android.os.Build; import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.remote.ui.RemoteBackupActivity; -import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteErrorActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteImportKeysActivity; import org.sufficientlysecure.keychain.remote.ui.RemotePassphraseDialogActivity; @@ -38,6 +36,8 @@ import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationAct import org.sufficientlysecure.keychain.remote.ui.RemoteSelectPubKeyActivity; import org.sufficientlysecure.keychain.remote.ui.RequestKeyPermissionActivity; import org.sufficientlysecure.keychain.remote.ui.SelectSignKeyIdActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyActivity; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -56,10 +56,12 @@ public class ApiPendingIntentFactory { switch (requiredInput.mType) { case SECURITY_TOKEN_MOVE_KEY_TO_CARD: case SECURITY_TOKEN_DECRYPT: + case SECURITY_TOKEN_AUTH: case SECURITY_TOKEN_SIGN: { return createSecurityTokenOperationPendingIntent(data, requiredInput, cryptoInput); } + case PASSPHRASE_AUTH: case PASSPHRASE: { return createPassphrasePendingIntent(data, requiredInput, cryptoInput); } @@ -139,6 +141,14 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } + PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) { + Intent intent = new Intent(mContext, RemoteSelectAuthenticationKeyActivity.class); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); + intent.putExtra(RemoteSelectAuthenticationKeyActivity.EXTRA_PACKAGE_NAME, packageName); + + return createInternal(data, intent); + } + PendingIntent createBackupPendingIntent(Intent data, long[] masterKeyIds, boolean backupSecret) { Intent intent = new Intent(mContext, RemoteBackupActivity.class); intent.putExtra(RemoteBackupActivity.EXTRA_MASTER_KEY_IDS, masterKeyIds); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java new file mode 100644 index 000000000..aa4d775ce --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.remote; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.openintents.ssh.authentication.ISshAuthenticationService; +import org.openintents.ssh.authentication.SshAuthenticationApi; +import org.openintents.ssh.authentication.SshAuthenticationApiError; +import org.openintents.ssh.authentication.response.KeySelectionResponse; +import org.openintents.ssh.authentication.response.PublicKeyResponse; +import org.openintents.ssh.authentication.response.SigningResponse; +import org.openintents.ssh.authentication.response.SshPublicKeyResponse; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; +import org.sufficientlysecure.keychain.pgp.SshPublicKey; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.ssh.AuthenticationData; +import org.sufficientlysecure.keychain.ssh.AuthenticationOperation; +import org.sufficientlysecure.keychain.ssh.AuthenticationParcel; +import org.sufficientlysecure.keychain.ssh.AuthenticationResult; + +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.util.*; + + +public class SshAuthenticationService extends Service { + private static final String TAG = "SshAuthService"; + + private ApiPermissionHelper mApiPermissionHelper; + private KeyRepository mKeyRepository; + private ApiDataAccessObject mApiDao; + private ApiPendingIntentFactory mApiPendingIntentFactory; + + private static final List SUPPORTED_VERSIONS = Collections.unmodifiableList(Collections.singletonList(1)); + private static final int INVALID_API_VERSION = -1; + + private static final int HASHALGORITHM_NONE = SshAuthenticationApiError.INVALID_HASH_ALGORITHM; + + @Override + public void onCreate() { + super.onCreate(); + mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this)); + mKeyRepository = KeyRepository.create(this); + mApiDao = new ApiDataAccessObject(this); + + mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); + } + + private final ISshAuthenticationService.Stub mSSHAgent = new ISshAuthenticationService.Stub() { + @Override + public Intent execute(Intent intent) { + return checkIntent(intent); + } + + }; + + @Override + public IBinder onBind(Intent intent) { + return mSSHAgent; + } + + private Intent checkIntent(Intent intent) { + Intent errorResult = checkRequirements(intent); + if (errorResult == null) { + return executeInternal(intent); + } else { + return errorResult; + } + } + + private Intent executeInternal(Intent intent) { + switch (intent.getAction()) { + case SshAuthenticationApi.ACTION_SIGN: + return authenticate(intent); + case SshAuthenticationApi.ACTION_SELECT_KEY: + return getAuthenticationKey(intent); + case SshAuthenticationApi.ACTION_GET_PUBLIC_KEY: + return getAuthenticationPublicKey(intent, false); + case SshAuthenticationApi.ACTION_GET_SSH_PUBLIC_KEY: + return getAuthenticationPublicKey(intent, true); + default: + return createErrorResult(SshAuthenticationApiError.UNKNOWN_ACTION, "Unknown action"); + } + } + + private Intent authenticate(Intent data) { + Intent errorIntent = checkForKeyId(data); + if (errorIntent != null) { + return errorIntent; + } + + // keyid == masterkeyid -> authkeyid + // keyId is the pgp master keyId, the keyId used will be the first authentication + // key in the keyring designated by the master keyId + String keyIdString = data.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID); + long masterKeyId = Long.valueOf(keyIdString); + + int hashAlgorithmTag = getHashAlgorithm(data); + if (hashAlgorithmTag == HASHALGORITHM_NONE) { + return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No valid hash algorithm!"); + } + + byte[] challenge = data.getByteArrayExtra(SshAuthenticationApi.EXTRA_CHALLENGE); + if (challenge == null || challenge.length == 0) { + return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No challenge given"); + } + + // carries the metadata necessary for authentication + AuthenticationData.Builder authData = AuthenticationData.builder(); + authData.setAuthenticationMasterKeyId(masterKeyId); + + CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId); + + long authSubKeyId; + try { + // get first usable subkey capable of authentication + authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId(); + } catch (PgpKeyNotFoundException e) { + return createExceptionErrorResult(SshAuthenticationApiError.NO_AUTH_KEY, + "authentication key for master key id not found in keychain", e); + } + + authData.setAuthenticationSubKeyId(authSubKeyId); + + authData.setAllowedAuthenticationKeyIds(getAllowedKeyIds()); + + authData.setHashAlgorithm(hashAlgorithmTag); + + CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); + if (inputParcel == null) { + // fresh request, assign UUID + inputParcel = CryptoInputParcel.createCryptoInputParcel(new Date()); + } + + AuthenticationParcel authParcel = AuthenticationParcel + .createAuthenticationParcel(authData.build(), challenge); + + // execute authentication operation! + AuthenticationOperation authOperation = new AuthenticationOperation(this, mKeyRepository); + AuthenticationResult authResult = authOperation.execute(authData.build(), inputParcel, authParcel); + + if (authResult.isPending()) { + RequiredInputParcel requiredInput = authResult.getRequiredInputParcel(); + PendingIntent pi = mApiPendingIntentFactory.requiredInputPi(data, requiredInput, + authResult.mCryptoInputParcel); + // return PendingIntent to be executed by client + return packagePendingIntent(pi); + } else if (authResult.success()) { + return new SigningResponse(authResult.getSignature()).toIntent(); + } else { + LogEntryParcel errorMsg = authResult.getLog().getLast(); + return createErrorResult(SshAuthenticationApiError.INTERNAL_ERROR, getString(errorMsg.mType.getMsgId())); + } + } + + private Intent checkForKeyId(Intent data) { + long authMasterKeyId = getKeyId(data); + if (authMasterKeyId == Constants.key.none) { + return createErrorResult(SshAuthenticationApiError.NO_KEY_ID, + "No key id in request"); + } + return null; + } + + private long getKeyId(Intent data) { + String keyIdString = data.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID); + long authMasterKeyId = Constants.key.none; + if (keyIdString != null) { + try { + authMasterKeyId = Long.valueOf(keyIdString); + } catch (NumberFormatException e) { + return Constants.key.none; + } + } + return authMasterKeyId; + } + + private int getHashAlgorithm(Intent data) { + int hashAlgorithm = data.getIntExtra(SshAuthenticationApi.EXTRA_HASH_ALGORITHM, HASHALGORITHM_NONE); + + switch (hashAlgorithm) { + case SshAuthenticationApi.SHA1: + return HashAlgorithmTags.SHA1; + case SshAuthenticationApi.RIPEMD160: + return HashAlgorithmTags.RIPEMD160; + case SshAuthenticationApi.SHA224: + return HashAlgorithmTags.SHA224; + case SshAuthenticationApi.SHA256: + return HashAlgorithmTags.SHA256; + case SshAuthenticationApi.SHA384: + return HashAlgorithmTags.SHA384; + case SshAuthenticationApi.SHA512: + return HashAlgorithmTags.SHA512; + default: + return HASHALGORITHM_NONE; + } + } + + private Intent getAuthenticationKey(Intent data) { + long masterKeyId = getKeyId(data); + if (masterKeyId != Constants.key.none) { + String description; + + try { + description = getDescription(masterKeyId); + } catch (PgpKeyNotFoundException e) { + return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY, + "Could not create description", e); + } + + return new KeySelectionResponse(String.valueOf(masterKeyId), description).toIntent(); + } else { + return redirectToKeySelection(data); + } + } + + private Intent redirectToKeySelection(Intent data) { + String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); + PendingIntent pi = mApiPendingIntentFactory.createSelectAuthenticationKeyIdPendingIntent(data, currentPkg); + return packagePendingIntent(pi); + } + + private Intent packagePendingIntent(PendingIntent pi) { + Intent result = new Intent(); + result.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, + SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT, pi); + return result; + } + + private Intent getAuthenticationPublicKey(Intent data, boolean asSshKey) { + long masterKeyId = getKeyId(data); + if (masterKeyId != Constants.key.none) { + try { + if (asSshKey) { + return getSSHPublicKey(masterKeyId); + } else { + return getX509PublicKey(masterKeyId); + } + } catch (KeyRepository.NotFoundException e) { + return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY, + "Key for master key id not found", e); + } catch (PgpKeyNotFoundException e) { + return createExceptionErrorResult(SshAuthenticationApiError.NO_AUTH_KEY, + "Authentication key for master key id not found in keychain", e); + } catch (NoSuchAlgorithmException e) { + return createExceptionErrorResult(SshAuthenticationApi.RESULT_CODE_ERROR, + "", e); + } + } else { + return createErrorResult(SshAuthenticationApiError.NO_KEY_ID, + "No key id in request"); + } + } + + private Intent getX509PublicKey(long masterKeyId) throws KeyRepository.NotFoundException, PgpKeyNotFoundException, NoSuchAlgorithmException { + byte[] encodedPublicKey; + int algorithm; + + PublicKey publicKey; + try { + publicKey = getPublicKey(masterKeyId).getJcaPublicKey(); + } catch (PgpGeneralException e) { // this should probably never happen + return createExceptionErrorResult(SshAuthenticationApiError.GENERIC_ERROR, + "Error converting public key", e); + } + + encodedPublicKey = publicKey.getEncoded(); + algorithm = translateAlgorithm(publicKey.getAlgorithm()); + + return new PublicKeyResponse(encodedPublicKey, algorithm).toIntent(); + } + + private int translateAlgorithm(String algorithm) throws NoSuchAlgorithmException { + switch (algorithm) { + case "RSA": + return SshAuthenticationApi.RSA; + case "ECDSA": + return SshAuthenticationApi.ECDSA; + case "EdDSA": + return SshAuthenticationApi.EDDSA; + case "DSA": + return SshAuthenticationApi.DSA; + default: + throw new NoSuchAlgorithmException("Error matching key algorithm to API supported algorithm: " + + algorithm); + } + } + + private Intent getSSHPublicKey(long masterKeyId) throws KeyRepository.NotFoundException, PgpKeyNotFoundException { + String sshPublicKeyBlob; + + CanonicalizedPublicKey publicKey = getPublicKey(masterKeyId); + + SshPublicKey sshPublicKey = new SshPublicKey(publicKey); + try { + sshPublicKeyBlob = sshPublicKey.getEncodedKey(); + } catch (PgpGeneralException e) { + return createExceptionErrorResult(SshAuthenticationApiError.GENERIC_ERROR, + "Error converting public key to SSH format", e); + } + + return new SshPublicKeyResponse(sshPublicKeyBlob).toIntent(); + } + + private CanonicalizedPublicKey getPublicKey(long masterKeyId) + throws PgpKeyNotFoundException, KeyRepository.NotFoundException { + KeyRepository keyRepository = KeyRepository.create(getApplicationContext()); + long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId) + .getSecretAuthenticationId(); + return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId) + .getPublicKey(authSubKeyId); + } + + private String getDescription(long masterKeyId) throws PgpKeyNotFoundException { + CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId); + + String description = ""; + long authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId(); + description += cachedPublicKeyRing.getPrimaryUserId(); + description += " (" + Long.toHexString(authSubKeyId) + ")"; + + return description; + } + + private HashSet getAllowedKeyIds() { + String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); + return mApiDao.getAllowedKeyIdsForApp(KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg)); + } + + /** + * @return null if basic requirements are met + */ + private Intent checkRequirements(Intent data) { + if (data == null) { + return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No parameter bundle"); + } + + // check version + int apiVersion = data.getIntExtra(SshAuthenticationApi.EXTRA_API_VERSION, INVALID_API_VERSION); + if (!SUPPORTED_VERSIONS.contains(apiVersion)) { + String errorMsg = "Incompatible API versions:\n" + + "used : " + data.getIntExtra(SshAuthenticationApi.EXTRA_API_VERSION, INVALID_API_VERSION) + "\n" + + "supported : " + SUPPORTED_VERSIONS; + + return createErrorResult(SshAuthenticationApiError.INCOMPATIBLE_API_VERSIONS, errorMsg); + } + + // check if caller is allowed to access OpenKeychain + Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data); + if (result != null) { + return result; // disallowed, redirect to registration + } + + return null; + } + + private Intent createErrorResult(int errorCode, String errorMessage) { + Log.e(TAG, errorMessage); + Intent result = new Intent(); + result.putExtra(SshAuthenticationApi.EXTRA_ERROR, new SshAuthenticationApiError(errorCode, errorMessage)); + result.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR); + return result; + } + + private Intent createExceptionErrorResult(int errorCode, String errorMessage, Exception e) { + String message = errorMessage + " : " + e.getMessage(); + return createErrorResult(errorCode, message); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java index 53055b40a..bab1a1b8b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java @@ -25,6 +25,7 @@ import java.util.List; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; +import android.net.Uri; import android.support.annotation.Nullable; import android.support.v4.content.AsyncTaskLoader; @@ -41,6 +42,8 @@ public class KeyLoader extends AsyncTaskLoader> { KeyRings.MASTER_KEY_ID, KeyRings.CREATION, KeyRings.HAS_ENCRYPT, + KeyRings.HAS_AUTHENTICATE, + KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED, KeyRings.NAME, KeyRings.EMAIL, @@ -51,34 +54,37 @@ public class KeyLoader extends AsyncTaskLoader> { private static final int INDEX_MASTER_KEY_ID = 1; private static final int INDEX_CREATION = 2; private static final int INDEX_HAS_ENCRYPT = 3; - private static final int INDEX_VERIFIED = 4; - private static final int INDEX_NAME = 5; - private static final int INDEX_EMAIL = 6; - private static final int INDEX_COMMENT = 7; + private static final int INDEX_HAS_AUTHENTICATE = 4; + private static final int INDEX_HAS_ANY_SECRET = 5; + private static final int INDEX_VERIFIED = 6; + private static final int INDEX_NAME = 7; + private static final int INDEX_EMAIL = 8; + private static final int INDEX_COMMENT = 9; private static final String QUERY_WHERE = Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0 AND " + KeyRings.IS_EXPIRED + " = 0"; private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC"; private final ContentResolver contentResolver; - private final String emailAddress; + private final KeySelector keySelector; private List cachedResult; - - KeyLoader(Context context, ContentResolver contentResolver, String emailAddress) { + KeyLoader(Context context, ContentResolver contentResolver, KeySelector keySelector) { super(context); this.contentResolver = contentResolver; - this.emailAddress = emailAddress; + this.keySelector = keySelector; } @Override public List loadInBackground() { ArrayList keyInfos = new ArrayList<>(); + Cursor cursor; + + String selection = QUERY_WHERE + " AND " + keySelector.getSelection(); + cursor = contentResolver.query(keySelector.getKeyRingUri(), QUERY_PROJECTION, selection, null, QUERY_ORDER); - Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFindByEmailUri(emailAddress), - QUERY_PROJECTION, QUERY_WHERE, null, QUERY_ORDER); if (cursor == null) { return null; } @@ -123,6 +129,8 @@ public class KeyLoader extends AsyncTaskLoader> { public abstract long getMasterKeyId(); public abstract long getCreationDate(); public abstract boolean getHasEncrypt(); + public abstract boolean getHasAuthenticate(); + public abstract boolean getHasAnySecret(); public abstract boolean getIsVerified(); @Nullable @@ -136,6 +144,8 @@ public class KeyLoader extends AsyncTaskLoader> { long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); long creationDate = cursor.getLong(INDEX_CREATION) * 1000L; boolean hasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0; + boolean hasAuthenticate = cursor.getInt(INDEX_HAS_AUTHENTICATE) != 0; + boolean hasAnySecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; boolean isVerified = cursor.getInt(INDEX_VERIFIED) == 2; String name = cursor.getString(INDEX_NAME); @@ -143,7 +153,17 @@ public class KeyLoader extends AsyncTaskLoader> { String comment = cursor.getString(INDEX_COMMENT); return new AutoValue_KeyLoader_KeyInfo( - masterKeyId, creationDate, hasEncrypt, isVerified, name, email, comment); + masterKeyId, creationDate, hasEncrypt, hasAuthenticate, hasAnySecret, isVerified, name, email, comment); + } + } + + @AutoValue + public abstract static class KeySelector { + public abstract Uri getKeyRingUri(); + public abstract String getSelection(); + + static KeySelector create(Uri keyRingUri, String selection) { + return new AutoValue_KeyLoader_KeySelector(keyRingUri, selection); } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java index abd08792e..aa1a8dd64 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java @@ -32,7 +32,9 @@ import android.support.v4.content.Loader; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeySelector; import org.sufficientlysecure.keychain.util.Log; @@ -90,7 +92,10 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks> { @Override public Loader> onCreateLoader(int id, Bundle args) { - return new KeyLoader(context, context.getContentResolver(), duplicateAddress); + KeySelector keySelector = KeySelector.create( + KeyRings.buildUnifiedKeyRingsFindByEmailUri(duplicateAddress), null); + + return new KeyLoader(context, context.getContentResolver(), keySelector); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java new file mode 100644 index 000000000..359bc4aae --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * 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 . + */ + +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Drawable.ConstantState; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.text.format.DateUtils; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.mikepenz.materialdrawer.util.KeyboardUtil; + +import org.openintents.ssh.authentication.SshAuthenticationApi; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyPresenter.RemoteSelectAuthenticationKeyView; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; + +import java.util.List; + + +public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; + + public static final int LOADER_ID_KEYS = 0; + + + private RemoteSelectAuthenticationKeyPresenter presenter; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + presenter = new RemoteSelectAuthenticationKeyPresenter(getBaseContext(), LOADER_ID_KEYS); + + KeyboardUtil.hideKeyboard(this); + + if (savedInstanceState == null) { + RemoteSelectAuthenticationKeyDialogFragment frag = new RemoteSelectAuthenticationKeyDialogFragment(); + frag.show(getSupportFragmentManager(), "selectAuthenticationKeyDialog"); + } + } + + @Override + protected void onStart() { + super.onStart(); + + Intent intent = getIntent(); + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + + + presenter.setupFromIntentData(packageName); + presenter.startLoaders(getSupportLoaderManager()); + } + + private void onKeySelected(long masterKeyId) { + Intent callingIntent = getIntent(); + Intent originalIntent = callingIntent.getParcelableExtra( + RemoteSecurityTokenOperationActivity.EXTRA_DATA); + + Uri appUri = callingIntent.getData(); + + Uri allowedKeysUri = appUri.buildUpon() + .appendPath(KeychainContract.PATH_ALLOWED_KEYS) + .build(); + + ApiDataAccessObject apiDao = new ApiDataAccessObject(getBaseContext()); + apiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId); + + originalIntent.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, String.valueOf(masterKeyId)); + + setResult(RESULT_OK, originalIntent); + finish(); + } + + public static class RemoteSelectAuthenticationKeyDialogFragment extends DialogFragment { + private RemoteSelectAuthenticationKeyPresenter presenter; + private RemoteSelectAuthenticationKeyView mvpView; + + private Button buttonSelect; + private Button buttonCancel; + private RecyclerView keyChoiceList; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); + + LayoutInflater layoutInflater = LayoutInflater.from(theme); + @SuppressLint("InflateParams") + View view = layoutInflater.inflate(R.layout.api_remote_select_authentication_key, null, false); + alert.setView(view); + + buttonSelect = (Button) view.findViewById(R.id.button_select); + buttonCancel = (Button) view.findViewById(R.id.button_cancel); + + keyChoiceList = (RecyclerView) view.findViewById(R.id.authentication_key_list); + keyChoiceList.setLayoutManager(new LinearLayoutManager(activity)); + keyChoiceList.addItemDecoration( + new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true)); + + setupListenersForPresenter(); + mvpView = createMvpView(view, layoutInflater); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter = ((RemoteSelectAuthenticationKeyActivity) getActivity()).presenter; + presenter.setView(mvpView); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (presenter != null) { + presenter.onCancel(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + if (presenter != null) { + presenter.setView(null); + presenter = null; + } + } + + @NonNull + private RemoteSelectAuthenticationKeyView createMvpView(View view, LayoutInflater layoutInflater) { + final ImageView iconClientApp = (ImageView) view.findViewById(R.id.icon_client_app); + final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + keyChoiceList.setAdapter(keyChoiceAdapter); + + return new RemoteSelectAuthenticationKeyView() { + @Override + public void finish(long masterKeyId) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + ((RemoteSelectAuthenticationKeyActivity)activity).onKeySelected(masterKeyId); + } + + @Override + public void finishAsCancelled() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + activity.setResult(RESULT_CANCELED); + activity.finish(); + } + + @Override + public void setTitleClientIcon(Drawable drawable) { + iconClientApp.setImageDrawable(drawable); + keyChoiceAdapter.setSelectionDrawable(drawable); + } + + @Override + public void setKeyListData(List data) { + keyChoiceAdapter.setData(data); + } + + @Override + public void setActiveItem(Integer position) { + keyChoiceAdapter.setActiveItem(position); + } + + @Override + public void setEnableSelectButton(boolean enabled) { + buttonSelect.setEnabled(enabled); + } + }; + } + + private void setupListenersForPresenter() { + buttonSelect.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + presenter.onClickSelect(); + } + }); + + buttonCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + presenter.onClickCancel(); + } + }); + + keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), + new RecyclerItemClickListener.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + presenter.onKeyItemClick(position); + } + })); + } + } + + private static class KeyChoiceAdapter extends Adapter { + private final LayoutInflater layoutInflater; + private final Resources resources; + private List data; + private Drawable iconUnselected; + private Drawable iconSelected; + private Integer activeItem; + + KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { + this.layoutInflater = layoutInflater; + this.resources = resources; + } + + @Override + public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View keyChoiceItemView = layoutInflater.inflate(R.layout.authentication_key_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + @Override + public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { + KeyInfo keyInfo = data.get(position); + Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected; + holder.bind(keyInfo, icon); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + void setSelectionDrawable(Drawable drawable) { + ConstantState constantState = drawable.getConstantState(); + if (constantState == null) { + return; + } + + iconSelected = constantState.newDrawable(resources); + + iconUnselected = constantState.newDrawable(resources); + DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null)); + + notifyDataSetChanged(); + } + + void setActiveItem(Integer newActiveItem) { + Integer prevActiveItem = this.activeItem; + this.activeItem = newActiveItem; + + if (prevActiveItem != null) { + notifyItemChanged(prevActiveItem); + } + if (newActiveItem != null) { + notifyItemChanged(newActiveItem); + } + } + } + + private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vCreation; + private final ImageView vIcon; + + KeyChoiceViewHolder(View itemView) { + super(itemView); + + vName = (TextView) itemView.findViewById(R.id.key_list_item_name); + vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + vIcon = (ImageView) itemView.findViewById(R.id.key_list_item_icon); + } + + void bind(KeyInfo keyInfo, Drawable selectionIcon) { + vName.setText(keyInfo.getName()); + + Context context = vCreation.getContext(); + String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + vIcon.setImageDrawable(selectionIcon); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java new file mode 100644 index 000000000..525317931 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * 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 . + */ + +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeySelector; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.List; + + +class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks> { + private final PackageManager packageManager; + private final Context context; + private final int loaderId; + + + private RemoteSelectAuthenticationKeyView view; + private Integer selectedItem; + private List keyInfoData; + + + RemoteSelectAuthenticationKeyPresenter(Context context, int loaderId) { + this.context = context; + + packageManager = context.getPackageManager(); + + this.loaderId = loaderId; + } + + public void setView(RemoteSelectAuthenticationKeyView view) { + this.view = view; + } + + void setupFromIntentData(String packageName) { + try { + setPackageInfo(packageName); + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Unable to find info of calling app!"); + view.finishAsCancelled(); + } + } + + private void setPackageInfo(String packageName) throws NameNotFoundException { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); + + view.setTitleClientIcon(appIcon); + } + + void startLoaders(LoaderManager loaderManager) { + loaderManager.restartLoader(loaderId, null, this); + } + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + String selection = KeyRings.HAS_ANY_SECRET + " != 0 AND " + KeyRings.HAS_AUTHENTICATE + " != 0"; + KeySelector keySelector = KeySelector.create( + KeyRings.buildUnifiedKeyRingsUri(), selection); + return new KeyLoader(context, context.getContentResolver(), keySelector); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + this.keyInfoData = data; + view.setKeyListData(data); + } + + @Override + public void onLoaderReset(Loader loader) { + if (view != null) { + view.setKeyListData(null); + } + } + + void onClickSelect() { + if (keyInfoData == null) { + Log.e(Constants.TAG, "got click on select with no data…?"); + return; + } + if (selectedItem == null) { + Log.e(Constants.TAG, "got click on select with no selection…?"); + return; + } + + long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId(); + view.finish(masterKeyId); + } + + void onClickCancel() { + view.finishAsCancelled(); + } + + public void onCancel() { + view.finishAsCancelled(); + } + + void onKeyItemClick(int position) { + if (selectedItem != null && position == selectedItem) { + selectedItem = null; + } else { + selectedItem = position; + } + view.setActiveItem(selectedItem); + view.setEnableSelectButton(selectedItem != null); + } + + interface RemoteSelectAuthenticationKeyView { + void finish(long masterKeyId); + void finishAsCancelled(); + + void setTitleClientIcon(Drawable drawable); + + void setKeyListData(List data); + void setActiveItem(Integer position); + void setEnableSelectButton(boolean enabled); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java index 6d6b7e812..e5bc66611 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java @@ -159,6 +159,11 @@ class OpenPgpCommandApduFactory { MAX_APDU_NE_EXT); } + @NonNull + CommandApdu createInternalAuthCommand(byte[] authData) { + return CommandApdu.create(CLA, INS_INTERNAL_AUTHENTICATE, P1_EMPTY, P2_EMPTY, authData, MAX_APDU_NE_EXT); + } + @NonNull CommandApdu createGenerateKeyCommand(int slot) { return CommandApdu.create(CLA, INS_GENERATE_ASYMMETRIC_KEY_PAIR, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java index 644c992ec..b9be3cbe0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -620,17 +620,8 @@ public class SecurityTokenConnection { return response.getData(); } - /** - * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value - * - * @param hash the hash for signing - * @return a big integer representing the MPI for the given hash - */ - public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException { - if (!mPw1ValidatedForSignature) { - verifyPinForSignature(); - } + private byte[] prepareDsi(byte[] hash, int hashAlgo) throws IOException { byte[] dsi; Log.i(Constants.TAG, "Hash: " + hashAlgo); @@ -679,13 +670,14 @@ public class SecurityTokenConnection { default: throw new IOException("Not supported hash algo!"); } + return dsi; + } + private byte[] prepareData(byte[] hash, int hashAlgo, KeyFormat keyFormat) throws IOException { byte[] data; - - KeyFormat signKeyFormat = mOpenPgpCapabilities.getFormatForKeyType(KeyType.SIGN); - switch (signKeyFormat.keyFormatType()) { + switch (keyFormat.keyFormatType()) { case RSAKeyFormatType: - data = dsi; + data = prepareDsi(hash, hashAlgo); break; case ECKeyFormatType: data = hash; @@ -693,25 +685,16 @@ public class SecurityTokenConnection { default: throw new IOException("Not supported key type!"); } + return data; + } - // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) - CommandApdu command = commandFactory.createComputeDigitalSignatureCommand(data); - ResponseApdu response = communicate(command); - - if (!response.isSuccess()) { - throw new CardException("Failed to sign", response.getSw()); - } - - if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { - mPw1ValidatedForSignature = false; - } - - byte[] signature = response.getData(); + private byte[] encodeSignature(byte[] signature, KeyFormat keyFormat) throws IOException { // Make sure the signature we received is actually the expected number of bytes long! - switch (signKeyFormat.keyFormatType()) { + switch (keyFormat.keyFormatType()) { case RSAKeyFormatType: - int modulusLength = ((RSAKeyFormat) signKeyFormat).getModulusLength(); + // no encoding necessary + int modulusLength = ((RSAKeyFormat) keyFormat).getModulusLength(); if (signature.length != (modulusLength / 8)) { throw new IOException("Bad signature length! Expected " + (modulusLength / 8) + " bytes, got " + signature.length); @@ -736,10 +719,69 @@ public class SecurityTokenConnection { signature = baos.toByteArray(); break; } - return signature; } + /** + * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value + * + * @param hash the hash for signing + * @return a big integer representing the MPI for the given hash + */ + public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException { + if (!mPw1ValidatedForSignature) { + verifyPinForSignature(); + } + + KeyFormat signKeyFormat = mOpenPgpCapabilities.getFormatForKeyType(KeyType.SIGN); + + byte[] data = prepareData(hash, hashAlgo, signKeyFormat); + + // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) + CommandApdu command = commandFactory.createComputeDigitalSignatureCommand(data); + ResponseApdu response = communicate(command); + + if (!response.isSuccess()) { + throw new CardException("Failed to sign", response.getSw()); + } + + if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { + mPw1ValidatedForSignature = false; + } + + return encodeSignature(response.getData(), signKeyFormat); + } + + /** + * Call INTERNAL AUTHENTICATE command and returns the MPI value + * + * @param hash the hash for signing + * @return a big integer representing the MPI for the given hash + */ + public byte[] calculateAuthenticationSignature(byte[] hash, int hashAlgo) throws IOException { + if (!mPw1ValidatedForDecrypt) { + verifyPinForOther(); + } + + KeyFormat authKeyFormat = mOpenPgpCapabilities.getFormatForKeyType(KeyType.AUTH); + + byte[] data = prepareData(hash, hashAlgo, authKeyFormat); + + // Command APDU for INTERNAL AUTHENTICATE (page 55) + CommandApdu command = commandFactory.createInternalAuthCommand(data); + ResponseApdu response = communicate(command); + + if (!response.isSuccess()) { + throw new CardException("Failed to sign", response.getSw()); + } + + if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { + mPw1ValidatedForSignature = false; + } + + return encodeSignature(response.getData(), authKeyFormat); + } + /** * Transceives APDU * Splits extended APDU into short APDUs and chains them if necessary diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java index 4a0fe8069..34c7352a9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java @@ -15,8 +15,11 @@ import org.sufficientlysecure.keychain.util.Passphrase; public class RequiredInputParcel implements Parcelable { public enum RequiredInputType { - PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, SECURITY_TOKEN_SIGN, SECURITY_TOKEN_DECRYPT, - SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY + PASSPHRASE, PASSPHRASE_SYMMETRIC, PASSPHRASE_AUTH, + BACKUP_CODE, + SECURITY_TOKEN_SIGN, SECURITY_TOKEN_AUTH, SECURITY_TOKEN_DECRYPT, + SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, + ENABLE_ORBOT, UPLOAD_FAIL_RETRY } public Date mSignatureTime; @@ -98,6 +101,14 @@ public class RequiredInputParcel implements Parcelable { signatureTime, masterKeyId, subKeyId); } + public static RequiredInputParcel createSecurityTokenAuthenticationOperation( + long masterKeyId, long subKeyId, + byte[] inputHash, int signAlgo) { + return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_AUTH, + new byte[][] { inputHash }, new int[] { signAlgo }, + null, masterKeyId, subKeyId); + } + public static RequiredInputParcel createSecurityTokenDecryptOperation( long masterKeyId, long subKeyId, byte[] encryptedSessionKey) { return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_DECRYPT, @@ -109,6 +120,12 @@ public class RequiredInputParcel implements Parcelable { null, null, null, null, null); } + public static RequiredInputParcel createRequiredAuthenticationPassphrase( + long masterKeyId, long subKeyId) { + return new RequiredInputParcel(RequiredInputType.PASSPHRASE_AUTH, + null, null, null, masterKeyId, subKeyId); + } + public static RequiredInputParcel createRequiredSignPassphrase( long masterKeyId, long subKeyId, Date signatureTime) { return new RequiredInputParcel(RequiredInputType.PASSPHRASE, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationData.java new file mode 100644 index 000000000..c18976d99 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationData.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * Copyright (C) 2017 Vincent Breitmoser + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh; + +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.sufficientlysecure.keychain.Constants; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * AuthenticationData holds metadata pertaining to the signing of a + * AuthenticationParcel via a AuthenticationOperation + */ +@AutoValue +public abstract class AuthenticationData implements Parcelable { + public abstract long getAuthenticationMasterKeyId(); + public abstract Long getAuthenticationSubKeyId(); + @Nullable + public abstract List getAllowedAuthenticationKeyIds(); + + public abstract int getHashAlgorithm(); + + public static Builder builder() { + return new AutoValue_AuthenticationData.Builder() + .setAuthenticationMasterKeyId(Constants.key.none) + .setAuthenticationSubKeyId(Constants.key.none) + .setHashAlgorithm(HashAlgorithmTags.SHA512); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract AuthenticationData build(); + + public abstract Builder setAuthenticationMasterKeyId(long authenticationMasterKeyId); + public abstract Builder setAuthenticationSubKeyId(Long authenticationSubKeyId); + + public abstract Builder setHashAlgorithm(int hashAlgorithm); + + + abstract Builder setAllowedAuthenticationKeyIds(List allowedAuthenticationKeyIds); + public Builder setAllowedAuthenticationKeyIds(Collection allowedAuthenticationKeyIds) { + setAllowedAuthenticationKeyIds(Collections.unmodifiableList(new ArrayList<>(allowedAuthenticationKeyIds))); + return this; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java new file mode 100644 index 000000000..174efc4da --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh; + +import android.content.Context; +import android.support.annotation.NonNull; +import org.bouncycastle.openpgp.PGPAuthenticationSignatureGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; +import org.sufficientlysecure.keychain.operations.BaseOperation; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Passphrase; + +import java.util.Collection; + +/** + * This class supports a single, low-level, authentication operation. + *

+ * The operation of this class takes a AuthenticationParcel + * as input, and signs the included challenge as parametrized in the + * AuthenticationData object. It returns its status + * and a possible signature as a AuthenticationResult. + *

+ * + * @see AuthenticationParcel + */ +public class AuthenticationOperation extends BaseOperation { + + private static final String TAG = "AuthenticationOperation"; + + public AuthenticationOperation(Context context, KeyRepository keyRepository) { + super(context, keyRepository, null); + } + + @NonNull + @Override + public AuthenticationResult execute(AuthenticationParcel input, + CryptoInputParcel cryptoInput) { + return executeInternal(input.getAuthenticationData(), cryptoInput, input); + } + + @NonNull + public AuthenticationResult execute(AuthenticationData data, + CryptoInputParcel cryptoInput, + AuthenticationParcel authenticationParcel) { + return executeInternal(data, cryptoInput, authenticationParcel); + } + + /** + * Signs challenge based on given parameters + */ + private AuthenticationResult executeInternal(AuthenticationData data, + CryptoInputParcel cryptoInput, + AuthenticationParcel authenticationParcel) { + int indent = 0; + OperationLog log = new OperationLog(); + + log.add(LogType.MSG_AUTH, indent); + indent += 1; + + Log.d(TAG, data.toString()); + + long opTime; + long startTime = System.currentTimeMillis(); + + byte[] signature; + + byte[] challenge = authenticationParcel.getChallenge(); + + int hashAlgorithm = data.getHashAlgorithm(); + + long authMasterKeyId = data.getAuthenticationMasterKeyId(); + Long authSubKeyId = data.getAuthenticationSubKeyId(); + if (authSubKeyId == null) { + try { // Get the key id of the authentication key belonging to the master key id + authSubKeyId = mKeyRepository.getCachedPublicKeyRing(authMasterKeyId).getSecretAuthenticationId(); + } catch (PgpKeyNotFoundException e) { + log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + } + + // Get keyring with the authentication key + CanonicalizedSecretKeyRing authKeyRing; + try { + authKeyRing = mKeyRepository.getCanonicalizedSecretKeyRing(authMasterKeyId); + } catch (KeyRepository.NotFoundException e) { + log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + CanonicalizedSecretKey authKey = authKeyRing.getSecretKey(authSubKeyId); + + // Make sure the client is allowed to access this key + Collection allowedAuthenticationKeyIds = data.getAllowedAuthenticationKeyIds(); + if (allowedAuthenticationKeyIds != null && !allowedAuthenticationKeyIds.contains(authMasterKeyId)) { + // this key is in our db, but NOT allowed! + log.add(LogType.MSG_AUTH_ERROR_KEY_NOT_ALLOWED, indent + 1); + return new AuthenticationResult(AuthenticationResult.RESULT_KEY_DISALLOWED, log); + } + + // Make sure key is not expired or revoked + if (authKeyRing.isExpired() || authKeyRing.isRevoked() + || authKey.isExpired() || authKey.isRevoked()) { + log.add(LogType.MSG_AUTH_ERROR_REVOKED_OR_EXPIRED, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + // Make sure the selected key is allowed to authenticate + if (!authKey.canAuthenticate()) { + log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + CanonicalizedSecretKey.SecretKeyType secretKeyType; + try { + secretKeyType = mKeyRepository + .getCachedPublicKeyRing(authMasterKeyId) + .getSecretKeyType(authSubKeyId); + } catch (KeyRepository.NotFoundException e) { + log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + switch (secretKeyType) { + case DIVERT_TO_CARD: + case PASSPHRASE_EMPTY: { + boolean isUnlocked; + try { + isUnlocked = authKey.unlock(new Passphrase()); + } catch (PgpGeneralException e) { + log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + if (!isUnlocked) { + throw new AssertionError( + "PASSPHRASE_EMPTY/DIVERT_TO_CARD keyphrase not unlocked with empty passphrase." + + " This is a programming error!"); + } + break; + } + + case PASSPHRASE: { + Passphrase localPassphrase = cryptoInput.getPassphrase(); + if (localPassphrase == null) { + try { + localPassphrase = getCachedPassphrase(authMasterKeyId, authKey.getKeyId()); + } catch (PassphraseCacheInterface.NoSecretKeyException ignored) { + } + } + if (localPassphrase == null) { + log.add(LogType.MSG_AUTH_PENDING_PASSPHRASE, indent + 1); + return new AuthenticationResult(log, + RequiredInputParcel.createRequiredAuthenticationPassphrase( + authMasterKeyId, authKey.getKeyId()), + cryptoInput); + } + + boolean isUnlocked; + try { + isUnlocked = authKey.unlock(localPassphrase); + } catch (PgpGeneralException e) { + log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + if (!isUnlocked) { + log.add(LogType.MSG_AUTH_ERROR_BAD_PASSPHRASE, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + break; + } + + case GNU_DUMMY: { + log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + default: { + throw new AssertionError("Unhandled SecretKeyType! (should not happen)"); + } + + } + + PGPAuthenticationSignatureGenerator signatureGenerator; + try { + signatureGenerator = authKey.getAuthenticationSignatureGenerator( + hashAlgorithm, cryptoInput.getCryptoData()); + } catch (PgpGeneralException e) { + log.add(LogType.MSG_AUTH_ERROR_NFC, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + signatureGenerator.update(challenge, 0, challenge.length); + + try { + signature = signatureGenerator.generate().getSignature(); + } catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) { + // this secret key diverts to a OpenPGP card, thus requires user interaction + log.add(LogType.MSG_AUTH_PENDING_NFC, indent); + return new AuthenticationResult(log, RequiredInputParcel.createSecurityTokenAuthenticationOperation( + authKey.getRing().getMasterKeyId(), authKey.getKeyId(), + e.hashToSign, e.hashAlgo), cryptoInput); + } catch (PGPException e) { + log.add(LogType.MSG_AUTH_ERROR_SIG, indent); + return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); + } + + opTime = System.currentTimeMillis() - startTime; + Log.d(TAG, "Authentication operation duration : " + String.format("%.2f", opTime / 1000.0) + "s"); + + log.add(LogType.MSG_AUTH_OK, indent); + AuthenticationResult result = new AuthenticationResult(AuthenticationResult.RESULT_OK, log); + + result.setSignature(signature); + result.mOperationTime = opTime; + + return result; + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationParcel.java new file mode 100644 index 000000000..faa471268 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationParcel.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * Copyright (C) 2014 Vincent Breitmoser + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh; + +import android.os.Parcelable; + +import com.google.auto.value.AutoValue; + +/** + * AuthenticationParcel holds the challenge to be signed for authentication + */ +@AutoValue +public abstract class AuthenticationParcel implements Parcelable { + + public abstract AuthenticationData getAuthenticationData(); + + @SuppressWarnings("mutable") + public abstract byte[] getChallenge(); + + public static AuthenticationParcel createAuthenticationParcel(AuthenticationData authenticationData, + byte[] challenge) { + return new AutoValue_AuthenticationParcel(authenticationData, challenge); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationResult.java new file mode 100644 index 000000000..1f897aef1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationResult.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * Copyright (C) 2015 Vincent Breitmoser + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh; + +import android.os.Parcel; + +import org.sufficientlysecure.keychain.operations.results.InputPendingResult; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + +/** + * AuthenticationResult holds the result of a AuthenticationOperation + */ +public class AuthenticationResult extends InputPendingResult { + + public static final int RESULT_KEY_DISALLOWED = RESULT_ERROR + 32; + + byte[] mSignature; + public long mOperationTime; + + public void setSignature(byte[] signature) { + mSignature = signature; + } + + public byte[] getSignature() { + return mSignature; + } + + public AuthenticationResult(int result, OperationLog log) { + super(result, log); + } + + public AuthenticationResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + } + + public AuthenticationResult(Parcel source) { + super(source); + mSignature = source.readInt() != 0 ? source.createByteArray() : null; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + if (mSignature != null) { + dest.writeInt(1); + dest.writeByteArray(mSignature); + } else { + dest.writeInt(0); + } + } + + public static final Creator CREATOR = new Creator() { + public AuthenticationResult createFromParcel(final Parcel source) { + return new AuthenticationResult(source); + } + + public AuthenticationResult[] newArray(final int size) { + return new AuthenticationResult[size]; + } + }; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshDSAPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshDSAPublicKey.java new file mode 100644 index 000000000..5396abf91 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshDSAPublicKey.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh.key; + +import java.math.BigInteger; + +public class SshDSAPublicKey extends SshPublicKey { + public static final String KEY_ID = "ssh-dss"; + + private BigInteger mP; + private BigInteger mQ; + private BigInteger mG; + private BigInteger mY; + + public SshDSAPublicKey(BigInteger p, BigInteger q, BigInteger g, BigInteger y) { + super(KEY_ID); + mP = p; + mQ = q; + mG = g; + mY = y; + } + + @Override + protected void putData(SshEncodedData data) { + data.putMPInt(mP); + data.putMPInt(mQ); + data.putMPInt(mG); + data.putMPInt(mY); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshECDSAPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshECDSAPublicKey.java new file mode 100644 index 000000000..718ed501e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshECDSAPublicKey.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh.key; + +import java.math.BigInteger; + +public class SshECDSAPublicKey extends SshPublicKey { + public static final String KEY_ID = "ecdsa-sha2-"; + + private BigInteger mQ; + + private String mCurve; + + public SshECDSAPublicKey(String curve, BigInteger q) { + super(KEY_ID + curve); + mCurve = curve; + mQ = q; + } + + @Override + protected void putData(SshEncodedData data) { + data.putString(mCurve); + data.putString(mQ.toByteArray()); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEd25519PublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEd25519PublicKey.java new file mode 100644 index 000000000..ea5684f73 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEd25519PublicKey.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh.key; + +public class SshEd25519PublicKey extends SshPublicKey { + public static final String KEY_ID = "ssh-ed25519"; + + private byte[] mAbyte; + + public SshEd25519PublicKey(byte[] aByte) { + super(KEY_ID); + mAbyte = aByte; + } + + @Override + protected void putData(SshEncodedData data) { + data.putString(mAbyte); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEncodedData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEncodedData.java new file mode 100644 index 000000000..9455aa92d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshEncodedData.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ssh.key; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; + +public class SshEncodedData { + private ByteArrayOutputStream mData; + + public SshEncodedData() { + this(64); + } + + public SshEncodedData(int initialLength) { + mData = new ByteArrayOutputStream(initialLength); + } + + public void putString(String string) { + byte[] buffer = string.getBytes(); + putString(buffer); + } + + public void putString(byte[] buffer) { + putUInt32(buffer.length); + mData.write(buffer, 0, buffer.length); + } + + public void putMPInt(BigInteger mpInt) { + byte buffer[] = mpInt.toByteArray(); + if ((buffer.length == 1) && (buffer[0] == 0)) { + putUInt32(0); + } else { + putString(buffer); + } + } + + public void putUInt32(int uInt) { + mData.write(uInt >> 24); + mData.write(uInt >> 16); + mData.write(uInt >> 8); + mData.write(uInt); + } + + public void putByte(byte octet) { + mData.write(octet); + } + + public void putBoolean(boolean flag) { + if (flag) { + mData.write(1); + } else { + mData.write(0); + } + } + + public byte[] getBytes() { + + return mData.toByteArray(); + } +} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshPublicKey.java new file mode 100644 index 000000000..8a72e3999 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshPublicKey.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh.key; + +import android.util.Base64; + +public abstract class SshPublicKey { + protected SshEncodedData mData; + + private String mKeyType; + + public SshPublicKey(String keytype) { + mData = new SshEncodedData(); + mKeyType = keytype; + mData.putString(mKeyType); + } + + protected abstract void putData(SshEncodedData data); + + public String getPublicKeyBlob() { + String publicKeyBlob = ""; + publicKeyBlob += mKeyType + " "; + + putData(mData); + + String keyBlob = Base64.encodeToString(mData.getBytes(), Base64.NO_WRAP); + publicKeyBlob += keyBlob; + + return publicKeyBlob; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshRSAPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshRSAPublicKey.java new file mode 100644 index 000000000..187c8e3b3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/key/SshRSAPublicKey.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 Christian Hagau + * + * 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 . + */ + +package org.sufficientlysecure.keychain.ssh.key; + +import java.math.BigInteger; + +public class SshRSAPublicKey extends SshPublicKey { + public static final String KEY_ID = "ssh-rsa"; + + private BigInteger mExponent; + private BigInteger mModulus; + + public SshRSAPublicKey(BigInteger exponent, BigInteger modulus) { + super(KEY_ID); + mExponent = exponent; + mModulus = modulus; + } + + @Override + protected void putData(SshEncodedData data) { + data.putMPInt(mExponent); + data.putMPInt(mModulus); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 8450f81fa..1834c2762 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -25,6 +25,7 @@ package org.sufficientlysecure.keychain.ui; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Map; import android.content.Intent; import android.os.AsyncTask; @@ -63,6 +64,8 @@ import org.sufficientlysecure.keychain.util.Passphrase; */ 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"; @@ -233,6 +236,24 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { } break; } + case SECURITY_TOKEN_AUTH: { + long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint( + stConnection.getKeyFingerprint(KeyType.AUTH)); + + if (tokenKeyId != mRequiredInput.getSubKeyId()) { + throw new IOException(getString(R.string.error_wrong_security_token)); + } + + for (int i = 0; i < mRequiredInput.mInputData.length; i++) { + byte[] hash = mRequiredInput.mInputData[i]; + int algo = mRequiredInput.mSignAlgos[i]; + byte[] signedHash = stConnection.calculateAuthenticationSignature(hash, algo); + mInputParcel = mInputParcel.withCryptoData(hash, signedHash); + + } + + break; + } case SECURITY_TOKEN_MOVE_KEY_TO_CARD: { Passphrase adminPin = new Passphrase("12345678"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index 52e99b0ac..3bd95eb5a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -18,11 +18,6 @@ package org.sufficientlysecure.keychain.ui; -import java.io.BufferedWriter; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStreamWriter; - import android.app.Activity; import android.app.ActivityOptions; import android.content.ClipData; @@ -50,13 +45,15 @@ import android.view.animation.AlphaAnimation; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; - import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.pgp.SshPublicKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; @@ -68,6 +65,11 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import org.sufficientlysecure.keychain.util.Log; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; + public class ViewKeyAdvShareFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks { @@ -133,6 +135,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements View vKeyShareButton = view.findViewById(R.id.view_key_action_key_share); View vKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); ImageButton vKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger); + View vKeySshShareButton = view.findViewById(R.id.view_key_action_key_ssh_share); + View vKeySshClipboardButton = view.findViewById(R.id.view_key_action_key_ssh_clipboard); View vKeyUploadButton = view.findViewById(R.id.view_key_action_upload); vKeySafeSlingerButton.setColorFilter(FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorTertiaryText), PorterDuff.Mode.SRC_IN); @@ -152,13 +156,13 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements vKeyShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - shareKey(false); + shareKey(false, false); } }); vKeyClipboardButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - shareKey(true); + shareKey(true, false); } }); @@ -168,6 +172,18 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startSafeSlinger(mDataUri); } }); + vKeySshShareButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + shareKey(false, true); + } + }); + vKeySshClipboardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + shareKey(true, true); + } + }); vKeyUploadButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -192,17 +208,52 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startActivityForResult(safeSlingerIntent, 0); } - private void shareKey(boolean toClipboard) { + private boolean hasAuthenticationKey() { + KeyRepository keyRepository = KeyRepository.create(getContext()); + long masterKeyId = Constants.key.none; + long authSubKeyId = Constants.key.none; + try { + masterKeyId = keyRepository.getCachedPublicKeyRing(mDataUri).extractOrGetMasterKeyId(); + CachedPublicKeyRing cachedPublicKeyRing = keyRepository.getCachedPublicKeyRing(masterKeyId); + authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId(); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "key not found!", e); + } + return authSubKeyId != Constants.key.none; + } + + private String getShareKeyContent(boolean asSshKey) + throws PgpKeyNotFoundException, KeyRepository.NotFoundException, IOException, PgpGeneralException { + + KeyRepository keyRepository = KeyRepository.create(getContext()); + + String content; + long masterKeyId = keyRepository.getCachedPublicKeyRing(mDataUri).extractOrGetMasterKeyId(); + if (asSshKey) { + long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + CanonicalizedPublicKey publicKey = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId) + .getPublicKey(authSubKeyId); + SshPublicKey sshPublicKey = new SshPublicKey(publicKey); + content = sshPublicKey.getEncodedKey(); + } else { + content = keyRepository.getPublicKeyRingAsArmoredString(masterKeyId); + } + + return content; + } + + private void shareKey(boolean toClipboard, boolean asSshKey) { Activity activity = getActivity(); if (activity == null || mFingerprint == null) { return; } - KeyRepository keyRepository = - KeyRepository.create(getContext()); + if (asSshKey && !hasAuthenticationKey()) { + Notify.create(activity, R.string.authentication_subkey_not_found, Style.ERROR).show(); + return; + } try { - long masterKeyId = keyRepository.getCachedPublicKeyRing(mDataUri).extractOrGetMasterKeyId(); - String content = keyRepository.getPublicKeyRingAsArmoredString(masterKeyId); + String content = getShareKeyContent(asSshKey); if (toClipboard) { ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java index d307482b0..440aa5ea4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java @@ -78,6 +78,7 @@ public class AddSubkeyDialogFragment extends DialogFragment { private RadioButton mUsageSign; private RadioButton mUsageEncrypt; private RadioButton mUsageSignAndEncrypt; + private RadioButton mUsageAuthentication; private boolean mWillBeMasterKey; @@ -121,6 +122,7 @@ public class AddSubkeyDialogFragment extends DialogFragment { mUsageSign = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign); mUsageEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_encrypt); mUsageSignAndEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign_and_encrypt); + mUsageAuthentication = (RadioButton) view.findViewById(R.id.add_subkey_usage_authentication); if(mWillBeMasterKey) { dialog.setTitle(R.string.title_change_master_key); @@ -296,6 +298,8 @@ public class AddSubkeyDialogFragment extends DialogFragment { flags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE; } else if (mUsageSignAndEncrypt.isChecked()) { flags |= KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE; + } else if (mUsageAuthentication.isChecked()) { + flags |= KeyFlags.AUTHENTICATION; } diff --git a/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml b/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml index b232ed423..e6e0b25f0 100644 --- a/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml +++ b/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml @@ -72,6 +72,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/usage_sign_and_encrypt" /> + + diff --git a/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml b/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml new file mode 100644 index 000000000..72dda13b0 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +