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}