clean up session cipher, sealed session cipher, and old message version

This commit is contained in:
Ryan ZHAO 2021-02-19 13:36:05 +11:00
parent f0020ea811
commit 1e1b3e02e1
8 changed files with 7 additions and 899 deletions

View File

@ -218,10 +218,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional<Long> smsMessageId, boolean isPushNotification) { private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional<Long> smsMessageId, boolean isPushNotification) {
try { try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context); LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore, new SessionProtocolImpl(context), apiDB); SignalServiceCipher cipher = new SignalServiceCipher(new SessionProtocolImpl(context), apiDB);
SignalServiceContent content = cipher.decrypt(envelope); SignalServiceContent content = cipher.decrypt(envelope);

View File

@ -1,212 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.libsignal;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.libsignal.protocol.PreKeySignalMessage;
import org.session.libsignal.libsignal.protocol.SignalMessage;
import org.session.libsignal.libsignal.ratchet.AliceSignalProtocolParameters;
import org.session.libsignal.libsignal.ratchet.BobSignalProtocolParameters;
import org.session.libsignal.libsignal.ratchet.RatchetingSession;
import org.session.libsignal.libsignal.state.IdentityKeyStore;
import org.session.libsignal.libsignal.state.PreKeyBundle;
import org.session.libsignal.libsignal.state.PreKeyStore;
import org.session.libsignal.libsignal.state.SessionRecord;
import org.session.libsignal.libsignal.state.SessionStore;
import org.session.libsignal.libsignal.state.SignalProtocolStore;
import org.session.libsignal.libsignal.state.SignedPreKeyStore;
import org.session.libsignal.libsignal.util.guava.Optional;
/**
* SessionBuilder is responsible for setting up encrypted sessions.
* Once a session has been established, {@link org.session.libsignal.libsignal.SessionCipher}
* can be used to encrypt/decrypt messages in that session.
* <p>
* Sessions are built from one of three different possible vectors:
* <ol>
* <li>A {@link org.session.libsignal.libsignal.state.PreKeyBundle} retrieved from a server.</li>
* <li>A {@link PreKeySignalMessage} received from a client.</li>
* </ol>
*
* Sessions are constructed per recipientId + deviceId tuple. Remote logical users are identified
* by their recipientId, and each logical recipientId can have multiple physical devices.
*
* @author Moxie Marlinspike
*/
public class SessionBuilder {
private static final String TAG = SessionBuilder.class.getSimpleName();
private final SessionStore sessionStore;
private final PreKeyStore preKeyStore;
private final SignedPreKeyStore signedPreKeyStore;
private final IdentityKeyStore identityKeyStore;
private final SignalProtocolAddress remoteAddress;
/**
* Constructs a SessionBuilder.
*
* @param sessionStore The {@link org.session.libsignal.libsignal.state.SessionStore} to store the constructed session in.
* @param preKeyStore The {@link org.session.libsignal.libsignal.state.PreKeyStore} where the client's local {@link org.session.libsignal.libsignal.state.PreKeyRecord}s are stored.
* @param identityKeyStore The {@link org.session.libsignal.libsignal.state.IdentityKeyStore} containing the client's identity key information.
* @param remoteAddress The address of the remote user to build a session with.
*/
public SessionBuilder(SessionStore sessionStore,
PreKeyStore preKeyStore,
SignedPreKeyStore signedPreKeyStore,
IdentityKeyStore identityKeyStore,
SignalProtocolAddress remoteAddress)
{
this.sessionStore = sessionStore;
this.preKeyStore = preKeyStore;
this.signedPreKeyStore = signedPreKeyStore;
this.identityKeyStore = identityKeyStore;
this.remoteAddress = remoteAddress;
}
/**
* Constructs a SessionBuilder
* @param store The {@link SignalProtocolStore} to store all state information in.
* @param remoteAddress The address of the remote user to build a session with.
*/
public SessionBuilder(SignalProtocolStore store, SignalProtocolAddress remoteAddress) {
this(store, store, store, store, remoteAddress);
}
/**
* Build a new session from a received {@link PreKeySignalMessage}.
*
* After a session is constructed in this way, the embedded {@link SignalMessage}
* can be decrypted.
*
* @param message The received {@link PreKeySignalMessage}.
* @throws org.session.libsignal.libsignal.InvalidKeyIdException when there is no local
* {@link org.session.libsignal.libsignal.state.PreKeyRecord}
* that corresponds to the PreKey ID in
* the message.
* @throws org.session.libsignal.libsignal.InvalidKeyException when the message is formatted incorrectly.
* @throws org.session.libsignal.libsignal.UntrustedIdentityException when the {@link IdentityKey} of the sender is untrusted.
*/
/*package*/ Optional<Integer> process(SessionRecord sessionRecord, PreKeySignalMessage message)
throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException
{
IdentityKey theirIdentityKey = message.getIdentityKey();
if (!identityKeyStore.isTrustedIdentity(remoteAddress, theirIdentityKey, IdentityKeyStore.Direction.RECEIVING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), theirIdentityKey);
}
Optional<Integer> unsignedPreKeyId = processV3(sessionRecord, message);
identityKeyStore.saveIdentity(remoteAddress, theirIdentityKey);
return unsignedPreKeyId;
}
private Optional<Integer> processV3(SessionRecord sessionRecord, PreKeySignalMessage message)
throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException
{
if (sessionRecord.hasSessionState(message.getMessageVersion(), message.getBaseKey().serialize())) {
Log.w(TAG, "We've already setup a session for this V3 message, letting bundled message fall through...");
return Optional.absent();
}
ECKeyPair ourSignedPreKey = signedPreKeyStore.loadSignedPreKey(message.getSignedPreKeyId()).getKeyPair();
BobSignalProtocolParameters.Builder parameters = BobSignalProtocolParameters.newBuilder();
parameters.setTheirBaseKey(message.getBaseKey())
.setTheirIdentityKey(message.getIdentityKey())
.setOurIdentityKey(identityKeyStore.getIdentityKeyPair())
.setOurSignedPreKey(ourSignedPreKey)
.setOurRatchetKey(ourSignedPreKey);
if (message.getPreKeyId().isPresent()) {
parameters.setOurOneTimePreKey(Optional.of(preKeyStore.loadPreKey(message.getPreKeyId().get()).getKeyPair()));
} else {
parameters.setOurOneTimePreKey(Optional.<ECKeyPair>absent());
}
if (!sessionRecord.isFresh()) sessionRecord.archiveCurrentState();
RatchetingSession.initializeSession(sessionRecord.getSessionState(), parameters.create());
sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId());
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
sessionRecord.getSessionState().setAliceBaseKey(message.getBaseKey().serialize());
if (message.getPreKeyId().isPresent()) {
return message.getPreKeyId();
} else {
return Optional.absent();
}
}
/**
* Build a new session from a {@link org.session.libsignal.libsignal.state.PreKeyBundle} retrieved from
* a server.
*
* @param preKey A PreKey for the destination recipient, retrieved from a server.
* @throws InvalidKeyException when the {@link org.session.libsignal.libsignal.state.PreKeyBundle} is
* badly formatted.
* @throws org.session.libsignal.libsignal.UntrustedIdentityException when the sender's
* {@link IdentityKey} is not
* trusted.
*/
public void process(PreKeyBundle preKey) throws InvalidKeyException, UntrustedIdentityException {
synchronized (SessionCipher.SESSION_LOCK) {
if (!identityKeyStore.isTrustedIdentity(remoteAddress, preKey.getIdentityKey(), IdentityKeyStore.Direction.SENDING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), preKey.getIdentityKey());
}
if (preKey.getSignedPreKey() != null &&
!Curve.verifySignature(preKey.getIdentityKey().getPublicKey(),
preKey.getSignedPreKey().serialize(),
preKey.getSignedPreKeySignature()))
{
throw new InvalidKeyException("Invalid signature on device key!");
}
if (preKey.getSignedPreKey() == null) {
throw new InvalidKeyException("No signed prekey!");
}
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
ECKeyPair ourBaseKey = Curve.generateKeyPair();
ECPublicKey theirSignedPreKey = preKey.getSignedPreKey();
Optional<ECPublicKey> theirOneTimePreKey = Optional.fromNullable(preKey.getPreKey());
Optional<Integer> theirOneTimePreKeyId = theirOneTimePreKey.isPresent() ? Optional.of(preKey.getPreKeyId()) :
Optional.<Integer>absent();
AliceSignalProtocolParameters.Builder parameters = AliceSignalProtocolParameters.newBuilder();
parameters.setOurBaseKey(ourBaseKey)
.setOurIdentityKey(identityKeyStore.getIdentityKeyPair())
.setTheirIdentityKey(preKey.getIdentityKey())
.setTheirSignedPreKey(theirSignedPreKey)
.setTheirRatchetKey(theirSignedPreKey)
.setTheirOneTimePreKey(theirOneTimePreKey);
if (!sessionRecord.isFresh()) sessionRecord.archiveCurrentState();
RatchetingSession.initializeSession(sessionRecord.getSessionState(), parameters.create());
sessionRecord.getSessionState().setUnacknowledgedPreKeyMessage(theirOneTimePreKeyId, preKey.getSignedPreKeyId(), ourBaseKey.getPublicKey());
sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId());
sessionRecord.getSessionState().setRemoteRegistrationId(preKey.getRegistrationId());
sessionRecord.getSessionState().setAliceBaseKey(ourBaseKey.getPublicKey().serialize());
identityKeyStore.saveIdentity(remoteAddress, preKey.getIdentityKey());
sessionStore.storeSession(remoteAddress, sessionRecord);
}
}
}

