/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.authenticator;
import java.io.IOException;
import java.io.OutputStream;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
import org.spongycastle.openpgp.operator.PGPDigestCalculator;
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.spongycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.NetworkErrorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.widget.Toast;
import org.kontalk.BuildConfig;
import org.kontalk.R;
import org.kontalk.client.EndpointServer;
import org.kontalk.crypto.PGP;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.crypto.PersonalKeyExporter;
import org.kontalk.provider.Keyring;
import org.kontalk.ui.MainActivity;
import org.kontalk.ui.NumberValidation;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.XMPPUtils;
/**
* The authenticator.
* @author Daniele Ricci
* @version 1.0
*/
public class Authenticator extends AbstractAccountAuthenticator {
public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE;
public static final String ACCOUNT_TYPE_LEGACY = "org.kontalk.legacy.account";
public static final String DATA_PRIVATEKEY = "org.kontalk.key.private";
public static final String DATA_PUBLICKEY = "org.kontalk.key.public";
public static final String DATA_BRIDGECERT = "org.kontalk.key.bridgeCert";
public static final String DATA_NAME = "org.kontalk.key.name";
public static final String DATA_USER_PASSPHRASE = "org.kontalk.userPassphrase";
public static final String DATA_SERVER_URI = "org.kontalk.server";
/** @deprecated This was obviously deprecated from the beginning. */
@Deprecated
public static final String DATA_AUTHTOKEN = "org.kontalk.token";
@SuppressWarnings("WeakerAccess")
final Context mContext;
private final Handler mHandler;
public Authenticator(Context context) {
super(context);
mContext = context;
mHandler = new Handler(Looper.getMainLooper());
}
public static Account getDefaultAccount(Context ctx) {
return getDefaultAccount(AccountManager.get(ctx));
}
public static Account getDefaultAccount(AccountManager m) {
Account[] accs = m.getAccountsByType(ACCOUNT_TYPE);
return (accs.length > 0) ? accs[0] : null;
}
public static String getDefaultAccountName(Context ctx) {
Account acc = getDefaultAccount(ctx);
return (acc != null) ? acc.name : null;
}
public static String getSelfJID(Context ctx) {
String name = getDefaultAccountName(ctx);
return (name != null) ?
XMPPUtils.createLocalJID(ctx, MessageUtils.sha1(name)) : null;
}
public static boolean isSelfJID(Context ctx, String bareJid) {
String jid = getSelfJID(ctx);
return jid != null && jid.equalsIgnoreCase(bareJid);
}
public static String getDefaultDisplayName(Context context) {
AccountManager am = AccountManager.get(context);
Account account = getDefaultAccount(am);
return getDisplayName(am, account);
}
public static String getDisplayName(AccountManager am, Account account) {
return account != null ? am.getUserData(account, DATA_NAME) : null;
}
public static EndpointServer getDefaultServer(Context context) {
AccountManager am = AccountManager.get(context);
Account account = getDefaultAccount(am);
return getServer(am, account);
}
public static EndpointServer getServer(AccountManager am, Account account) {
if (account != null) {
String uri = am.getUserData(account, DATA_SERVER_URI);
return uri != null ? new EndpointServer(uri) : null;
}
return null;
}
public static boolean hasPersonalKey(AccountManager am, Account account) {
return account != null &&
am.getUserData(account, DATA_PRIVATEKEY) != null &&
am.getUserData(account, DATA_PUBLICKEY) != null &&
am.getUserData(account, DATA_BRIDGECERT) != null;
}
public static PersonalKey loadDefaultPersonalKey(Context ctx, String passphrase)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
AccountManager m = AccountManager.get(ctx);
Account acc = getDefaultAccount(m);
String privKeyData = m.getUserData(acc, DATA_PRIVATEKEY);
String pubKeyData = m.getUserData(acc, DATA_PUBLICKEY);
String bridgeCertData = m.getUserData(acc, DATA_BRIDGECERT);
if (privKeyData != null && pubKeyData != null && bridgeCertData != null)
return PersonalKey
.load(Base64.decode(privKeyData, Base64.DEFAULT),
Base64.decode(pubKeyData, Base64.DEFAULT),
passphrase,
Base64.decode(bridgeCertData, Base64.DEFAULT)
);
else
return null;
}
public static void exportDefaultPersonalKey(Context ctx, OutputStream dest, String passphrase, String exportPassphrase, boolean bridgeCertificate)
throws CertificateException, NoSuchProviderException, PGPException,
IOException, KeyStoreException, NoSuchAlgorithmException {
AccountManager m = AccountManager.get(ctx);
Account acc = getDefaultAccount(m);
String privKeyData = m.getUserData(acc, DATA_PRIVATEKEY);
byte[] privateKey = Base64.decode(privKeyData, Base64.DEFAULT);
byte[] bridgeCert = null;
if (bridgeCertificate) {
// bridge certificate is just plain data
String bridgeCertData = m.getUserData(acc, DATA_BRIDGECERT);
bridgeCert = Base64.decode(bridgeCertData, Base64.DEFAULT);
}
String pubKeyData = m.getUserData(acc, DATA_PUBLICKEY);
byte[] publicKey = Base64.decode(pubKeyData, Base64.DEFAULT);
// trusted keys
Map<String, Keyring.TrustedFingerprint> trustedKeys = Keyring.getTrustedKeys(ctx);
PersonalKeyExporter exp = new PersonalKeyExporter();
exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase, bridgeCert, trustedKeys, acc.name);
}
public static void setDefaultPersonalKey(Context ctx, byte[] publicKeyData, byte[] privateKeyData,
byte[] bridgeCertData, String passphrase) {
AccountManager am = AccountManager.get(ctx);
Account acc = getDefaultAccount(am);
// password is optional when updating just the public key
if (passphrase != null)
am.setPassword(acc, passphrase);
// private key data is optional when updating just the public key
if (privateKeyData != null)
am.setUserData(acc, Authenticator.DATA_PRIVATEKEY, Base64.encodeToString(privateKeyData, Base64.NO_WRAP));
am.setUserData(acc, Authenticator.DATA_PUBLICKEY, Base64.encodeToString(publicKeyData, Base64.NO_WRAP));
am.setUserData(acc, Authenticator.DATA_BRIDGECERT, Base64.encodeToString(bridgeCertData, Base64.NO_WRAP));
}
/**
* Set a new passphrase for the default account.
* Please note that this method does not invalidate the cached key or passphrase.
*/
public static void changePassphrase(Context ctx, String oldPassphrase, String newPassphrase, boolean fromUser)
throws PGPException, IOException {
// TODO let handle this to PGP or PersonalKey
AccountManager am = AccountManager.get(ctx);
Account acc = getDefaultAccount(am);
// get old secret key ring
String privKeyData = am.getUserData(acc, DATA_PRIVATEKEY);
byte[] privateKeyData = Base64.decode(privKeyData, Base64.DEFAULT);
KeyFingerPrintCalculator fpr = new BcKeyFingerprintCalculator();
PGPSecretKeyRing oldSecRing = new PGPSecretKeyRing(privateKeyData, fpr);
// old decryptor
PGPDigestCalculatorProvider calcProv = new JcaPGPDigestCalculatorProviderBuilder().build();
PBESecretKeyDecryptor oldDecryptor = new JcePBESecretKeyDecryptorBuilder(calcProv)
.setProvider(PGP.PROVIDER)
.build(oldPassphrase.toCharArray());
// new encryptor
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1);
PBESecretKeyEncryptor newEncryptor = new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256, sha1Calc)
.setProvider(PGP.PROVIDER).build(newPassphrase.toCharArray());
// create new secret key ring
PGPSecretKeyRing newSecRing = PGPSecretKeyRing.copyWithNewPassword(oldSecRing, oldDecryptor, newEncryptor);
// replace key data in AccountManager
byte[] newPrivateKeyData = newSecRing.getEncoded();
am.setUserData(acc, DATA_PRIVATEKEY, Base64.encodeToString(newPrivateKeyData, Base64.NO_WRAP));
am.setUserData(acc, DATA_USER_PASSPHRASE, String.valueOf(fromUser));
// replace password for account
am.setPassword(acc, newPassphrase);
}
public static void setPassphrase(Context ctx, String passphrase, boolean fromUser) {
AccountManager am = AccountManager.get(ctx);
Account acc = getDefaultAccount(am);
am.setUserData(acc, DATA_USER_PASSPHRASE, String.valueOf(fromUser));
// replace password for account
am.setPassword(acc, passphrase);
}
public static boolean isUserPassphrase(Context ctx) {
AccountManager am = AccountManager.get(ctx);
Account acc = getDefaultAccount(am);
return Boolean.parseBoolean(am.getUserData(acc, DATA_USER_PASSPHRASE));
}
public static void removeDefaultAccount(Context ctx, AccountManagerCallback<Boolean> callback) {
AccountManager am = AccountManager.get(ctx);
Account account = getDefaultAccount(am);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
final boolean result = am.removeAccountExplicitly(account);
callback.run(new AccountManagerFuture<Boolean>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
@Override
public Boolean getResult() throws OperationCanceledException, IOException, AuthenticatorException {
return result;
}
@Override
public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException {
return result;
}
});
}
else {
am.removeAccount(getDefaultAccount(am), callback, null);
}
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
final Bundle bundle = new Bundle();
if (getDefaultAccount(mContext) != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, R.string.only_one_account_supported,
Toast.LENGTH_LONG).show();
}
});
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_CANCELED);
}
else {
final Intent intent = new Intent(mContext, NumberValidation.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
}
return bundle;
}
/**
* System is requesting to confirm our credentials - this usually means that
* something has changed (e.g. new SIM card). We request the user to insert
* his/her personal key passphrase - which might not have been set, in that
* case that's unfortunate because it's an unrecoverable situation.
*/
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response,
Account account, Bundle options) throws NetworkErrorException {
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT,
MainActivity.passwordRequest(mContext));
return bundle;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response,
String accountType) {
throw new UnsupportedOperationException();
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
final Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "This authenticator does not support authentication tokens.");
return bundle;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response,
Account account, String[] features) throws NetworkErrorException {
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return result;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
throw new UnsupportedOperationException();
}
}