mirror of https://codeberg.org/Mercury-IM/Smack
265 lines
11 KiB
Java
265 lines
11 KiB
Java
/**
|
|
*
|
|
* Copyright 2017 Paul Schaub
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.jivesoftware.smackx.omemo.internal;
|
|
|
|
import org.jivesoftware.smack.packet.Message;
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
import org.jivesoftware.smackx.omemo.OmemoFingerprint;
|
|
import org.jivesoftware.smackx.omemo.OmemoManager;
|
|
import org.jivesoftware.smackx.omemo.OmemoStore;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoElement;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoElement.OmemoHeader.Key;
|
|
import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
|
|
import org.jivesoftware.smackx.omemo.exceptions.MultipleCryptoFailedException;
|
|
import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
|
|
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* This class represents a OMEMO session between us and another device.
|
|
*
|
|
* @param <T_IdKeyPair> IdentityKeyPair class
|
|
* @param <T_IdKey> IdentityKey class
|
|
* @param <T_PreKey> PreKey class
|
|
* @param <T_SigPreKey> SignedPreKey class
|
|
* @param <T_Sess> Session class
|
|
* @param <T_Addr> Address class
|
|
* @param <T_ECPub> Elliptic Curve PublicKey class
|
|
* @param <T_Bundle> Bundle class
|
|
* @param <T_Ciph> Cipher class
|
|
* @author Paul Schaub
|
|
*/
|
|
public abstract class OmemoSession<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
|
|
|
|
protected final T_Ciph cipher;
|
|
protected final OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore;
|
|
protected final OmemoDevice remoteDevice;
|
|
protected final OmemoManager omemoManager;
|
|
protected T_IdKey identityKey;
|
|
protected int preKeyId = -1;
|
|
|
|
/**
|
|
* Constructor used when we establish the session.
|
|
*
|
|
* @param omemoManager OmemoManager of our device
|
|
* @param omemoStore OmemoStore where we want to store the session and get key information from
|
|
* @param remoteDevice the OmemoDevice we want to establish the session with
|
|
* @param identityKey identityKey of the recipient
|
|
*/
|
|
public OmemoSession(OmemoManager omemoManager,
|
|
OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
|
|
OmemoDevice remoteDevice, T_IdKey identityKey) {
|
|
this(omemoManager, omemoStore, remoteDevice);
|
|
this.identityKey = identityKey;
|
|
}
|
|
|
|
/**
|
|
* Another constructor used when they establish the session with us.
|
|
*
|
|
* @param omemoManager OmemoManager of our device
|
|
* @param omemoStore OmemoStore we want to store the session and their key in
|
|
* @param remoteDevice identityKey of the partner
|
|
*/
|
|
public OmemoSession(OmemoManager omemoManager, OmemoStore<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> omemoStore,
|
|
OmemoDevice remoteDevice) {
|
|
this.omemoManager = omemoManager;
|
|
this.omemoStore = omemoStore;
|
|
this.remoteDevice = remoteDevice;
|
|
this.cipher = createCipher(remoteDevice);
|
|
}
|
|
|
|
/**
|
|
* Try to decrypt the transported message key using the double ratchet session.
|
|
*
|
|
* @param element omemoElement
|
|
* @param keyId our keyId
|
|
* @return tuple of cipher generated from the unpacked message key and the authtag
|
|
* @throws CryptoFailedException if decryption using the double ratchet fails
|
|
* @throws NoRawSessionException if we have no session, but the element was NOT a PreKeyMessage
|
|
*/
|
|
public CipherAndAuthTag decryptTransportedKey(OmemoElement element, int keyId) throws CryptoFailedException,
|
|
NoRawSessionException {
|
|
byte[] unpackedKey = null;
|
|
List<CryptoFailedException> decryptExceptions = new ArrayList<>();
|
|
List<Key> keys = element.getHeader().getKeys();
|
|
// Find key with our ID.
|
|
for (OmemoElement.OmemoHeader.Key k : keys) {
|
|
if (k.getId() == keyId) {
|
|
try {
|
|
unpackedKey = decryptMessageKey(k.getData());
|
|
break;
|
|
} catch (CryptoFailedException e) {
|
|
// There might be multiple keys with our id, but we can only decrypt one.
|
|
// So we can't throw the exception, when decrypting the first duplicate which is not for us.
|
|
decryptExceptions.add(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unpackedKey == null) {
|
|
if (!decryptExceptions.isEmpty()) {
|
|
throw MultipleCryptoFailedException.from(decryptExceptions);
|
|
}
|
|
|
|
throw new CryptoFailedException("Transported key could not be decrypted, since no provided message key. Provides keys: " + keys);
|
|
}
|
|
|
|
byte[] messageKey = new byte[16];
|
|
byte[] authTag = null;
|
|
|
|
if (unpackedKey.length == 32) {
|
|
authTag = new byte[16];
|
|
//copy key part into messageKey
|
|
System.arraycopy(unpackedKey, 0, messageKey, 0, 16);
|
|
//copy tag part into authTag
|
|
System.arraycopy(unpackedKey, 16, authTag, 0,16);
|
|
} else if (element.isKeyTransportElement() && unpackedKey.length == 16) {
|
|
messageKey = unpackedKey;
|
|
} else {
|
|
throw new CryptoFailedException("MessageKey has wrong length: "
|
|
+ unpackedKey.length + ". Probably legacy auth tag format.");
|
|
}
|
|
|
|
return new CipherAndAuthTag(messageKey, element.getHeader().getIv(), authTag);
|
|
}
|
|
|
|
/**
|
|
* Use the symmetric key in cipherAndAuthTag to decrypt the payload of the omemoMessage.
|
|
* The decrypted payload will be the body of the returned Message.
|
|
*
|
|
* @param element omemoElement containing a payload.
|
|
* @param cipherAndAuthTag cipher and authentication tag.
|
|
* @return Message containing the decrypted payload in its body.
|
|
* @throws CryptoFailedException
|
|
*/
|
|
public static Message decryptMessageElement(OmemoElement element, CipherAndAuthTag cipherAndAuthTag) throws CryptoFailedException {
|
|
if (!element.isMessageElement()) {
|
|
throw new IllegalArgumentException("decryptMessageElement cannot decrypt OmemoElement which is no MessageElement!");
|
|
}
|
|
|
|
if (cipherAndAuthTag.getAuthTag() == null || cipherAndAuthTag.getAuthTag().length != 16) {
|
|
throw new CryptoFailedException("AuthenticationTag is null or has wrong length: "
|
|
+ (cipherAndAuthTag.getAuthTag() == null ? "null" : cipherAndAuthTag.getAuthTag().length));
|
|
}
|
|
byte[] encryptedBody = new byte[element.getPayload().length + 16];
|
|
byte[] payload = element.getPayload();
|
|
System.arraycopy(payload, 0, encryptedBody, 0, payload.length);
|
|
System.arraycopy(cipherAndAuthTag.getAuthTag(), 0, encryptedBody, payload.length, 16);
|
|
|
|
try {
|
|
String plaintext = new String(cipherAndAuthTag.getCipher().doFinal(encryptedBody), StringUtils.UTF8);
|
|
Message decrypted = new Message();
|
|
decrypted.setBody(plaintext);
|
|
return decrypted;
|
|
|
|
} catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) {
|
|
throw new CryptoFailedException("decryptMessageElement could not decipher message body: "
|
|
+ e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to decrypt the message.
|
|
* First decrypt the message key using our session with the sender.
|
|
* Second use the decrypted key to decrypt the message.
|
|
* The decrypted content of the 'encrypted'-element becomes the body of the clear text message.
|
|
*
|
|
* @param element OmemoElement
|
|
* @param keyId the key we want to decrypt (usually our own device id)
|
|
* @return message as plaintext
|
|
* @throws CryptoFailedException
|
|
* @throws NoRawSessionException
|
|
*/
|
|
// TODO find solution for what we actually want to decrypt (String, Message, List<ExtensionElements>...)
|
|
public Message decryptMessageElement(OmemoElement element, int keyId) throws CryptoFailedException, NoRawSessionException {
|
|
if (!element.isMessageElement()) {
|
|
throw new IllegalArgumentException("OmemoElement is not a messageElement!");
|
|
}
|
|
|
|
CipherAndAuthTag cipherAndAuthTag = decryptTransportedKey(element, keyId);
|
|
return decryptMessageElement(element, cipherAndAuthTag);
|
|
}
|
|
|
|
/**
|
|
* Create a new SessionCipher used to encrypt/decrypt keys. The cipher typically implements the ratchet and KDF-chains.
|
|
*
|
|
* @param contact OmemoDevice
|
|
* @return SessionCipher
|
|
*/
|
|
public abstract T_Ciph createCipher(OmemoDevice contact);
|
|
|
|
/**
|
|
* Get the id of the preKey used to establish the session.
|
|
*
|
|
* @return id
|
|
*/
|
|
public int getPreKeyId() {
|
|
return this.preKeyId;
|
|
}
|
|
|
|
/**
|
|
* Encrypt a message key for the recipient. This key can be deciphered by the recipient with its corresponding
|
|
* session cipher. The key is then used to decipher the message.
|
|
*
|
|
* @param messageKey serialized key to encrypt
|
|
* @return A CiphertextTuple containing the ciphertext and the messageType
|
|
* @throws CryptoFailedException
|
|
*/
|
|
public abstract CiphertextTuple encryptMessageKey(byte[] messageKey) throws CryptoFailedException;
|
|
|
|
/**
|
|
* Decrypt a messageKey using our sessionCipher. We can use that key to decipher the actual message.
|
|
* Same as encryptMessageKey, just the other way round.
|
|
*
|
|
* @param encryptedKey encrypted key
|
|
* @return serialized decrypted key or null
|
|
* @throws CryptoFailedException when decryption fails.
|
|
* @throws NoRawSessionException when no session was found in the double ratchet library
|
|
*/
|
|
public abstract byte[] decryptMessageKey(byte[] encryptedKey) throws CryptoFailedException, NoRawSessionException;
|
|
|
|
/**
|
|
* Return the identityKey of the session.
|
|
*
|
|
* @return identityKey
|
|
*/
|
|
public T_IdKey getIdentityKey() {
|
|
return identityKey;
|
|
}
|
|
|
|
/**
|
|
* Set the identityKey of the remote device.
|
|
* @param identityKey identityKey
|
|
*/
|
|
public void setIdentityKey(T_IdKey identityKey) {
|
|
this.identityKey = identityKey;
|
|
}
|
|
|
|
/**
|
|
* Return the fingerprint of the contacts identityKey.
|
|
*
|
|
* @return fingerprint or null
|
|
*/
|
|
public OmemoFingerprint getFingerprint() {
|
|
return (this.identityKey != null ? omemoStore.keyUtil().getFingerprint(this.identityKey) : null);
|
|
}
|
|
}
|