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:
commit
2491447ee7
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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
14
node/Native.d.ts
vendored
@ -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; }
|
||||
|
104
node/index.ts
104
node/index.ts
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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())
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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::{
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
23
rust/protocol/src/proto/service.proto
Normal file
23
rust/protocol/src/proto/service.proto
Normal 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;
|
||||
}
|
6
rust/protocol/src/proto/service.rs
Normal file
6
rust/protocol/src/proto/service.rs
Normal file
@ -0,0 +1,6 @@
|
||||
//
|
||||
// Copyright 2021 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/signalservice.rs"));
|
@ -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(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
106
swift/Sources/SignalClient/messages/PlaintextContent.swift
Normal file
106
swift/Sources/SignalClient/messages/PlaintextContent.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user