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}