mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2024-06-27 14:04:52 +02:00
Paul Schaub
1f731f6318
Changes: Rework integration tests New structure of base integration test classes bump dependency on signal-protocol-java from 2.4.0 to 2.6.2 Introduced CachingOmemoStore implementations Use CachingOmemoStore classes in integration tests Removed OmemoSession classes (replaced with more logical OmemoRatchet classes) Consequently also removed load/storeOmemoSession methods from OmemoStore Removed some clutter from KeyUtil classes Moved trust decision related code from OmemoStore to TrustCallback Require authenticated connection for many functions Add async initialization function in OmemoStore Refactor omemo test package (/java/org/jivesoftware/smack/omemo -> /java/org/jivesoftware/smackx) Remove OmemoStore method isFreshInstallation() as well as defaultDeviceId related stuff FileBasedOmemoStore: Add cleaner methods to store/load base data types (Using tryWithResource, only for future releases, once Android API gets bumped) Attempt to make OmemoManager thread safe new logic for getInstanceFor() deviceId determination OmemoManagers encrypt methods now don't throw exceptions when encryption for some devices fails. Instead message gets encrypted when possible and more information about failures gets returned alongside the message itself Added OmemoMessage class for that purpose Reworked entire OmemoService class Use safer logic for creating trust-ignoring messages (like ratchet-update messages) Restructure elements/provider in order to prepare for OMEMO namespace bumps Remove OmemoManager.regenerate() methods in favor of getInstanceFor(connection, randomDeviceId) Removed some unnecessary configuration options Prepare for support of more AES message key types Simplify session creation Where possible, avoid side effects in methods Add UntrustedOmemoIdentityException Add TrustState enum More improved tests
289 lines
12 KiB
Java
289 lines
12 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.util;
|
|
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE;
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH;
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE;
|
|
import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.NoSuchProviderException;
|
|
import java.security.SecureRandom;
|
|
import java.util.ArrayList;
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.KeyGenerator;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
import org.jivesoftware.smackx.omemo.OmemoRatchet;
|
|
import org.jivesoftware.smackx.omemo.OmemoService;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoElement;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoElement_VAxolotl;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoHeaderElement_VAxolotl;
|
|
import org.jivesoftware.smackx.omemo.element.OmemoKeyElement;
|
|
import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
|
|
import org.jivesoftware.smackx.omemo.exceptions.NoIdentityKeyException;
|
|
import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
|
|
import org.jivesoftware.smackx.omemo.exceptions.UntrustedOmemoIdentityException;
|
|
import org.jivesoftware.smackx.omemo.internal.CiphertextTuple;
|
|
import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
|
|
import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
|
|
import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
|
|
|
|
|
|
/**
|
|
* Class used to build OMEMO messages.
|
|
*
|
|
* @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 class OmemoMessageBuilder<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> {
|
|
|
|
private final OmemoDevice userDevice;
|
|
private final OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet;
|
|
private final OmemoTrustCallback trustCallback;
|
|
|
|
private byte[] messageKey;
|
|
private final byte[] initializationVector;
|
|
|
|
private byte[] ciphertextMessage;
|
|
private final ArrayList<OmemoKeyElement> keys = new ArrayList<>();
|
|
|
|
/**
|
|
* Create an OmemoMessageBuilder.
|
|
*
|
|
* @param userDevice our OmemoDevice
|
|
* @param callback trustCallback for querying trust decisions
|
|
* @param ratchet our OmemoRatchet
|
|
* @param aesKey aes message key used for message encryption
|
|
* @param iv initialization vector used for message encryption
|
|
* @param message message we want to send
|
|
*
|
|
* @throws NoSuchPaddingException
|
|
* @throws BadPaddingException
|
|
* @throws InvalidKeyException
|
|
* @throws NoSuchAlgorithmException
|
|
* @throws IllegalBlockSizeException
|
|
* @throws UnsupportedEncodingException
|
|
* @throws NoSuchProviderException
|
|
* @throws InvalidAlgorithmParameterException
|
|
*/
|
|
public OmemoMessageBuilder(OmemoDevice userDevice,
|
|
OmemoTrustCallback callback,
|
|
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
|
|
byte[] aesKey,
|
|
byte[] iv,
|
|
String message)
|
|
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
|
|
IllegalBlockSizeException,
|
|
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
|
|
this.userDevice = userDevice;
|
|
this.trustCallback = callback;
|
|
this.ratchet = ratchet;
|
|
this.messageKey = aesKey;
|
|
this.initializationVector = iv;
|
|
setMessage(message);
|
|
}
|
|
|
|
/**
|
|
* Create an OmemoMessageBuilder.
|
|
*
|
|
* @param userDevice our OmemoDevice
|
|
* @param callback trustCallback for querying trust decisions
|
|
* @param ratchet our OmemoRatchet
|
|
* @param message message we want to send
|
|
*
|
|
* @throws NoSuchPaddingException
|
|
* @throws BadPaddingException
|
|
* @throws InvalidKeyException
|
|
* @throws NoSuchAlgorithmException
|
|
* @throws IllegalBlockSizeException
|
|
* @throws UnsupportedEncodingException
|
|
* @throws NoSuchProviderException
|
|
* @throws InvalidAlgorithmParameterException
|
|
*/
|
|
public OmemoMessageBuilder(OmemoDevice userDevice,
|
|
OmemoTrustCallback callback,
|
|
OmemoRatchet<T_IdKeyPair, T_IdKey, T_PreKey, T_SigPreKey, T_Sess, T_Addr, T_ECPub, T_Bundle, T_Ciph> ratchet,
|
|
String message)
|
|
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
|
|
UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException {
|
|
this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message);
|
|
}
|
|
|
|
/**
|
|
* Encrypt the message with the aes key.
|
|
* Move the AuthTag from the end of the cipherText to the end of the messageKey afterwards.
|
|
* This prevents an attacker which compromised one recipient device to switch out the cipherText for other recipients.
|
|
* @see <a href="https://conversations.im/omemo/audit.pdf">OMEMO security audit</a>.
|
|
*
|
|
* @param message plaintext message
|
|
* @throws NoSuchPaddingException
|
|
* @throws NoSuchAlgorithmException
|
|
* @throws NoSuchProviderException
|
|
* @throws InvalidAlgorithmParameterException
|
|
* @throws InvalidKeyException
|
|
* @throws UnsupportedEncodingException
|
|
* @throws BadPaddingException
|
|
* @throws IllegalBlockSizeException
|
|
*/
|
|
private void setMessage(String message) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
|
|
if (message == null) {
|
|
return;
|
|
}
|
|
|
|
// Encrypt message body
|
|
SecretKey secretKey = new SecretKeySpec(messageKey, KEYTYPE);
|
|
IvParameterSpec ivSpec = new IvParameterSpec(initializationVector);
|
|
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
|
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
|
|
|
|
byte[] body;
|
|
byte[] ciphertext;
|
|
|
|
body = (message.getBytes(StringUtils.UTF8));
|
|
ciphertext = cipher.doFinal(body);
|
|
|
|
byte[] clearKeyWithAuthTag = new byte[messageKey.length + 16];
|
|
byte[] cipherTextWithoutAuthTag = new byte[ciphertext.length - 16];
|
|
|
|
moveAuthTag(messageKey, ciphertext, clearKeyWithAuthTag, cipherTextWithoutAuthTag);
|
|
|
|
ciphertextMessage = cipherTextWithoutAuthTag;
|
|
messageKey = clearKeyWithAuthTag;
|
|
}
|
|
|
|
/**
|
|
* Move the auth tag from the end of the cipherText to the messageKey.
|
|
*
|
|
* @param messageKey source messageKey without authTag
|
|
* @param cipherText source cipherText with authTag
|
|
* @param messageKeyWithAuthTag destination messageKey with authTag
|
|
* @param cipherTextWithoutAuthTag destination cipherText without authTag
|
|
*/
|
|
static void moveAuthTag(byte[] messageKey,
|
|
byte[] cipherText,
|
|
byte[] messageKeyWithAuthTag,
|
|
byte[] cipherTextWithoutAuthTag) {
|
|
// Check dimensions of arrays
|
|
if (messageKeyWithAuthTag.length != messageKey.length + 16) {
|
|
throw new IllegalArgumentException("Length of messageKeyWithAuthTag must be length of messageKey + " +
|
|
"length of AuthTag (16)");
|
|
}
|
|
|
|
if (cipherTextWithoutAuthTag.length != cipherText.length - 16) {
|
|
throw new IllegalArgumentException("Length of cipherTextWithoutAuthTag must be length of cipherText " +
|
|
"- length of AuthTag (16)");
|
|
}
|
|
|
|
// Move auth tag from cipherText to messageKey
|
|
System.arraycopy(messageKey, 0, messageKeyWithAuthTag, 0, 16);
|
|
System.arraycopy(cipherText, 0, cipherTextWithoutAuthTag, 0, cipherTextWithoutAuthTag.length);
|
|
System.arraycopy(cipherText, cipherText.length - 16, messageKeyWithAuthTag, 16, 16);
|
|
}
|
|
|
|
/**
|
|
* Add a new recipient device to the message.
|
|
*
|
|
* @param contactsDevice device of the recipient
|
|
* @throws NoIdentityKeyException if we have no identityKey of that device. Can be fixed by fetching and
|
|
* processing the devices bundle.
|
|
* @throws CorruptedOmemoKeyException if the identityKey of that device is corrupted.
|
|
* @throws UndecidedOmemoIdentityException if the user hasn't yet decided whether to trust that device or not.
|
|
* @throws UntrustedOmemoIdentityException if the user has decided not to trust that device.
|
|
*/
|
|
public void addRecipient(OmemoDevice contactsDevice)
|
|
throws NoIdentityKeyException, CorruptedOmemoKeyException, UndecidedOmemoIdentityException,
|
|
UntrustedOmemoIdentityException {
|
|
|
|
OmemoFingerprint fingerprint;
|
|
fingerprint = OmemoService.getInstance().getOmemoStoreBackend().getFingerprint(userDevice, contactsDevice);
|
|
|
|
switch (trustCallback.getTrust(contactsDevice, fingerprint)) {
|
|
|
|
case undecided:
|
|
throw new UndecidedOmemoIdentityException(contactsDevice);
|
|
|
|
case trusted:
|
|
CiphertextTuple encryptedKey = ratchet.doubleRatchetEncrypt(contactsDevice, messageKey);
|
|
keys.add(new OmemoKeyElement(encryptedKey.getCiphertext(), contactsDevice.getDeviceId(), encryptedKey.isPreKeyMessage()));
|
|
break;
|
|
|
|
case untrusted:
|
|
throw new UntrustedOmemoIdentityException(contactsDevice, fingerprint);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assemble an OmemoMessageElement from the current state of the builder.
|
|
*
|
|
* @return OmemoMessageElement
|
|
*/
|
|
public OmemoElement finish() {
|
|
OmemoHeaderElement_VAxolotl header = new OmemoHeaderElement_VAxolotl(
|
|
userDevice.getDeviceId(),
|
|
keys,
|
|
initializationVector
|
|
);
|
|
return new OmemoElement_VAxolotl(header, ciphertextMessage);
|
|
}
|
|
|
|
/**
|
|
* Generate a new AES key used to encrypt the message.
|
|
*
|
|
* @param keyType Key Type
|
|
* @param keyLength Key Length in bit
|
|
* @return new AES key
|
|
* @throws NoSuchAlgorithmException
|
|
*/
|
|
public static byte[] generateKey(String keyType, int keyLength) throws NoSuchAlgorithmException {
|
|
KeyGenerator generator = KeyGenerator.getInstance(keyType);
|
|
generator.init(keyLength);
|
|
return generator.generateKey().getEncoded();
|
|
}
|
|
|
|
/**
|
|
* Generate a 16 byte initialization vector for AES encryption.
|
|
*
|
|
* @return iv
|
|
*/
|
|
public static byte[] generateIv() {
|
|
SecureRandom random = new SecureRandom();
|
|
byte[] iv = new byte[16];
|
|
random.nextBytes(iv);
|
|
return iv;
|
|
}
|
|
}
|