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