001/** 002 * 003 * Copyright 2017 Paul Schaub, 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; 018 019import java.io.IOException; 020import java.security.InvalidAlgorithmParameterException; 021import java.security.InvalidKeyException; 022import java.security.NoSuchAlgorithmException; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import javax.crypto.BadPaddingException; 029import javax.crypto.IllegalBlockSizeException; 030import javax.crypto.NoSuchPaddingException; 031 032import org.jivesoftware.smackx.omemo.element.OmemoElement; 033import org.jivesoftware.smackx.omemo.element.OmemoKeyElement; 034import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; 035import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; 036import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException; 037import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; 038import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException; 039import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; 040import org.jivesoftware.smackx.omemo.internal.CiphertextTuple; 041import org.jivesoftware.smackx.omemo.internal.OmemoDevice; 042 043public abstract class OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> { 044 private static final Logger LOGGER = Logger.getLogger(OmemoRatchet.class.getName()); 045 046 protected final OmemoManager omemoManager; 047 protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store; 048 049 /** 050 * Constructor. 051 * 052 * @param omemoManager omemoManager 053 * @param store omemoStore 054 */ 055 public OmemoRatchet(OmemoManager omemoManager, 056 OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> store) { 057 this.omemoManager = omemoManager; 058 this.store = store; 059 } 060 061 /** 062 * Decrypt a double-ratchet-encrypted message key. 063 * 064 * @param sender sender of the message. 065 * @param encryptedKey key encrypted with the ratchet of the sender. 066 * @return decrypted message key. 067 * 068 * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. 069 * @throws NoRawSessionException when no double ratchet session was found. 070 * @throws CryptoFailedException if the OMEMO cryptography failed. 071 * @throws UntrustedOmemoIdentityException if the OMEMO identity is not trusted. 072 * @throws IOException if an I/O error occurred. 073 */ 074 public abstract byte[] doubleRatchetDecrypt(OmemoDevice sender, byte[] encryptedKey) 075 throws CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException, 076 UntrustedOmemoIdentityException, IOException; 077 078 /** 079 * Encrypt a messageKey with the double ratchet session of the recipient. 080 * 081 * @param recipient recipient of the message. 082 * @param messageKey key we want to encrypt. 083 * @return encrypted message key. 084 */ 085 public abstract CiphertextTuple doubleRatchetEncrypt(OmemoDevice recipient, byte[] messageKey); 086 087 /** 088 * Try to decrypt the transported message key using the double ratchet session. 089 * 090 * @param element omemoElement 091 * @return tuple of cipher generated from the unpacked message key and the auth-tag 092 * 093 * @throws CryptoFailedException if decryption using the double ratchet fails 094 * @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage 095 * @throws IOException if an I/O error occurred. 096 */ 097 CipherAndAuthTag retrieveMessageKeyAndAuthTag(OmemoDevice sender, OmemoElement element) throws CryptoFailedException, 098 NoRawSessionException, IOException { 099 int keyId = omemoManager.getDeviceId(); 100 byte[] unpackedKey = null; 101 List<CryptoFailedException> decryptExceptions = new ArrayList<>(); 102 List<OmemoKeyElement> keys = element.getHeader().getKeys(); 103 104 boolean preKey = false; 105 106 // Find key with our ID. 107 for (OmemoKeyElement k : keys) { 108 if (k.getId() == keyId) { 109 try { 110 unpackedKey = doubleRatchetDecrypt(sender, k.getData()); 111 preKey = k.isPreKey(); 112 break; 113 } catch (CryptoFailedException e) { 114 // There might be multiple keys with our id, but we can only decrypt one. 115 // So we can't throw the exception, when decrypting the first duplicate which is not for us. 116 decryptExceptions.add(e); 117 } catch (CorruptedOmemoKeyException e) { 118 decryptExceptions.add(new CryptoFailedException(e)); 119 } catch (UntrustedOmemoIdentityException e) { 120 LOGGER.log(Level.WARNING, "Received message from " + sender + " contained unknown identityKey. Ignore message.", e); 121 } 122 } 123 } 124 125 if (unpackedKey == null) { 126 if (!decryptExceptions.isEmpty()) { 127 throw MultipleCryptoFailedException.from(decryptExceptions); 128 } 129 130 throw new CryptoFailedException("Transported key could not be decrypted, since no suitable message key " + 131 "was provided. Provides keys: " + keys); 132 } 133 134 // Split in AES auth-tag and key 135 byte[] messageKey = new byte[16]; 136 byte[] authTag = null; 137 138 if (unpackedKey.length == 32) { 139 authTag = new byte[16]; 140 // copy key part into messageKey 141 System.arraycopy(unpackedKey, 0, messageKey, 0, 16); 142 // copy tag part into authTag 143 System.arraycopy(unpackedKey, 16, authTag, 0, 16); 144 } else if (element.isKeyTransportElement() && unpackedKey.length == 16) { 145 messageKey = unpackedKey; 146 } else { 147 throw new CryptoFailedException("MessageKey has wrong length: " 148 + unpackedKey.length + ". Probably legacy auth tag format."); 149 } 150 151 return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag, preKey); 152 } 153 154 /** 155 * Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage. 156 * The decrypted payload will be the body of the returned Message. 157 * 158 * @param element omemoElement containing a payload. 159 * @param cipherAndAuthTag cipher and authentication tag. 160 * @return decrypted plain text. 161 * 162 * @throws CryptoFailedException if decryption using AES key fails. 163 */ 164 static String decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) 165 throws CryptoFailedException { 166 if (!element.isMessageElement()) { 167 throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!"); 168 } 169 170 if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) { 171 throw new CryptoFailedException("AuthenticationTag is null or has wrong length: " 172 + (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length)); 173 } 174 175 byte[] encryptedBody = payloadAndAuthTag(element, cipherAndAuthTag.getAuthTag()); 176 177 try { 178 return cipherAndAuthTag.decrypt(encryptedBody); 179 } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | NoSuchAlgorithmException 180 | NoSuchPaddingException | InvalidAlgorithmParameterException e) { 181 throw new CryptoFailedException("decryptMessageElement could not decipher message body", e); 182 } 183 } 184 185 /** 186 * Return the concatenation of the payload of the OmemoElement and the given auth tag. 187 * 188 * @param element omemoElement (message element) 189 * @param authTag authTag 190 * @return payload + authTag 191 */ 192 static byte[] payloadAndAuthTag(OmemoElement element, byte[] authTag) { 193 if (!element.isMessageElement()) { 194 throw new IllegalArgumentException("OmemoElement has no payload."); 195 } 196 197 byte[] payload = new byte[element.getPayload().length + authTag.length]; 198 System.arraycopy(element.getPayload(), 0, payload, 0, element.getPayload().length); 199 System.arraycopy(authTag, 0, payload, element.getPayload().length, authTag.length); 200 return payload; 201 } 202 203}