/*
* 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.crypto;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator;
import org.kontalk.model.Contact;
import org.kontalk.model.message.OutMessage;
import org.kontalk.model.message.Transmission;
import org.kontalk.util.CPIMMessage;
import org.kontalk.util.EncodingUtils;
/**
*
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
final class Encryptor {
private static final Logger LOGGER = Logger.getLogger(Encryptor.class.getName());
// should always be a power of 2
private static final int BUFFER_SIZE = 1 << 8;
private final PersonalKey myKey;
private final OutMessage message;
Encryptor(PersonalKey myKey, OutMessage message) {
this.myKey = myKey;
this.message = message;
}
String encryptMessageRFC3923() {
return encryptData(message.getContent().getPlainText(), "text/plain");
}
String encryptString(String plainText) {
byte[] plainData = EncodingUtils.stringToBytes(plainText);
List<PGPUtils.PGPCoderKey> receiverKeys = this.loadKeysOrNull();
if (receiverKeys == null)
return "";
return encrypt(plainData, receiverKeys);
}
String encryptStanzaRFC3923(String xml) {
String data = "<xmpp xmlns='jabber:client'>" + xml + "</xmpp>";
return encryptData(data, "application/xmpp+xml");
}
private String encryptData(String data, String mime) {
List<PGPUtils.PGPCoderKey> receiverKeys = this.loadKeysOrNull();
if (receiverKeys == null)
return "";
// secure the message against replay attacks using Message/CPIM
String from = myKey.getUserId();
String[] tos = receiverKeys.stream()
.map(key -> key.userID)
.toArray(String[]::new);
CPIMMessage cpim = new CPIMMessage(from, tos, new Date(), mime, data);
byte[] plainText;
try {
plainText = cpim.toByteArray();
} catch (UnsupportedEncodingException ex) {
LOGGER.log(Level.WARNING, "CPIM's charset not supported", ex);
plainText = cpim.toString().getBytes();
}
return encrypt(plainText, receiverKeys);
}
private String encrypt(byte[] plainText, List<PGPUtils.PGPCoderKey> receiverKeys) {
if (message.getCoderStatus().getEncryption() != Coder.Encryption.DECRYPTED) {
LOGGER.warning("message does not want to be encrypted");
return "";
}
ByteArrayInputStream in = new ByteArrayInputStream(plainText);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
encryptAndSign(in, out, myKey, receiverKeys);
} catch(IOException | PGPException ex) {
LOGGER.log(Level.WARNING, "can't encrypt data", ex);
message.setSecurityErrors(EnumSet.of(Coder.Error.UNKNOWN_ERROR));
return "";
}
return EncodingUtils.bytesToBase64(out.toByteArray());
}
Optional<File> encryptAttachment(File file) {
List<PGPUtils.PGPCoderKey> receiverKeys = this.loadKeysOrNull();
if (receiverKeys == null)
return Optional.empty();
File tempFile;
try {
tempFile = File.createTempFile("kontalk_enc_att", ".dat");
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "can't create temporary file.", ex);
return Optional.empty();
}
try (FileInputStream in = new FileInputStream(file);
FileOutputStream out = new FileOutputStream(tempFile)) {
encryptAndSign(in, out, myKey, receiverKeys);
} catch (IOException | PGPException ex) {
LOGGER.log(Level.WARNING, "can't encrypt attachment", ex);
return Optional.empty();
}
LOGGER.info("attachment encryption successful");
return Optional.of(tempFile);
}
private List<PGPUtils.PGPCoderKey> loadKeysOrNull() {
List<Contact> contacts = message.getTransmissions().stream()
.map(Transmission::getContact)
.collect(Collectors.toList());
List<PGPUtils.PGPCoderKey> receiverKeys = contacts.stream()
.map(c -> Coder.contactkey(c).orElse(null))
.collect(Collectors.toList());
if (receiverKeys.stream().anyMatch(Objects::isNull)) {
message.setSecurityErrors(EnumSet.of(Coder.Error.KEY_UNAVAILABLE));
return null;
}
return receiverKeys;
}
/**
* Encrypt, sign and write input stream data to output stream.
* Input and output stream are closed.
*/
private static void encryptAndSign(
InputStream plainInput, OutputStream encryptedOutput,
PersonalKey myKey, List<PGPUtils.PGPCoderKey> receiverKeys)
throws IOException, PGPException {
// setup data encryptor & generator
BcPGPDataEncryptorBuilder encryptor = new BcPGPDataEncryptorBuilder(PGPEncryptedData.AES_192);
encryptor.setWithIntegrityPacket(true);
encryptor.setSecureRandom(new SecureRandom());
// add public key recipients
PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(encryptor);
receiverKeys.forEach(key ->
encGen.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key.encryptKey)));
OutputStream encryptedOut = encGen.open(encryptedOutput, new byte[BUFFER_SIZE]);
// setup compressed data generator
PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
OutputStream compressedOut = compGen.open(encryptedOut, new byte[BUFFER_SIZE]);
// setup signature generator
int algo = myKey.getSigningAlgorithm();
PGPSignatureGenerator sigGen = new PGPSignatureGenerator(
new BcPGPContentSignerBuilder(algo, HashAlgorithmTags.SHA256));
sigGen.init(PGPSignature.BINARY_DOCUMENT, myKey.getPrivateSigningKey());
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
spGen.setSignerUserID(false, myKey.getUserId());
sigGen.setUnhashedSubpackets(spGen.generate());
sigGen.generateOnePassVersion(false).encode(compressedOut);
// Initialize literal data generator
PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
OutputStream literalOut = literalGen.open(
compressedOut,
PGPLiteralData.BINARY,
"",
new Date(),
new byte[BUFFER_SIZE]);
// read the "in" stream, compress, encrypt and write to the "out" stream
// this must be done if clear data is bigger than the buffer size
// but there are other ways to optimize...
byte[] buf = new byte[BUFFER_SIZE];
int len;
while ((len = plainInput.read(buf)) > 0) {
literalOut.write(buf, 0, len);
sigGen.update(buf, 0, len);
}
literalGen.close();
// generate the signature, compress, encrypt and write to the "out" stream
sigGen.generate().encode(compressedOut);
compGen.close();
encGen.close();
}
}