0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

Merge pull request #316 from signalapp/jrose/DecryptionErrorMessage-and-PlaintextContent-2

Add DecryptionErrorMessage and PlaintextContent (alternate)
This commit is contained in:
Jordan Rose 2021-05-26 16:27:49 -07:00 committed by GitHub
commit 2491447ee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1247 additions and 8 deletions

View File

@ -101,6 +101,14 @@ public final class Native {
public static native void CryptographicMac_Update(long mac, byte[] input);
public static native void CryptographicMac_UpdateWithOffset(long mac, byte[] input, int offset, int len);
public static native long DecryptionErrorMessage_Deserialize(byte[] data);
public static native void DecryptionErrorMessage_Destroy(long handle);
public static native long DecryptionErrorMessage_ExtractFromSerializedContent(byte[] bytes);
public static native long DecryptionErrorMessage_ForOriginalMessage(byte[] originalBytes, int originalType, long originalTimestamp);
public static native long DecryptionErrorMessage_GetRatchetKey(long m);
public static native byte[] DecryptionErrorMessage_GetSerialized(long obj);
public static native long DecryptionErrorMessage_GetTimestamp(long obj);
public static native byte[] DeviceTransfer_GenerateCertificate(byte[] privateKey, String name, int daysToExpire);
public static native byte[] DeviceTransfer_GeneratePrivateKey();
@ -138,6 +146,13 @@ public final class Native {
public static native byte[] NumericFingerprintGenerator_GetScannableEncoding(long obj);
public static native long NumericFingerprintGenerator_New(int iterations, int version, byte[] localIdentifier, byte[] localKey, byte[] remoteIdentifier, byte[] remoteKey);
public static native long PlaintextContent_Deserialize(byte[] data);
public static native byte[] PlaintextContent_DeserializeAndGetContent(byte[] bytes);
public static native void PlaintextContent_Destroy(long handle);
public static native long PlaintextContent_FromDecryptionErrorMessage(long m);
public static native byte[] PlaintextContent_GetBody(long obj);
public static native byte[] PlaintextContent_GetSerialized(long obj);
public static native void PreKeyBundle_Destroy(long handle);
public static native int PreKeyBundle_GetDeviceId(long obj);
public static native long PreKeyBundle_GetIdentityKey(long p);
@ -236,6 +251,7 @@ public final class Native {
public static native CiphertextMessage SessionCipher_EncryptMessage(byte[] ptext, long protocolAddress, SessionStore sessionStore, IdentityKeyStore identityKeyStore, Object ctx);
public static native void SessionRecord_ArchiveCurrentState(long sessionRecord);
public static native boolean SessionRecord_CurrentRatchetKeyMatches(long s, long key);
public static native long SessionRecord_Deserialize(byte[] data);
public static native void SessionRecord_Destroy(long handle);
public static native long SessionRecord_FromSingleSessionState(byte[] sessionState);

View File

@ -18,6 +18,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.groups.GroupCipher;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.PlaintextContent;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.protocol.SenderKeyMessage;
import org.whispersystems.libsignal.protocol.SignalMessage;
@ -173,10 +174,16 @@ public class SealedSessionCipher {
SignalProtocolAddress sender = new SignalProtocolAddress(message.getSenderCertificate().getSenderUuid(), 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()));
case CiphertextMessage.SENDERKEY_TYPE: return new GroupCipher(signalProtocolStore, sender).decrypt(message.getContent());
default: throw new InvalidMessageException("Unknown type: " + 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()));
case CiphertextMessage.SENDERKEY_TYPE:
return new GroupCipher(signalProtocolStore, sender).decrypt(message.getContent());
case CiphertextMessage.PLAINTEXT_CONTENT_TYPE:
return Native.PlaintextContent_DeserializeAndGetContent(message.getContent());
default:
throw new InvalidMessageException("Unknown type: " + message.getType());
}
}

View File

