mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
clean up session cipher, sealed session cipher, and old message version
This commit is contained in:
parent
f0020ea811
commit
1e1b3e02e1
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user