View File

@ -1,350 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.libsignal;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.libsignal.protocol.CiphertextMessage;
import org.session.libsignal.libsignal.protocol.PreKeySignalMessage;
import org.session.libsignal.libsignal.protocol.SignalMessage;
import org.session.libsignal.libsignal.ratchet.ChainKey;
import org.session.libsignal.libsignal.ratchet.MessageKeys;
import org.session.libsignal.libsignal.ratchet.RootKey;
import org.session.libsignal.libsignal.state.IdentityKeyStore;
import org.session.libsignal.libsignal.state.PreKeyStore;
import org.session.libsignal.libsignal.state.SessionRecord;
import org.session.libsignal.libsignal.state.SessionState;
import org.session.libsignal.libsignal.state.SessionStore;
import org.session.libsignal.libsignal.state.SignalProtocolStore;
import org.session.libsignal.libsignal.state.SignedPreKeyStore;
import org.session.libsignal.libsignal.util.Pair;
import org.session.libsignal.libsignal.util.guava.Optional;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static org.session.libsignal.libsignal.state.SessionState.UnacknowledgedPreKeyMessageItems;
/**
* The main entry point for Signal Protocol encrypt/decrypt operations.
*
* Once a session has been established with {@link SessionBuilder},
* this class can be used for all encrypt/decrypt operations within
* that session.
*
* @author Moxie Marlinspike
*/
public class SessionCipher {
public static final Object SESSION_LOCK = new Object();
private final SessionStore sessionStore;
private final IdentityKeyStore identityKeyStore;
private final SessionBuilder sessionBuilder;
private final PreKeyStore preKeyStore;
private final SignalProtocolAddress remoteAddress;
/**
* Construct a SessionCipher for encrypt/decrypt operations on a session.
* In order to use SessionCipher, a session must have already been created
* and stored using {@link SessionBuilder}.
*
* @param sessionStore The {@link SessionStore} that contains a session for this recipient.
* @param remoteAddress The remote address that messages will be encrypted to or decrypted from.
*/
public SessionCipher(SessionStore sessionStore, PreKeyStore preKeyStore,
SignedPreKeyStore signedPreKeyStore, IdentityKeyStore identityKeyStore,
SignalProtocolAddress remoteAddress)
{
this.sessionStore = sessionStore;
this.preKeyStore = preKeyStore;
this.identityKeyStore = identityKeyStore;
this.remoteAddress = remoteAddress;
this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityKeyStore, remoteAddress);
}
public SessionCipher(SignalProtocolStore store, SignalProtocolAddress remoteAddress) {
this(store, store, store, store, remoteAddress);
}
/**
* Encrypt a message.
*
* @param paddedMessage The plaintext message bytes, optionally padded to a constant multiple.
* @return A ciphertext message encrypted to the recipient+device tuple.
*/
public CiphertextMessage encrypt(byte[] paddedMessage) throws UntrustedIdentityException {
synchronized (SESSION_LOCK) {
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
SessionState sessionState = sessionRecord.getSessionState();
ChainKey chainKey = sessionState.getSenderChainKey();
MessageKeys messageKeys = chainKey.getMessageKeys();
ECPublicKey senderEphemeral = sessionState.getSenderRatchetKey();
int previousCounter = sessionState.getPreviousCounter();
int sessionVersion = sessionState.getSessionVersion();
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new SignalMessage(sessionVersion, messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody,
sessionState.getLocalIdentityKey(),
sessionState.getRemoteIdentityKey());
if (sessionState.hasUnacknowledgedPreKeyMessage()) {
UnacknowledgedPreKeyMessageItems items = sessionState.getUnacknowledgedPreKeyMessageItems();
int localRegistrationId = sessionState.getLocalRegistrationId();
ciphertextMessage = new PreKeySignalMessage(sessionVersion, localRegistrationId, items.getPreKeyId(),
items.getSignedPreKeyId(), items.getBaseKey(),
sessionState.getLocalIdentityKey(),
(SignalMessage) ciphertextMessage);
}
sessionState.setSenderChainKey(chainKey.getNextChainKey());
if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionState.getRemoteIdentityKey(), IdentityKeyStore.Direction.SENDING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), sessionState.getRemoteIdentityKey());
}
identityKeyStore.saveIdentity(remoteAddress, sessionState.getRemoteIdentityKey());
sessionStore.storeSession(remoteAddress, sessionRecord);
return ciphertextMessage;
}
}
public byte[] decrypt(PreKeySignalMessage ciphertext)
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException,
InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException
{
synchronized (SESSION_LOCK) {
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
Optional<Integer> unsignedPreKeyId = sessionBuilder.process(sessionRecord, ciphertext);
byte[] plaintext = decrypt(sessionRecord, ciphertext.getWhisperMessage());
sessionStore.storeSession(remoteAddress, sessionRecord);
if (unsignedPreKeyId.isPresent()) {
preKeyStore.removePreKey(unsignedPreKeyId.get());
}
return plaintext;
}
}
public byte[] decrypt(SignalMessage ciphertext)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
NoSessionException, UntrustedIdentityException
{
synchronized (SESSION_LOCK) {
if (!sessionStore.containsSession(remoteAddress)) {
throw new NoSessionException("No session for: " + remoteAddress);
}
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
byte[] plaintext = decrypt(sessionRecord, ciphertext);
if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey(), IdentityKeyStore.Direction.RECEIVING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), sessionRecord.getSessionState().getRemoteIdentityKey());
}
identityKeyStore.saveIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey());
sessionStore.storeSession(remoteAddress, sessionRecord);
return plaintext;
}
}
private byte[] decrypt(SessionRecord sessionRecord, SignalMessage ciphertext)
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException
{
synchronized (SESSION_LOCK) {
Iterator<SessionState> previousStates = sessionRecord.getPreviousSessionStates().iterator();
List<Exception> exceptions = new LinkedList<Exception>();
try {
SessionState sessionState = new SessionState(sessionRecord.getSessionState());
byte[] plaintext = decrypt(sessionState, ciphertext);
sessionRecord.setState(sessionState);
return plaintext;
} catch (InvalidMessageException e) {
exceptions.add(e);
}
while (previousStates.hasNext()) {
try {
SessionState promotedState = new SessionState(previousStates.next());
byte[] plaintext = decrypt(promotedState, ciphertext);
previousStates.remove();
sessionRecord.promoteState(promotedState);
return plaintext;
} catch (InvalidMessageException e) {
exceptions.add(e);
}
}
throw new InvalidMessageException("No valid sessions.", exceptions);
}
}
private byte[] decrypt(SessionState sessionState, SignalMessage ciphertextMessage)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
{
if (!sessionState.hasSenderChain()) {
throw new InvalidMessageException("Uninitialized session!");
}
if (ciphertextMessage.getMessageVersion() != sessionState.getSessionVersion()) {
throw new InvalidMessageException(String.format("Message version %d, but session version %d",
ciphertextMessage.getMessageVersion(),
sessionState.getSessionVersion()));
}
ECPublicKey theirEphemeral = ciphertextMessage.getSenderRatchetKey();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(sessionState.getRemoteIdentityKey(),
sessionState.getLocalIdentityKey(),
messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
sessionState.clearUnacknowledgedPreKeyMessage();
return plaintext;
}
public int getRemoteRegistrationId() {
synchronized (SESSION_LOCK) {
SessionRecord record = sessionStore.loadSession(remoteAddress);
return record.getSessionState().getRemoteRegistrationId();
}
}
public int getSessionVersion() {
synchronized (SESSION_LOCK) {
if (!sessionStore.containsSession(remoteAddress)) {
// Loki - If we have no session then we must be using the FallbackSessionCipher
return 3;
}
SessionRecord record = sessionStore.loadSession(remoteAddress);
return record.getSessionState().getSessionVersion();
}
}
private ChainKey getOrCreateChainKey(SessionState sessionState, ECPublicKey theirEphemeral)
throws InvalidMessageException
{
try {
if (sessionState.hasReceiverChain(theirEphemeral)) {
return sessionState.getReceiverChainKey(theirEphemeral);
} else {
RootKey rootKey = sessionState.getRootKey();
ECKeyPair ourEphemeral = sessionState.getSenderRatchetKeyPair();
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
ECKeyPair ourNewEphemeral = Curve.generateKeyPair();
Pair<RootKey, ChainKey> senderChain = receiverChain.first().createChain(theirEphemeral, ourNewEphemeral);
sessionState.setRootKey(senderChain.first());
sessionState.addReceiverChain(theirEphemeral, receiverChain.second());
sessionState.setPreviousCounter(Math.max(sessionState.getSenderChainKey().getIndex()-1, 0));
sessionState.setSenderChain(ourNewEphemeral, senderChain.second());
return receiverChain.second();
}
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private MessageKeys getOrCreateMessageKeys(SessionState sessionState,
ECPublicKey theirEphemeral,
ChainKey chainKey, int counter)
throws InvalidMessageException, DuplicateMessageException
{
if (chainKey.getIndex() > counter) {
if (sessionState.hasMessageKeys(theirEphemeral, counter)) {
return sessionState.removeMessageKeys(theirEphemeral, counter);
} else {
throw new DuplicateMessageException("Received message with old counter: " +
chainKey.getIndex() + " , " + counter);
}
}
if (counter - chainKey.getIndex() > 2000) {
throw new InvalidMessageException("Over 2000 messages into the future!");
}
while (chainKey.getIndex() < counter) {
MessageKeys messageKeys = chainKey.getMessageKeys();
sessionState.setMessageKeys(theirEphemeral, messageKeys);
chainKey = chainKey.getNextChainKey();
}
sessionState.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText)
throws InvalidMessageException
{
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new InvalidMessageException(e);
} catch (BadPaddingException e) {
throw new InvalidMessageException(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, IvParameterSpec iv) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
}

View File

@ -154,10 +154,4 @@ public class SignalMessage implements CiphertextMessage {
public int getType() { public int getType() {
return CiphertextMessage.WHISPER_TYPE; return CiphertextMessage.WHISPER_TYPE;
} }
public static boolean isLegacy(byte[] message) {
return message != null && message.length >= 1 &&
ByteUtil.highBitsToInt(message[0]) != CiphertextMessage.CURRENT_VERSION;
}
} }

View File

@ -1,290 +0,0 @@
package org.session.libsignal.metadata;
import org.session.libsignal.metadata.certificate.CertificateValidator;
import org.session.libsignal.metadata.certificate.InvalidCertificateException;
import org.session.libsignal.metadata.certificate.SenderCertificate;
import org.session.libsignal.metadata.protocol.UnidentifiedSenderMessage;
import org.session.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
import org.session.libsignal.libsignal.DuplicateMessageException;
import org.session.libsignal.libsignal.IdentityKey;
import org.session.libsignal.libsignal.IdentityKeyPair;
import org.session.libsignal.libsignal.InvalidKeyException;
import org.session.libsignal.libsignal.InvalidKeyIdException;
import org.session.libsignal.libsignal.InvalidMacException;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.InvalidVersionException;
import org.session.libsignal.libsignal.LegacyMessageException;
import org.session.libsignal.libsignal.NoSessionException;
import org.session.libsignal.libsignal.SessionCipher;
import org.session.libsignal.libsignal.SignalProtocolAddress;
import org.session.libsignal.libsignal.UntrustedIdentityException;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.libsignal.kdf.HKDFv3;
import org.session.libsignal.libsignal.protocol.CiphertextMessage;
import org.session.libsignal.libsignal.protocol.PreKeySignalMessage;
import org.session.libsignal.libsignal.protocol.SignalMessage;
import org.session.libsignal.libsignal.state.SignalProtocolStore;
import org.session.libsignal.libsignal.util.ByteUtil;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.util.Pair;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SealedSessionCipher {
private final SignalProtocolStore signalProtocolStore;
private final SignalProtocolAddress localAddress;
public SealedSessionCipher(SignalProtocolStore signalProtocolStore, SignalProtocolAddress localAddress)
{
this.signalProtocolStore = signalProtocolStore;
this.localAddress = localAddress;
}
public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext)
throws InvalidKeyException, UntrustedIdentityException
{
CiphertextMessage message = new SessionCipher(signalProtocolStore, destinationAddress).encrypt(paddedPlaintext);
return encrypt(destinationAddress, senderCertificate, message);
}
public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, CiphertextMessage message)
throws InvalidKeyException
{
try {
IdentityKeyPair ourIdentity = signalProtocolStore.getIdentityKeyPair();
byte[] theirPublicKey = Hex.fromStringCondensed(destinationAddress.getName());
ECPublicKey theirIdentity = new IdentityKey(theirPublicKey, 0).getPublicKey();
ECKeyPair ephemeral = Curve.generateKeyPair();
byte[] ephemeralSalt = ByteUtil.combine("UnidentifiedDelivery".getBytes(), theirIdentity.serialize(), ephemeral.getPublicKey().serialize());
EphemeralKeys ephemeralKeys = calculateEphemeralKeys(theirIdentity, ephemeral.getPrivateKey(), ephemeralSalt);
byte[] staticKeyCiphertext = encrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, ourIdentity.getPublicKey().serialize());
byte[] staticSalt = ByteUtil.combine(ephemeralKeys.chainKey, staticKeyCiphertext);
StaticKeys staticKeys = calculateStaticKeys(theirIdentity, ourIdentity.getPrivateKey(), staticSalt);
UnidentifiedSenderMessageContent content = new UnidentifiedSenderMessageContent(message.getType(), senderCertificate, message.serialize());
byte[] messageBytes = encrypt(staticKeys.cipherKey, staticKeys.macKey, content.getSerialized());
return new UnidentifiedSenderMessage(ephemeral.getPublicKey(), staticKeyCiphertext, messageBytes).getSerialized();
} catch (IOException e) {
throw new InvalidKeyException(e);
}
}
/**
* Decrypt a sealed session message.
* This will return a Pair<Integer, byte[]> which is the CipherTextMessage type and the decrypted message content
*/
public Pair<SignalProtocolAddress, Pair<Integer, byte[]>> decrypt(CertificateValidator validator, byte[] ciphertext, long timestamp, String prefixedPublicKey)
throws
InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolInvalidMessageException, ProtocolInvalidKeyException,
ProtocolNoSessionException, ProtocolLegacyMessageException,
ProtocolInvalidVersionException, ProtocolDuplicateMessageException,
ProtocolInvalidKeyIdException, ProtocolUntrustedIdentityException,
SelfSendException, IOException
{
UnidentifiedSenderMessageContent content;
try {
IdentityKeyPair ourIdentity = signalProtocolStore.getIdentityKeyPair();
UnidentifiedSenderMessage wrapper = new UnidentifiedSenderMessage(ciphertext);
byte[] ephemeralSalt = ByteUtil.combine("UnidentifiedDelivery".getBytes(), ourIdentity.getPublicKey().serialize(), wrapper.getEphemeral().serialize());
EphemeralKeys ephemeralKeys = calculateEphemeralKeys(wrapper.getEphemeral(), ourIdentity.getPrivateKey(), ephemeralSalt);
byte[] staticKeyBytes = decrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, wrapper.getEncryptedStatic());
ECPublicKey staticKey = Curve.decodePoint(staticKeyBytes, 0);
byte[] staticSalt = ByteUtil.combine(ephemeralKeys.chainKey, wrapper.getEncryptedStatic());
StaticKeys staticKeys = calculateStaticKeys(staticKey, ourIdentity.getPrivateKey(), staticSalt);
byte[] messageBytes = decrypt(staticKeys.cipherKey, staticKeys.macKey, wrapper.getEncryptedMessage());
content = new UnidentifiedSenderMessageContent(messageBytes);
validator.validate(content.getSenderCertificate(), timestamp);
if (content.getSenderCertificate().getSender().equals(localAddress.getName()) &&
content.getSenderCertificate().getSenderDeviceId() == localAddress.getDeviceId())
{
throw new SelfSendException();
}
} catch (InvalidKeyException e) {
throw new InvalidMetadataMessageException(e);
} catch (InvalidMacException e) {
throw new InvalidMetadataMessageException(e);
} catch (InvalidCertificateException e) {
throw new InvalidMetadataMessageException(e);
}
try {
Pair<Integer, byte[]> dataPair = new Pair<>(content.getType(), decrypt(content));
return new Pair<>(
new SignalProtocolAddress(content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId()),
dataPair
);
} catch (InvalidMessageException e) {
throw new ProtocolInvalidMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (NoSessionException e) {
throw new ProtocolNoSessionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (LegacyMessageException e) {
throw new ProtocolLegacyMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (InvalidVersionException e) {
throw new ProtocolInvalidVersionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (DuplicateMessageException e) {
throw new ProtocolDuplicateMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (InvalidKeyIdException e) {
throw new ProtocolInvalidKeyIdException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
} catch (UntrustedIdentityException e) {
throw new ProtocolUntrustedIdentityException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
}
}
public int getSessionVersion(SignalProtocolAddress remoteAddress) {
return new SessionCipher(signalProtocolStore, remoteAddress).getSessionVersion();
}
public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
return new SessionCipher(signalProtocolStore, remoteAddress).getRemoteRegistrationId();
}
private EphemeralKeys calculateEphemeralKeys(ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt) throws InvalidKeyException {
try {
byte[] ephemeralSecret = Curve.calculateAgreement(ephemeralPublic, ephemeralPrivate);
byte[] ephemeralDerived = new HKDFv3().deriveSecrets(ephemeralSecret, salt, new byte[0], 96);
byte[][] ephemeralDerivedParts = ByteUtil.split(ephemeralDerived, 32, 32, 32);
return new EphemeralKeys(ephemeralDerivedParts[0], ephemeralDerivedParts[1], ephemeralDerivedParts[2]);
} catch (ParseException e) {
throw new AssertionError(e);
}
}
private StaticKeys calculateStaticKeys(ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt) throws InvalidKeyException {
try {
byte[] staticSecret = Curve.calculateAgreement(staticPublic, staticPrivate);
byte[] staticDerived = new HKDFv3().deriveSecrets(staticSecret, salt, new byte[0], 96);
byte[][] staticDerivedParts = ByteUtil.split(staticDerived, 32, 32, 32);
return new StaticKeys(staticDerivedParts[1], staticDerivedParts[2]);
} catch (ParseException e) {
throw new AssertionError(e);
}
}
private byte[] decrypt(UnidentifiedSenderMessageContent message)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException, DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, LegacyMessageException, NoSessionException
{
SignalProtocolAddress sender = new SignalProtocolAddress(message.getSenderCertificate().getSender(), message.getSenderCertificate().getSenderDeviceId());
switch (message.getType()) {
case CiphertextMessage.WHISPER_TYPE: return new SessionCipher(signalProtocolStore, sender).decrypt(new SignalMessage(message.getContent()));
case CiphertextMessage.PREKEY_TYPE: return new SessionCipher(signalProtocolStore, sender).decrypt(new PreKeySignalMessage(message.getContent()));
default: throw new InvalidMessageException("Unknown type: " + message.getType());
}
}
private byte[] encrypt(SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] ourFullMac = mac.doFinal(ciphertext);
byte[] ourMac = ByteUtil.trim(ourFullMac, 10);
return ByteUtil.combine(ciphertext, ourMac);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private byte[] decrypt(SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext) throws InvalidMacException {
try {
if (ciphertext.length < 10) {
throw new InvalidMacException("Ciphertext not long enough for MAC!");
}
byte[][] ciphertextParts = ByteUtil.split(ciphertext, ciphertext.length - 10, 10);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
byte[] digest = mac.doFinal(ciphertextParts[0]);
byte[] ourMac = ByteUtil.trim(digest, 10);
byte[] theirMac = ciphertextParts[1];
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new InvalidMacException("Bad mac!");
}
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
return cipher.doFinal(ciphertextParts[0]);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private static class EphemeralKeys {
private final byte[] chainKey;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey) {
this.chainKey = chainKey;
this.cipherKey = new SecretKeySpec(cipherKey, "AES");
this.macKey = new SecretKeySpec(macKey, "HmacSHA256");
}
}
private static class StaticKeys {
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private StaticKeys(byte[] cipherKey, byte[] macKey) {
this.cipherKey = new SecretKeySpec(cipherKey, "AES");
this.macKey = new SecretKeySpec(macKey, "HmacSHA256");
}
}
}

View File

@ -878,7 +878,6 @@ public class SignalServiceMessageSender {
{ {
List<OutgoingPushMessage> messages = new LinkedList<>(); List<OutgoingPushMessage> messages = new LinkedList<>();
PushTransportDetails transportDetails = new PushTransportDetails(3);
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey); boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
String encryptionPublicKey; String encryptionPublicKey;
@ -888,7 +887,7 @@ public class SignalServiceMessageSender {
} else { } else {
encryptionPublicKey = publicKey; encryptionPublicKey = publicKey;
} }
byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey); byte[] ciphertext = sessionProtocolImpl.encrypt(PushTransportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
String body = Base64.encodeBytes(ciphertext); String body = Base64.encodeBytes(ciphertext);
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE : int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE; SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;

View File

@ -11,11 +11,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.session.libsignal.libsignal.ecc.ECKeyPair; import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.metadata.InvalidMetadataMessageException; import org.session.libsignal.metadata.InvalidMetadataMessageException;
import org.session.libsignal.metadata.ProtocolInvalidMessageException; import org.session.libsignal.metadata.ProtocolInvalidMessageException;
import org.session.libsignal.metadata.SealedSessionCipher;
import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.SessionCipher;
import org.session.libsignal.libsignal.SignalProtocolAddress;
import org.session.libsignal.libsignal.state.SignalProtocolStore;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachment; import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
@ -55,18 +51,12 @@ public class SignalServiceCipher {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = SignalServiceCipher.class.getSimpleName(); private static final String TAG = SignalServiceCipher.class.getSimpleName();
private final SignalProtocolStore signalProtocolStore;
private final SignalServiceAddress localAddress;
private final SessionProtocol sessionProtocolImpl; private final SessionProtocol sessionProtocolImpl;
private final LokiAPIDatabaseProtocol apiDB; private final LokiAPIDatabaseProtocol apiDB;
public SignalServiceCipher(SignalServiceAddress localAddress, public SignalServiceCipher(SessionProtocol sessionProtocolImpl,
SignalProtocolStore signalProtocolStore,
SessionProtocol sessionProtocolImpl,
LokiAPIDatabaseProtocol apiDB) LokiAPIDatabaseProtocol apiDB)
{ {
this.signalProtocolStore = signalProtocolStore;
this.localAddress = localAddress;
this.sessionProtocolImpl = sessionProtocolImpl; this.sessionProtocolImpl = sessionProtocolImpl;
this.apiDB = apiDB; this.apiDB = apiDB;
} }
@ -135,13 +125,8 @@ public class SignalServiceCipher {
protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException
{ {
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSource(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, new SignalProtocolAddress(localAddress.getNumber(), 1));
byte[] paddedMessage; byte[] paddedMessage;
Metadata metadata; Metadata metadata;
int sessionVersion;
if (envelope.isClosedGroupCiphertext()) { if (envelope.isClosedGroupCiphertext()) {
String groupPublicKey = envelope.getSource(); String groupPublicKey = envelope.getSource();
@ -149,20 +134,16 @@ public class SignalServiceCipher {
paddedMessage = plaintextAndSenderPublicKey.getFirst(); paddedMessage = plaintextAndSenderPublicKey.getFirst();
String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isUnidentifiedSender()) { } else if (envelope.isUnidentifiedSender()) {
ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair(); ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair();
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair); kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair);
paddedMessage = plaintextAndSenderPublicKey.getFirst(); paddedMessage = plaintextAndSenderPublicKey.getFirst();
String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice()));
} else { } else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
} }
byte[] data = PushTransportDetails.getStrippedPaddingMessageBody(paddedMessage);
PushTransportDetails transportDetails = new PushTransportDetails(sessionVersion);
byte[] data = transportDetails.getStrippedPaddingMessageBody(paddedMessage);
return new Plaintext(metadata, data); return new Plaintext(metadata, data);
} }

View File

@ -13,16 +13,7 @@ public class PushTransportDetails {
private static final String TAG = PushTransportDetails.class.getSimpleName(); private static final String TAG = PushTransportDetails.class.getSimpleName();
private final int messageVersion; public static byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
public PushTransportDetails(int messageVersion) {
this.messageVersion = messageVersion;
}
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageWithPadding;
int paddingStart = 0; int paddingStart = 0;
for (int i=messageWithPadding.length-1;i>=0;i--) { for (int i=messageWithPadding.length-1;i>=0;i--) {
@ -41,10 +32,7 @@ public class PushTransportDetails {
return strippedMessage; return strippedMessage;
} }
public byte[] getPaddedMessageBody(byte[] messageBody) { public static byte[] getPaddedMessageBody(byte[] messageBody) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageBody;
// NOTE: This is dumb. We have our own padding scheme, but so does the cipher. // NOTE: This is dumb. We have our own padding scheme, but so does the cipher.
// The +1 -1 here is to make sure the Cipher has room to add one padding byte, // The +1 -1 here is to make sure the Cipher has room to add one padding byte,
// otherwise it'll add a full 16 extra bytes. // otherwise it'll add a full 16 extra bytes.
@ -55,7 +43,7 @@ public class PushTransportDetails {
return paddedMessage; return paddedMessage;
} }
private int getPaddedMessageLength(int messageLength) { private static int getPaddedMessageLength(int messageLength) {
int messageLengthWithTerminator = messageLength + 1; int messageLengthWithTerminator = messageLength + 1;
int messagePartCount = messageLengthWithTerminator / 160; int messagePartCount = messageLengthWithTerminator / 160;