@ -12,6 +12,7 @@ public interface CiphertextMessage {
public static final int WHISPER_TYPE = 2;
public static final int PREKEY_TYPE = 3;
public static final int SENDERKEY_TYPE = 7;
public static final int PLAINTEXT_CONTENT_TYPE = 8;
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
public static final int ENCRYPTED_MESSAGE_OVERHEAD = 53;

View File

@ -0,0 +1,58 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.libsignal.protocol;
import org.signal.client.internal.Native;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
public final class DecryptionErrorMessage {
final long handle;
@Override
protected void finalize() {
Native.DecryptionErrorMessage_Destroy(this.handle);
}
DecryptionErrorMessage(long handle) {
this.handle = handle;
}
public DecryptionErrorMessage(byte[] serialized) throws InvalidMessageException {
handle = Native.DecryptionErrorMessage_Deserialize(serialized);
}
public static DecryptionErrorMessage forOriginalMessage(byte[] originalBytes, int messageType, long timestamp) {
return new DecryptionErrorMessage(
Native.DecryptionErrorMessage_ForOriginalMessage(originalBytes, messageType, timestamp));
}
public byte[] serialize() {
return Native.DecryptionErrorMessage_GetSerialized(this.handle);
}
public Optional<ECPublicKey> getRatchetKey() {
long keyHandle = Native.DecryptionErrorMessage_GetRatchetKey(this.handle);
if (keyHandle == 0) {
return Optional.absent();
} else {
return Optional.of(new ECPublicKey(keyHandle));
}
}
public long getTimestamp() {
return Native.DecryptionErrorMessage_GetTimestamp(this.handle);
}
/// For testing only
public static DecryptionErrorMessage extractFromSerializedContent(byte[] serializedContentBytes) throws InvalidMessageException {
return new DecryptionErrorMessage(
Native.DecryptionErrorMessage_ExtractFromSerializedContent(serializedContentBytes));
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.libsignal.protocol;
import org.signal.client.internal.Native;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
public final class PlaintextContent implements CiphertextMessage {
private final long handle;
@Override
protected void finalize() {
Native.PlaintextContent_Destroy(this.handle);
}
public long nativeHandle() {
return handle;
}
// Used by Rust.
@SuppressWarnings("unused")
private PlaintextContent(long handle) {
this.handle = handle;
}
public PlaintextContent(DecryptionErrorMessage message) {
handle = Native.PlaintextContent_FromDecryptionErrorMessage(message.handle);
}
@Override
public byte[] serialize() {
return Native.PlaintextContent_GetSerialized(this.handle);
}
@Override
public int getType() {
return CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
}
public byte[] getBody() {
return Native.PlaintextContent_GetBody(this.handle);
}
}

View File

@ -90,6 +90,10 @@ public class SessionRecord {
return Native.SessionRecord_HasSenderChain(this.handle);
}
public boolean currentRatchetKeyMatches(ECPublicKey key) {
return Native.SessionRecord_CurrentRatchetKeyMatches(this.handle, key.nativeHandle());
}
/** @return a serialized version of the current SessionRecord. */
public byte[] serialize() {
return Native.SessionRecord_Serialize(this.handle);

View File

@ -14,6 +14,7 @@ import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SessionBuilder;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.ecc.Curve;
@ -22,9 +23,12 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.groups.GroupCipher;
import org.whispersystems.libsignal.groups.GroupSessionBuilder;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.libsignal.protocol.PlaintextContent;
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.signal.client.internal.Native;
@ -224,6 +228,43 @@ public class SealedSessionCipherTest extends TestCase {
}
}
public void testDecryptionErrorMessage() throws InvalidCertificateException, InvalidKeyException, InvalidMessageException, InvalidMetadataMessageException, InvalidMetadataVersionException, ProtocolDuplicateMessageException, ProtocolInvalidKeyException, ProtocolInvalidKeyIdException, ProtocolInvalidMessageException, ProtocolInvalidVersionException, ProtocolLegacyMessageException, ProtocolNoSessionException, ProtocolUntrustedIdentityException, SelfSendException, UntrustedIdentityException {
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
initializeSessions(aliceStore, bobStore, bobAddress);
ECKeyPair trustRoot = Curve.generateKeyPair();
CertificateValidator certificateValidator = new CertificateValidator(trustRoot.getPublicKey());
SenderCertificate senderCertificate = createCertificateFor(trustRoot, UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"), "+14151111111", 1, aliceStore.getIdentityKeyPair().getPublicKey().getPublicKey(), 31337);
SealedSessionCipher aliceCipher = new SealedSessionCipher(aliceStore, UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"), "+14151111111", 1);
// Send a message from Alice to Bob to set up the session.
byte[] ciphertext = aliceCipher.encrypt(bobAddress, senderCertificate, "smert za smert".getBytes());
SealedSessionCipher bobCipher = new SealedSessionCipher(bobStore, UUID.fromString("e80f7bbe-5b94-471e-bd8c-2173654ea3d1"), "+14152222222", 1);
bobCipher.decrypt(certificateValidator, ciphertext, 31335);
// Pretend Bob's reply fails to decrypt.
SignalProtocolAddress aliceAddress = new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
SessionCipher bobUnsealedCipher = new SessionCipher(bobStore, aliceAddress);
CiphertextMessage bobMessage = bobUnsealedCipher.encrypt("reply".getBytes());
DecryptionErrorMessage errorMessage = DecryptionErrorMessage.forOriginalMessage(bobMessage.serialize(), bobMessage.getType(), 408);
PlaintextContent errorMessageContent = new PlaintextContent(errorMessage);
UnidentifiedSenderMessageContent errorMessageUsmc = new UnidentifiedSenderMessageContent(errorMessageContent, senderCertificate, UnidentifiedSenderMessageContent.CONTENT_HINT_SUPPLEMENTARY, Optional.<byte[]>absent());
byte[] errorMessageCiphertext = aliceCipher.encrypt(bobAddress, errorMessageUsmc);
DecryptionResult result = bobCipher.decrypt(certificateValidator, errorMessageCiphertext, 31335);
DecryptionErrorMessage bobErrorMessage = DecryptionErrorMessage.extractFromSerializedContent(result.getPaddedMessage());
assertEquals(bobErrorMessage.getTimestamp(), 408);
SessionRecord bobSessionWithAlice = bobStore.loadSession(aliceAddress);
assert(bobSessionWithAlice.currentRatchetKeyMatches(bobErrorMessage.getRatchetKey().get()));
}
private SenderCertificate createCertificateFor(ECKeyPair trustRoot, UUID uuid, String e164, int deviceId, ECPublicKey identityKey, long expires)
throws InvalidKeyException, InvalidCertificateException {
ECKeyPair serverKey = Curve.generateKeyPair();

View File

@ -95,7 +95,9 @@ public class SessionCipherTest extends TestCase {
CiphertextMessage message2 = aliceCipher.encrypt(alicePlaintext);
SessionRecord bobSession = bobStore.loadSession(aliceAddress);
assertFalse(bobSession.currentRatchetKeyMatches(Curve.generateKeyPair().getPublicKey()));
bobSession.archiveCurrentState();
assertFalse(bobSession.currentRatchetKeyMatches(Curve.generateKeyPair().getPublicKey()));
bobStore.storeSession(aliceAddress, bobSession);
byte[] bobPlaintext2 = bobCipher.decrypt(new SignalMessage(message2.serialize()));

14
node/Native.d.ts vendored
View File

@ -46,8 +46,15 @@ export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }
export function Aes256GcmSiv_Decrypt(aesGcmSiv: Wrapper<Aes256GcmSiv>, ctext: Buffer, nonce: Buffer, associatedData: Buffer): Buffer;
export function Aes256GcmSiv_Encrypt(aesGcmSiv: Wrapper<Aes256GcmSiv>, ptext: Buffer, nonce: Buffer, associatedData: Buffer): Buffer;
export function Aes256GcmSiv_New(key: Buffer): Aes256GcmSiv;
export function CiphertextMessage_FromPlaintextContent(m: Wrapper<PlaintextContent>): CiphertextMessage;
export function CiphertextMessage_Serialize(obj: Wrapper<CiphertextMessage>): Buffer;
export function CiphertextMessage_Type(msg: Wrapper<CiphertextMessage>): number;
export function DecryptionErrorMessage_Deserialize(buffer: Buffer): DecryptionErrorMessage;
export function DecryptionErrorMessage_ExtractFromSerializedContent(bytes: Buffer): DecryptionErrorMessage;
export function DecryptionErrorMessage_ForOriginalMessage(originalBytes: Buffer, originalType: number, originalTimestamp: number): DecryptionErrorMessage;
export function DecryptionErrorMessage_GetRatchetKey(m: Wrapper<DecryptionErrorMessage>): PublicKey | null;
export function DecryptionErrorMessage_GetTimestamp(obj: Wrapper<DecryptionErrorMessage>): number;
export function DecryptionErrorMessage_Serialize(obj: Wrapper<DecryptionErrorMessage>): Buffer;
export function Fingerprint_DisplayString(obj: Wrapper<Fingerprint>): string;
export function Fingerprint_New(iterations: number, version: number, localIdentifier: Buffer, localKey: Wrapper<PublicKey>, remoteIdentifier: Buffer, remoteKey: Wrapper<PublicKey>): Fingerprint;
export function Fingerprint_ScannableEncoding(obj: Wrapper<Fingerprint>): Buffer;
@ -55,6 +62,10 @@ export function GroupCipher_DecryptMessage(sender: Wrapper<ProtocolAddress>, mes
export function GroupCipher_EncryptMessage(sender: Wrapper<ProtocolAddress>, distributionId: Uuid, message: Buffer, store: SenderKeyStore, ctx: null): Promise<CiphertextMessage>;
export function HKDF_DeriveSecrets(outputLength: number, version: number, ikm: Buffer, label: Buffer | null, salt: Buffer | null): Buffer;
export function IdentityKeyPair_Serialize(publicKey: Wrapper<PublicKey>, privateKey: Wrapper<PrivateKey>): Buffer;
export function PlaintextContent_Deserialize(buffer: Buffer): PlaintextContent;
export function PlaintextContent_FromDecryptionErrorMessage(m: Wrapper<DecryptionErrorMessage>): PlaintextContent;
export function PlaintextContent_GetBody(obj: Wrapper<PlaintextContent>): Buffer;
export function PlaintextContent_Serialize(obj: Wrapper<PlaintextContent>): Buffer;
export function PreKeyBundle_GetDeviceId(obj: Wrapper<PreKeyBundle>): number;
export function PreKeyBundle_GetIdentityKey(p: Wrapper<PreKeyBundle>): PublicKey;
export function PreKeyBundle_GetPreKeyId(obj: Wrapper<PreKeyBundle>): number | null;
@ -145,6 +156,7 @@ export function SessionCipher_DecryptPreKeySignalMessage(message: Wrapper<PreKey
export function SessionCipher_DecryptSignalMessage(message: Wrapper<SignalMessage>, protocolAddress: Wrapper<ProtocolAddress>, sessionStore: SessionStore, identityKeyStore: IdentityKeyStore, ctx: null): Promise<Buffer>;
export function SessionCipher_EncryptMessage(ptext: Buffer, protocolAddress: Wrapper<ProtocolAddress>, sessionStore: SessionStore, identityKeyStore: IdentityKeyStore, ctx: null): Promise<CiphertextMessage>;
export function SessionRecord_ArchiveCurrentState(sessionRecord: Wrapper<SessionRecord>): void;
export function SessionRecord_CurrentRatchetKeyMatches(s: Wrapper<SessionRecord>, key: Wrapper<PublicKey>): boolean;
export function SessionRecord_Deserialize(buffer: Buffer): SessionRecord;
export function SessionRecord_GetLocalRegistrationId(obj: Wrapper<SessionRecord>): number;
export function SessionRecord_GetRemoteRegistrationId(obj: Wrapper<SessionRecord>): number;
@ -176,7 +188,9 @@ export function UnidentifiedSenderMessageContent_Serialize(obj: Wrapper<Unidenti
export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void): void
interface Aes256GcmSiv { readonly __type: unique symbol; }
interface CiphertextMessage { readonly __type: unique symbol; }
interface DecryptionErrorMessage { readonly __type: unique symbol; }
interface Fingerprint { readonly __type: unique symbol; }
interface PlaintextContent { readonly __type: unique symbol; }
interface PreKeyBundle { readonly __type: unique symbol; }
interface PreKeyRecord { readonly __type: unique symbol; }
interface PreKeySignalMessage { readonly __type: unique symbol; }

View File

@ -24,6 +24,7 @@ export const enum CiphertextMessageType {
Whisper = 2,
PreKey = 3,
SenderKey = 7,
Plaintext = 8,
}
export const enum Direction {
@ -621,6 +622,10 @@ export class SessionRecord {
hasCurrentState(): boolean {
return NativeImpl.SessionRecord_HasCurrentState(this);
}
currentRatchetKeyMatches(key: PublicKey): boolean {
return NativeImpl.SessionRecord_CurrentRatchetKeyMatches(this, key);
}
}
export class ServerCertificate {
@ -1201,6 +1206,10 @@ export class SealedSenderDecryptionResult {
}
}
interface CiphertextMessageConvertible {
asCiphertextMessage(): CiphertextMessage;
}
export class CiphertextMessage {
readonly _nativeHandle: Native.CiphertextMessage;
@ -1214,6 +1223,10 @@ export class CiphertextMessage {
return new CiphertextMessage(nativeHandle);
}
static from(message: CiphertextMessageConvertible): CiphertextMessage {
return message.asCiphertextMessage();
}
serialize(): Buffer {
return NativeImpl.CiphertextMessage_Serialize(this);
}
@ -1223,6 +1236,97 @@ export class CiphertextMessage {
}
}
export class PlaintextContent implements CiphertextMessageConvertible {
readonly _nativeHandle: Native.PlaintextContent;
private constructor(nativeHandle: Native.PlaintextContent) {
this._nativeHandle = nativeHandle;
}
static deserialize(buffer: Buffer): PlaintextContent {
return new PlaintextContent(
NativeImpl.PlaintextContent_Deserialize(buffer)
);
}
static from(message: DecryptionErrorMessage): PlaintextContent {
return new PlaintextContent(
NativeImpl.PlaintextContent_FromDecryptionErrorMessage(message)
);
}
serialize(): Buffer {
return NativeImpl.PlaintextContent_Serialize(this);
}
body(): Buffer {
return NativeImpl.PlaintextContent_GetBody(this);
}
asCiphertextMessage(): CiphertextMessage {
return CiphertextMessage._fromNativeHandle(
NativeImpl.CiphertextMessage_FromPlaintextContent(this)
);
}
}
export class DecryptionErrorMessage {
readonly _nativeHandle: Native.DecryptionErrorMessage;
private constructor(nativeHandle: Native.DecryptionErrorMessage) {
this._nativeHandle = nativeHandle;
}
static _fromNativeHandle(
nativeHandle: Native.DecryptionErrorMessage
): DecryptionErrorMessage {
return new DecryptionErrorMessage(nativeHandle);
}
static forOriginal(
bytes: Buffer,
type: CiphertextMessageType,
timestamp: number
): DecryptionErrorMessage {
return new DecryptionErrorMessage(
NativeImpl.DecryptionErrorMessage_ForOriginalMessage(
bytes,
type,
timestamp
)
);
}
static deserialize(buffer: Buffer): DecryptionErrorMessage {
return new DecryptionErrorMessage(
NativeImpl.DecryptionErrorMessage_Deserialize(buffer)
);
}
static extractFromSerializedBody(buffer: Buffer): DecryptionErrorMessage {
return new DecryptionErrorMessage(
NativeImpl.DecryptionErrorMessage_ExtractFromSerializedContent(buffer)
);
}
serialize(): Buffer {
return NativeImpl.DecryptionErrorMessage_Serialize(this);
}
timestamp(): number {
return NativeImpl.DecryptionErrorMessage_GetTimestamp(this);
}
ratchetKey(): PublicKey | undefined {
const keyHandle = NativeImpl.DecryptionErrorMessage_GetRatchetKey(this);
if (keyHandle) {
return PublicKey._fromNativeHandle(keyHandle);
} else {
return undefined;
}
}
}
export function processPreKeyBundle(
bundle: PreKeyBundle,
address: ProtocolAddress,

View File

@ -715,8 +715,21 @@ describe('SignalClient', () => {
assert.deepEqual(session.localRegistrationId(), 5);
assert.deepEqual(session.remoteRegistrationId(), 5);
assert(session.hasCurrentState());
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
)
);
session.archiveCurrentState();
assert(!session.hasCurrentState());
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
)
);
} else {
assert.fail('no session found');
}
});
it('handles duplicated messages', async () => {
@ -1292,6 +1305,171 @@ describe('SignalClient', () => {
assert.deepEqual(message, bPtext);
});
});
it('DecryptionMessageError', async () => {
const aKeys = new InMemoryIdentityKeyStore();
const bKeys = new InMemoryIdentityKeyStore();
const aSess = new InMemorySessionStore();
const bSess = new InMemorySessionStore();
const bPreK = new InMemoryPreKeyStore();
const bSPreK = new InMemorySignedPreKeyStore();
const bPreKey = SignalClient.PrivateKey.generate();
const bSPreKey = SignalClient.PrivateKey.generate();
const aIdentityKey = await aKeys.getIdentityKey();
const bIdentityKey = await bKeys.getIdentityKey();
const aE164 = '+14151111111';
const aDeviceId = 1;
const bDeviceId = 3;
const aUuid = '9d0652a3-dcc3-4d11-975f-74d61598733f';
const bUuid = '796abedb-ca4e-4f18-8803-1fde5b921f9f';
const trustRoot = SignalClient.PrivateKey.generate();
const serverKey = SignalClient.PrivateKey.generate();
const serverCert = SignalClient.ServerCertificate.new(
1,
serverKey.getPublicKey(),
trustRoot
);
const expires = 1605722925;
const senderCert = SignalClient.SenderCertificate.new(
aUuid,
aE164,
aDeviceId,
aIdentityKey.getPublicKey(),
expires,
serverCert,
serverKey
);
const bRegistrationId = await bKeys.getLocalRegistrationId();
const bPreKeyId = 31337;
const bSignedPreKeyId = 22;
const bSignedPreKeySig = bIdentityKey.sign(
bSPreKey.getPublicKey().serialize()
);
const bPreKeyBundle = SignalClient.PreKeyBundle.new(
bRegistrationId,
bDeviceId,
bPreKeyId,
bPreKey.getPublicKey(),
bSignedPreKeyId,
bSPreKey.getPublicKey(),
bSignedPreKeySig,
bIdentityKey.getPublicKey()
);
const bPreKeyRecord = SignalClient.PreKeyRecord.new(
bPreKeyId,
bPreKey.getPublicKey(),
bPreKey
);
bPreK.savePreKey(bPreKeyId, bPreKeyRecord);
const bSPreKeyRecord = SignalClient.SignedPreKeyRecord.new(
bSignedPreKeyId,
42, // timestamp
bSPreKey.getPublicKey(),
bSPreKey,
bSignedPreKeySig
);
bSPreK.saveSignedPreKey(bSignedPreKeyId, bSPreKeyRecord);
// Set up the session with a message from A to B.
const bAddress = SignalClient.ProtocolAddress.new(bUuid, bDeviceId);
await SignalClient.processPreKeyBundle(
bPreKeyBundle,
bAddress,
aSess,
aKeys
);
const aPlaintext = Buffer.from('hi there', 'utf8');
const aCiphertext = await SignalClient.sealedSenderEncryptMessage(
aPlaintext,
bAddress,
senderCert,
aSess,
aKeys
);
await SignalClient.sealedSenderDecryptMessage(
aCiphertext,
trustRoot.getPublicKey(),
43, // timestamp,
null,
bUuid,
bDeviceId,
bSess,
bKeys,
bPreK,
bSPreK
);
// Pretend to send a message from B back to A that "fails".
const aAddress = SignalClient.ProtocolAddress.new(aUuid, aDeviceId);
const bCiphertext = await SignalClient.signalEncrypt(
Buffer.from('reply', 'utf8'),
aAddress,
bSess,
bKeys
);
const errorMessage = SignalClient.DecryptionErrorMessage.forOriginal(
bCiphertext.serialize(),
bCiphertext.type(),
45 // timestamp
);
const errorContent = SignalClient.PlaintextContent.from(errorMessage);
const errorUSMC = SignalClient.UnidentifiedSenderMessageContent.new(
SignalClient.CiphertextMessage.from(errorContent),
senderCert,
SignalClient.ContentHint.Supplementary,
null // group ID
);
const errorSealedSenderMessage = await SignalClient.sealedSenderEncrypt(
errorUSMC,
bAddress,
aKeys
);
const bErrorUSMC = await SignalClient.sealedSenderDecryptToUsmc(
errorSealedSenderMessage,
bKeys
);
assert.equal(
bErrorUSMC.msgType(),
SignalClient.CiphertextMessageType.Plaintext
);
const bErrorContent = SignalClient.PlaintextContent.deserialize(
bErrorUSMC.contents()
);
const bErrorMessage = SignalClient.DecryptionErrorMessage.extractFromSerializedBody(
bErrorContent.body()
);
assert.equal(bErrorMessage.timestamp(), 45);
const bSessionWithA = await bSess.getSession(aAddress);
assert(
bSessionWithA?.currentRatchetKeyMatches(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
bErrorMessage.ratchetKey()!
)
);
});
it('AES-GCM-SIV test vector', () => {
// RFC 8452, appendix C.2
const key = Buffer.from(

View File

@ -356,6 +356,15 @@ impl<'a> SimpleArgTypeInfo<'a> for CiphertextMessageRef<'a> {
)
.transpose()
})
.or_else(|| {
native_handle_from_message(
env,
foreign,
"org/whispersystems/libsignal/protocol/PlaintextContent",
Self::PlaintextContent,
)
.transpose()
})
.unwrap_or(Err(SignalJniError::BadJniParameter("CiphertextMessage")))
}
}
@ -477,6 +486,11 @@ impl ResultTypeInfo for CiphertextMessage {
"org/whispersystems/libsignal/protocol/SenderKeyMessage",
box_object::<SenderKeyMessage>(Ok(m))?,
),
CiphertextMessage::PlaintextContent(m) => jobject_from_native_handle(
&env,
"org/whispersystems/libsignal/protocol/PlaintextContent",
box_object::<PlaintextContent>(Ok(m))?,
),
};
Ok(obj?.into_inner())

View File

@ -517,6 +517,7 @@ pub enum CiphertextMessageRef<'a> {
SignalMessage(&'a SignalMessage),
PreKeySignalMessage(&'a PreKeySignalMessage),
SenderKeyMessage(&'a SenderKeyMessage),
PlaintextContent(&'a PlaintextContent),
}
impl<'a> CiphertextMessageRef<'a> {
@ -525,6 +526,7 @@ impl<'a> CiphertextMessageRef<'a> {
CiphertextMessageRef::SignalMessage(_) => CiphertextMessageType::Whisper,
CiphertextMessageRef::PreKeySignalMessage(_) => CiphertextMessageType::PreKey,
CiphertextMessageRef::SenderKeyMessage(_) => CiphertextMessageType::SenderKey,
CiphertextMessageRef::PlaintextContent(_) => CiphertextMessageType::Plaintext,
}
}
@ -533,6 +535,7 @@ impl<'a> CiphertextMessageRef<'a> {
CiphertextMessageRef::SignalMessage(x) => x.serialized(),
CiphertextMessageRef::PreKeySignalMessage(x) => x.serialized(),
CiphertextMessageRef::SenderKeyMessage(x) => x.serialized(),
CiphertextMessageRef::PlaintextContent(x) => x.serialized(),
}
}
}

View File

@ -14,7 +14,9 @@ use crate::support::*;
use crate::*;
bridge_handle!(CiphertextMessage, clone = false, jni = false);
bridge_handle!(DecryptionErrorMessage);
bridge_handle!(Fingerprint, jni = NumericFingerprintGenerator);
bridge_handle!(PlaintextContent);
bridge_handle!(PreKeyBundle);
bridge_handle!(PreKeyRecord);
bridge_handle!(PreKeySignalMessage);
@ -443,6 +445,61 @@ fn SenderKeyDistributionMessage_GetSignatureKey(
Ok(*m.signing_key()?)
}
bridge_deserialize!(DecryptionErrorMessage::try_from);
bridge_get!(DecryptionErrorMessage::timestamp -> u64);
bridge_get_bytearray!(
DecryptionErrorMessage::serialized as Serialize,
jni = "DecryptionErrorMessage_1GetSerialized"
);
#[bridge_fn]
fn DecryptionErrorMessage_GetRatchetKey(m: &DecryptionErrorMessage) -> Option<PublicKey> {
m.ratchet_key().cloned()
}
#[bridge_fn]
fn DecryptionErrorMessage_ForOriginalMessage(
original_bytes: &[u8],
original_type: u8,
original_timestamp: u64,
) -> Result<DecryptionErrorMessage> {
let original_type = CiphertextMessageType::try_from(original_type).map_err(|_| {
SignalProtocolError::InvalidArgument(format!("unknown message type {}", original_type))
})?;
Ok(DecryptionErrorMessage::for_original(
original_bytes,
original_type,
original_timestamp,
)?)
}
#[bridge_fn]
fn DecryptionErrorMessage_ExtractFromSerializedContent(
bytes: &[u8],
) -> Result<DecryptionErrorMessage> {
extract_decryption_error_message_from_serialized_content(bytes)
}
bridge_deserialize!(PlaintextContent::try_from);
bridge_get_bytearray!(
PlaintextContent::serialized as Serialize,
jni = "PlaintextContent_1GetSerialized"
);
bridge_get_bytearray!(PlaintextContent::body);
#[bridge_fn]
fn PlaintextContent_FromDecryptionErrorMessage(m: &DecryptionErrorMessage) -> PlaintextContent {
PlaintextContent::from(m.clone())
}
/// Save an allocation by decrypting all in one go.
///
/// Only useful for APIs that *do* decrypt all in one go, which is currently just Java.
#[bridge_fn_buffer(ffi = false, node = false)]
fn PlaintextContent_DeserializeAndGetContent<E: Env>(env: E, bytes: &[u8]) -> Result<E::Buffer> {
Ok(env.buffer(PlaintextContent::try_from(bytes)?.body()))
}
#[bridge_fn]
fn PreKeyBundle_New(
registration_id: u32,
@ -723,6 +780,7 @@ pub enum FfiCiphertextMessageType {
Whisper = 2,
PreKey = 3,
SenderKey = 7,
Plaintext = 8,
}
const_assert_eq!(
@ -737,6 +795,10 @@ const_assert_eq!(
FfiCiphertextMessageType::SenderKey as u8,
CiphertextMessageType::SenderKey as u8
);
const_assert_eq!(
FfiCiphertextMessageType::Plaintext as u8,
CiphertextMessageType::Plaintext as u8
);
#[bridge_fn(jni = false)]
fn CiphertextMessage_Type(msg: &CiphertextMessage) -> u8 {
@ -745,6 +807,11 @@ fn CiphertextMessage_Type(msg: &CiphertextMessage) -> u8 {
bridge_get_bytearray!(CiphertextMessage::serialize as Serialize, jni = false);
#[bridge_fn(jni = false)]
fn CiphertextMessage_FromPlaintextContent(m: &PlaintextContent) -> CiphertextMessage {
CiphertextMessage::PlaintextContent(m.clone())
}
#[bridge_fn(ffi = false, node = false)]
fn SessionRecord_NewFresh() -> SessionRecord {
SessionRecord::new_fresh()
@ -770,6 +837,11 @@ fn SessionRecord_ArchiveCurrentState(session_record: &mut SessionRecord) -> Resu
session_record.archive_current_state()
}
#[bridge_fn]
fn SessionRecord_CurrentRatchetKeyMatches(s: &SessionRecord, key: &PublicKey) -> Result<bool> {
s.current_ratchet_key_matches(key)
}
bridge_get!(SessionRecord::has_current_session_state as HasCurrentState -> bool, jni = false);
bridge_deserialize!(SessionRecord::deserialize);

View File

@ -6,8 +6,9 @@
fn main() {
let protos = [
"src/proto/fingerprint.proto",
"src/proto/storage.proto",
"src/proto/sealed_sender.proto",
"src/proto/service.proto",
"src/proto/storage.proto",
"src/proto/wire.proto",
];
prost_build::compile_protos(&protos, &["src"]).expect("Protobufs in src are valid");

View File

@ -56,7 +56,8 @@ pub use {
identity_key::{IdentityKey, IdentityKeyPair},
kdf::HKDF,
protocol::{
CiphertextMessage, CiphertextMessageType, PreKeySignalMessage,
extract_decryption_error_message_from_serialized_content, CiphertextMessage,
CiphertextMessageType, DecryptionErrorMessage, PlaintextContent, PreKeySignalMessage,
SenderKeyDistributionMessage, SenderKeyMessage, SignalMessage,
},
ratchet::{

View File

@ -1,9 +1,10 @@
//
// Copyright 2020 Signal Messenger, LLC.
// Copyright 2020-2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub mod fingerprint;
pub mod sealed_sender;
pub mod service;
pub mod storage;
pub mod wire;

View File

@ -40,6 +40,7 @@ message UnidentifiedSenderMessage {
// Further cases should line up with Envelope.Type, even though old cases don't.
reserved 3 to 6;
SENDERKEY_MESSAGE = 7;
PLAINTEXT_CONTENT = 8;
}
enum ContentHint {

View File

@ -0,0 +1,23 @@
//
// Copyright 2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
syntax = "proto2";
package signalservice;
message Content {
optional bytes /* DataMessage */ data_message = 1;
optional bytes /* SyncMessage */ sync_message = 2;
optional bytes /* CallMessage */ call_message = 3;
optional bytes /* NullMessage */ null_message = 4;
optional bytes /* ReceiptMessage */ receipt_message = 5;
optional bytes /* TypingMessage */ typing_message = 6;
optional bytes /* SenderKeyDistributionMessage */ sender_key_distribution_message = 7;
optional bytes /* DecryptionErrorMessage */ decryption_error_message = 8;
}
message DecryptionErrorMessage {
optional bytes ratchet_key = 1; // set to the public ratchet key from the SignalMessage if a 1-1 payload fails to decrypt
optional uint64 timestamp = 2;
}

View File

@ -0,0 +1,6 @@
//
// Copyright 2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
include!(concat!(env!("OUT_DIR"), "/signalservice.rs"));

View File

@ -22,6 +22,7 @@ pub enum CiphertextMessage {
SignalMessage(SignalMessage),
PreKeySignalMessage(PreKeySignalMessage),
SenderKeyMessage(SenderKeyMessage),
PlaintextContent(PlaintextContent),
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, num_enum::TryFromPrimitive)]
@ -29,8 +30,8 @@ pub enum CiphertextMessage {
pub enum CiphertextMessageType {
Whisper = 2,
PreKey = 3,
// Further cases should line up with Envelope.Type (proto), even though old cases don't.
SenderKey = 7,
Plaintext = 8,
}
impl CiphertextMessage {
@ -39,6 +40,7 @@ impl CiphertextMessage {
CiphertextMessage::SignalMessage(_) => CiphertextMessageType::Whisper,
CiphertextMessage::PreKeySignalMessage(_) => CiphertextMessageType::PreKey,
CiphertextMessage::SenderKeyMessage(_) => CiphertextMessageType::SenderKey,
CiphertextMessage::PlaintextContent(_) => CiphertextMessageType::Plaintext,
}
}
@ -47,6 +49,7 @@ impl CiphertextMessage {
CiphertextMessage::SignalMessage(x) => x.serialized(),
CiphertextMessage::PreKeySignalMessage(x) => x.serialized(),
CiphertextMessage::SenderKeyMessage(x) => x.serialized(),
CiphertextMessage::PlaintextContent(x) => x.serialized(),
}
}
}
@ -652,6 +655,170 @@ impl TryFrom<&[u8]> for SenderKeyDistributionMessage {
}
}
#[derive(Debug, Clone)]
pub struct PlaintextContent {
serialized: Box<[u8]>,
}
impl PlaintextContent {
/// Identifies a serialized PlaintextContent.
///
/// This ensures someone doesn't try to serialize an arbitrary Content message as
/// PlaintextContent; only messages that are okay to send as plaintext should be allowed.
const PLAINTEXT_CONTEXT_IDENTIFIER_BYTE: u8 = 0xC0;
/// Marks the end of a message and the start of any padding.
///
/// Usually messages are padded to avoid exposing patterns,
/// but PlaintextContent messages are all fixed-length anyway, so there won't be any padding.
const PADDING_BOUNDARY_BYTE: u8 = 0x80;
#[inline]
pub fn body(&self) -> &[u8] {
&self.serialized[1..]
}
#[inline]
pub fn serialized(&self) -> &[u8] {
&self.serialized
}
}
impl From<DecryptionErrorMessage> for PlaintextContent {
fn from(message: DecryptionErrorMessage) -> Self {
let proto_structure = proto::service::Content {
decryption_error_message: Some(message.serialized().to_vec()),
..Default::default()
};
let mut serialized = Vec::new();
serialized.push(Self::PLAINTEXT_CONTEXT_IDENTIFIER_BYTE);
proto_structure
.encode(&mut serialized)
.expect("can always encode to a Vec");
serialized.push(Self::PADDING_BOUNDARY_BYTE);
Self {
serialized: Box::from(serialized),
}
}
}
impl TryFrom<&[u8]> for PlaintextContent {
type Error = SignalProtocolError;
fn try_from(value: &[u8]) -> Result<Self> {
if value.is_empty() {
return Err(SignalProtocolError::CiphertextMessageTooShort(0));
}
if value[0] != Self::PLAINTEXT_CONTEXT_IDENTIFIER_BYTE {
return Err(SignalProtocolError::UnrecognizedMessageVersion(
value[0] as u32,
));
}
Ok(Self {
serialized: Box::from(value),
})
}
}
#[derive(Debug, Clone)]
pub struct DecryptionErrorMessage {
ratchet_key: Option<PublicKey>,
timestamp: u64,
serialized: Box<[u8]>,
}
impl DecryptionErrorMessage {
pub fn for_original(
original_bytes: &[u8],
original_type: CiphertextMessageType,
original_timestamp: u64,
) -> Result<Self> {
let ratchet_key = match original_type {
CiphertextMessageType::Whisper => {
Some(*SignalMessage::try_from(original_bytes)?.sender_ratchet_key())
}
CiphertextMessageType::PreKey => Some(
*PreKeySignalMessage::try_from(original_bytes)?
.message()
.sender_ratchet_key(),
),
CiphertextMessageType::SenderKey => None,
CiphertextMessageType::Plaintext => {
return Err(SignalProtocolError::InvalidArgument(
"cannot create a DecryptionErrorMessage for plaintext content; it is not encrypted".to_string()
));
}
};
let proto_message = proto::service::DecryptionErrorMessage {
timestamp: Some(original_timestamp),
ratchet_key: ratchet_key.map(|k| k.serialize().into()),
};
let mut serialized = Vec::new();
proto_message.encode(&mut serialized)?;
Ok(Self {
ratchet_key,
timestamp: original_timestamp,
serialized: serialized.into_boxed_slice(),
})
}
#[inline]
pub fn timestamp(&self) -> u64 {
self.timestamp
}
#[inline]
pub fn ratchet_key(&self) -> Option<&PublicKey> {
self.ratchet_key.as_ref()
}
#[inline]
pub fn serialized(&self) -> &[u8] {
&self.serialized
}
}
impl TryFrom<&[u8]> for DecryptionErrorMessage {
type Error = SignalProtocolError;
fn try_from(value: &[u8]) -> Result<Self> {
let proto_structure = proto::service::DecryptionErrorMessage::decode(value)?;
let timestamp = proto_structure
.timestamp
.ok_or(SignalProtocolError::InvalidProtobufEncoding)?;
let ratchet_key = proto_structure
.ratchet_key
.map(|k| PublicKey::deserialize(&k))
.transpose()?;
Ok(Self {
timestamp,
ratchet_key,
serialized: Box::from(value),
})
}
}
/// For testing
pub fn extract_decryption_error_message_from_serialized_content(
bytes: &[u8],
) -> Result<DecryptionErrorMessage> {
if bytes.last() != Some(&PlaintextContent::PADDING_BOUNDARY_BYTE) {
return Err(SignalProtocolError::InvalidProtobufEncoding);
}
let content = proto::service::Content::decode(bytes.split_last().expect("checked above").1)?;
content
.decryption_error_message
.as_deref()
.ok_or_else(|| {
SignalProtocolError::InvalidArgument(
"Content does not contain DecryptionErrorMessage".to_owned(),
)
})
.and_then(DecryptionErrorMessage::try_from)
}
#[cfg(test)]
mod tests {
use super::*;
@ -797,4 +964,82 @@ mod tests {
);
Ok(())
}
#[test]
fn test_decryption_error_message() -> Result<()> {
let mut csprng = OsRng;
let identity_key_pair = KeyPair::generate(&mut csprng);
let base_key_pair = KeyPair::generate(&mut csprng);
let message = create_signal_message(&mut csprng)?;
let timestamp = 0x2_0000_0001;
{
let error_message = DecryptionErrorMessage::for_original(
message.serialized(),
CiphertextMessageType::Whisper,
timestamp,
)?;
let error_message = DecryptionErrorMessage::try_from(error_message.serialized())?;
assert_eq!(
error_message.ratchet_key(),
Some(message.sender_ratchet_key())
);
assert_eq!(error_message.timestamp(), timestamp);
}
let pre_key_signal_message = PreKeySignalMessage::new(
3,
365,
None,
97,
base_key_pair.public_key,
identity_key_pair.public_key.into(),
message,
)?;
{
let error_message = DecryptionErrorMessage::for_original(
pre_key_signal_message.serialized(),
CiphertextMessageType::PreKey,
timestamp,
)?;
let error_message = DecryptionErrorMessage::try_from(error_message.serialized())?;
assert_eq!(
error_message.ratchet_key(),
Some(pre_key_signal_message.message().sender_ratchet_key())
);
assert_eq!(error_message.timestamp(), timestamp);
}
let sender_key_message = SenderKeyMessage::new(
3,
Uuid::nil(),
1,
2,
Box::from(b"test".to_owned()),
&mut csprng,
&base_key_pair.private_key,
)?;
{
let error_message = DecryptionErrorMessage::for_original(
sender_key_message.serialized(),
CiphertextMessageType::SenderKey,
timestamp,
)?;
let error_message = DecryptionErrorMessage::try_from(error_message.serialized())?;
assert_eq!(error_message.ratchet_key(), None);
assert_eq!(error_message.timestamp(), timestamp);
}
Ok(())
}
#[test]
fn test_decryption_error_message_for_plaintext() {
assert!(matches!(
DecryptionErrorMessage::for_original(&[], CiphertextMessageType::Plaintext, 5,),
Err(SignalProtocolError::InvalidArgument(_))
));
}
}

View File

@ -335,6 +335,7 @@ impl From<ProtoMessageType> for CiphertextMessageType {
ProtoMessageType::Message => Self::Whisper,
ProtoMessageType::PrekeyMessage => Self::PreKey,
ProtoMessageType::SenderkeyMessage => Self::SenderKey,
ProtoMessageType::PlaintextContent => Self::Plaintext,
};
// Keep raw values in sync from now on, for efficient codegen.
assert!(result == Self::PreKey || message_type as i32 == result as i32);
@ -348,6 +349,7 @@ impl From<CiphertextMessageType> for ProtoMessageType {
CiphertextMessageType::PreKey => Self::PrekeyMessage,
CiphertextMessageType::Whisper => Self::Message,
CiphertextMessageType::SenderKey => Self::SenderkeyMessage,
CiphertextMessageType::Plaintext => Self::PlaintextContent,
};
// Keep raw values in sync from now on, for efficient codegen.
assert!(result == Self::PrekeyMessage || message_type as i32 == result as i32);

View File

@ -636,4 +636,11 @@ impl SessionRecord {
pub fn get_sender_chain_key_bytes(&self) -> Result<Vec<u8>> {
self.session_state()?.get_sender_chain_key_bytes()
}
pub fn current_ratchet_key_matches(&self, key: &PublicKey) -> Result<bool> {
match &self.current_session {
Some(session) => Ok(&session.sender_ratchet_key()? == key),
None => Ok(false),
}
}
}

View File

@ -656,3 +656,141 @@ fn test_sealed_sender_multi_recipient() -> Result<(), SignalProtocolError> {
Ok(())
})
}
#[test]
fn test_decryption_error_in_sealed_sender() -> Result<(), SignalProtocolError> {
block_on(async {
let mut rng = OsRng;
let alice_device_id = 23;
let bob_device_id = 42;
let alice_e164 = "+14151111111".to_owned();
let alice_uuid = "9d0652a3-dcc3-4d11-975f-74d61598733f".to_string();
let bob_uuid = "796abedb-ca4e-4f18-8803-1fde5b921f9f".to_string();
let alice_uuid_address = ProtocolAddress::new(alice_uuid.clone(), 1);
let bob_uuid_address = ProtocolAddress::new(bob_uuid.clone(), bob_device_id);
let mut alice_store = support::test_in_memory_protocol_store()?;
let mut bob_store = support::test_in_memory_protocol_store()?;
let alice_pubkey = *alice_store.get_identity_key_pair(None).await?.public_key();
let alice_pre_key_bundle = create_pre_key_bundle(&mut alice_store, &mut rng).await?;
process_prekey_bundle(
&alice_uuid_address,
&mut bob_store.session_store,
&mut bob_store.identity_store,
&alice_pre_key_bundle,
&mut rng,
None,
)
.await?;
// Send one message to establish a session.
let bob_first_message = message_encrypt(
b"swim camp",
&alice_uuid_address,
&mut bob_store.session_store,
&mut bob_store.identity_store,
None,
)
.await?;
message_decrypt(
&bob_first_message,
&bob_uuid_address,
&mut alice_store.session_store,
&mut alice_store.identity_store,
&mut alice_store.pre_key_store,
&mut alice_store.signed_pre_key_store,
&mut rng,
None,
)
.await?;
// Pretend the second message fails to decrypt.
let bob_message = message_encrypt(
b"space camp",
&alice_uuid_address,
&mut bob_store.session_store,
&mut bob_store.identity_store,
None,
)
.await?;
let original_ratchet_key = match bob_message {
CiphertextMessage::PreKeySignalMessage(ref m) => m.message().sender_ratchet_key(),
_ => panic!("without ACKs, every message should be a PreKeySignalMessage"),
};
// Skip over the part where Bob sends this to Alice and Alice fails to decrypt it,
// for whatever reason.
let trust_root = KeyPair::generate(&mut rng);
let server_key = KeyPair::generate(&mut rng);
let server_cert =
ServerCertificate::new(1, server_key.public_key, &trust_root.private_key, &mut rng)?;
let expires = 1605722925;
let sender_cert = SenderCertificate::new(
alice_uuid.clone(),
Some(alice_e164.clone()),
alice_pubkey,
alice_device_id,
expires,
server_cert,
&server_key.private_key,
&mut rng,
)?;
let error_message = DecryptionErrorMessage::for_original(
bob_message.serialize(),
bob_message.message_type(),
408,
)?;
let error_message_content = PlaintextContent::from(error_message);
let error_message_usmc = UnidentifiedSenderMessageContent::new(
CiphertextMessageType::Plaintext,
sender_cert.clone(),
error_message_content.serialized().to_vec(),
ContentHint::Default,
None,
)?;
let alice_ctext = sealed_sender_encrypt_from_usmc(
&bob_uuid_address,
&error_message_usmc,
&mut alice_store.identity_store,
None,
&mut rng,
)
.await?;
let bob_usmc =
sealed_sender_decrypt_to_usmc(&alice_ctext, &mut bob_store.identity_store, None)
.await?;
assert!(matches!(
bob_usmc.msg_type()?,
CiphertextMessageType::Plaintext,
));
let bob_plaintext = PlaintextContent::try_from(bob_usmc.contents()?)?;
let bob_error_message =
extract_decryption_error_message_from_serialized_content(bob_plaintext.body())
.expect("present");
assert_eq!(bob_error_message.ratchet_key(), Some(original_ratchet_key));
assert_eq!(bob_error_message.timestamp(), 408);
Ok(())
})
}

View File

@ -27,6 +27,9 @@ public class CiphertextMessage {
public static var senderKey: Self {
return Self(SignalCiphertextMessageType_SenderKey)
}
public static var plaintext: Self {
return Self(SignalCiphertextMessageType_Plaintext)
}
}
deinit {
@ -37,6 +40,12 @@ public class CiphertextMessage {
nativeHandle = rawPtr
}
public init(_ plaintextContent: PlaintextContent) {
var result: OpaquePointer?
failOnError(signal_ciphertext_message_from_plaintext_content(&result, plaintextContent.nativeHandle))
nativeHandle = result!
}
public func serialize() -> [UInt8] {
return failOnError {
try invokeFnReturningArray {

View File

@ -0,0 +1,106 @@
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalFfi
import Foundation
public class PlaintextContent {
internal private(set) var nativeHandle: OpaquePointer
deinit {
failOnError(signal_plaintext_content_destroy(nativeHandle))
}
public init<Bytes: ContiguousBytes>(bytes: Bytes) throws {
nativeHandle = try bytes.withUnsafeBytes {
var result: OpaquePointer?
try checkError(signal_plaintext_content_deserialize(&result, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count))
return result
}!
}
public init(_ decryptionError: DecryptionErrorMessage) {
var result: OpaquePointer?
failOnError(signal_plaintext_content_from_decryption_error_message(&result, decryptionError.nativeHandle))
nativeHandle = result!
}
public func serialize() -> [UInt8] {
return failOnError {
try invokeFnReturningArray {
signal_plaintext_content_serialize($0, $1, nativeHandle)
}
}
}
public var body: [UInt8] {
return failOnError {
try invokeFnReturningArray {
signal_plaintext_content_get_body($0, $1, nativeHandle)
}
}
}
}
public class DecryptionErrorMessage {
fileprivate private(set) var nativeHandle: OpaquePointer
deinit {
failOnError(signal_decryption_error_message_destroy(nativeHandle))
}
fileprivate init(owned rawPtr: OpaquePointer) {
nativeHandle = rawPtr
}
public init<Bytes: ContiguousBytes>(bytes: Bytes) throws {
nativeHandle = try bytes.withUnsafeBytes {
var result: OpaquePointer?
try checkError(signal_decryption_error_message_deserialize(&result, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count))
return result
}!
}
public init<Bytes: ContiguousBytes>(originalMessageBytes bytes: Bytes, type: CiphertextMessage.MessageType, timestamp: UInt64) throws {
nativeHandle = try bytes.withUnsafeBytes {
var result: OpaquePointer?
try checkError(signal_decryption_error_message_for_original_message(&result, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count, type.rawValue, timestamp))
return result
}!
}
// For testing
public static func extractFromSerializedContent<Bytes: ContiguousBytes>(_ bytes: Bytes) throws -> DecryptionErrorMessage {
try bytes.withUnsafeBytes {
var result: OpaquePointer?
try checkError(signal_decryption_error_message_extract_from_serialized_content(&result, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count))
return DecryptionErrorMessage(owned: result!)
}!
}
public func serialize() -> [UInt8] {
return failOnError {
try invokeFnReturningArray {
signal_decryption_error_message_serialize($0, $1, nativeHandle)
}
}
}
public var ratchetKey: PublicKey? {
return failOnError {
try invokeFnReturningOptionalPublicKey {
signal_decryption_error_message_get_ratchet_key($0, nativeHandle)
}
}
}
public var timestamp: UInt64 {
return failOnError {
try invokeFnReturningInteger {
signal_decryption_error_message_get_timestamp($0, nativeHandle)
}
}
}
}

View File

@ -51,4 +51,10 @@ public class SessionRecord: ClonableHandleOwner {
signal_session_record_get_remote_registration_id($0, nativeHandle)
}
}
public func currentRatchetKeyMatches(_ key: PublicKey) throws -> Bool {
var result: Bool = false
try checkError(signal_session_record_current_ratchet_key_matches(&result, nativeHandle, key.nativeHandle))
return result
}
}

View File

@ -18,6 +18,7 @@ typedef enum {
SignalCiphertextMessageType_Whisper = 2,
SignalCiphertextMessageType_PreKey = 3,
SignalCiphertextMessageType_SenderKey = 7,
SignalCiphertextMessageType_Plaintext = 8,
} SignalCiphertextMessageType;
typedef enum {
@ -77,8 +78,12 @@ typedef struct SignalAes256GcmSiv SignalAes256GcmSiv;
typedef struct SignalCiphertextMessage SignalCiphertextMessage;
typedef struct SignalDecryptionErrorMessage SignalDecryptionErrorMessage;
typedef struct SignalFingerprint SignalFingerprint;
typedef struct SignalPlaintextContent SignalPlaintextContent;
typedef struct SignalPreKeyBundle SignalPreKeyBundle;
typedef struct SignalPreKeyRecord SignalPreKeyRecord;
@ -321,10 +326,20 @@ SignalFfiError *signal_aes256_gcm_siv_decrypt(const unsigned char **out,
SignalFfiError *signal_ciphertext_message_destroy(SignalCiphertextMessage *p);
SignalFfiError *signal_decryption_error_message_destroy(SignalDecryptionErrorMessage *p);
SignalFfiError *signal_decryption_error_message_clone(SignalDecryptionErrorMessage **new_obj,
const SignalDecryptionErrorMessage *obj);
SignalFfiError *signal_fingerprint_destroy(SignalFingerprint *p);
SignalFfiError *signal_fingerprint_clone(SignalFingerprint **new_obj, const SignalFingerprint *obj);
SignalFfiError *signal_plaintext_content_destroy(SignalPlaintextContent *p);
SignalFfiError *signal_plaintext_content_clone(SignalPlaintextContent **new_obj,
const SignalPlaintextContent *obj);
SignalFfiError *signal_pre_key_bundle_destroy(SignalPreKeyBundle *p);
SignalFfiError *signal_pre_key_bundle_clone(SignalPreKeyBundle **new_obj,
@ -628,6 +643,45 @@ SignalFfiError *signal_sender_key_distribution_message_new(SignalSenderKeyDistri
SignalFfiError *signal_sender_key_distribution_message_get_signature_key(SignalPublicKey **out,
const SignalSenderKeyDistributionMessage *m);
SignalFfiError *signal_decryption_error_message_deserialize(SignalDecryptionErrorMessage **p,
const unsigned char *data,
size_t data_len);
SignalFfiError *signal_decryption_error_message_get_timestamp(uint64_t *out,
const SignalDecryptionErrorMessage *obj);
SignalFfiError *signal_decryption_error_message_serialize(const unsigned char **out,
size_t *out_len,
const SignalDecryptionErrorMessage *obj);
SignalFfiError *signal_decryption_error_message_get_ratchet_key(SignalPublicKey **out,
const SignalDecryptionErrorMessage *m);
SignalFfiError *signal_decryption_error_message_for_original_message(SignalDecryptionErrorMessage **out,
const unsigned char *original_bytes,
size_t original_bytes_len,
uint8_t original_type,
uint64_t original_timestamp);
SignalFfiError *signal_decryption_error_message_extract_from_serialized_content(SignalDecryptionErrorMessage **out,
const unsigned char *bytes,
size_t bytes_len);
SignalFfiError *signal_plaintext_content_deserialize(SignalPlaintextContent **p,
const unsigned char *data,
size_t data_len);
SignalFfiError *signal_plaintext_content_serialize(const unsigned char **out,
size_t *out_len,
const SignalPlaintextContent *obj);
SignalFfiError *signal_plaintext_content_get_body(const unsigned char **out,
size_t *out_len,
const SignalPlaintextContent *obj);
SignalFfiError *signal_plaintext_content_from_decryption_error_message(SignalPlaintextContent **out,
const SignalDecryptionErrorMessage *m);
SignalFfiError *signal_pre_key_bundle_new(SignalPreKeyBundle **out,
uint32_t registration_id,
uint32_t device_id,
@ -838,8 +892,15 @@ SignalFfiError *signal_ciphertext_message_serialize(const unsigned char **out,
size_t *out_len,
const SignalCiphertextMessage *obj);
SignalFfiError *signal_ciphertext_message_from_plaintext_content(SignalCiphertextMessage **out,
const SignalPlaintextContent *m);
SignalFfiError *signal_session_record_archive_current_state(SignalSessionRecord *session_record);
SignalFfiError *signal_session_record_current_ratchet_key_matches(bool *out,
const SignalSessionRecord *s,
const SignalPublicKey *key);
SignalFfiError *signal_session_record_has_current_state(bool *out, const SignalSessionRecord *obj);
SignalFfiError *signal_session_record_deserialize(SignalSessionRecord **p,

View File

@ -225,8 +225,10 @@ class SessionTests: TestCaseBase {
let session: SessionRecord! = try! alice_store.loadSession(for: bob_address, context: NullContext())
XCTAssertNotNil(session)
XCTAssertTrue(session.hasCurrentState)
XCTAssertFalse(try! session.currentRatchetKeyMatches(IdentityKeyPair.generate().publicKey))
session.archiveCurrentState()
XCTAssertFalse(session.hasCurrentState)
XCTAssertFalse(try! session.currentRatchetKeyMatches(IdentityKeyPair.generate().publicKey))
// A redundant archive shouldn't break anything.
session.archiveCurrentState()
XCTAssertFalse(session.hasCurrentState)
@ -303,6 +305,72 @@ class SessionTests: TestCaseBase {
}
func testDecryptionErrorMessage() throws {
let alice_address = try! ProtocolAddress(name: "9d0652a3-dcc3-4d11-975f-74d61598733f", deviceId: 1)
let bob_address = try! ProtocolAddress(name: "6838237D-02F6-4098-B110-698253D15961", deviceId: 1)
let alice_store = InMemorySignalProtocolStore()
let bob_store = InMemorySignalProtocolStore()
// Notice the reverse initialization. Bob will send the first message to Alice in this example.
initializeSessions(alice_store: bob_store, bob_store: alice_store, bob_address: alice_address)
let bob_first_message = try signalEncrypt(message: Array("swim camp".utf8),
for: alice_address,
sessionStore: bob_store,
identityStore: bob_store,
context: NullContext()).serialize()
_ = try signalDecryptPreKey(message: PreKeySignalMessage(bytes: bob_first_message),
from: bob_address,
sessionStore: alice_store,
identityStore: alice_store,
preKeyStore: alice_store,
signedPreKeyStore: alice_store,
context: NullContext())
let bob_message = try signalEncrypt(message: Array("space camp".utf8),
for: alice_address,
sessionStore: bob_store,
identityStore: bob_store,
context: NullContext())
let error_message = try DecryptionErrorMessage(originalMessageBytes: bob_message.serialize(),
type: bob_message.messageType,
timestamp: 408)
let trust_root = IdentityKeyPair.generate()
let server_keys = IdentityKeyPair.generate()
let server_cert = try! ServerCertificate(keyId: 1, publicKey: server_keys.publicKey, trustRoot: trust_root.privateKey)
let sender_addr = try! SealedSenderAddress(e164: "+14151111111",
uuidString: alice_address.name,
deviceId: 1)
let sender_cert = try! SenderCertificate(sender: sender_addr,
publicKey: alice_store.identityKeyPair(context: NullContext()).publicKey,
expiration: 31337,
signerCertificate: server_cert,
signerKey: server_keys.privateKey)
let error_message_usmc = try UnidentifiedSenderMessageContent(
CiphertextMessage(PlaintextContent(error_message)),
from: sender_cert,
contentHint: .supplementary,
groupId: [])
let ciphertext = try sealedSenderEncrypt(error_message_usmc,
for: bob_address,
identityStore: alice_store,
context: NullContext())
let bob_usmc = try UnidentifiedSenderMessageContent(message: ciphertext,
identityStore: bob_store,
context: NullContext())
XCTAssertEqual(bob_usmc.messageType, .plaintext)
let bob_content = try PlaintextContent(bytes: bob_usmc.contents)
let bob_error_message = try DecryptionErrorMessage.extractFromSerializedContent(bob_content.body)
XCTAssertEqual(bob_error_message.timestamp, 408)
let bob_session_with_alice = try XCTUnwrap(bob_store.loadSession(for: alice_address, context: NullContext()))
XCTAssert(try bob_session_with_alice.currentRatchetKeyMatches(XCTUnwrap(bob_error_message.ratchetKey)))
}
static var allTests: [(String, (SessionTests) -> () throws -> Void)] {
return [
("testSessionCipher", testSessionCipher),
@ -310,6 +378,7 @@ class SessionTests: TestCaseBase {
("testSealedSenderSession", testSealedSenderSession),
("testArchiveSession", testArchiveSession),
("testSealedSenderGroupCipher", testSealedSenderGroupCipher),
("testDecryptionErrorMessage", testDecryptionErrorMessage)
]
}
}