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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/authentication_key_item.xml b/OpenKeychain/src/main/res/layout/authentication_key_item.xml
new file mode 100644
index 000000000..5e8ed1477
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/authentication_key_item.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml
index 1ec80a9f8..b144d0201 100644
--- a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml
@@ -153,6 +153,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"No keys exported."
"Note: only subkeys support ElGamal."
"Couldn't find key %08X."
+ "This key does not have an authentication subkey."
"
- "%d bad secret key ignored. Perhaps you exported with the option\n --export-secret-subkeys\nMake sure you export with\n --export-secret-keys\ninstead.""
@@ -705,6 +706,7 @@
"Confirm key"
"Update from keyserver"
"Share with…"
+ "Share as SSH public key with…"
"Share over NFC"
"Upload to keyserver"
"Main Info"
@@ -1270,6 +1272,19 @@
"Starting sign and/or encrypt operation"
"Preparing symmetric encryption"
+
+ "Starting authentication operation"
+ "Selected authentication key cannot be found"
+ "Key selected for authentication is not allowed"
+ "Revoked/Expired key cannot be used for authentication"
+ "Unknown error unlocking key!"
+ "NFC token required, requesting user input…"
+ "Password required, requesting user input…"
+ "Bad password!"
+ "NFC data error!"
+ "Encountered OpenPGP signature exception!"
+ "Authentication operation successful!"
+
"Generating certifications"
- "Certifying one user ID for key %2$s"
@@ -1847,6 +1862,7 @@
Un-Suppress
View Key
Got it
+ Authentication
Forget
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java
new file mode 100644
index 000000000..2a35a65c0
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.operations;
+
+
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLog;
+import org.sufficientlysecure.keychain.KeychainTestRunner;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.provider.KeyRepository;
+import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+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 org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.PrintStream;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.Signature;
+import java.util.ArrayList;
+
+@RunWith(KeychainTestRunner.class)
+public class AuthenticationOperationTest {
+
+ private static UncachedKeyRing mStaticRing;
+ private static Passphrase mKeyPhrase;
+
+ private static PrintStream oldShadowStream;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ oldShadowStream = ShadowLog.stream;
+ // ShadowLog.stream = System.out;
+
+ /* keyring generation:
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel();
+
+ builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
+ Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.CERTIFY_OTHER, 0L));
+ builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
+ Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.AUTHENTICATION, 0L));
+ builder.addUserId("blah");
+ builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(new Passphrase("x")));
+
+ PgpEditKeyResult result = op.createSecretKeyRing(builder.build());
+ new FileOutputStream("/tmp/authenticate.sec").write(result.getRing().getEncoded());
+ */
+
+ mKeyPhrase = new Passphrase("x");
+ mStaticRing = KeyringTestingHelper.readRingFromResource("/test-keys/authenticate.sec");
+
+ }
+
+ @Before
+ public void setUp() {
+ KeyWritableRepository databaseInteractor =
+ KeyWritableRepository.create(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ databaseInteractor.saveSecretKeyRing(mStaticRing);
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testAuthenticate() throws Exception {
+
+ byte[] challenge = "dies ist ein challenge ☭".getBytes();
+ byte[] signature;
+
+ KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application);
+
+ long masterKeyId = mStaticRing.getMasterKeyId();
+ Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId();
+
+ { // sign challenge
+ AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application,
+ keyRepository);
+
+ AuthenticationData.Builder authData = AuthenticationData.builder();
+ authData.setAuthenticationMasterKeyId(masterKeyId);
+ authData.setAuthenticationSubKeyId(authSubKeyId);
+ authData.setHashAlgorithm(HashAlgorithmTags.SHA512);
+
+// ArrayList allowedKeyIds = new ArrayList<>(1);
+// allowedKeyIds.add(mStaticRing.getMasterKeyId());
+// authData.setAllowedAuthenticationKeyIds(allowedKeyIds);
+
+ AuthenticationParcel authenticationParcel = AuthenticationParcel
+ .createAuthenticationParcel(authData.build(), challenge);
+
+ CryptoInputParcel inputParcel = CryptoInputParcel.createCryptoInputParcel();
+ inputParcel = inputParcel.withPassphrase(mKeyPhrase);
+
+ AuthenticationResult result = op.execute(authData.build(), inputParcel, authenticationParcel);
+
+ Assert.assertTrue("authentication must succeed", result.success());
+
+ signature = result.getSignature();
+ }
+ { // verify signature
+ CanonicalizedPublicKey canonicalizedPublicKey = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId)
+ .getPublicKey(authSubKeyId);
+ PublicKey publicKey = canonicalizedPublicKey.getJcaPublicKey();
+
+ Signature signatureVerifier = Signature.getInstance("SHA512withECDSA");
+ signatureVerifier.initVerify(publicKey);
+ signatureVerifier.update(challenge);
+ boolean isSignatureValid = signatureVerifier.verify(signature);
+
+ Assert.assertTrue("signature must be valid", isSignatureValid);
+ }
+ }
+
+ @Test
+ public void testAccessControl() throws Exception {
+
+ byte[] challenge = "dies ist ein challenge ☭".getBytes();
+
+ KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application);
+
+ long masterKeyId = mStaticRing.getMasterKeyId();
+ Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId();
+
+ { // sign challenge - should succeed with selected key allowed
+ AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application,
+ keyRepository);
+
+ AuthenticationData.Builder authData = AuthenticationData.builder();
+ authData.setAuthenticationMasterKeyId(masterKeyId);
+ authData.setAuthenticationSubKeyId(authSubKeyId);
+ authData.setHashAlgorithm(HashAlgorithmTags.SHA512);
+
+ ArrayList allowedKeyIds = new ArrayList<>(1);
+ allowedKeyIds.add(mStaticRing.getMasterKeyId());
+ authData.setAllowedAuthenticationKeyIds(allowedKeyIds);
+
+ AuthenticationParcel authenticationParcel = AuthenticationParcel
+ .createAuthenticationParcel(authData.build(), challenge);
+
+ CryptoInputParcel inputParcel = CryptoInputParcel.createCryptoInputParcel();
+ inputParcel = inputParcel.withPassphrase(mKeyPhrase);
+
+ AuthenticationResult result = op.execute(authData.build(), inputParcel, authenticationParcel);
+
+ Assert.assertTrue("authentication must succeed with selected key allowed", result.success());
+ }
+ { // sign challenge - should fail with selected key disallowed
+ AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application,
+ keyRepository);
+
+ AuthenticationData.Builder authData = AuthenticationData.builder();
+ authData.setAuthenticationMasterKeyId(masterKeyId);
+ authData.setAuthenticationSubKeyId(authSubKeyId);
+ authData.setHashAlgorithm(HashAlgorithmTags.SHA512);
+
+ ArrayList allowedKeyIds = new ArrayList<>(1);
+ authData.setAllowedAuthenticationKeyIds(allowedKeyIds);
+
+
+ AuthenticationParcel authenticationParcel = AuthenticationParcel
+ .createAuthenticationParcel(authData.build(), challenge);
+
+ CryptoInputParcel inputParcel = CryptoInputParcel.createCryptoInputParcel();
+ inputParcel = inputParcel.withPassphrase(mKeyPhrase);
+
+ AuthenticationResult result = op.execute(authData.build(), inputParcel, authenticationParcel);
+
+ Assert.assertFalse("authentication must fail with selected key disallowed", result.success());
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java
new file mode 100644
index 000000000..e7003b6e6
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLog;
+import org.sufficientlysecure.keychain.KeychainTestRunner;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
+import org.sufficientlysecure.keychain.pgp.SshPublicKey;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.provider.KeyRepository;
+import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.PrintStream;
+import java.security.Security;
+
+@RunWith(KeychainTestRunner.class)
+public class SshPublicKeyTest {
+
+ private static UncachedKeyRing mStaticRing;
+ private static Passphrase mKeyPhrase;
+
+ private static PrintStream oldShadowStream;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ oldShadowStream = ShadowLog.stream;
+ // ShadowLog.stream = System.out;
+
+ /* keyring generation:
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel();
+
+ builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
+ Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.CERTIFY_OTHER, 0L));
+ builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
+ Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.AUTHENTICATION, 0L));
+ builder.addUserId("blah");
+ builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(new Passphrase("x")));
+
+ PgpEditKeyResult result = op.createSecretKeyRing(builder.build());
+ new FileOutputStream("/tmp/authenticate.sec").write(result.getRing().getEncoded());
+ */
+
+ mKeyPhrase = new Passphrase("x");
+ mStaticRing = KeyringTestingHelper.readRingFromResource("/test-keys/authenticate.sec");
+ }
+
+ @Before
+ public void setUp() {
+ KeyWritableRepository databaseInteractor =
+ KeyWritableRepository.create(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ databaseInteractor.saveSecretKeyRing(mStaticRing);
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testECDSA() throws Exception {
+ KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application);
+
+ long masterKeyId = mStaticRing.getMasterKeyId();
+ long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId();
+ CanonicalizedPublicKey canonicalizedPublicKey = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId)
+ .getPublicKey(authSubKeyId);
+
+ SshPublicKey publicKeyUtils = new SshPublicKey(canonicalizedPublicKey);
+ String publicKeyBlob = publicKeyUtils.getEncodedKey();
+
+ String publicKeyBlobExpected = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTY"
+ + "AAABBBJm2rlv9/8dgVm6VbN9OJDK1pA1Cb7HjJZv+zyiZGbpUrNWN81L1z45mnOfYafAzZMZ9SBy4J954wjp4d/pICIg=";
+
+ Assert.assertEquals("Public key blobs must be equal", publicKeyBlobExpected, publicKeyBlob);
+
+ }
+}
diff --git a/OpenKeychain/src/test/resources/test-keys/authenticate.sec b/OpenKeychain/src/test/resources/test-keys/authenticate.sec
new file mode 100644
index 000000000..a1d7ccaf8
Binary files /dev/null and b/OpenKeychain/src/test/resources/test-keys/authenticate.sec differ
diff --git a/libkeychain/src/main/java/org/bouncycastle/openpgp/PGPAuthenticationSignatureGenerator.java b/libkeychain/src/main/java/org/bouncycastle/openpgp/PGPAuthenticationSignatureGenerator.java
new file mode 100644
index 000000000..1ee235642
--- /dev/null
+++ b/libkeychain/src/main/java/org/bouncycastle/openpgp/PGPAuthenticationSignatureGenerator.java
@@ -0,0 +1,201 @@
+package org.bouncycastle.openpgp;
+
+import org.bouncycastle.bcpg.MPInteger;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.bcpg.SignaturePacket;
+import org.bouncycastle.bcpg.SignatureSubpacket;
+import org.bouncycastle.openpgp.operator.PGPContentSigner;
+import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
+import org.bouncycastle.util.BigIntegers;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+
+/**
+ * Generator for PGP Signatures.
+ */
+public class PGPAuthenticationSignatureGenerator
+{
+ private OutputStream sigOut;
+ private PGPContentSignerBuilder contentSignerBuilder;
+ private PGPContentSigner contentSigner;
+ private int sigType;
+ private byte lastb;
+ private int providedKeyAlgorithm = -1;
+
+ /**
+ * Create a signature generator built on the passed in contentSignerBuilder.
+ *
+ * @param contentSignerBuilder builder to produce PGPContentSigner objects for generating signatures.
+ */
+ public PGPAuthenticationSignatureGenerator(
+ PGPContentSignerBuilder contentSignerBuilder)
+ {
+ this.contentSignerBuilder = contentSignerBuilder;
+ }
+
+ /**
+ * Initialise the generator for signing.
+ *
+ * @param signatureType
+ * @param key
+ * @throws PGPException
+ */
+ public void init(
+ int signatureType,
+ PGPPrivateKey key)
+ throws PGPException
+ {
+ contentSigner = contentSignerBuilder.build(signatureType, key);
+ sigOut = contentSigner.getOutputStream();
+ sigType = contentSigner.getType();
+ lastb = 0;
+
+ if (providedKeyAlgorithm >= 0 && providedKeyAlgorithm != contentSigner.getKeyAlgorithm())
+ {
+ throw new PGPException("key algorithm mismatch");
+ }
+ }
+
+ public void update(
+ byte b)
+ {
+ if (sigType == PGPSignature.CANONICAL_TEXT_DOCUMENT)
+ {
+ if (b == '\r')
+ {
+ byteUpdate((byte)'\r');
+ byteUpdate((byte)'\n');
+ }
+ else if (b == '\n')
+ {
+ if (lastb != '\r')
+ {
+ byteUpdate((byte)'\r');
+ byteUpdate((byte)'\n');
+ }
+ }
+ else
+ {
+ byteUpdate(b);
+ }
+
+ lastb = b;
+ }
+ else
+ {
+ byteUpdate(b);
+ }
+ }
+
+ public void update(
+ byte[] b)
+ {
+ this.update(b, 0, b.length);
+ }
+
+ public void update(
+ byte[] b,
+ int off,
+ int len)
+ {
+ if (sigType == PGPSignature.CANONICAL_TEXT_DOCUMENT)
+ {
+ int finish = off + len;
+
+ for (int i = off; i != finish; i++)
+ {
+ this.update(b[i]);
+ }
+ }
+ else
+ {
+ blockUpdate(b, off, len);
+ }
+ }
+
+ private void byteUpdate(byte b)
+ {
+ try
+ {
+ sigOut.write(b);
+ }
+ catch (IOException e)
+ {
+ throw new PGPRuntimeOperationException(e.getMessage(), e);
+ }
+ }
+
+ private void blockUpdate(byte[] block, int off, int len)
+ {
+ try
+ {
+ sigOut.write(block, off, len);
+ }
+ catch (IOException e)
+ {
+ throw new PGPRuntimeOperationException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Return a signature object containing the current signature state.
+ *
+ * @return PGPSignature
+ * @throws PGPException
+ */
+ public PGPSignature generate()
+ throws PGPException
+ {
+ MPInteger[] sigValues;
+ ByteArrayOutputStream sOut = new ByteArrayOutputStream();
+ SignatureSubpacket[] hPkts, unhPkts;
+ hPkts = new SignatureSubpacket[0];
+ unhPkts = new SignatureSubpacket[0];
+
+ try
+ {
+ ByteArrayOutputStream hOut = new ByteArrayOutputStream();
+ byte[] data = hOut.toByteArray();
+ sOut.write(data);
+ }
+ catch (IOException e)
+ {
+ throw new PGPException("exception encoding hashed data.", e);
+ }
+
+ byte[] trailer = sOut.toByteArray();
+
+ blockUpdate(trailer, 0, trailer.length);
+
+ if (contentSigner.getKeyAlgorithm() == PublicKeyAlgorithmTags.RSA_SIGN
+ || contentSigner.getKeyAlgorithm() == PublicKeyAlgorithmTags.RSA_GENERAL) // an RSA signature
+ {
+ sigValues = new MPInteger[1];
+ sigValues[0] = new MPInteger(new BigInteger(1, contentSigner.getSignature()));
+ }
+ else if (contentSigner.getKeyAlgorithm() == PublicKeyAlgorithmTags.EDDSA)
+ {
+ byte[] sig = contentSigner.getSignature();
+
+ sigValues = new MPInteger[2];
+
+ sigValues[0] = new MPInteger(BigIntegers.fromUnsignedByteArray(sig, 0, 32));
+ sigValues[1] = new MPInteger(BigIntegers.fromUnsignedByteArray(sig, 32, 32));
+ }
+ else
+ {
+ sigValues = PGPUtil.dsaSigToMpi(contentSigner.getSignature());
+ }
+
+ byte[] digest = contentSigner.getDigest();
+ byte[] fingerPrint = new byte[2];
+
+ fingerPrint[0] = digest[0];
+ fingerPrint[1] = digest[1];
+
+ return new PGPSignature(new SignaturePacket(sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), contentSigner.getHashAlgorithm(), hPkts, unhPkts, fingerPrint, sigValues));
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 828239ce0..7c3b44eab 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,4 +17,6 @@ include ':openpgp-api-lib'
project(':openpgp-api-lib').projectDir = new File('extern/openpgp-api-lib/openpgp-api')
//include ':extern:KeybaseLib:Lib'
include ':KeybaseLib'
-project(':KeybaseLib').projectDir = new File('extern/KeybaseLib/Lib')
\ No newline at end of file
+project(':KeybaseLib').projectDir = new File('extern/KeybaseLib/Lib')
+include ':sshauthentication-api'
+project(':sshauthentication-api').projectDir = new File('sshauthentication-api')
diff --git a/sshauthentication-api/build.gradle b/sshauthentication-api/build.gradle
new file mode 100644
index 000000000..c4b1e9554
--- /dev/null
+++ b/sshauthentication-api/build.gradle
@@ -0,0 +1,36 @@
+apply plugin: 'com.android.library'
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.2'
+ // NOTE: Do not place application dependencies here
+ }
+}
+
+android {
+ if (project.hasProperty('rootProject.ext.compileSdkVersion')) {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ } else {
+ compileSdkVersion 25
+ }
+ if (project.hasProperty('rootProject.ext.buildToolsVersion')) {
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ } else {
+ buildToolsVersion '25.0.2'
+ }
+
+ defaultConfig {
+ minSdkVersion 9
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+ }
+
+ // Do not abort build if lint finds errors
+ lintOptions {
+ abortOnError false
+ }
+}
diff --git a/sshauthentication-api/src/main/AndroidManifest.xml b/sshauthentication-api/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..f33b391d1
--- /dev/null
+++ b/sshauthentication-api/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/sshauthentication-api/src/main/aidl/org/openintents/ssh/authentication/ISshAuthenticationService.aidl b/sshauthentication-api/src/main/aidl/org/openintents/ssh/authentication/ISshAuthenticationService.aidl
new file mode 100644
index 000000000..1815b0ae2
--- /dev/null
+++ b/sshauthentication-api/src/main/aidl/org/openintents/ssh/authentication/ISshAuthenticationService.aidl
@@ -0,0 +1,6 @@
+// ISshAuthenticationService.aidl
+package org.openintents.ssh.authentication;
+
+interface ISshAuthenticationService {
+ Intent execute(in Intent intent);
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApi.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApi.java
new file mode 100644
index 000000000..adce16718
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApi.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 Christian Hagau
+ * Copyright (C) 2017 Michael Perk
+ *
+ * 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.openintents.ssh.authentication;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.util.Log;
+
+public class SshAuthenticationApi {
+ private static final String TAG = "SshAuthenticationApi";
+
+ public static final String SERVICE_INTENT = "org.openintents.ssh.authentication.ISshAuthenticationService";
+
+ public static final String EXTRA_API_VERSION = "api_version";
+ public static final int API_VERSION = 1;
+
+ public static final String EXTRA_RESULT_CODE = "result_code";
+ public static final int RESULT_CODE_ERROR = 0;
+ public static final int RESULT_CODE_SUCCESS = 1;
+ public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = 2;
+
+ /**
+ * ACTION_SIGN
+ *
+ * Sign a given challenge
+ *
+ * required extras:
+ * String EXTRA_KEY_ID
+ * byte[] EXTRA_CHALLENGE
+ * int EXTRA_HASH_ALGORITHM
+ *
+ * returned extras:
+ * byte[] EXTRA_SIGNATURE
+ */
+ public static final String ACTION_SIGN = "org.openintents.ssh.action.SIGN";
+ public static final String EXTRA_CHALLENGE = "challenge";
+ public static final String EXTRA_HASH_ALGORITHM = "hash_algorithm";
+ public static final String EXTRA_SIGNATURE = "signature";
+
+ /* hash algorithms used in signature generation */
+ public static final int SHA1 = 0;
+ public static final int SHA224 = 1;
+ public static final int SHA256 = 2;
+ public static final int SHA384 = 3;
+ public static final int SHA512 = 4;
+ public static final int RIPEMD160 = 5;
+
+ /**
+ * ACTION_SELECT_KEY
+ *
+ * Select a key
+ *
+ * returned extras:
+ * String EXTRA_KEY_ID
+ * String EXTRA_KEY_DESCRIPTION
+ */
+ public static final String ACTION_SELECT_KEY = "org.openintents.ssh.action.SELECT_KEY";
+ public static final String EXTRA_KEY_DESCRIPTION = "key_description";
+
+ /**
+ * ACTION_GET_PUBLIC_KEY
+ *
+ * Get the public key for a key
+ *
+ * returns the public key encoded according to the ASN.1 type
+ * 'SubjectPublicKeyInfo' as defined in the X.509 standard,
+ * see RFC5280, RFC3279 and draft-ietf-curdle-pkix
+ * and their respective updates
+ *
+ * required extras:
+ * String EXTRA_KEY_ID
+ *
+ * returned extras:
+ * byte[] EXTRA_PUBLIC_KEY
+ * int EXTRA_PUBLIC_KEY_ALGORITHM
+ */
+ public static final String ACTION_GET_PUBLIC_KEY = "org.openintents.ssh.action.GET_PUBLIC_KEY";
+ public static final String EXTRA_PUBLIC_KEY = "public_key";
+ public static final String EXTRA_PUBLIC_KEY_ALGORITHM = "public_key_algorithm";
+
+ /* public key algorithms */
+ public static final int RSA = 0;
+ public static final int ECDSA = 1;
+ public static final int EDDSA = 2;
+ public static final int DSA = 3;
+
+ /**
+ * ACTION_GET_SSH_PUBLIC_KEY
+ *
+ * Get the SSH public key for a key
+ *
+ * returns the public key in SSH public key format,
+ * as described in RFC4253, RFC5656 and draft-ietf-curdle-ssh-ed25519
+ * and their respective updates
+ *
+ * required extras:
+ * String EXTRA_KEY_ID
+ *
+ * returned extras:
+ * String EXTRA_SSH_PUBLIC_KEY
+ */
+ public static final String ACTION_GET_SSH_PUBLIC_KEY = "org.openintents.ssh.action.GET_SSH_PUBLIC_KEY";
+ public static final String EXTRA_SSH_PUBLIC_KEY = "ssh_public_key";
+
+ /**
+ * Error result of type SshAuthenticationApiError
+ */
+ public static final String EXTRA_ERROR = "error";
+
+ /**
+ * Pending Intent requiring user interaction
+ */
+ public static final String EXTRA_PENDING_INTENT = "intent";
+
+ /**
+ * Key identifier
+ */
+ public static final String EXTRA_KEY_ID = "key_id";
+
+
+ private final ISshAuthenticationService mService;
+ private final Context mContext;
+
+
+ public SshAuthenticationApi(Context context, ISshAuthenticationService service) {
+ mService = service;
+ mContext = context;
+ }
+
+ public interface ISshAgentCallback {
+ void onReturn(final Intent result);
+ }
+
+ private class SshAgentAsyncTask extends AsyncTask {
+ Intent data;
+ ISshAgentCallback callback;
+
+ private SshAgentAsyncTask(Intent data, ISshAgentCallback callback) {
+ this.data = data;
+ this.callback = callback;
+ }
+
+ @Override
+ protected Intent doInBackground(Void... unused) {
+ return executeApi(data);
+ }
+
+ protected void onPostExecute(Intent result) {
+ callback.onReturn(result);
+ }
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void executeApiAsync(Intent data, ISshAgentCallback callback) {
+ SshAgentAsyncTask task = new SshAgentAsyncTask(data, callback);
+
+ // don't serialize async tasks, always execute them in parallel
+ // http://commonsware.com/blog/2012/04/20/asynctask-threading-regression-confirmed.html
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+ } else {
+ task.execute((Void[]) null);
+ }
+ }
+
+ public Intent executeApi(Intent data) {
+ try {
+ // always send version from client
+ data.putExtra(SshAuthenticationApi.EXTRA_API_VERSION, SshAuthenticationApi.API_VERSION);
+
+ Intent result;
+
+ // blocks until result is ready
+ result = mService.execute(data);
+
+ // set class loader to current context to allow unparcelling of SshAuthenticationApiError
+ // http://stackoverflow.com/a/3806769
+ result.setExtrasClassLoader(mContext.getClassLoader());
+
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in executeApi call", e);
+ return createExceptionErrorResult(SshAuthenticationApiError.CLIENT_SIDE_ERROR,
+ "Exception in executeApi call", e);
+ }
+ }
+
+ 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/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApiError.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApiError.java
new file mode 100644
index 000000000..c0e23952e
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationApiError.java
@@ -0,0 +1,88 @@
+/*
+ * 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.openintents.ssh.authentication;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class SshAuthenticationApiError implements Parcelable {
+
+ /**
+ * Error codes
+ */
+ /* values chosen for compatibility with openpgp-api */
+ public static final int CLIENT_SIDE_ERROR = -1;
+ public static final int GENERIC_ERROR = 0;
+ public static final int INCOMPATIBLE_API_VERSIONS = 1;
+ public static final int INTERNAL_ERROR = 2;
+
+ /* values chosen for no intersection with openpgp-api */
+ public static final int UNKNOWN_ACTION = -128;
+ public static final int NO_KEY_ID = -129;
+ public static final int NO_SUCH_KEY = -130;
+ public static final int NO_AUTH_KEY = -131;
+
+ /* values chosen to be invalid enumeration values in their respective domain */
+ public static final int INVALID_ALGORITHM = -254;
+ public static final int INVALID_HASH_ALGORITHM = -253;
+
+
+ private int mError;
+ private String mMessage;
+
+
+ public SshAuthenticationApiError(int error, String message) {
+ mError = error;
+ mMessage = message;
+ }
+
+ protected SshAuthenticationApiError(Parcel in) {
+ mError = in.readInt();
+ mMessage = in.readString();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public SshAuthenticationApiError createFromParcel(Parcel in) {
+ return new SshAuthenticationApiError(in);
+ }
+
+ @Override
+ public SshAuthenticationApiError[] newArray(int size) {
+ return new SshAuthenticationApiError[size];
+ }
+ };
+
+ public int getError() {
+ return mError;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mError);
+ dest.writeString(mMessage);
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationConnection.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationConnection.java
new file mode 100644
index 000000000..680314ec4
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/SshAuthenticationConnection.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 Christian Hagau
+ * Copyright (C) 2017 Jonas Dippel
+ *
+ * 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.openintents.ssh.authentication;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+public class SshAuthenticationConnection {
+
+ private final Context mContext;
+ private final String mTargetPackage;
+
+ private ISshAuthenticationService mSSHAgent;
+
+ /**
+ * Callback for signaling connection events
+ */
+ public interface OnBound {
+ /**
+ * Called when the connection is bound to the service
+ *
+ * @param sshAgent the bound remote service stub
+ */
+ void onBound(ISshAuthenticationService sshAgent);
+
+ /**
+ * Called when the connection is disconnected due to some error
+ */
+ void onError();
+ }
+
+ private OnBound mOnBoundListener;
+
+ /**
+ * Construct an SshAuthenticationConnection instance with the desired attributes
+ *
+ * @param context the application context
+ * @param targetPackage the package of the service to bind to
+ */
+ public SshAuthenticationConnection(Context context, String targetPackage) {
+ mContext = context;
+ mTargetPackage = targetPackage;
+ }
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mSSHAgent = ISshAuthenticationService.Stub.asInterface(service);
+ mOnBoundListener.onBound(mSSHAgent);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mSSHAgent = null;
+ mOnBoundListener.onError();
+ }
+ };
+
+ /**
+ * Connect to the Service via a Connection
+ *
+ * @return false if service was not found or an error occured, else true
+ */
+ public boolean connect(final OnBound onBoundListener) {
+ mOnBoundListener = onBoundListener;
+ if (mSSHAgent == null) {
+ Intent intent = new Intent(SshAuthenticationApi.SERVICE_INTENT);
+ intent.setPackage(mTargetPackage);
+ return mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ } else {
+ mOnBoundListener.onBound(mSSHAgent);
+ return true;
+ }
+ }
+
+ /**
+ * Check whether the Connection is bound to a service
+ *
+ * @return whether the Connection is bound to a service
+ */
+ public boolean isConnected() {
+ return mSSHAgent != null;
+ }
+
+ public void disconnect() {
+ mSSHAgent = null;
+ mContext.unbindService(mServiceConnection);
+ }
+
+ public ISshAuthenticationService getService() {
+ return mSSHAgent;
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/KeySelectionRequest.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/KeySelectionRequest.java
new file mode 100644
index 000000000..273bb1c5d
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/KeySelectionRequest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.openintents.ssh.authentication.request;
+
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+
+public class KeySelectionRequest extends Request {
+
+ @Override
+ protected void getData(Intent intent) {
+ }
+
+ @Override
+ protected void putData(Intent request) {
+ }
+
+ @Override
+ protected String getAction() {
+ return SshAuthenticationApi.ACTION_SELECT_KEY;
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/PublicKeyRequest.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/PublicKeyRequest.java
new file mode 100644
index 000000000..d9f5c5837
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/PublicKeyRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.openintents.ssh.authentication.request;
+
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+
+public class PublicKeyRequest extends Request {
+
+ private String mKeyId;
+
+ public PublicKeyRequest(String keyId) {
+ mKeyId = keyId;
+ }
+
+ @Override
+ protected void getData(Intent intent) {
+ mKeyId = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
+ }
+
+ @Override
+ protected void putData(Intent request) {
+ request.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, mKeyId);
+ }
+
+ @Override
+ protected String getAction() {
+ return SshAuthenticationApi.ACTION_GET_PUBLIC_KEY;
+ }
+
+ public String getKeyID() {
+ return mKeyId;
+ }
+
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/Request.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/Request.java
new file mode 100644
index 000000000..9b9d8a1b1
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/Request.java
@@ -0,0 +1,41 @@
+/*
+ * 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.openintents.ssh.authentication.request;
+
+import android.content.Intent;
+
+public abstract class Request {
+
+ public Intent toIntent() {
+ Intent request = new Intent();
+ request.setAction(getAction());
+
+ putData(request);
+
+ return request;
+ }
+
+ private void populatefromIntent(Intent intent) {
+ getData(intent);
+ }
+
+ protected abstract void getData(Intent intent);
+
+ protected abstract void putData(Intent request);
+
+ protected abstract String getAction();
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SigningRequest.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SigningRequest.java
new file mode 100644
index 000000000..4feff3a6b
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SigningRequest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 Christian Hagau
+ * Copyright (C) 2017 Michael Perk
+ *
+ * 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.openintents.ssh.authentication.request;
+
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public class SigningRequest extends Request {
+
+ private byte[] mChallenge;
+ private String mKeyIdentifier;
+ private int mHashAlgorithm;
+
+ public SigningRequest(byte[] challenge, String keyIdentifier, int hashAlgorithm) {
+ mHashAlgorithm = hashAlgorithm;
+ mKeyIdentifier = keyIdentifier;
+ mChallenge = challenge;
+ }
+
+ @Override
+ protected String getAction() {
+ return SshAuthenticationApi.ACTION_SIGN;
+ }
+
+ @Override
+ protected void getData(Intent intent) {
+ mHashAlgorithm = intent.getIntExtra(SshAuthenticationApi.EXTRA_HASH_ALGORITHM, SshAuthenticationApiError.INVALID_HASH_ALGORITHM);
+ mKeyIdentifier = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
+ mChallenge = intent.getByteArrayExtra(SshAuthenticationApi.EXTRA_CHALLENGE);
+ }
+
+ @Override
+ protected void putData(Intent request) {
+ request.putExtra(SshAuthenticationApi.EXTRA_HASH_ALGORITHM, mHashAlgorithm);
+ request.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, mKeyIdentifier);
+ request.putExtra(SshAuthenticationApi.EXTRA_CHALLENGE, mChallenge);
+ }
+
+ public int getHashAlgorithm() {
+ return mHashAlgorithm;
+ }
+
+ public String getKeyIdentifier() {
+ return mKeyIdentifier;
+ }
+
+ public byte[] getChallenge() {
+ return mChallenge;
+ }
+
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SshPublicKeyRequest.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SshPublicKeyRequest.java
new file mode 100644
index 000000000..20ad8356d
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/request/SshPublicKeyRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.openintents.ssh.authentication.request;
+
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+
+public class SshPublicKeyRequest extends Request {
+
+ private String mKeyId;
+
+ public SshPublicKeyRequest(String keyId) {
+ mKeyId = keyId;
+ }
+
+ @Override
+ protected void getData(Intent intent) {
+ mKeyId = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
+ }
+
+ @Override
+ protected void putData(Intent request) {
+ request.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, mKeyId);
+ }
+
+ @Override
+ protected String getAction() {
+ return SshAuthenticationApi.ACTION_GET_SSH_PUBLIC_KEY;
+ }
+
+ public String getKeyId() {
+ return mKeyId;
+ }
+
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/KeySelectionResponse.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/KeySelectionResponse.java
new file mode 100644
index 000000000..e3842673e
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/KeySelectionResponse.java
@@ -0,0 +1,66 @@
+/*
+ * 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.openintents.ssh.authentication.response;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public class KeySelectionResponse extends Response {
+
+ private String mKeyId;
+ private String mKeyDescription;
+
+ public KeySelectionResponse(Intent data) {
+ super(data);
+ }
+
+ public KeySelectionResponse(PendingIntent pendingIntent) {
+ super(pendingIntent);
+ }
+
+ public KeySelectionResponse(SshAuthenticationApiError error) {
+ super(error);
+ }
+
+ public KeySelectionResponse(String keyId, String keyDescription) {
+ super();
+ mKeyId = keyId;
+ mKeyDescription = keyDescription;
+ }
+
+ @Override
+ protected void getResults(Intent intent) {
+ mKeyId = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
+ mKeyDescription = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_DESCRIPTION);
+ }
+
+ @Override
+ protected void putResults(Intent intent) {
+ intent.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, mKeyId);
+ intent.putExtra(SshAuthenticationApi.EXTRA_KEY_DESCRIPTION, mKeyDescription);
+ }
+
+ public String getKeyId() {
+ return mKeyId;
+ }
+
+ public String getKeyDescription() {
+ return mKeyDescription;
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/PublicKeyResponse.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/PublicKeyResponse.java
new file mode 100644
index 000000000..30856f526
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/PublicKeyResponse.java
@@ -0,0 +1,68 @@
+/*
+ * 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.openintents.ssh.authentication.response;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public class PublicKeyResponse extends Response {
+ public static final int INVALID_ALGORITHM = SshAuthenticationApiError.INVALID_ALGORITHM;
+
+ private byte[] mEncodedPublicKey;
+ private int mKeyAlgorithm;
+
+ public PublicKeyResponse(Intent data) {
+ super(data);
+ }
+
+ public PublicKeyResponse(PendingIntent pendingIntent) {
+ super(pendingIntent);
+ }
+
+ public PublicKeyResponse(SshAuthenticationApiError error) {
+ super(error);
+ }
+
+ public PublicKeyResponse(byte[] encodedPublicKey,
+ int keyAlgorithm) {
+ super();
+ mEncodedPublicKey = encodedPublicKey;
+ mKeyAlgorithm = keyAlgorithm;
+ }
+
+ @Override
+ protected void getResults(Intent intent) {
+ mEncodedPublicKey = intent.getByteArrayExtra(SshAuthenticationApi.EXTRA_PUBLIC_KEY);
+ mKeyAlgorithm = intent.getIntExtra(SshAuthenticationApi.EXTRA_PUBLIC_KEY_ALGORITHM, INVALID_ALGORITHM);
+ }
+
+ @Override
+ protected void putResults(Intent intent) {
+ intent.putExtra(SshAuthenticationApi.EXTRA_PUBLIC_KEY, mEncodedPublicKey);
+ intent.putExtra(SshAuthenticationApi.EXTRA_PUBLIC_KEY_ALGORITHM, mKeyAlgorithm);
+ }
+
+ public byte[] getEncodedPublicKey() {
+ return mEncodedPublicKey;
+ }
+
+ public int getKeyAlgorithm() {
+ return mKeyAlgorithm;
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/Response.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/Response.java
new file mode 100644
index 000000000..997f82917
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/Response.java
@@ -0,0 +1,107 @@
+/*
+ * 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.openintents.ssh.authentication.response;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public abstract class Response {
+ public static final int RESULT_CODE_ERROR = SshAuthenticationApi.RESULT_CODE_ERROR;
+ public static final int RESULT_CODE_SUCCESS = SshAuthenticationApi.RESULT_CODE_SUCCESS;
+ public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED;
+
+ protected int mResultCode;
+
+ protected PendingIntent mPendingIntent;
+
+ protected SshAuthenticationApiError mError;
+
+ protected Response() {
+ mResultCode = RESULT_CODE_SUCCESS;
+ }
+
+ public Response(Intent data) {
+ populateFromIntent(data);
+ }
+
+ public Response(PendingIntent pendingIntent) {
+ mPendingIntent = pendingIntent;
+ mResultCode = RESULT_CODE_USER_INTERACTION_REQUIRED;
+ }
+
+ public Response(SshAuthenticationApiError error) {
+ mError = error;
+ mResultCode = RESULT_CODE_ERROR;
+ }
+
+ private void populateFromIntent(Intent intent) {
+ int resultCode = intent.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR);
+
+ switch (resultCode) {
+ case SshAuthenticationApi.RESULT_CODE_SUCCESS:
+ mResultCode = RESULT_CODE_SUCCESS;
+ getResults(intent);
+ break;
+ case SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ mResultCode = RESULT_CODE_USER_INTERACTION_REQUIRED;
+ mPendingIntent = intent.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT);
+ break;
+ case SshAuthenticationApi.RESULT_CODE_ERROR:
+ mResultCode = RESULT_CODE_ERROR;
+ mError = intent.getParcelableExtra(SshAuthenticationApi.EXTRA_ERROR);
+ }
+ }
+
+ public Intent toIntent() {
+ Intent intent = new Intent();
+
+ switch (mResultCode) {
+ case RESULT_CODE_SUCCESS:
+ intent.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_SUCCESS);
+ putResults(intent);
+ break;
+ case RESULT_CODE_USER_INTERACTION_REQUIRED:
+ intent.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
+ intent.putExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT, mPendingIntent);
+ break;
+ case RESULT_CODE_ERROR:
+ intent.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR);
+ intent.putExtra(SshAuthenticationApi.EXTRA_ERROR, mError);
+ }
+
+ return intent;
+ }
+
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ public SshAuthenticationApiError getError() {
+ return mError;
+ }
+
+ protected abstract void getResults(Intent intent);
+
+ protected abstract void putResults(Intent intent);
+
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SigningResponse.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SigningResponse.java
new file mode 100644
index 000000000..3e9ee032e
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SigningResponse.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 Christian Hagau
+ * Copyright (C) 2017 Jonas Dippel, Michael Perk
+ *
+ * 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.openintents.ssh.authentication.response;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public class SigningResponse extends Response {
+
+ private byte[] mSignature;
+
+ public SigningResponse(Intent data) {
+ super(data);
+ }
+
+ public SigningResponse(PendingIntent pendingIntent) {
+ super(pendingIntent);
+ }
+
+ public SigningResponse(SshAuthenticationApiError error) {
+ super(error);
+ }
+
+ public SigningResponse(byte[] signature) {
+ super();
+ mSignature = signature;
+ }
+
+ @Override
+ protected void getResults(Intent intent) {
+ mSignature = intent.getByteArrayExtra(SshAuthenticationApi.EXTRA_SIGNATURE);
+ }
+
+ @Override
+ protected void putResults(Intent intent) {
+ intent.putExtra(SshAuthenticationApi.EXTRA_SIGNATURE, mSignature);
+ }
+
+ public byte[] getSignature() {
+ return mSignature;
+ }
+
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SshPublicKeyResponse.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SshPublicKeyResponse.java
new file mode 100644
index 000000000..952347ab7
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/response/SshPublicKeyResponse.java
@@ -0,0 +1,58 @@
+/*
+ * 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.openintents.ssh.authentication.response;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+
+public class SshPublicKeyResponse extends Response {
+
+ private String mSshPublicKey;
+
+ public SshPublicKeyResponse(Intent data) {
+ super(data);
+ }
+
+ public SshPublicKeyResponse(PendingIntent pendingIntent) {
+ super(pendingIntent);
+ }
+
+ public SshPublicKeyResponse(SshAuthenticationApiError error) {
+ super(error);
+ }
+
+ public SshPublicKeyResponse(String sshPublicKey) {
+ super();
+ mSshPublicKey = sshPublicKey;
+ }
+
+ @Override
+ protected void getResults(Intent intent) {
+ mSshPublicKey = intent.getStringExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY);
+ }
+
+ @Override
+ protected void putResults(Intent intent) {
+ intent.putExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY, mSshPublicKey);
+ }
+
+ public String getSshPublicKey() {
+ return mSshPublicKey;
+ }
+}
diff --git a/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/util/SshAuthenticationApiUtils.java b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/util/SshAuthenticationApiUtils.java
new file mode 100644
index 000000000..8402b9fef
--- /dev/null
+++ b/sshauthentication-api/src/main/java/org/openintents/ssh/authentication/util/SshAuthenticationApiUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.openintents.ssh.authentication.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class SshAuthenticationApiUtils {
+ public static List getAgentProviders(Context context) {
+ Intent intent = new Intent(SshAuthenticationApi.SERVICE_INTENT);
+ List resolvedInfo = context.getPackageManager().queryIntentServices(intent, 0);
+ ArrayList providers = new ArrayList<>();
+ if (resolvedInfo != null) {
+ for (ResolveInfo resolveInfoEntry : resolvedInfo) {
+ providers.add(resolveInfoEntry.serviceInfo.packageName);
+ }
+ }
+ return providers;
+ }
+
+}