OmemoMessageBuilder.java
/**
*
* Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
*
* 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.jivesoftware.smackx.omemo.util;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import org.jivesoftware.smack.util.RandomUtil;
import org.jivesoftware.smackx.omemo.OmemoRatchet;
import org.jivesoftware.smackx.omemo.OmemoService;
import org.jivesoftware.smackx.omemo.element.OmemoElement;
import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
import org.jivesoftware.smackx.omemo.internal.OmemoAesCipher;
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
/**
* Class used to build OMEMO messages.
*
* @param <T_IdKeyPair> IdentityKeyPair class
* @param <T_IdKey> IdentityKey class
* @param <T_PreKey> PreKey class
* @param <T_SigPreKey> SignedPreKey class
* @param <T_Sess> Session class
* @param <T_Addr> Address class
* @param <T_ECPub> Elliptic Curve PublicKey class
* @param <T_Bundle> Bundle class
* @param <T_Ciph> Cipher class
* @author Paul Schaub
*/
public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
private final OmemoDevice userDevice;
private final OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet;
private final OmemoTrustCallback trustCallback;
private byte[] messageKey;
private final byte[] initializationVector;
private byte[] ciphertextMessage;
private final ArrayList<OmemoKeyElement> keys = new ArrayList<>();
/**
* Create an OmemoMessageBuilder.
*
* @param userDevice our OmemoDevice
* @param callback trustCallback for querying trust decisions
* @param ratchet our OmemoRatchet
* @param aesKey aes message key used for message encryption
* @param iv initialization vector used for message encryption
* @param message message we want to send
*
* @throws NoSuchPaddingException if the requested padding mechanism is not availble.
* @throws BadPaddingException if the input data is not padded properly.
* @throws InvalidKeyException if the key is invalid.
* @throws NoSuchAlgorithmException if no such algorithm is available.
* @throws IllegalBlockSizeException if the input data length is incorrect.
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
*/
public OmemoMessageBuilder(OmemoDevice userDevice,
OmemoTrustCallback callback,
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
byte[] aesKey,
byte[] iv,
String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
IllegalBlockSizeException,
InvalidAlgorithmParameterException {
this.userDevice = userDevice;
this.trustCallback = callback;
this.ratchet = ratchet;
this.messageKey = aesKey;
this.initializationVector = iv;
setMessage(message);
}
/**
* Create an OmemoMessageBuilder.
*
* @param userDevice our OmemoDevice
* @param callback trustCallback for querying trust decisions
* @param ratchet our OmemoRatchet
* @param message message we want to send
*
* @throws NoSuchPaddingException if the requested padding mechanism is not availble.
* @throws BadPaddingException if the input data is not padded properly.
* @throws InvalidKeyException if the key is invalid.
* @throws NoSuchAlgorithmException if no such algorithm is available.
* @throws IllegalBlockSizeException if the input data length is incorrect.
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
*/
public OmemoMessageBuilder(OmemoDevice userDevice,
OmemoTrustCallback callback,
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
String message)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
InvalidAlgorithmParameterException {
this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message);
}
/**
* Encrypt the message with the aes key.
* Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards.
* This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients.
*
* @see <a href="https://conversations.im/omemo/audit.pdf">OMEMO security audit</a>.
*
* @param message plaintext message
*
* @throws NoSuchPaddingException if the requested padding mechanism is not availble.
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
* @throws InvalidKeyException if the key is invalid.
* @throws BadPaddingException if the input data is not padded properly.
* @throws IllegalBlockSizeException if the input data length is incorrect.
*/
private void setMessage(String message)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
if (message == null) {
return;
}
// Encrypt message body
byte[] ciphertext = OmemoAesCipher.encryptAesGcmNoPadding(message, messageKey, initializationVector);
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
moveAuthTag(messageKey, ciphertext, clearKeyWithAuthTag, cipherTextWithoutAuthTag);
ciphertextMessage = cipherTextWithoutAuthTag;
messageKey = clearKeyWithAuthTag;
}
/**
* Move the auth tag from the end of the cipherText to the messageKey.
*
* @param messageKey source messageKey without authTag
* @param cipherText source cipherText with authTag
* @param messageKeyWithAuthTag destination messageKey with authTag
* @param cipherTextWithoutAuthTag destination cipherText without authTag
*/
static void moveAuthTag(byte[] messageKey,
byte[] cipherText,
byte[] messageKeyWithAuthTag,
byte[] cipherTextWithoutAuthTag) {
// Check dimensions of arrays
if (messageKeyWithAuthTag.length != messageKey.length + 16) {
throw new IllegalArgumentException("Length of messageKeyWithAuthTag must be length of messageKey + " +
"length of AuthTag (16)");
}
if (cipherTextWithoutAuthTag.length != cipherText.length - 16) {
throw new IllegalArgumentException("Length of cipherTextWithoutAuthTag must be length of cipherText " +
"- length of AuthTag (16)");
}
// Move auth tag from cipherText to messageKey
System.arraycopy(messageKey, 0, messageKeyWithAuthTag, 0, 16);
System.arraycopy(cipherText, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
System.arraycopy(cipherText, cipherText.length - 16, messageKeyWithAuthTag, 16, 16);
}
/**
* Add a new recipient device to the message.
*
* @param contactsDevice device of the recipient
*
* @throws NoIdentityKeyException if we have no identityKey of that device. Can be fixed by fetching and
* processing the devices bundle.
* @throws CorruptedOmemoKeyException if the identityKey of that device is corrupted.
* @throws UndecidedOmemoIdentityException if the user hasn't yet decided whether to trust that device or not.
* @throws UntrustedOmemoIdentityException if the user has decided not to trust that device.
* @throws IOException if an I/O error occurred.
*/
public void addRecipient(OmemoDevice contactsDevice)
throws NoIdentityKeyException, CorruptedOmemoKeyException, UndecidedOmemoIdentityException,
UntrustedOmemoIdentityException, IOException {
OmemoFingerprint fingerprint;
fingerprint = OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(userDevice, contactsDevice);
switch (trustCallback.getTrust(contactsDevice, fingerprint)) {
case undecided:
throw new UndecidedOmemoIdentityException(contactsDevice);
case trusted:
CiphertextTuple encryptedKey = ratchet.doubleRatchetEncrypt(contactsDevice, messageKey);
keys.add(new OmemoKeyElement(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage()));
break;
case untrusted:
throw new UntrustedOmemoIdentityException(contactsDevice, fingerprint);
}
}
/**
* Assemble an OmemoMessageElement from the current state of the builder.
*
* @return OMEMO element
*/
public OmemoElement finish() {
OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(
userDevice.getDeviceId(),
keys,
initializationVector
);
return new OmemoElement_VAxolotl(header, ciphertextMessage);
}
/**
* Generate a new AES key used to encrypt the message.
*
* @param keyType Key Type
* @param keyLength Key Length in bit
* @return new AES key
*
* @throws NoSuchAlgorithmException if no such algorithm is available.
*/
public static byte[] generateKey(String keyType, int keyLength) throws NoSuchAlgorithmException {
KeyGenerator generator = KeyGenerator.getInstance(keyType);
generator.init(keyLength);
return generator.generateKey().getEncoded();
}
/**
* Generate a 12 byte initialization vector for AES encryption.
*
* @return initialization vector
*/
public static byte[] generateIv() {
byte[] iv = new byte[12];
RandomUtil.fillWithSecureRandom(iv);
return iv;
}
}