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}