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