001/** 002 * 003 * Copyright 2017 Paul Schaub 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.internal; 018 019import java.io.UnsupportedEncodingException; 020import java.util.ArrayList; 021import java.util.List; 022 023import javax.crypto.BadPaddingException; 024import javax.crypto.IllegalBlockSizeException; 025 026import org.jivesoftware.smack.packet.Message; 027import org.jivesoftware.smack.util.StringUtils; 028 029import org.jivesoftware.smackx.omemo.OmemoFingerprint; 030import org.jivesoftware.smackx.omemo.OmemoManager; 031import org.jivesoftware.smackx.omemo.OmemoStore; 032import org.jivesoftware.smackx.omemo.element.OmemoElement; 033import org.jivesoftware.smackx.omemo.element.OmemoElement.OmemoHeader.Key; 034import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; 035import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException; 036import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; 037 038/** 039 * This class represents a OMEMO session between us and another device. 040 * 041 * @param <T_IdKeyPair> IdentityKeyPair class 042 * @param <T_IdKey> IdentityKey class 043 * @param <T_PreKey> PreKey class 044 * @param <T_SigPreKey> SignedPreKey class 045 * @param <T_Sess> Session class 046 * @param <T_Addr> Address class 047 * @param <T_ECPub> Elliptic Curve PublicKey class 048 * @param <T_Bundle> Bundle class 049 * @param <T_Ciph> Cipher class 050 * @author Paul Schaub 051 */ 052public abstract class OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> { 053 054 protected final T_Ciph cipher; 055 protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore; 056 protected final OmemoDevice remoteDevice; 057 protected final OmemoManager omemoManager; 058 protected T_IdKey identityKey; 059 protected int preKeyId = -1; 060 061 /** 062 * Constructor used when we establish the session. 063 * 064 * @param omemoManager OmemoManager of our device 065 * @param omemoStore OmemoStore where we want to store the session and get key information from 066 * @param remoteDevice the OmemoDevice we want to establish the session with 067 * @param identityKey identityKey of the recipient 068 */ 069 public OmemoSession(OmemoManager omemoManager, 070 OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, 071 OmemoDevice remoteDevice, T_IdKey identityKey) { 072 this(omemoManager, omemoStore, remoteDevice); 073 this.identityKey = identityKey; 074 } 075 076 /** 077 * Another constructor used when they establish the session with us. 078 * 079 * @param omemoManager OmemoManager of our device 080 * @param omemoStore OmemoStore we want to store the session and their key in 081 * @param remoteDevice identityKey of the partner 082 */ 083 public OmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore, 084 OmemoDevice remoteDevice) { 085 this.omemoManager = omemoManager; 086 this.omemoStore = omemoStore; 087 this.remoteDevice = remoteDevice; 088 this.cipher = createCipher(remoteDevice); 089 } 090 091 /** 092 * Try to decrypt the transported message key using the double ratchet session. 093 * 094 * @param element omemoElement 095 * @param keyId our keyId 096 * @return tuple of cipher generated from the unpacked message key and the authtag 097 * @throws CryptoFailedException if decryption using the double ratchet fails 098 * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage 099 */ 100 public CipherAndAuthTag decryptTransportedKey(OmemoElement element, int keyId) throws CryptoFailedException, 101 NoRawSessionException { 102 byte[] unpackedKey = null; 103 List<CryptoFailedException> decryptExceptions = new ArrayList<>(); 104 List<Key> keys = element.getHeader().getKeys(); 105 // Find key with our ID. 106 for (OmemoElement.OmemoHeader.Key k : keys) { 107 if (k.getId() == keyId) { 108 try { 109 unpackedKey = decryptMessageKey(k.getData()); 110 break; 111 } catch (CryptoFailedException e) { 112 // There might be multiple keys with our id, but we can only decrypt one. 113 // So we can't throw the exception, when decrypting the first duplicate which is not for us. 114 decryptExceptions.add(e); 115 } 116 } 117 } 118 119 if (unpackedKey == null) { 120 if (!decryptExceptions.isEmpty()) { 121 throw MultipleCryptoFailedException.from(decryptExceptions); 122 } 123 124 throw new CryptoFailedException("Transported key could not be decrypted, since no provided message key. Provides keys: " + keys); 125 } 126 127 byte[] messageKey = new byte[16]; 128 byte[] authTag = null; 129 130 if (unpackedKey.length == 32) { 131 authTag = new byte[16]; 132 // copy key part into messageKey 133 System.arraycopy(unpackedKey, 0, messageKey, 0, 16); 134 // copy tag part into authTag 135 System.arraycopy(unpackedKey, 16, authTag, 0,16); 136 } else if (element.isKeyTransportElement() && unpackedKey.length == 16) { 137 messageKey = unpackedKey; 138 } else { 139 throw new CryptoFailedException("MessageKey has wrong length: " 140 + unpackedKey.length + ". Probably legacy auth tag format."); 141 } 142 143 return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag); 144 } 145 146 /** 147 * Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage. 148 * The decrypted payload will be the body of the returned Message. 149 * 150 * @param element omemoElement containing a payload. 151 * @param cipherAndAuthTag cipher and authentication tag. 152 * @return Message containing the decrypted payload in its body. 153 * @throws CryptoFailedException 154 */ 155 public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException { 156 if (!element.isMessageElement()) { 157 throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!"); 158 } 159 160 if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) { 161 throw new CryptoFailedException("AuthenticationTag is null or has wrong length: " 162 + (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length)); 163 } 164 byte[] encryptedBody = new byte[element.getPayload().length + 16]; 165 byte[] payload = element.getPayload(); 166 System.arraycopy(payload, 0, encryptedBody, 0, payload.length); 167 System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16); 168 169 try { 170 String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8); 171 Message decrypted = new Message(); 172 decrypted.setBody(plaintext); 173 return decrypted; 174 175 } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { 176 throw new CryptoFailedException("decryptMessageElement could not decipher message body: " 177 + e.getMessage()); 178 } 179 } 180 181 /** 182 * Try to decrypt the message. 183 * First decrypt the message key using our session with the sender. 184 * Second use the decrypted key to decrypt the message. 185 * The decrypted content of the 'encrypted'-element becomes the body of the clear text message. 186 * 187 * @param element OmemoElement 188 * @param keyId the key we want to decrypt (usually our own device id) 189 * @return message as plaintext 190 * @throws CryptoFailedException 191 * @throws NoRawSessionException 192 */ 193 // TODO find solution for what we actually want to decrypt (String, Message, List<ExtensionElements>...) 194 public Message decryptMessageElement(OmemoElement element, int keyId) throws CryptoFailedException, NoRawSessionException { 195 if (!element.isMessageElement()) { 196 throw new IllegalArgumentException("OmemoElement is not a messageElement!"); 197 } 198 199 CipherAndAuthTag cipherAndAuthTag = decryptTransportedKey(element, keyId); 200 return decryptMessageElement(element, cipherAndAuthTag); 201 } 202 203 /** 204 * Create a new SessionCipher used to encrypt/decrypt keys. The cipher typically implements the ratchet and KDF-chains. 205 * 206 * @param contact OmemoDevice 207 * @return SessionCipher 208 */ 209 public abstract T_Ciph createCipher(OmemoDevice contact); 210 211 /** 212 * Get the id of the preKey used to establish the session. 213 * 214 * @return id 215 */ 216 public int getPreKeyId() { 217 return this.preKeyId; 218 } 219 220 /** 221 * Encrypt a message key for the recipient. This key can be deciphered by the recipient with its corresponding 222 * session cipher. The key is then used to decipher the message. 223 * 224 * @param messageKey serialized key to encrypt 225 * @return A CiphertextTuple containing the ciphertext and the messageType 226 * @throws CryptoFailedException 227 */ 228 public abstract CiphertextTuple encryptMessageKey(byte[] messageKey) throws CryptoFailedException; 229 230 /** 231 * Decrypt a messageKey using our sessionCipher. We can use that key to decipher the actual message. 232 * Same as encryptMessageKey, just the other way round. 233 * 234 * @param encryptedKey encrypted key 235 * @return serialized decrypted key or null 236 * @throws CryptoFailedException when decryption fails. 237 * @throws NoRawSessionException when no session was found in the double ratchet library 238 */ 239 public abstract byte[] decryptMessageKey(byte[] encryptedKey) throws CryptoFailedException, NoRawSessionException; 240 241 /** 242 * Return the identityKey of the session. 243 * 244 * @return identityKey 245 */ 246 public T_IdKey getIdentityKey() { 247 return identityKey; 248 } 249 250 /** 251 * Set the identityKey of the remote device. 252 * @param identityKey identityKey 253 */ 254 public void setIdentityKey(T_IdKey identityKey) { 255 this.identityKey = identityKey; 256 } 257 258 /** 259 * Return the fingerprint of the contacts identityKey. 260 * 261 * @return fingerprint or null 262 */ 263 public OmemoFingerprint getFingerprint() { 264 return (this.identityKey != null ? omemoStore.keyUtil().getFingerprint(this.identityKey) : null); 265 } 266}