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