001/**
002 *
003 * Copyright 2017 Paul Schaub, 2019-2021 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.omemo.util;
018
019import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
020import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
021
022import java.io.IOException;
023import java.security.InvalidAlgorithmParameterException;
024import java.security.InvalidKeyException;
025import java.security.NoSuchAlgorithmException;
026import java.util.ArrayList;
027
028import javax.crypto.BadPaddingException;
029import javax.crypto.IllegalBlockSizeException;
030import javax.crypto.KeyGenerator;
031import javax.crypto.NoSuchPaddingException;
032
033import org.jivesoftware.smack.util.RandomUtil;
034
035import org.jivesoftware.smackx.omemo.OmemoRatchet;
036import org.jivesoftware.smackx.omemo.OmemoService;
037import org.jivesoftware.smackx.omemo.element.OmemoElement;
038import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
039import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
040import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
041import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
042import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
043import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
044import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
045import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
046import org.jivesoftware.smackx.omemo.internal.OmemoAesCipher;
047import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
048import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
049import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
050
051
052/**
053 * Class used to build OMEMO messages.
054 *
055 * @param <T_IdKeyPair> IdentityKeyPair class
056 * @param <T_IdKey>     IdentityKey class
057 * @param <T_PreKey>    PreKey class
058 * @param <T_SigPreKey> SignedPreKey class
059 * @param <T_Sess>      Session class
060 * @param <T_Addr>      Address class
061 * @param <T_ECPub>     Elliptic Curve PublicKey class
062 * @param <T_Bundle>    Bundle class
063 * @param <T_Ciph>      Cipher class
064 * @author Paul Schaub
065 */
066public class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
067
068    private final OmemoDevice userDevice;
069    private final OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet;
070    private final OmemoTrustCallback trustCallback;
071
072    private byte[] messageKey;
073    private final byte[] initializationVector;
074
075    private byte[] ciphertextMessage;
076    private final ArrayList<OmemoKeyElement> keys = new ArrayList<>();
077
078    /**
079     * Create an OmemoMessageBuilder.
080     *
081     * @param userDevice our OmemoDevice
082     * @param callback trustCallback for querying trust decisions
083     * @param ratchet our OmemoRatchet
084     * @param aesKey aes message key used for message encryption
085     * @param iv initialization vector used for message encryption
086     * @param message message we want to send
087     *
088     * @throws NoSuchPaddingException if the requested padding mechanism is not available.
089     * @throws BadPaddingException if the input data is not padded properly.
090     * @throws InvalidKeyException if the key is invalid.
091     * @throws NoSuchAlgorithmException if no such algorithm is available.
092     * @throws IllegalBlockSizeException if the input data length is incorrect.
093     * @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
094     */
095    public OmemoMessageBuilder(OmemoDevice userDevice,
096                               OmemoTrustCallback callback,
097                               OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
098                               byte[] aesKey,
099                               byte[] iv,
100                               String message)
101            throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
102            IllegalBlockSizeException,
103            InvalidAlgorithmParameterException {
104        this.userDevice = userDevice;
105        this.trustCallback = callback;
106        this.ratchet = ratchet;
107        this.messageKey = aesKey;
108        this.initializationVector = iv;
109        setMessage(message);
110    }
111
112    /**
113     * Create an OmemoMessageBuilder.
114     *
115     * @param userDevice our OmemoDevice
116     * @param callback trustCallback for querying trust decisions
117     * @param ratchet our OmemoRatchet
118     * @param message message we want to send
119     *
120     * @throws NoSuchPaddingException if the requested padding mechanism is not available.
121     * @throws BadPaddingException if the input data is not padded properly.
122     * @throws InvalidKeyException if the key is invalid.
123     * @throws NoSuchAlgorithmException if no such algorithm is available.
124     * @throws IllegalBlockSizeException if the input data length is incorrect.
125     * @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
126     */
127    public OmemoMessageBuilder(OmemoDevice userDevice,
128                               OmemoTrustCallback callback,
129                               OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
130                               String message)
131            throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
132            InvalidAlgorithmParameterException {
133        this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message);
134    }
135
136    /**
137     * Encrypt the message with the aes key.
138     * Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards.
139     * This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients.
140     *
141     * @see <a href="https://conversations.im/omemo/audit.pdf">OMEMO security audit</a>.
142     *
143     * @param message plaintext message
144     *
145     * @throws NoSuchPaddingException if the requested padding mechanism is not available.
146     * @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
147     * @throws InvalidKeyException if the key is invalid.
148     * @throws BadPaddingException if the input data is not padded properly.
149     * @throws IllegalBlockSizeException if the input data length is incorrect.
150     */
151    private void setMessage(String message)
152            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
153            InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
154        if (message == null) {
155            return;
156        }
157
158        // Encrypt message body
159        byte[] ciphertext = OmemoAesCipher.encryptAesGcmNoPadding(message, messageKey, initializationVector);
160
161        byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
162        byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
163
164        moveAuthTag(messageKey, ciphertext, clearKeyWithAuthTag, cipherTextWithoutAuthTag);
165
166        ciphertextMessage = cipherTextWithoutAuthTag;
167        messageKey = clearKeyWithAuthTag;
168    }
169
170    /**
171     * Move the auth tag from the end of the cipherText to the messageKey.
172     *
173     * @param messageKey source messageKey without authTag
174     * @param cipherText source cipherText with authTag
175     * @param messageKeyWithAuthTag destination messageKey with authTag
176     * @param cipherTextWithoutAuthTag destination cipherText without authTag
177     */
178    static void moveAuthTag(byte[] messageKey,
179                            byte[] cipherText,
180                            byte[] messageKeyWithAuthTag,
181                            byte[] cipherTextWithoutAuthTag) {
182        // Check dimensions of arrays
183        if (messageKeyWithAuthTag.length != messageKey.length + 16) {
184            throw new IllegalArgumentException("Length of messageKeyWithAuthTag must be length of messageKey + " +
185                    "length of AuthTag (16)");
186        }
187
188        if (cipherTextWithoutAuthTag.length != cipherText.length - 16) {
189            throw new IllegalArgumentException("Length of cipherTextWithoutAuthTag must be length of cipherText " +
190                    "- length of AuthTag (16)");
191        }
192
193        // Move auth tag from cipherText to messageKey
194        System.arraycopy(messageKey, 0, messageKeyWithAuthTag, 0, 16);
195        System.arraycopy(cipherText, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
196        System.arraycopy(cipherText, cipherText.length - 16, messageKeyWithAuthTag, 16, 16);
197    }
198
199    /**
200     * Add a new recipient device to the message.
201     *
202     * @param contactsDevice device of the recipient
203     *
204     * @throws NoIdentityKeyException if we have no identityKey of that device. Can be fixed by fetching and
205     *                                processing the devices bundle.
206     * @throws CorruptedOmemoKeyException if the identityKey of that device is corrupted.
207     * @throws UndecidedOmemoIdentityException if the user hasn't yet decided whether to trust that device or not.
208     * @throws UntrustedOmemoIdentityException if the user has decided not to trust that device.
209     * @throws IOException if an I/O error occurred.
210     */
211    public void addRecipient(OmemoDevice contactsDevice)
212            throws NoIdentityKeyException, CorruptedOmemoKeyException, UndecidedOmemoIdentityException,
213            UntrustedOmemoIdentityException, IOException {
214
215        OmemoFingerprint fingerprint;
216        fingerprint = OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(userDevice, contactsDevice);
217
218        switch (trustCallback.getTrust(contactsDevice, fingerprint)) {
219
220            case undecided:
221                throw new UndecidedOmemoIdentityException(contactsDevice);
222
223            case trusted:
224                CiphertextTuple encryptedKey = ratchet.doubleRatchetEncrypt(contactsDevice, messageKey);
225                keys.add(new OmemoKeyElement(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage()));
226                break;
227
228            case untrusted:
229                throw new UntrustedOmemoIdentityException(contactsDevice, fingerprint);
230
231        }
232    }
233
234    /**
235     * Assemble an OmemoMessageElement from the current state of the builder.
236     *
237     * @return OMEMO element
238     */
239    public OmemoElement finish() {
240        OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(
241                userDevice.getDeviceId(),
242                keys,
243                initializationVector
244        );
245        return new OmemoElement_VAxolotl(header, ciphertextMessage);
246    }
247
248    /**
249     * Generate a new AES key used to encrypt the message.
250     *
251     * @param keyType Key Type
252     * @param keyLength Key Length in bit
253     * @return new AES key
254     *
255     * @throws NoSuchAlgorithmException if no such algorithm is available.
256     */
257    public static byte[] generateKey(String keyType, int keyLength) throws NoSuchAlgorithmException {
258        KeyGenerator generator = KeyGenerator.getInstance(keyType);
259        generator.init(keyLength);
260        return generator.generateKey().getEncoded();
261    }
262
263    /**
264     * Generate a 12 byte initialization vector for AES encryption.
265     *
266     * @return initialization vector
267     */
268    public static byte[] generateIv() {
269        byte[] iv = new byte[12];
270        RandomUtil.fillWithSecureRandom(iv);
271        return iv;
272    }
273}