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}