/* * Kontalk Java client * Copyright (C) 2016 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.model; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; import java.security.NoSuchProviderException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.kontalk.misc.KonException; import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; import org.kontalk.persistence.Config; import org.kontalk.util.EncodingUtils; /** * The user account. * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ public final class Account { private static final Logger LOGGER = Logger.getLogger(Account.class.getName()); private static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; private static final String BRIDGE_CERT_FILENAME = "kontalk-login.crt"; private final Path mKeyDir; private final Config mConf; private PersonalKey mKey = null; Account(Path keyDir, Config config) { mKeyDir = keyDir; mConf = config; } public Optional<PersonalKey> getPersonalKey() { return Optional.ofNullable(mKey); } public PersonalKey load(char[] password) throws KonException { // read key files byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); byte[] bridgeCertData = this.readFile(BRIDGE_CERT_FILENAME, false); // load key try { mKey = PersonalKey.load(privateKeyData, password, bridgeCertData); } catch (PGPException | IOException | CertificateException | NoSuchProviderException ex) { LOGGER.log(Level.WARNING, "can't load personal key", ex); throw new KonException(KonException.Error.LOAD_KEY, ex); } return mKey; } public void setAccount(byte[] privateKeyData, char[] password) throws KonException { // try to load key PersonalKey key; try { key = PersonalKey.load(privateKeyData, password); } catch (PGPException | IOException | CertificateException | NoSuchProviderException ex) { LOGGER.log(Level.WARNING, "can't import personal key", ex); throw new KonException(KonException.Error.IMPORT_KEY, ex); } // key seems valid. Save to config dir byte[] bridgeCertData; try { bridgeCertData = X509Bridge.encode(key.getBridgeCertificate()); } catch (CertificateEncodingException | IOException ex) { LOGGER.log(Level.WARNING, "can't encode bridge certificaate"); throw new KonException(KonException.Error.IMPORT_KEY, ex); } this.writeBytesToFile(bridgeCertData, BRIDGE_CERT_FILENAME, false); this.writePrivateKey(privateKeyData, password, new char[0]); // success! use the new key mKey = key; // parse JID from user ID in key, could be wrong, e.g. an email address // overwritten when connecting to server String address = PGPUtils.parseUID(key.getUserId())[2]; Config.getInstance().setProperty(Config.ACC_JID, address); LOGGER.info("new account, temporary JID: "+address); } public char[] getPassword() { return Config.getInstance().getString(Config.ACC_PASS).toCharArray(); } public void setPassword(char[] oldPassword, char[] newPassword) throws KonException { byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); this.writePrivateKey(privateKeyData, oldPassword, newPassword); } private void writePrivateKey(byte[] privateKeyData, char[] oldPassword, char[] newPassword) throws KonException { // old password if (oldPassword.length < 1) oldPassword = mConf.getString(Config.ACC_PASS).toCharArray(); // new password boolean unset = newPassword.length == 0; if (unset) newPassword = EncodingUtils.randomString(40).toCharArray(); // write new try { privateKeyData = PGPUtils.copySecretKeyRingWithNewPassword(privateKeyData, oldPassword, newPassword).getEncoded(); } catch (IOException | PGPException ex) { LOGGER.log(Level.WARNING, "can't change password", ex); throw new KonException(KonException.Error.CHANGE_PASS, ex); } this.writeBytesToFile(privateKeyData, PRIVATE_KEY_FILENAME, true); // new saved password String savedPass = unset ? new String(newPassword) : ""; mConf.setProperty(Config.ACC_PASS, savedPass); } public boolean isPresent() { return this.fileExists(PRIVATE_KEY_FILENAME) && this.fileExists(BRIDGE_CERT_FILENAME); } public boolean isPasswordProtected() { // using configuration option to determine this return mConf.getString(Config.ACC_PASS).isEmpty(); } private byte[] readFile(String filename, boolean disarm) throws KonException { byte[] bytes; try (InputStream input = new FileInputStream(new File(mKeyDir.toString(), filename))) { bytes = disarm ? PGPUtils.mayDisarm(input) : IOUtils.toByteArray(input); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't read key file", ex); throw new KonException(KonException.Error.READ_FILE, ex); } return bytes; } private void writeBytesToFile(byte[] bytes, String filename, boolean armored) throws KonException { try { OutputStream outStream = new FileOutputStream(new File(mKeyDir.toString(), filename)); if (armored) outStream = new ArmoredOutputStream(outStream); outStream.write(bytes); outStream.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't write key file", ex); throw new KonException(KonException.Error.WRITE_FILE, ex); } } private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } }