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}