/*
* 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.crypto;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Iterator;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPKeyPair;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.operator.OperatorCreationException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Base64InputStream;
import android.util.Base64OutputStream;
import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.PGP.PGPDecryptedKeyPairRing;
import org.kontalk.crypto.PGP.PGPKeyPairRing;
/** Personal asymmetric encryption key. */
public class PersonalKey implements Parcelable {
private static final String TAG = Kontalk.TAG;
private static final KeyFingerPrintCalculator sFingerprintCalculator =
PGP.sFingerprintCalculator;
public static final int MIN_PASSPHRASE_LENGTH = 4;
/** Decrypted key pair (for direct usage). */
private final PGPDecryptedKeyPairRing mPair;
/** X.509 bridge certificate. */
private final X509Certificate mBridgeCert;
private PersonalKey(PGPDecryptedKeyPairRing keyPair, X509Certificate bridgeCert) {
mPair = keyPair;
mBridgeCert = bridgeCert;
}
private PersonalKey(PGPKeyPair authKp, PGPKeyPair signKp, PGPKeyPair encryptKp, X509Certificate bridgeCert) {
this(new PGPDecryptedKeyPairRing(authKp, signKp, encryptKp), bridgeCert);
}
private PersonalKey(Parcel in) throws PGPException, IOException {
mPair = PGP.fromParcel(in);
mBridgeCert = null;
// TODO mBridgeCert = X509Bridge.fromParcel(in);
}
public PGPKeyPair getEncryptKeyPair() {
return mPair.encryptKey;
}
public PGPKeyPair getSignKeyPair() {
return mPair.signKey;
}
public PGPKeyPair getAuthKeyPair() {
return mPair.authKey;
}
public X509Certificate getBridgeCertificate() {
return mBridgeCert;
}
public PrivateKey getBridgePrivateKey() throws PGPException {
return PGP.convertPrivateKey(mPair.authKey.getPrivateKey());
}
public PGPPublicKeyRing getPublicKeyRing() throws IOException {
return new PGPPublicKeyRing(getEncodedPublicKeyRing(), sFingerprintCalculator);
}
public byte[] getEncodedPublicKeyRing() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
mPair.authKey.getPublicKey().encode(out);
mPair.signKey.getPublicKey().encode(out);
mPair.encryptKey.getPublicKey().encode(out);
return out.toByteArray();
}
/** Returns the first user ID on the key that matches the given network. */
public String getUserId(String network) {
return PGP.getUserId(mPair.authKey.getPublicKey(), network);
}
public String getFingerprint() {
return PGP.getFingerprint(mPair.authKey.getPublicKey());
}
public PGPKeyPairRing storeNetwork(String userId, String network, String name, String passphrase) throws PGPException, IOException {
return store(name, userId + '@' + network, null, passphrase);
}
public PGPKeyPairRing store(String name, String email, String comment, String passphrase) throws PGPException, IOException {
// name[ (comment)] <[email]>
StringBuilder userid = new StringBuilder(name);
if (comment != null) userid
.append(" (")
.append(comment)
.append(')');
userid.append(" <");
if (email != null)
userid.append(email);
userid.append('>');
return PGP.store(mPair, userid.toString(), passphrase);
}
/**
* Updates the public key.
* @return the public keyring.
*/
public PGPPublicKeyRing update(byte[] keyData) throws IOException {
PGPPublicKeyRing ring = new PGPPublicKeyRing(keyData, sFingerprintCalculator);
// FIXME should loop through the ring and check for master/subkey
mPair.authKey = new PGPKeyPair(ring.getPublicKey(), mPair.authKey.getPrivateKey());
return ring;
}
public PersonalKey copy(X509Certificate bridgeCert) {
return new PersonalKey(mPair, bridgeCert);
}
public String toBase64() {
ObjectOutputStream os = null;
try {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
Base64OutputStream enc = new Base64OutputStream(buf, Base64.NO_WRAP);
os = new ObjectOutputStream(enc);
PGP.serialize(mPair, os);
os.close();
return buf.toString();
}
catch (Exception e) {
// shouldn't happen - crash
throw new RuntimeException(e);
}
finally {
try {
if (os != null)
os.close();
}
catch (IOException ignored) {
}
}
}
public static PersonalKey fromBase64(String data) {
ObjectInputStream is = null;
try {
ByteArrayInputStream buf = new ByteArrayInputStream(data.getBytes());
Base64InputStream dec = new Base64InputStream(buf, Base64.NO_WRAP);
is = new ObjectInputStream(dec);
PGPDecryptedKeyPairRing pair = PGP.unserialize(is);
dec.close();
return new PersonalKey(pair, null);
}
catch (Exception e) {
// shouldn't happen - crash
throw new RuntimeException(e);
}
finally {
try {
if (is != null)
is.close();
}
catch (IOException ignored) {
}
}
}
/** Checks that the given personal key data is correct. */
public static PGPKeyPairRing test(InputStream privateKeyData, InputStream publicKeyData, String passphrase, InputStream bridgeCertData)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
// X.509 bridge certificate
X509Certificate bridgeCert = (bridgeCertData != null) ?
X509Bridge.load(bridgeCertData) : null;
return test(secRing, pubRing, passphrase, bridgeCert);
}
/** Checks that the given personal key data is correct. */
public static PGPKeyPairRing test(byte[] privateKeyData, byte[] publicKeyData, String passphrase, byte[] bridgeCertData)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
// X.509 bridge certificate
X509Certificate bridgeCert = (bridgeCertData != null) ?
X509Bridge.load(bridgeCertData) : null;
return test(secRing, pubRing, passphrase, bridgeCert);
}
private static PGPKeyPairRing test(PGPSecretKeyRing secRing, PGPPublicKeyRing pubRing, String passphrase, X509Certificate bridgeCert)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
// for now we just do a test load
load(secRing, pubRing, passphrase, bridgeCert);
return new PGPKeyPairRing(pubRing, secRing);
}
/** Creates a {@link PersonalKey} from private and public key input streams. */
public static PersonalKey load(InputStream privateKeyData, InputStream publicKeyData, String passphrase, InputStream bridgeCertData)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
// X.509 bridge certificate
X509Certificate bridgeCert = (bridgeCertData != null) ?
X509Bridge.load(bridgeCertData) : null;
return load(secRing, pubRing, passphrase, bridgeCert);
}
/** Creates a {@link PersonalKey} from private and public key byte buffers. */
public static PersonalKey load(byte[] privateKeyData, byte[] publicKeyData, String passphrase, byte[] bridgeCertData)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
// X.509 bridge certificate
X509Certificate bridgeCert = (bridgeCertData != null) ?
X509Bridge.load(bridgeCertData) : null;
return load(secRing, pubRing, passphrase, bridgeCert);
}
/** Creates a {@link PersonalKey} from private and public key byte buffers. */
public static PersonalKey load(byte[] privateKeyData, byte[] publicKeyData, String passphrase, X509Certificate bridgeCert)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPSecretKeyRing secRing = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
PGPPublicKeyRing pubRing = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
return load(secRing, pubRing, passphrase, bridgeCert);
}
@SuppressWarnings("unchecked")
public static PersonalKey load(PGPSecretKeyRing secRing, PGPPublicKeyRing pubRing, String passphrase, X509Certificate bridgeCert)
throws PGPException, IOException, CertificateException, NoSuchProviderException {
PGPDigestCalculatorProvider sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build();
PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder(sha1Calc)
.setProvider(PGP.PROVIDER)
.build(passphrase.toCharArray());
PGPKeyPair authKp, signKp, encryptKp;
PGPPublicKey authPub = null;
PGPPrivateKey authPriv = null;
PGPPublicKey signPub = null;
PGPPrivateKey signPriv = null;
PGPPublicKey encPub = null;
PGPPrivateKey encPriv = null;
// public keys
Iterator<PGPPublicKey> pkeys = pubRing.getPublicKeys();
while (pkeys.hasNext()) {
PGPPublicKey key = pkeys.next();
int keyFlags = PGP.getKeyFlags(key);
if (key.isMasterKey()) {
// legacy support
// if key flags has CAN_AUTHENTICATE, use the new key format
if ((keyFlags & PGPKeyFlags.CAN_AUTHENTICATE) == PGPKeyFlags.CAN_AUTHENTICATE) {
authPub = key;
}
else {
// no authentication key flags, presuming old key format
// use the master key for both authentication and signing
authPub = signPub = key;
}
}
else {
// legacy support
// if key flags has CAN_SIGN, use the new key format
if ((keyFlags & PGPKeyFlags.CAN_SIGN) == PGPKeyFlags.CAN_SIGN) {
signPub = key;
}
else {
// no encryption key flags, presuming old key format
// use the subkey for encryption
encPub = key;
}
}
}
// secret keys
Iterator<PGPSecretKey> skeys = secRing.getSecretKeys();
while (skeys.hasNext()) {
PGPSecretKey key = skeys.next();
int keyFlags = PGP.getKeyFlags(key.getPublicKey());
if (key.isMasterKey()) {
// legacy support
// if key flags has CAN_AUTHENTICATE, use the new key format
if ((keyFlags & PGPKeyFlags.CAN_AUTHENTICATE) == PGPKeyFlags.CAN_AUTHENTICATE) {
authPriv = key.extractPrivateKey(decryptor);
}
else {
// no authentication key flags, presuming old key format
// use the master key for both authentication and signing
authPriv = signPriv = key.extractPrivateKey(decryptor);
}
}
else {
// legacy support
// if key flags has CAN_SIGN, use the new key format
if ((keyFlags & PGPKeyFlags.CAN_SIGN) == PGPKeyFlags.CAN_SIGN) {
signPriv = key.extractPrivateKey(decryptor);
}
else {
// no encryption key flags, presuming old key format
// use the subkey for encryption
encPriv = key.extractPrivateKey(decryptor);
}
}
}
if (encPriv != null && encPub != null && signPriv != null && signPub != null) {
authKp = new PGPKeyPair(authPub, authPriv);
signKp = new PGPKeyPair(signPub, signPriv);
encryptKp = new PGPKeyPair(encPub, encPriv);
return new PersonalKey(authKp, signKp, encryptKp, bridgeCert);
}
throw new PGPException("invalid key data");
}
public static PersonalKey create(Date timestamp) throws IOException {
try {
PGPDecryptedKeyPairRing kp = PGP.create(timestamp);
return new PersonalKey(kp, null);
}
catch (Exception e) {
throw new IOException("unable to generate keypair", e);
}
}
/**
* Revokes the whole key pair using the master (signing) key.
* @param store true to store the key in this object
* @return the revoked master public key
*/
public PGPPublicKey revoke(boolean store)
throws PGPException, IOException, SignatureException {
PGPPublicKey revoked = PGP.revokeKey(mPair.authKey);
if (store)
mPair.authKey = new PGPKeyPair(revoked, mPair.authKey.getPrivateKey());
return revoked;
}
/** Stores the public keyring to the system {@link AccountManager}. */
public void updateAccountManager(Context context)
throws IOException, InvalidKeyException,
IllegalStateException, NoSuchAlgorithmException, SignatureException,
CertificateException, NoSuchProviderException, PGPException, OperatorCreationException {
AccountManager am = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
Account account = Authenticator.getDefaultAccount(am);
if (account != null) {
PGPPublicKeyRing pubRing = getPublicKeyRing();
// regenerate bridge certificate
byte[] bridgeCertData = X509Bridge.createCertificate(pubRing,
mPair.authKey.getPrivateKey()).getEncoded();
byte[] publicKeyData = pubRing.getEncoded();
am.setUserData(account, Authenticator.DATA_PUBLICKEY,
Base64.encodeToString(publicKeyData, Base64.NO_WRAP));
am.setUserData(account, Authenticator.DATA_BRIDGECERT,
Base64.encodeToString(bridgeCertData, Base64.NO_WRAP));
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
try {
PGP.toParcel(mPair, dest);
}
catch (Exception e) {
throw new RuntimeException("error writing key to parcel", e);
}
}
public static final Parcelable.Creator<PersonalKey> CREATOR =
new Parcelable.Creator<PersonalKey>() {
public PersonalKey createFromParcel(Parcel source) {
try {
return new PersonalKey(source);
}
catch (Exception e) {
Log.w(TAG, "error creating from parcel", e);
return null;
}
}
@Override
public PersonalKey[] newArray(int size) {
return new PersonalKey[size];
}
};
}