Move libsignal-service-java into this repo.

libsignal-service-java repo commit: 1a01c22636
master
Android Team 2019-11-20 13:16:10 -05:00 committed by Greyson Parrelli
parent acf78b6b63
commit b20e8616ec
168 changed files with 16444 additions and 24 deletions

View File

@ -115,7 +115,7 @@ dependencies {
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'org.whispersystems:signal-service-android:2.15.3'
implementation project(':libsignal-service')
implementation 'org.signal:ringrtc-android:0.2.0'
@ -453,9 +453,3 @@ def getLastCommitTimestamp() {
return os.toString() + "000"
}
}
task qa {
group 'Verification'
description 'Quality Assurance. Run before pushing.'
dependsOn 'testPlayReleaseUnitTest', 'lintPlayRelease', 'assemblePlayDebug'
}

View File

@ -354,33 +354,18 @@ dependencyVerification {
['org.signal:ringrtc-android:0.2.0',
'bce32dfe6c4fe107ccd4b9f155dcfaefe574e7740ef57190614b7e34c914f5f0'],
['org.signal:signal-metadata-android:0.1.0',
'e79ca9231ec07b05849bc048c643fe2cec48ee781ba5aa8165847a3c90178684'],
['org.signal:signal-metadata-java:0.1.0',
'f3faa23b7d9b5096d12979c35679d1e3b5e007522d8bef167a28e456f2a7c7d9'],
['org.threeten:threetenbp:1.3.6',
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
['org.whispersystems:curve25519-android:0.5.0',
'b502bcf83efe001f09a7a9efda6f0fa772c43ed5924e97816296ed3503caa092'],
['org.whispersystems:curve25519-java:0.5.0',
'0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d'],
['org.whispersystems:signal-protocol-android:2.8.1',
'9eff33fa6a541334f647906cb6f4a9bb26efa6988c7abdb985f3995a6d10aa48'],
['org.whispersystems:signal-protocol-java:2.8.1',
'b19db36839ab008fdccefc7f8c005f2ea43dc7c7298a209bc424e6f9b6d5617b'],
['org.whispersystems:signal-service-android:2.15.3',
'24e7a7760f14a261d44b8d76fe6004157f7e09d7898c88ddb501ceabea47b6f6'],
['org.whispersystems:signal-service-java:2.15.3',
'bc6d58924daf7c15e700164ce602e2647260c41820a43837f3f48f3d5be2e963'],
['pl.tajchert:waitingdots:0.1.0',
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],

View File

@ -63,6 +63,7 @@ class WitnessPlugin implements Plugin<Project> {
stringBuilder.append ' verify = [\n'
allArtifacts(project)
.findAll { dep -> !dep.id.componentIdentifier.displayName.startsWith('project :') }
.collect { dep -> "['$dep.moduleVersion.id.group:$dep.name:$dep.moduleVersion.id.version',\n '${calculateSha256(dep.file)}']" }
.sort()
.each {
@ -72,7 +73,7 @@ class WitnessPlugin implements Plugin<Project> {
stringBuilder.append " ]\n"
stringBuilder.append "}\n"
new File("witness-verifications.gradle").write(stringBuilder.toString())
project.file("witness-verifications.gradle").write(stringBuilder.toString())
}
}

1
libsignal/service/.gitignore vendored 100644
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,155 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
apply plugin: 'java-library'
apply plugin: 'com.google.protobuf'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'witness'
apply from: 'witness-verifications.gradle'
sourceCompatibility = 1.7
archivesBaseName = "signal-service-java"
version = lib_signal_service_version_number
group = lib_signal_service_group_info
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
api 'com.googlecode.libphonenumber:libphonenumber:8.10.7'
api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
api "org.signal:signal-metadata-java:${lib_signal_metadata_version}"
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
testImplementation 'junit:junit:3.8.2'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
}
dependencyVerification {
configuration = '(runtime|compile)Classpath'
}
tasks.whenTaskAdded { task ->
if (task.name.equals("lint")) {
task.enabled = false
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
sourceSets {
main.proto.srcDir 'protobuf'
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('sonatypeRepo') ? sonatypeRepo
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getRepositoryUsername() {
return hasProperty('whisperSonatypeUsername') ? whisperSonatypeUsername : ""
}
def getRepositoryPassword() {
return hasProperty('whisperSonatypePassword') ? whisperSonatypePassword : ""
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}
uploadArchives {
configuration = configurations.archives
repositories.mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
pom.project {
name 'signal-service-java'
packaging 'jar'
description 'Signal Service communication library for Java'
url 'https://github.com/WhisperSystems/libsignal-service-java'
scm {
url 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
connection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
developerConnection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
}
licenses {
license {
name 'GPLv3'
url 'https://www.gnu.org/licenses/gpl-3.0.txt'
distribution 'repo'
}
}
developers {
developer {
name 'Moxie Marlinspike'
}
}
}
}
}
task installArchives(type: Upload) {
description "Installs the artifacts to the local Maven repository."
configuration = configurations['archives']
repositories {
mavenDeployer {
repository url: "file://${System.properties['user.home']}/.m2/repository"
}
}
}
task packageJavadoc(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
}
task packageSources(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
artifacts {
archives packageJavadoc
archives packageSources
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "ProvisioningProtos";
message ProvisionEnvelope {
optional bytes publicKey = 1;
optional bytes body = 2; // Encrypted ProvisionMessage
}
message ProvisionMessage {
optional bytes identityKeyPublic = 1;
optional bytes identityKeyPrivate = 2;
optional string number = 3;
optional string uuid = 8;
optional string provisioningCode = 4;
optional string userAgent = 5;
optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 provisioningVersion = 9;
}
enum ProvisioningVersion {
option allow_alias = true;
INITIAL = 0;
TABLET_SUPPORT = 1;
CURRENT = 1;
}

View File

@ -0,0 +1,422 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "SignalServiceProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
}
optional Type type = 1;
optional string sourceE164 = 2;
optional string sourceUuid = 11;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 serverTimestamp = 10;
}
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
}
message CallMessage {
message Offer {
optional uint64 id = 1;
optional string description = 2;
}
message Answer {
optional uint64 id = 1;
optional string description = 2;
}
message IceUpdate {
optional uint64 id = 1;
optional string sdpMid = 2;
optional uint32 sdpMLineIndex = 3;
optional string sdp = 4;
}
message Busy {
optional uint64 id = 1;
}
message Hangup {
optional uint64 id = 1;
}
optional Offer offer = 1;
optional Answer answer = 2;
repeated IceUpdate iceUpdate = 3;
optional Hangup hangup = 4;
optional Busy busy = 5;
}
message DataMessage {
enum Flags {
END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4;
}
message Quote {
message QuotedAttachment {
optional string contentType = 1;
optional string fileName = 2;
optional AttachmentPointer thumbnail = 3;
}
optional uint64 id = 1;
optional string authorE164 = 2;
optional string authorUuid = 5;
optional string text = 3;
repeated QuotedAttachment attachments = 4;
}
message Contact {
message Name {
optional string givenName = 1;
optional string familyName = 2;
optional string prefix = 3;
optional string suffix = 4;
optional string middleName = 5;
optional string displayName = 6;
}
message Phone {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message Email {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message PostalAddress {
enum Type {
HOME = 1;
WORK = 2;
CUSTOM = 3;
}
optional Type type = 1;
optional string label = 2;
optional string street = 3;
optional string pobox = 4;
optional string neighborhood = 5;
optional string city = 6;
optional string region = 7;
optional string postcode = 8;
optional string country = 9;
}
message Avatar {
optional AttachmentPointer avatar = 1;
optional bool isProfile = 2;
}
optional Name name = 1;
repeated Phone number = 3;
repeated Email email = 4;
repeated PostalAddress address = 5;
optional Avatar avatar = 6;
optional string organization = 7;
}
message Preview {
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
}
message Sticker {
optional bytes packId = 1;
optional bytes packKey = 2;
optional uint32 stickerId = 3;
optional AttachmentPointer data = 4;
}
enum ProtocolVersion {
option allow_alias = true;
INITIAL = 0;
MESSAGE_TIMERS = 1;
VIEW_ONCE = 2;
VIEW_ONCE_VIDEO = 3;
CURRENT = 3;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional Sticker sticker = 11;
optional uint32 requiredProtocolVersion = 12;
optional bool isViewOnce = 14;
}
message NullMessage {
optional bytes padding = 1;
}
message ReceiptMessage {
enum Type {
DELIVERY = 0;
READ = 1;
}
optional Type type = 1;
repeated uint64 timestamp = 2;
}
message TypingMessage {
enum Action {
STARTED = 0;
STOPPED = 1;
}
optional uint64 timestamp = 1;
optional Action action = 2;
optional bytes groupId = 3;
}
message Verified {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string destinationE164 = 1;
optional string destinationUuid = 5;
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
}
message SyncMessage {
message Sent {
message UnidentifiedDeliveryStatus {
optional string destinationE164 = 1;
optional string destinationUuid = 3;
optional bool unidentified = 2;
}
optional string destinationE164 = 1;
optional string destinationUuid = 7;
optional uint64 timestamp = 2;
optional DataMessage message = 3;
optional uint64 expirationStartTimestamp = 4;
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
optional bool isRecipientUpdate = 6 [default = false];
}
message Contacts {
optional AttachmentPointer blob = 1;
optional bool complete = 2 [default = false];
}
message Groups {
optional AttachmentPointer blob = 1;
}
message Blocked {
repeated string numbers = 1;
repeated string uuids = 3;
repeated bytes groupIds = 2;
}
message Request {
enum Type {
UNKNOWN = 0;
CONTACTS = 1;
GROUPS = 2;
BLOCKED = 3;
CONFIGURATION = 4;
}
optional Type type = 1;
}
message Read {
optional string senderE164 = 1;
optional string senderUuid = 3;
optional uint64 timestamp = 2;
}
message Configuration {
optional bool readReceipts = 1;
optional bool unidentifiedDeliveryIndicators = 2;
optional bool typingIndicators = 3;
optional bool linkPreviews = 4;
optional uint32 provisioningVersion = 5;
}
message StickerPackOperation {
enum Type {
INSTALL = 0;
REMOVE = 1;
}
optional bytes packId = 1;
optional bytes packKey = 2;
optional Type type = 3;
}
message ViewOnceOpen {
optional string senderE164 = 1;
optional string senderUuid = 3;
optional uint64 timestamp = 2;
}
message FetchLatest {
enum Type {
UNKNOWN = 0;
LOCAL_PROFILE = 1;
STORAGE_MANIFEST = 2;
}
optional Type type = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
optional ViewOnceOpen viewOnceOpen = 11;
optional FetchLatest fetchLatest = 12;
}
message AttachmentPointer {
enum Flags {
VOICE_MESSAGE = 1;
}
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
optional uint32 size = 4;
optional bytes thumbnail = 5;
optional bytes digest = 6;
optional string fileName = 7;
optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
optional string caption = 11;
optional string blurHash = 12;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
REQUEST_INFO = 4;
}
message Member {
optional string uuid = 1;
optional string e164 = 2;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string membersE164 = 4;
repeated Member members = 6;
optional AttachmentPointer avatar = 5;
}
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
optional string number = 1;
optional string uuid = 9;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
}
message GroupDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
message Member {
optional string uuid = 1;
optional string e164 = 2;
}
optional bytes id = 1;
optional string name = 2;
repeated string membersE164 = 3;
repeated Member members = 9;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.sticker";
option java_outer_classname = "StickerProtos";
message Pack {
message Sticker {
optional uint32 id = 1;
optional string emoji = 2;
}
optional string title = 1;
optional string author = 2;
optional Sticker cover = 3;
repeated Sticker stickers = 4;
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.websocket";
option java_outer_classname = "WebSocketProtos";
message WebSocketRequestMessage {
optional string verb = 1;
optional string path = 2;
optional bytes body = 3;
repeated string headers = 5;
optional uint64 id = 4;
}
message WebSocketResponseMessage {
optional uint64 id = 1;
optional uint32 status = 2;
optional string message = 3;
repeated string headers = 5;
optional bytes body = 4;
}
message WebSocketMessage {
enum Type {
UNKNOWN = 0;
REQUEST = 1;
RESPONSE = 2;
}
optional Type type = 1;
optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3;
}

View File

@ -0,0 +1,487 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisioningVersion;
/**
* The main interface for creating, registering, and
* managing a Signal Service account.
*
* @author Moxie Marlinspike
*/
public class SignalServiceAccountManager {
private static final String TAG = SignalServiceAccountManager.class.getSimpleName();
private final PushServiceSocket pushServiceSocket;
private final UUID userUuid;
private final String userE164;
private final String userAgent;
/**
* Construct a SignalServiceAccountManager.
*
* @param configuration The URL for the Signal Service.
* @param uuid The Signal Service UUID.
* @param e164 The Signal Service phone number.
* @param password A Signal Service password.
* @param userAgent A string which identifies the client software.
*/
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
UUID uuid, String e164, String password,
String userAgent)
{
this(configuration, new StaticCredentialsProvider(uuid, e164, password, null), userAgent);
}
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
CredentialsProvider credentialsProvider,
String userAgent)
{
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, userAgent);
this.userUuid = credentialsProvider.getUuid();
this.userE164 = credentialsProvider.getE164();
this.userAgent = userAgent;
}
public byte[] getSenderCertificate() throws IOException {
return this.pushServiceSocket.getSenderCertificate();
}
public byte[] getSenderCertificateLegacy() throws IOException {
return this.pushServiceSocket.getSenderCertificateLegacy();
}
public void setPin(Optional<String> pin) throws IOException {
if (pin.isPresent()) {
this.pushServiceSocket.setPin(pin.get());
} else {
this.pushServiceSocket.removePin();
}
}
public UUID getOwnUuid() throws IOException {
return this.pushServiceSocket.getOwnUuid();
}
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
* @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister.
* @throws IOException
*/
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
} else {
this.pushServiceSocket.unregisterGcmId();
}
}
/**
* Request a push challenge. A number will be pushed to the GCM (FCM) id. This can then be used
* during SMS/call requests to bypass the CAPTCHA.
*
* @param gcmRegistrationId The GCM (FCM) id to use.
* @param e164number The number to associate it with.
* @throws IOException
*/
public void requestPushChallenge(String gcmRegistrationId, String e164number) throws IOException {
this.pushServiceSocket.requestPushChallenge(gcmRegistrationId, e164number);
}
/**
* Request an SMS verification code. On success, the server will send
* an SMS verification code to this Signal user.
*
* @param androidSmsRetrieverSupported
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
}
/**
* Request a Voice verification code. On success, the server will
* make a voice call to this Signal user.
*
* @param locale
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
}
/**
* Verify a Signal Service account with a received SMS or voice verification code.
*
* @param verificationCode The verification code received via SMS or Voice
* (see {@link #requestSmsVerificationCode} and
* {@link #requestVoiceVerificationCode}).
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
* concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @return The UUID of the user that was registered.
* @throws IOException
*/
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
fetchesMessages, pin,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess);
}
/**
* Refresh account attributes with server.
*
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
*
* @throws IOException
*/
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
}
/**
* Register an identity key, signed prekey, and list of one time prekeys
* with the server.
*
* @param identityKey The client's long-term identity keypair.
* @param signedPreKey The client's signed prekey.
* @param oneTimePreKeys The client's list of one-time prekeys.
*
* @throws IOException
*/
public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException
{
this.pushServiceSocket.registerPreKeys(identityKey, signedPreKey, oneTimePreKeys);
}
/**
* @return The server's count of currently available (eg. unused) prekeys for this user.
* @throws IOException
*/
public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys();
}
/**
* Set the client's signed prekey.
*
* @param signedPreKey The client's new signed prekey.
* @throws IOException
*/
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
}
/**
* @return The server's view of the client's current signed prekey.
* @throws IOException
*/
public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey();
}
/**
* Checks whether a contact is currently registered with the server.
*
* @param e164number The contact to check.
* @return An optional ContactTokenDetails, present if registered, absent if not.
* @throws IOException
*/
public Optional<ContactTokenDetails> getContact(String e164number) throws IOException {
String contactToken = createDirectoryServerToken(e164number, true);
ContactTokenDetails contactTokenDetails = this.pushServiceSocket.getContactTokenDetails(contactToken);
if (contactTokenDetails != null) {
contactTokenDetails.setNumber(e164number);
}
return Optional.fromNullable(contactTokenDetails);
}
/**
* Checks which contacts in a set are registered with the server.
*
* @param e164numbers The contacts to check.
* @return A list of ContactTokenDetails for the registered users.
* @throws IOException
*/
public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
throws IOException
{
Map<String, String> contactTokensMap = createDirectoryServerTokenMap(e164numbers);
List<ContactTokenDetails> activeTokens = this.pushServiceSocket.retrieveDirectory(contactTokensMap.keySet());
for (ContactTokenDetails activeToken : activeTokens) {
activeToken.setNumber(contactTokensMap.get(activeToken.getToken()));
}
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
ContactDiscoveryCipher cipher = new ContactDiscoveryCipher();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave);
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic());
Quote quote = new Quote(attestationResponse.first().getQuote());
byte[] requestId = cipher.getRequestId(keys, attestationResponse.first());
cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave);
cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote);
RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
List<String> addressBook = new LinkedList<>();
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave);
byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();
for (byte aData : data) {
String candidate = addressBookIterator.next();
if (aData != 0) results.add('+' + candidate);
}
return results;
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
public void reportContactDiscoveryServiceMatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result match failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceMismatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMismatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result mismatch failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceAttestationError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceAttestationError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery attestation error failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceUnexpectedError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery unexpected error failed. Ignoring.", e);
}
}
public String getNewDeviceVerificationCode() throws IOException {
return this.pushServiceSocket.getNewDeviceVerificationCode();
}
public void addDevice(String deviceIdentifier,
ECPublicKey deviceKey,
IdentityKeyPair identityKeyPair,
Optional<byte[]> profileKey,
String code)
throws InvalidKeyException, IOException
{
ProvisioningCipher cipher = new ProvisioningCipher(deviceKey);
ProvisionMessage.Builder message = ProvisionMessage.newBuilder()
.setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize()))
.setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize()))
.setProvisioningCode(code)
.setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE);
if (userE164 != null) {
message.setNumber(userE164);
}
if (userUuid != null) {
message.setUuid(userUuid.toString());
}
if (profileKey.isPresent()) {
message.setProfileKey(ByteString.copyFrom(profileKey.get()));
}
byte[] ciphertext = cipher.encrypt(message.build());
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
}
public List<DeviceInfo> getDevices() throws IOException {
return this.pushServiceSocket.getDevices();
}
public void removeDevice(long deviceId) throws IOException {
this.pushServiceSocket.removeDevice(deviceId);
}
public TurnServerInfo getTurnServerInfo() throws IOException {
return this.pushServiceSocket.getTurnServerInfo();
}
public void setProfileName(byte[] key, String name)
throws IOException
{
if (name == null) name = "";
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
this.pushServiceSocket.setProfileName(ciphertextName);
}
public void setProfileAvatar(byte[] key, StreamDetails avatar)
throws IOException
{
ProfileAvatarData profileAvatarData = null;
if (avatar != null) {
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
avatar.getContentType(),
new ProfileCipherOutputStreamFactory(key));
}
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
this.pushServiceSocket.setSoTimeoutMillis(soTimeoutMillis);
}
public void cancelInFlightRequests() {
this.pushServiceSocket.cancelInFlightRequests();
}
private String createDirectoryServerToken(String e164number, boolean urlSafe) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
String encoded = Base64.encodeBytesWithoutPadding(token);
if (urlSafe) return encoded.replace('+', '-').replace('/', '_');
else return encoded;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Map<String, String> createDirectoryServerTokenMap(Collection<String> e164numbers) {
Map<String,String> tokenMap = new HashMap<>(e164numbers.size());
for (String number : e164numbers) {
tokenMap.put(createDirectoryServerToken(number, false), number);
}
return tokenMap;
}
}

View File

@ -0,0 +1,263 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
/**
* A SignalServiceMessagePipe represents a dedicated connection
* to the Signal Service, which the server can push messages
* down through.
*/
public class SignalServiceMessagePipe {
private static final String TAG = SignalServiceMessagePipe.class.getName();
private final WebSocketConnection websocket;
private final Optional<CredentialsProvider> credentialsProvider;
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
this.websocket = websocket;
this.credentialsProvider = credentialsProvider;
this.websocket.connect();
}
/**
* A blocking call that reads a message off the pipe. When this
* call returns, the message has been acknowledged and will not
* be retransmitted.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @return A new message.
*
* @throws InvalidVersionException
* @throws IOException
* @throws TimeoutException
*/
public SignalServiceEnvelope read(long timeout, TimeUnit unit)
throws InvalidVersionException, IOException, TimeoutException
{
return read(timeout, unit, new NullMessagePipeCallback());
}
/**
* A blocking call that reads a message off the pipe (see {@link #read(long, java.util.concurrent.TimeUnit)}
*
* Unlike {@link #read(long, java.util.concurrent.TimeUnit)}, this method allows you
* to specify a callback that will be called before the received message is acknowledged.
* This allows you to write the received message to durable storage before acknowledging
* receipt of it to the server.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @param callback A callback that will be called before the message receipt is
* acknowledged to the server.
* @return The message read (same as the message sent through the callback).
* @throws TimeoutException
* @throws IOException
* @throws InvalidVersionException
*/
public SignalServiceEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback)
throws TimeoutException, IOException, InvalidVersionException
{
if (!credentialsProvider.isPresent()) {
throw new IllegalArgumentException("You can't read messages if you haven't specified credentials");
}
while (true) {
WebSocketRequestMessage request = websocket.readRequest(unit.toMillis(timeout));
WebSocketResponseMessage response = createWebSocketResponse(request);
boolean signalKeyEncrypted = isSignalKeyEncrypted(request);
try {
if (isSignalServiceEnvelope(request)) {
SignalServiceEnvelope envelope = new SignalServiceEnvelope(request.getBody().toByteArray(),
credentialsProvider.get().getSignalingKey(),
signalKeyEncrypted);
callback.onMessage(envelope);
return envelope;
}
} finally {
websocket.sendResponse(response);
}
}
}
public SendMessageResponse send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
try {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/json");
}};
if (unidentifiedAccess.isPresent()) {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
.setVerb("PUT")
.setPath(String.format("/v1/messages/%s", list.getDestination()))
.addAllHeaders(headers)
.setBody(ByteString.copyFrom(JsonUtil.toJson(list).getBytes()))
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
if (Util.isEmpty(response.second())) return new SendMessageResponse(false);
else return JsonUtil.fromJson(response.second(), SendMessageResponse.class);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
try {
List<String> headers = new LinkedList<>();
if (unidentifiedAccess.isPresent()) {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
.setVerb("GET")
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
.addAllHeaders(headers)
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError(nsae);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException {
try {
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(new SecureRandom().nextLong())
.setVerb("GET")
.setPath("/v2/attachments/form/upload")
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
/**
* Close this connection to the server.
*/
public void shutdown() {
websocket.disconnect();
}
private boolean isSignalServiceEnvelope(WebSocketRequestMessage message) {
return "PUT".equals(message.getVerb()) && "/api/v1/message".equals(message.getPath());
}
private boolean isSignalKeyEncrypted(WebSocketRequestMessage message) {
List<String> headers = message.getHeadersList();
if (headers == null || headers.isEmpty()) {
return true;
}
for (String header : headers) {
String[] parts = header.split(":");
if (parts.length == 2 && parts[0] != null && parts[0].trim().equalsIgnoreCase("X-Signal-Key")) {
if (parts[1] != null && parts[1].trim().equalsIgnoreCase("false")) {
return false;
}
}
}
return true;
}
private WebSocketResponseMessage createWebSocketResponse(WebSocketRequestMessage request) {
if (isSignalServiceEnvelope(request)) {
return WebSocketResponseMessage.newBuilder()
.setId(request.getId())
.setStatus(200)
.setMessage("OK")
.build();
} else {
return WebSocketResponseMessage.newBuilder()
.setId(request.getId())
.setStatus(400)
.setMessage("Unknown")
.build();
}
}
/**
* For receiving a callback when a new message has been
* received.
*/
public static interface MessagePipeCallback {
public void onMessage(SignalServiceEnvelope envelope);
}
private static class NullMessagePipeCallback implements MessagePipeCallback {
@Override
public void onMessage(SignalServiceEnvelope envelope) {}
}
}

View File

@ -0,0 +1,258 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity;
import org.whispersystems.signalservice.internal.sticker.StickerProtos;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
/**
* The primary interface for receiving Signal Service messages.
*
* @author Moxie Marlinspike
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class SignalServiceMessageReceiver {
private final PushServiceSocket socket;
private final SignalServiceConfiguration urls;
private final CredentialsProvider credentialsProvider;
private final String userAgent;
private final ConnectivityListener connectivityListener;
private final SleepTimer sleepTimer;
/**
* Construct a SignalServiceMessageReceiver.
*
* @param urls The URL of the Signal Service.
* @param uuid The Signal Service UUID.
* @param e164 The Signal Service phone number.
* @param password The Signal Service user password.
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
*/
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
UUID uuid, String e164, String password,
String signalingKey, String userAgent,
ConnectivityListener listener,
SleepTimer timer)
{
this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), userAgent, listener, timer);
}
/**
* Construct a SignalServiceMessageReceiver.
*
* @param urls The URL of the Signal Service.
* @param credentials The Signal Service user's credentials.
*/
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
CredentialsProvider credentials,
String userAgent,
ConnectivityListener listener,
SleepTimer timer)
{
this.urls = urls;
this.credentialsProvider = credentials;
this.socket = new PushServiceSocket(urls, credentials, userAgent);
this.userAgent = userAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
}
/**
* Retrieves a SignalServiceAttachment.
*
* @param pointer The {@link SignalServiceAttachmentPointer}
* received in a {@link SignalServiceDataMessage}.
* @param destination The download destination for this attachment.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes)
throws IOException, InvalidMessageException
{
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
{
return socket.retrieveProfile(address, unidentifiedAccess);
}
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
}
/**
* Retrieves a SignalServiceAttachment.
*
* @param pointer The {@link SignalServiceAttachmentPointer}
* received in a {@link SignalServiceDataMessage}.
* @param destination The download destination for this attachment.
* @param listener An optional listener (may be null) to receive callbacks on download progress.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
throws IOException, InvalidMessageException
{
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
throws IOException, InvalidMessageException
{
byte[] data = socket.retrieveSticker(packId, stickerId);
return AttachmentCipherInputStream.createForStickerData(data, packKey);
}
/**
* Retrieves a {@link SignalServiceStickerManifest}.
*
* @param packId The 16-byte packId that identifies the sticker pack.
* @param packKey The 32-byte packKey that decrypts the sticker pack.
* @return The {@link SignalServiceStickerManifest} representing the sticker pack.
* @throws IOException
* @throws InvalidMessageException
*/
public SignalServiceStickerManifest retrieveStickerManifest(byte[] packId, byte[] packKey)
throws IOException, InvalidMessageException
{
byte[] manifestBytes = socket.retrieveStickerManifest(packId);
InputStream cipherStream = AttachmentCipherInputStream.createForStickerData(manifestBytes, packKey);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Util.copy(cipherStream, outputStream);
StickerProtos.Pack pack = StickerProtos.Pack.parseFrom(outputStream.toByteArray());
List<SignalServiceStickerManifest.StickerInfo> stickers = new ArrayList<>(pack.getStickersCount());
SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji())
: null;
for (StickerProtos.Pack.Sticker sticker : pack.getStickersList()) {
stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji()));
}
return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers);
}
/**
* Creates a pipe for receiving SignalService messages.
*
* Callers must call {@link SignalServiceMessagePipe#shutdown()} when finished with the pipe.
*
* @return A SignalServiceMessagePipe for receiving Signal Service messages.
*/
public SignalServiceMessagePipe createMessagePipe() {
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
urls.getSignalServiceUrls()[0].getTrustStore(),
Optional.of(credentialsProvider), userAgent, connectivityListener,
sleepTimer);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
}
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
urls.getSignalServiceUrls()[0].getTrustStore(),
Optional.<CredentialsProvider>absent(), userAgent, connectivityListener,
sleepTimer);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
}
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
return retrieveMessages(new NullMessageReceivedCallback());
}
public List<SignalServiceEnvelope> retrieveMessages(MessageReceivedCallback callback)
throws IOException
{
List<SignalServiceEnvelope> results = new LinkedList<>();
List<SignalServiceEnvelopeEntity> entities = socket.getMessages();
for (SignalServiceEnvelopeEntity entity : entities) {
SignalServiceEnvelope envelope;
if (entity.hasSource() && entity.getSourceDevice() > 0) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(entity.getSourceUuid()), entity.getSourceE164());
envelope = new SignalServiceEnvelope(entity.getType(), Optional.of(address),
entity.getSourceDevice(), entity.getTimestamp(),
entity.getMessage(), entity.getContent(),
entity.getServerTimestamp(), entity.getServerUuid());
} else {
envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(),
entity.getMessage(), entity.getContent(),
entity.getServerTimestamp(), entity.getServerUuid());
}
callback.onMessage(envelope);
results.add(envelope);
if (envelope.hasUuid()) socket.acknowledgeMessage(envelope.getUuid());
else socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp());
}
return results;
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
socket.setSoTimeoutMillis(soTimeoutMillis);
}
public interface MessageReceivedCallback {
public void onMessage(SignalServiceEnvelope envelope);
}
public static class NullMessageReceivedCallback implements MessageReceivedCallback {
@Override
public void onMessage(SignalServiceEnvelope envelope) {}
}
}

View File

@ -0,0 +1,271 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.InvalidMacException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Class for streaming an encrypted push attachment off disk.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipherInputStream extends FilterInputStream {
private static final int BLOCK_SIZE = 16;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 32;
private Cipher cipher;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
throws InvalidMessageException, IOException
{
try {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
if (digest == null) {
throw new InvalidMacException("Missing digest!");
}
try (FileInputStream fin = new FileInputStream(file)) {
verifyMac(fin, file.length(), mac, digest);
}
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
if (plaintextLength != 0) {
inputStream = new ContentLengthInputStream(inputStream, plaintextLength);
}
return inputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
public static InputStream createForStickerData(byte[] data, byte[] packKey)
throws InvalidMessageException, IOException
{
try {
byte[] combinedKeyMaterial = new HKDFv3().deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (data.length <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
try (InputStream inputStream = new ByteArrayInputStream(data)) {
verifyMac(inputStream, data.length, mac, null);
}
return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize)
throws IOException
{
super(inputStream);
try {
byte[] iv = new byte[BLOCK_SIZE];
readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
this.done = false;
this.totalRead = 0;
this.totalDataSize = totalDataSize;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
else if (!done) return readFinal(buffer, offset, length);
else return -1;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public long skip(long byteCount) throws IOException {
long skipped = 0L;
while (skipped < byteCount) {
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
int read = read(buf);
skipped += read;
}
return skipped;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException | BadPaddingException | ShortBufferException e) {
throw new IOException(e);
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private static void verifyMac(InputStream inputStream, long length, Mac mac, byte[] theirDigest)
throws InvalidMacException
{
try {
MessageDigest digest = MessageDigest.getInstance("SHA256");
int remainingData = Util.toIntExact(length) - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = inputStream.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
digest.update(buffer, 0, read);
remainingData -= read;
}
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(inputStream, theirMac);
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
}
byte[] ourDigest = digest.digest(theirMac);
if (theirDigest != null && !MessageDigest.isEqual(ourDigest, theirDigest)) {
throw new InvalidMacException("Digest doesn't match!");
}
} catch (IOException | ArithmeticException e1) {
throw new InvalidMacException(e1);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private void readFully(byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = super.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class AttachmentCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
private final Mac mac;
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
OutputStream outputStream)
throws IOException
{
super(outputStream);
try {
this.cipher = initializeCipher();
this.mac = initializeMac();
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
mac.update(cipher.getIV());
super.write(cipher.getIV());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] ciphertext = cipher.update(buffer, offset, length);
if (ciphertext != null) {
mac.update(ciphertext);
super.write(ciphertext);
}
}
@Override
public void write(int b) {
throw new AssertionError("NYI");
}
@Override
public void flush() throws IOException {
try {
byte[] ciphertext = cipher.doFinal();
byte[] auth = mac.doFinal(ciphertext);
super.write(ciphertext);
super.write(auth);
super.flush();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public static long getCiphertextLength(long plaintextLength) {
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
}
private Mac initializeMac() {
try {
return Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,55 @@
package org.whispersystems.signalservice.api.crypto;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public abstract class DigestingOutputStream extends FilterOutputStream {
private final MessageDigest runningDigest;
private byte[] digest;
public DigestingOutputStream(OutputStream outputStream) {
super(outputStream);
try {
this.runningDigest = MessageDigest.getInstance("SHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
runningDigest.update(buffer, 0, buffer.length);
out.write(buffer, 0, buffer.length);
}
public void write(byte[] buffer, int offset, int length) throws IOException {
runningDigest.update(buffer, offset, length);
out.write(buffer, offset, length);
}
public void write(int b) throws IOException {
runningDigest.update((byte)b);
out.write(b);
}
public void flush() throws IOException {
digest = runningDigest.digest();
out.flush();
}
public void close() throws IOException {
out.close();
}
public byte[] getTransmittedDigest() {
return digest;
}
}

View File

@ -0,0 +1,11 @@
package org.whispersystems.signalservice.api.crypto;
public class InvalidCiphertextException extends Exception {
public InvalidCiphertextException(Exception nested) {
super(nested);
}
public InvalidCiphertextException(String s) {
super(s);
}
}

View File

@ -0,0 +1,101 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipher {
public static final int NAME_PADDED_LENGTH = 26;
private final byte[] key;
public ProfileCipher(byte[] key) {
this.key = key;
}
public byte[] encryptName(byte[] input, int paddedLength) {
try {
byte[] inputPadded = new byte[paddedLength];
if (input.length > inputPadded.length) {
throw new IllegalArgumentException("Input is too long: " + new String(input));
}
System.arraycopy(input, 0, inputPadded, 0, input.length);
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decryptName(byte[] input) throws InvalidCiphertextException {
try {
if (input.length < 12 + 16 + 1) {
throw new InvalidCiphertextException("Too short: " + input.length);
}
byte[] nonce = new byte[12];
System.arraycopy(input, 0, nonce, 0, nonce.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
int plaintextLength = 0;
for (int i=paddedPlaintext.length-1;i>=0;i--) {
if (paddedPlaintext[i] != (byte)0x00) {
plaintextLength = i + 1;
break;
}
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(paddedPlaintext, 0, plaintext, 0, plaintextLength);
return plaintext;
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
public boolean verifyUnidentifiedAccess(byte[] theirUnidentifiedAccessVerifier) {
try {
if (theirUnidentifiedAccessVerifier == null || theirUnidentifiedAccessVerifier.length == 0) return false;
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(key);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
byte[] ourUnidentifiedAccessVerifier = mac.doFinal(new byte[32]);
return MessageDigest.isEqual(theirUnidentifiedAccessVerifier, ourUnidentifiedAccessVerifier);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,83 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipherInputStream extends FilterInputStream {
private final Cipher cipher;
private boolean finished = false;
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
super(in);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = new byte[12];
Util.readFully(in, nonce);
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public int read() {
throw new AssertionError("Not supported!");
}
@Override
public int read(byte[] input) throws IOException {
return read(input, 0, input.length);
}
@Override
public int read(byte[] output, int outputOffset, int outputLength) throws IOException {
if (finished) return -1;
try {
byte[] ciphertext = new byte[outputLength / 2];
int read = in.read(ciphertext, 0, ciphertext.length);
if (read == -1) {
if (cipher.getOutputSize(0) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
}
finished = true;
return cipher.doFinal(output, outputOffset);
} else {
if (cipher.getOutputSize(read) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
}
return cipher.update(ciphertext, 0, read, output, outputOffset);
}
} catch (IllegalBlockSizeException | ShortBufferException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new IOException(e);
}
}
}

View File

@ -0,0 +1,78 @@
package org.whispersystems.signalservice.api.crypto;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
public ProfileCipherOutputStream(OutputStream out, byte[] key) throws IOException {
super(out);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = generateNonce();
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
super.write(nonce, 0, nonce.length);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] output = cipher.update(buffer, offset, length);
super.write(output);
}
@Override
public void write(int b) throws IOException {
byte[] input = new byte[1];
input[0] = (byte)b;
byte[] output = cipher.update(input);
super.write(output);
}
@Override
public void flush() throws IOException {
try {
byte[] output = cipher.doFinal();
super.write(output);
super.flush();
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
private byte[] generateNonce() {
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
return nonce;
}
public static long getCiphertextLength(long plaintextLength) {
return 12 + 16 + plaintextLength;
}
}

View File

@ -0,0 +1,841 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SealedSessionCipher;
import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult;
import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.protocol.SignalMessage;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Sticker;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.util.Base64;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext.Type.DELIVER;
/**
* This is used to decrypt received {@link SignalServiceEnvelope}s.
*
* @author Moxie Marlinspike
*/
public class SignalServiceCipher {
@SuppressWarnings("unused")
private static final String TAG = SignalServiceCipher.class.getSimpleName();
private final SignalProtocolStore signalProtocolStore;
private final SignalServiceAddress localAddress;
private final CertificateValidator certificateValidator;
public SignalServiceCipher(SignalServiceAddress localAddress,
SignalProtocolStore signalProtocolStore,
CertificateValidator certificateValidator)
{
this.signalProtocolStore = signalProtocolStore;
this.localAddress = localAddress;
this.certificateValidator = certificateValidator;
}
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
byte[] unpaddedMessage)
throws UntrustedIdentityException, InvalidKeyException
{
if (unidentifiedAccess.isPresent()) {
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
} else {
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
String body = Base64.encodeBytes(message.serialize());
int type;
switch (message.getType()) {
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
default: throw new AssertionError("Bad type: " + message.getType());
}
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
}
}
/**
* Decrypt a received {@link SignalServiceEnvelope}
*
* @param envelope The received SignalServiceEnvelope
*
* @return a decrypted SignalServiceContent
*/
public SignalServiceContent decrypt(SignalServiceEnvelope envelope)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolInvalidKeyIdException, ProtocolLegacyMessageException,
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
SelfSendException, UnsupportedDataMessageException
{
try {
if (envelope.hasLegacyMessage()) {
Plaintext plaintext = decrypt(envelope, envelope.getLegacyMessage());
DataMessage message = DataMessage.parseFrom(plaintext.getData());
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (envelope.hasContent()) {
Plaintext plaintext = decrypt(envelope, envelope.getContent());
Content message = Content.parseFrom(plaintext.getData());
if (message.hasDataMessage()) {
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message.getDataMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasSyncMessage() && localAddress.matches(plaintext.getMetadata().getSender())) {
return new SignalServiceContent(createSynchronizeMessage(plaintext.getMetadata(), message.getSyncMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasCallMessage()) {
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasReceiptMessage()) {
return new SignalServiceContent(createReceiptMessage(plaintext.getMetadata(), message.getReceiptMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasTypingMessage()) {
return new SignalServiceContent(createTypingMessage(plaintext.getMetadata(), message.getTypingMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
false);
}
}
return null;
} catch (InvalidProtocolBufferException e) {
throw new InvalidMetadataMessageException(e);
}
}
private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException,
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
ProtocolInvalidKeyIdException, ProtocolNoSessionException,
SelfSendException
{
try {
byte[] paddedMessage;
Metadata metadata;
int sessionVersion;
if (!envelope.hasSource() && !envelope.isUnidentifiedSender()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Non-UD envelope is missing a source!"), null, 0);
}
if (envelope.isPreKeySignalMessage()) {
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isSignalMessage()) {
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isUnidentifiedSender()) {
SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp());
SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid().orNull()), result.getSenderE164());
SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId());
paddedMessage = result.getPaddedMessage();
metadata = new Metadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true);
sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress);
} else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionVersion);
byte[] data = transportDetails.getStrippedPaddingMessageBody(paddedMessage);
return new Plaintext(metadata, data);
} catch (DuplicateMessageException e) {
throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (LegacyMessageException e) {
throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidMessageException e) {
throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidKeyIdException e) {
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (UntrustedIdentityException e) {
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidVersionException e) {
throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (NoSessionException e) {
throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
}
}
private static SignalProtocolAddress getPreferredProtocolAddress(SignalProtocolStore store, SignalServiceAddress address, int sourceDevice) {
SignalProtocolAddress uuidAddress = address.getUuid().isPresent() ? new SignalProtocolAddress(address.getUuid().get().toString(), sourceDevice) : null;
SignalProtocolAddress e164Address = address.getNumber().isPresent() ? new SignalProtocolAddress(address.getNumber().get(), sourceDevice) : null;
if (uuidAddress != null && store.containsSession(uuidAddress)) {
return uuidAddress;
} else if (e164Address != null && store.containsSession(e164Address)) {
return e164Address;
} else {
return new SignalProtocolAddress(address.getIdentifier(), sourceDevice);
}
}
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content)
throws ProtocolInvalidMessageException, UnsupportedDataMessageException
{
SignalServiceGroup groupInfo = createGroupInfo(content);
List<SignalServiceAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE ) != 0);
boolean expirationUpdate = ((content.getFlags() & DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
boolean profileKeyUpdate = ((content.getFlags() & DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
SignalServiceDataMessage.Quote quote = createQuote(content);
List<SharedContact> sharedContacts = createSharedContacts(content);
List<Preview> previews = createPreviews(content);
Sticker sticker = createSticker(content);
if (content.getRequiredProtocolVersion() > DataMessage.ProtocolVersion.CURRENT.getNumber()) {
throw new UnsupportedDataMessageException(DataMessage.ProtocolVersion.CURRENT.getNumber(),
content.getRequiredProtocolVersion(),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice(),
Optional.fromNullable(groupInfo));
}
for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(createAttachmentPointer(pointer));
}
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice());
}
return new SignalServiceDataMessage(metadata.getTimestamp(),
groupInfo,
attachments,
content.getBody(),
endSession,
content.getExpireTimer(),
expirationUpdate,
content.hasProfileKey() ? content.getProfileKey().toByteArray() : null,
profileKeyUpdate,
quote,
sharedContacts,
previews,
sticker,
content.getIsViewOnce());
}
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
{
if (content.hasSent()) {
SyncMessage.Sent sentContent = content.getSent();
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164())
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.<SignalServiceAddress>absent();
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0);
}
for (SyncMessage.Sent.UnidentifiedDeliveryStatus status : sentContent.getUnidentifiedStatusList()) {
if (SignalServiceAddress.isValidAddress(status.getDestinationUuid(), status.getDestinationE164())) {
SignalServiceAddress recipient = new SignalServiceAddress(UuidUtil.parseOrNull(status.getDestinationUuid()), status.getDestinationE164());
unidentifiedStatuses.put(recipient, status.getUnidentified());
} else {
Log.w(TAG, "Encountered an invalid UnidentifiedDeliveryStatus in a SentTranscript! Ignoring.");
}
}
return SignalServiceSyncMessage.forSentTranscript(new SentTranscriptMessage(address,
sentContent.getTimestamp(),
dataMessage,
sentContent.getExpirationStartTimestamp(),
unidentifiedStatuses,
sentContent.getIsRecipientUpdate()));
}
if (content.hasRequest()) {
return SignalServiceSyncMessage.forRequest(new RequestMessage(content.getRequest()));
}
if (content.getReadList().size() > 0) {
List<ReadMessage> readMessages = new LinkedList<>();
for (SyncMessage.Read read : content.getReadList()) {
if (SignalServiceAddress.isValidAddress(read.getSenderUuid(), read.getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(read.getSenderUuid()), read.getSenderE164());
readMessages.add(new ReadMessage(address, read.getTimestamp()));
} else {
Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring.");
}
}
return SignalServiceSyncMessage.forRead(readMessages);
}
if (content.hasViewOnceOpen()) {
if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164());
ViewOnceOpenMessage timerRead = new ViewOnceOpenMessage(address, content.getViewOnceOpen().getTimestamp());
return SignalServiceSyncMessage.forViewOnceOpen(timerRead);
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("ViewOnceOpen message has no sender!"), null, 0);
}
}
if (content.hasVerified()) {
if (SignalServiceAddress.isValidAddress(content.getVerified().getDestinationUuid(), content.getVerified().getDestinationE164())) {
try {
Verified verified = content.getVerified();
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(verified.getDestinationUuid()), verified.getDestinationE164());
IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0);
VerifiedState verifiedState;
if (verified.getState() == Verified.State.DEFAULT) {
verifiedState = VerifiedState.DEFAULT;
} else if (verified.getState() == Verified.State.VERIFIED) {
verifiedState = VerifiedState.VERIFIED;
} else if (verified.getState() == Verified.State.UNVERIFIED) {
verifiedState = VerifiedState.UNVERIFIED;
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Unknown state: " + verified.getState().getNumber()),
metadata.getSender().getIdentifier(), metadata.getSenderDevice());
}
return SignalServiceSyncMessage.forVerified(new VerifiedMessage(destination, identityKey, verifiedState, System.currentTimeMillis()));
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice());
}
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Verified message has no sender!"), null, 0);
}
}
if (content.getStickerPackOperationList().size() > 0) {
List<StickerPackOperationMessage> operations = new LinkedList<>();
for (SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) {
byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null;
byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null;
StickerPackOperationMessage.Type type = null;
if (operation.hasType()) {
switch (operation.getType()) {
case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break;
case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break;
}
}
operations.add(new StickerPackOperationMessage(packId, packKey, type));
}
return SignalServiceSyncMessage.forStickerPackOperations(operations);
}
if (content.hasBlocked()) {
List<String> numbers = content.getBlocked().getNumbersList();
List<String> uuids = content.getBlocked().getUuidsList();
List<SignalServiceAddress> addresses = new ArrayList<>(numbers.size() + uuids.size());
List<byte[]> groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size());
for (String e164 : numbers) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(null, e164);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (String uuid : uuids) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(uuid, null);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (ByteString groupId : content.getBlocked().getGroupIdsList()) {
groupIds.add(groupId.toByteArray());
}
return SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds));
}
if (content.hasConfiguration()) {
Boolean readReceipts = content.getConfiguration().hasReadReceipts() ? content.getConfiguration().getReadReceipts() : null;
Boolean unidentifiedDeliveryIndicators = content.getConfiguration().hasUnidentifiedDeliveryIndicators() ? content.getConfiguration().getUnidentifiedDeliveryIndicators() : null;
Boolean typingIndicators = content.getConfiguration().hasTypingIndicators() ? content.getConfiguration().getTypingIndicators() : null;
Boolean linkPreviews = content.getConfiguration().hasLinkPreviews() ? content.getConfiguration().getLinkPreviews() : null;
return SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.fromNullable(readReceipts),
Optional.fromNullable(unidentifiedDeliveryIndicators),
Optional.fromNullable(typingIndicators),
Optional.fromNullable(linkPreviews)));
}
if (content.hasFetchLatest() && content.getFetchLatest().hasType()) {
switch (content.getFetchLatest().getType()) {
case LOCAL_PROFILE: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE);
case STORAGE_MANIFEST: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST);
}
}
return SignalServiceSyncMessage.empty();
}
private SignalServiceCallMessage createCallMessage(CallMessage content) {
if (content.hasOffer()) {
CallMessage.Offer offerContent = content.getOffer();
return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription()));
} else if (content.hasAnswer()) {
CallMessage.Answer answerContent = content.getAnswer();
return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription()));
} else if (content.getIceUpdateCount() > 0) {
List<IceUpdateMessage> iceUpdates = new LinkedList<>();
for (CallMessage.IceUpdate iceUpdate : content.getIceUpdateList()) {
iceUpdates.add(new IceUpdateMessage(iceUpdate.getId(), iceUpdate.getSdpMid(), iceUpdate.getSdpMLineIndex(), iceUpdate.getSdp()));
}
return SignalServiceCallMessage.forIceUpdates(iceUpdates);
} else if (content.hasHangup()) {
CallMessage.Hangup hangup = content.getHangup();
return SignalServiceCallMessage.forHangup(new HangupMessage(hangup.getId()));
} else if (content.hasBusy()) {
CallMessage.Busy busy = content.getBusy();
return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId()));
}
return SignalServiceCallMessage.empty();
}
private SignalServiceReceiptMessage createReceiptMessage(Metadata metadata, ReceiptMessage content) {
SignalServiceReceiptMessage.Type type;
if (content.getType() == ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY;
else if (content.getType() == ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ;
else type = SignalServiceReceiptMessage.Type.UNKNOWN;
return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp());
}
private SignalServiceTypingMessage createTypingMessage(Metadata metadata, TypingMessage content) throws ProtocolInvalidMessageException {
SignalServiceTypingMessage.Action action;
if (content.getAction() == TypingMessage.Action.STARTED) action = SignalServiceTypingMessage.Action.STARTED;
else if (content.getAction() == TypingMessage.Action.STOPPED) action = SignalServiceTypingMessage.Action.STOPPED;
else action = SignalServiceTypingMessage.Action.UNKNOWN;
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice());
}
return new SignalServiceTypingMessage(action, content.getTimestamp(),
content.hasGroupId() ? Optional.of(content.getGroupId().toByteArray()) :
Optional.<byte[]>absent());
}
private SignalServiceDataMessage.Quote createQuote(DataMessage content) {
if (!content.hasQuote()) return null;
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = new LinkedList<>();
for (DataMessage.Quote.QuotedAttachment attachment : content.getQuote().getAttachmentsList()) {
attachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
attachment.getFileName(),
attachment.hasThumbnail() ? createAttachmentPointer(attachment.getThumbnail()) : null));
}
if (SignalServiceAddress.isValidAddress(content.getQuote().getAuthorUuid(), content.getQuote().getAuthorE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getQuote().getAuthorUuid()), content.getQuote().getAuthorE164());
return new SignalServiceDataMessage.Quote(content.getQuote().getId(),
address,
content.getQuote().getText(),
attachments);
} else {
Log.w(TAG, "Quote was missing an author! Returning null.");
return null;
}
}
private List<Preview> createPreviews(DataMessage content) {
if (content.getPreviewCount() <= 0) return null;
List<Preview> results = new LinkedList<>();
for (DataMessage.Preview preview : content.getPreviewList()) {
SignalServiceAttachment attachment = null;
if (preview.hasImage()) {
attachment = createAttachmentPointer(preview.getImage());
}
results.add(new Preview(preview.getUrl(),
preview.getTitle(),
Optional.fromNullable(attachment)));
}
return results;
}
private Sticker createSticker(DataMessage content) {
if (!content.hasSticker() ||
!content.getSticker().hasPackId() ||
!content.getSticker().hasPackKey() ||
!content.getSticker().hasStickerId() ||
!content.getSticker().hasData())
{
return null;
}
DataMessage.Sticker sticker = content.getSticker();
return new Sticker(sticker.getPackId().toByteArray(),
sticker.getPackKey().toByteArray(),
sticker.getStickerId(),
createAttachmentPointer(sticker.getData()));
}
private List<SharedContact> createSharedContacts(DataMessage content) {
if (content.getContactCount() <= 0) return null;
List<SharedContact> results = new LinkedList<>();
for (DataMessage.Contact contact : content.getContactList()) {
SharedContact.Builder builder = SharedContact.newBuilder()
.setName(SharedContact.Name.newBuilder()
.setDisplay(contact.getName().getDisplayName())
.setFamily(contact.getName().getFamilyName())
.setGiven(contact.getName().getGivenName())
.setMiddle(contact.getName().getMiddleName())
.setPrefix(contact.getName().getPrefix())
.setSuffix(contact.getName().getSuffix())
.build());
if (contact.getAddressCount() > 0) {
for (DataMessage.Contact.PostalAddress address : contact.getAddressList()) {
SharedContact.PostalAddress.Type type = SharedContact.PostalAddress.Type.HOME;
switch (address.getType()) {
case WORK: type = SharedContact.PostalAddress.Type.WORK; break;
case HOME: type = SharedContact.PostalAddress.Type.HOME; break;
case CUSTOM: type = SharedContact.PostalAddress.Type.CUSTOM; break;
}
builder.withAddress(SharedContact.PostalAddress.newBuilder()
.setCity(address.getCity())
.setCountry(address.getCountry())
.setLabel(address.getLabel())
.setNeighborhood(address.getNeighborhood())
.setPobox(address.getPobox())
.setPostcode(address.getPostcode())
.setRegion(address.getRegion())
.setStreet(address.getStreet())
.setType(type)
.build());
}
}
if (contact.getNumberCount() > 0) {
for (DataMessage.Contact.Phone phone : contact.getNumberList()) {
SharedContact.Phone.Type type = SharedContact.Phone.Type.HOME;
switch (phone.getType()) {
case HOME: type = SharedContact.Phone.Type.HOME; break;
case WORK: type = SharedContact.Phone.Type.WORK; break;
case MOBILE: type = SharedContact.Phone.Type.MOBILE; break;
case CUSTOM: type = SharedContact.Phone.Type.CUSTOM; break;
}
builder.withPhone(SharedContact.Phone.newBuilder()
.setLabel(phone.getLabel())
.setType(type)
.setValue(phone.getValue())
.build());
}
}
if (contact.getEmailCount() > 0) {
for (DataMessage.Contact.Email email : contact.getEmailList()) {
SharedContact.Email.Type type = SharedContact.Email.Type.HOME;
switch (email.getType()) {
case HOME: type = SharedContact.Email.Type.HOME; break;
case WORK: type = SharedContact.Email.Type.WORK; break;
case MOBILE: type = SharedContact.Email.Type.MOBILE; break;
case CUSTOM: type = SharedContact.Email.Type.CUSTOM; break;
}
builder.withEmail(SharedContact.Email.newBuilder()
.setLabel(email.getLabel())
.setType(type)
.setValue(email.getValue())
.build());
}
}
if (contact.hasAvatar()) {
builder.setAvatar(SharedContact.Avatar.newBuilder()
.withAttachment(createAttachmentPointer(contact.getAvatar().getAvatar()))
.withProfileFlag(contact.getAvatar().getIsProfile())
.build());
}
if (contact.hasOrganization()) {
builder.withOrganization(contact.getOrganization());
}
results.add(builder.build());
}
return results;
}
private SignalServiceAttachmentPointer createAttachmentPointer(AttachmentPointer pointer) {
return new SignalServiceAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
pointer.getWidth(), pointer.getHeight(),
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
(pointer.getFlags() & AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0,
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent());
}
private SignalServiceGroup createGroupInfo(DataMessage content) throws ProtocolInvalidMessageException {
if (!content.hasGroup()) return null;
SignalServiceGroup.Type type;
switch (content.getGroup().getType()) {
case DELIVER: type = SignalServiceGroup.Type.DELIVER; break;
case UPDATE: type = SignalServiceGroup.Type.UPDATE; break;
case QUIT: type = SignalServiceGroup.Type.QUIT; break;
case REQUEST_INFO: type = SignalServiceGroup.Type.REQUEST_INFO; break;
default: type = SignalServiceGroup.Type.UNKNOWN; break;
}
if (content.getGroup().getType() != DELIVER) {
String name = null;
List<SignalServiceAddress> members = null;
SignalServiceAttachmentPointer avatar = null;
if (content.getGroup().hasName()) {
name = content.getGroup().getName();
}
if (content.getGroup().getMembersCount() > 0) {
members = new ArrayList<>(content.getGroup().getMembersCount());
for (SignalServiceProtos.GroupContext.Member member : content.getGroup().getMembersList()) {
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
members.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("GroupContext.Member had no address!"), null, 0);
}
}
} else if (content.getGroup().getMembersE164Count() > 0) {
members = new ArrayList<>(content.getGroup().getMembersE164Count());
for (String member : content.getGroup().getMembersE164List()) {
members.add(new SignalServiceAddress(null, member));
}
}
if (content.getGroup().hasAvatar()) {
AttachmentPointer pointer = content.getGroup().getAvatar();
avatar = new SignalServiceAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
Optional.of(pointer.getSize()),
Optional.<byte[]>absent(), 0, 0,
Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null),
Optional.<String>absent(),
false,
Optional.<String>absent(),
Optional.<String>absent());
}
return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
}
return new SignalServiceGroup(content.getGroup().getId().toByteArray());
}
private static class Metadata {
private final SignalServiceAddress sender;
private final int senderDevice;
private final long timestamp;
private final boolean needsReceipt;
private Metadata(SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
}
public SignalServiceAddress getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
public long getTimestamp() {
return timestamp;
}
public boolean isNeedsReceipt() {
return needsReceipt;
}
}
private static class Plaintext {
private final Metadata metadata;
private final byte[] data;
private Plaintext(Metadata metadata, byte[] data) {
this.metadata = metadata;
this.data = data;
}
public Metadata getMetadata() {
return metadata;
}
public byte[] getData() {
return data;
}
}
}

View File

@ -0,0 +1,54 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class UnidentifiedAccess {
private final byte[] unidentifiedAccessKey;
private final SenderCertificate unidentifiedCertificate;
public UnidentifiedAccess(byte[] unidentifiedAccessKey, byte[] unidentifiedCertificate)
throws InvalidCertificateException
{
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.unidentifiedCertificate = new SenderCertificate(unidentifiedCertificate);
}
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}
public SenderCertificate getUnidentifiedCertificate() {
return unidentifiedCertificate;
}
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
try {
byte[] nonce = new byte[12];
byte[] input = new byte[16];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertext = cipher.doFinal(input);
return ByteUtil.trim(ciphertext, 16);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.util.guava.Optional;
public class UnidentifiedAccessPair {
private final Optional<UnidentifiedAccess> targetUnidentifiedAccess;
private final Optional<UnidentifiedAccess> selfUnidentifiedAccess;
public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) {
this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess);
this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess);
}
public Optional<UnidentifiedAccess> getTargetUnidentifiedAccess() {
return targetUnidentifiedAccess;
}
public Optional<UnidentifiedAccess> getSelfUnidentifiedAccess() {
return selfUnidentifiedAccess;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String identifier;
public UntrustedIdentityException(String s, String identifier, IdentityKey identityKey) {
super(s);
this.identifier = identifier;
this.identityKey = identityKey;
}
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getIdentifier(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getIdentifier() {
return identifier;
}
}

View File

@ -0,0 +1,91 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SendMessageResult {
private final SignalServiceAddress address;
private final Success success;
private final boolean networkFailure;
private final boolean unregisteredFailure;
private final IdentityFailure identityFailure;
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) {
return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null);
}
public static SendMessageResult networkFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, true, false, null);
}
public static SendMessageResult unregisteredFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, false, true, null);
}
public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) {
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey));
}
public SignalServiceAddress getAddress() {
return address;
}
public Success getSuccess() {
return success;
}
public boolean isNetworkFailure() {
return networkFailure;
}
public boolean isUnregisteredFailure() {
return unregisteredFailure;
}
public IdentityFailure getIdentityFailure() {
return identityFailure;
}
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure) {
this.address = address;
this.success = success;
this.networkFailure = networkFailure;
this.unregisteredFailure = unregisteredFailure;
this.identityFailure = identityFailure;
}
public static class Success {
private final boolean unidentified;
private final boolean needsSync;
private Success(boolean unidentified, boolean needsSync) {
this.unidentified = unidentified;
this.needsSync = needsSync;
}
public boolean isUnidentified() {
return unidentified;
}
public boolean isNeedsSync() {
return needsSync;
}
}
public static class IdentityFailure {
private final IdentityKey identityKey;
private IdentityFailure(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.InputStream;
public abstract class SignalServiceAttachment {
private final String contentType;
protected SignalServiceAttachment(String contentType) {
this.contentType = contentType;
}
public String getContentType() {
return contentType;
}
public abstract boolean isStream();
public abstract boolean isPointer();
public SignalServiceAttachmentStream asStream() {
return (SignalServiceAttachmentStream)this;
}
public SignalServiceAttachmentPointer asPointer() {
return (SignalServiceAttachmentPointer)this;
}
public static Builder newStreamBuilder() {
return new Builder();
}
public static class Builder {
private InputStream inputStream;
private String contentType;
private String fileName;
private long length;
private ProgressListener listener;
private boolean voiceNote;
private int width;
private int height;
private String caption;
private String blurHash;
private Builder() {}
public Builder withStream(InputStream inputStream) {
this.inputStream = inputStream;
return this;
}
public Builder withContentType(String contentType) {
this.contentType = contentType;
return this;
}
public Builder withLength(long length) {
this.length = length;
return this;
}
public Builder withFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Builder withListener(ProgressListener listener) {
this.listener = listener;
return this;
}
public Builder withVoiceNote(boolean voiceNote) {
this.voiceNote = voiceNote;
return this;
}
public Builder withWidth(int width) {
this.width = width;
return this;
}
public Builder withHeight(int height) {
this.height = height;
return this;
}
public Builder withCaption(String caption) {
this.caption = caption;
return this;
}
public Builder withBlurHash(String blurHash) {
this.blurHash = blurHash;
return this;
}
public SignalServiceAttachmentStream build() {
if (inputStream == null) throw new IllegalArgumentException("Must specify stream!");
if (contentType == null) throw new IllegalArgumentException("No content type specified!");
if (length == 0) throw new IllegalArgumentException("No length specified!");
return new SignalServiceAttachmentStream(inputStream,
contentType,
length,
Optional.fromNullable(fileName),
voiceNote,
Optional.<byte[]>absent(),
width,
height,
Optional.fromNullable(caption),
Optional.fromNullable(blurHash),
listener);
}
}
/**
* An interface to receive progress information on upload/download of
* an attachment.
*/
public interface ProgressListener {
/**
* Called on a progress change event.
*
* @param total The total amount to transmit/receive in bytes.
* @param progress The amount that has been transmitted/received in bytes thus far
*/
public void onAttachmentProgress(long total, long progress);
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
/**
* Represents a received SignalServiceAttachment "handle." This
* is a pointer to the actual attachment content, which needs to be
* retrieved using {@link SignalServiceMessageReceiver#retrieveAttachment(SignalServiceAttachmentPointer, java.io.File, int)}
*
* @author Moxie Marlinspike
*/
public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
private final long id;
private final byte[] key;
private final Optional<Integer> size;
private final Optional<byte[]> preview;
private final Optional<byte[]> digest;
private final Optional<String> fileName;
private final boolean voiceNote;
private final int width;
private final int height;
private final Optional<String> caption;
private final Optional<String> blurHash;
public SignalServiceAttachmentPointer(long id, String contentType, byte[] key,
Optional<Integer> size, Optional<byte[]> preview,
int width, int height,
Optional<byte[]> digest, Optional<String> fileName,
boolean voiceNote, Optional<String> caption,
Optional<String> blurHash)
{
super(contentType);
this.id = id;
this.key = key;
this.size = size;
this.preview = preview;
this.width = width;
this.height = height;
this.digest = digest;
this.fileName = fileName;
this.voiceNote = voiceNote;
this.caption = caption;
this.blurHash = blurHash;
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public boolean isStream() {
return false;
}
@Override
public boolean isPointer() {
return true;
}
public Optional<Integer> getSize() {
return size;
}
public Optional<String> getFileName() {
return fileName;
}
public Optional<byte[]> getPreview() {
return preview;
}
public Optional<byte[]> getDigest() {
return digest;
}
public boolean getVoiceNote() {
return voiceNote;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Optional<String> getCaption() {
return caption;
}
public Optional<String> getBlurHash() {
return blurHash;
}
}

View File

@ -0,0 +1,96 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.InputStream;
/**
* Represents a local SignalServiceAttachment to be sent.
*/
public class SignalServiceAttachmentStream extends SignalServiceAttachment {
private final InputStream inputStream;
private final long length;
private final Optional<String> fileName;
private final ProgressListener listener;
private final Optional<byte[]> preview;
private final boolean voiceNote;
private final int width;
private final int height;
private final Optional<String> caption;
private final Optional<String> blurHash;
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, ProgressListener listener) {
this(inputStream, contentType, length, fileName, voiceNote, Optional.<byte[]>absent(), 0, 0, Optional.<String>absent(), Optional.<String>absent(), listener);
}
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, Optional<byte[]> preview, int width, int height, Optional<String> caption, Optional<String> blurHash, ProgressListener listener) {
super(contentType);
this.inputStream = inputStream;
this.length = length;
this.fileName = fileName;
this.listener = listener;
this.voiceNote = voiceNote;
this.preview = preview;
this.width = width;
this.height = height;
this.caption = caption;
this.blurHash = blurHash;
}
@Override
public boolean isStream() {
return true;
}
@Override
public boolean isPointer() {
return false;
}
public InputStream getInputStream() {
return inputStream;
}
public long getLength() {
return length;
}
public Optional<String> getFileName() {
return fileName;
}
public ProgressListener getListener() {
return listener;
}
public Optional<byte[]> getPreview() {
return preview;
}
public boolean getVoiceNote() {
return voiceNote;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Optional<String> getCaption() {
return caption;
}
public Optional<String> getBlurHash() {
return blurHash;
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalServiceContent {
private final SignalServiceAddress sender;
private final int senderDevice;
private final long timestamp;
private final boolean needsReceipt;
private final Optional<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
private final Optional<SignalServiceCallMessage> callMessage;
private final Optional<SignalServiceReceiptMessage> readMessage;
private final Optional<SignalServiceTypingMessage> typingMessage;
public SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.fromNullable(message);
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.of(callMessage);
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.of(receiptMessage);
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.of(typingMessage);
}
public Optional<SignalServiceDataMessage> getDataMessage() {
return message;
}
public Optional<SignalServiceSyncMessage> getSyncMessage() {
return synchronizeMessage;
}
public Optional<SignalServiceCallMessage> getCallMessage() {
return callMessage;
}
public Optional<SignalServiceReceiptMessage> getReceiptMessage() {
return readMessage;
}
public Optional<SignalServiceTypingMessage> getTypingMessage() {
return typingMessage;
}
public SignalServiceAddress getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
public long getTimestamp() {
return timestamp;
}
public boolean isNeedsReceipt() {
return needsReceipt;
}
}

View File

@ -0,0 +1,459 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.LinkedList;
import java.util.List;
/**
* Represents a decrypted Signal Service data message.
*/
public class SignalServiceDataMessage {
private final long timestamp;
private final Optional<List<SignalServiceAttachment>> attachments;
private final Optional<String> body;
private final Optional<SignalServiceGroup> group;
private final Optional<byte[]> profileKey;
private final boolean endSession;
private final boolean expirationUpdate;
private final int expiresInSeconds;
private final boolean profileKeyUpdate;
private final Optional<Quote> quote;
private final Optional<List<SharedContact>> contacts;
private final Optional<List<Preview>> previews;
private final Optional<Sticker> sticker;
private final boolean viewOnce;
/**
* Construct a SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, String body) {
this(timestamp, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) {
this(timestamp, (List<SignalServiceAttachment>)null, body, expiresInSeconds);
}
public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) {
this(timestamp, new LinkedList<SignalServiceAttachment>() {{add(attachment);}}, body);
}
/**
* Construct a SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, null, attachments, body, expiresInSeconds);
}
/**
* Construct a SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, group, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false);
}
/**
* Construct a SignalServiceDataMessage.
*
* @param timestamp The sent timestamp.
* @param group The group information (or null if none).
* @param attachments The attachments (or null if none).
* @param body The message contents.
* @param endSession Flag indicating whether this message should close a session.
* @param expiresInSeconds Number of seconds in which the message should disappear after being seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group,
List<SignalServiceAttachment> attachments,
String body, boolean endSession, int expiresInSeconds,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, boolean viewOnce)
{
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.endSession = endSession;
this.expiresInSeconds = expiresInSeconds;
this.expirationUpdate = expirationUpdate;
this.profileKey = Optional.fromNullable(profileKey);
this.profileKeyUpdate = profileKeyUpdate;
this.quote = Optional.fromNullable(quote);
this.sticker = Optional.fromNullable(sticker);
this.viewOnce = viewOnce;
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
} else {
this.attachments = Optional.absent();
}
if (sharedContacts != null && !sharedContacts.isEmpty()) {
this.contacts = Optional.of(sharedContacts);
} else {
this.contacts = Optional.absent();
}
if (previews != null && !previews.isEmpty()) {
this.previews = Optional.of(previews);
} else {
this.previews = Optional.absent();
}
}
public static Builder newBuilder() {
return new Builder();
}
/**
* @return The message timestamp.
*/
public long getTimestamp() {
return timestamp;
}
/**
* @return The message attachments (if any).
*/
public Optional<List<SignalServiceAttachment>> getAttachments() {
return attachments;
}
/**
* @return The message body (if any).
*/
public Optional<String> getBody() {
return body;
}
/**
* @return The message group info (if any).
*/
public Optional<SignalServiceGroup> getGroupInfo() {
return group;
}
public boolean isEndSession() {
return endSession;
}
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public boolean isProfileKeyUpdate() {
return profileKeyUpdate;
}
public boolean isGroupUpdate() {
return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER;
}
public int getExpiresInSeconds() {
return expiresInSeconds;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<Quote> getQuote() {
return quote;
}
public Optional<List<SharedContact>> getSharedContacts() {
return contacts;
}
public Optional<List<Preview>> getPreviews() {
return previews;
}
public Optional<Sticker> getSticker() {
return sticker;
}
public boolean isViewOnce() {
return viewOnce;
}
public static class Builder {
private List<SignalServiceAttachment> attachments = new LinkedList<>();
private List<SharedContact> sharedContacts = new LinkedList<>();
private List<Preview> previews = new LinkedList<>();
private long timestamp;
private SignalServiceGroup group;
private String body;
private boolean endSession;
private int expiresInSeconds;
private boolean expirationUpdate;
private byte[] profileKey;
private boolean profileKeyUpdate;
private Quote quote;
private Sticker sticker;
private boolean viewOnce;
private Builder() {}
public Builder withTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder asGroupMessage(SignalServiceGroup group) {
this.group = group;
return this;
}
public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachments.add(attachment);
return this;
}
public Builder withAttachments(List<SignalServiceAttachment> attachments) {
this.attachments.addAll(attachments);
return this;
}
public Builder withBody(String body) {
this.body = body;
return this;
}
public Builder asEndSessionMessage() {
return asEndSessionMessage(true);
}
public Builder asEndSessionMessage(boolean endSession) {
this.endSession = endSession;
return this;
}
public Builder asExpirationUpdate() {
return asExpirationUpdate(true);
}
public Builder asExpirationUpdate(boolean expirationUpdate) {
this.expirationUpdate = expirationUpdate;
return this;
}
public Builder withExpiration(int expiresInSeconds) {
this.expiresInSeconds = expiresInSeconds;
return this;
}
public Builder withProfileKey(byte[] profileKey) {
this.profileKey = profileKey;
return this;
}
public Builder asProfileKeyUpdate(boolean profileKeyUpdate) {
this.profileKeyUpdate = profileKeyUpdate;
return this;
}
public Builder withQuote(Quote quote) {
this.quote = quote;
return this;
}
public Builder withSharedContact(SharedContact contact) {
this.sharedContacts.add(contact);
return this;
}
public Builder withSharedContacts(List<SharedContact> contacts) {
this.sharedContacts.addAll(contacts);
return this;
}
public Builder withPreviews(List<Preview> previews) {
this.previews.addAll(previews);
return this;
}
public Builder withSticker(Sticker sticker) {
this.sticker = sticker;
return this;
}
public Builder withViewOnce(boolean viewOnce) {
this.viewOnce = viewOnce;
return this;
}
public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce);
}
}
public static class Quote {
private final long id;
private final SignalServiceAddress author;
private final String text;
private final List<QuotedAttachment> attachments;
public Quote(long id, SignalServiceAddress author, String text, List<QuotedAttachment> attachments) {
this.id = id;
this.author = author;
this.text = text;
this.attachments = attachments;
}
public long getId() {
return id;
}
public SignalServiceAddress getAuthor() {
return author;
}
public String getText() {
return text;
}
public List<QuotedAttachment> getAttachments() {
return attachments;
}
public static class QuotedAttachment {
private final String contentType;
private final String fileName;
private final SignalServiceAttachment thumbnail;
public QuotedAttachment(String contentType, String fileName, SignalServiceAttachment thumbnail) {
this.contentType = contentType;
this.fileName = fileName;
this.thumbnail = thumbnail;
}
public String getContentType() {
return contentType;
}
public String getFileName() {
return fileName;
}
public SignalServiceAttachment getThumbnail() {
return thumbnail;
}
}
}
public static class Preview {
private final String url;
private final String title;
private final Optional<SignalServiceAttachment> image;
public Preview(String url, String title, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.image = image;
}
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
public Optional<SignalServiceAttachment> getImage() {
return image;
}
}
public static class Sticker {
private final byte[] packId;
private final byte[] packKey;
private final int stickerId;
private final SignalServiceAttachment attachment;
public Sticker(byte[] packId, byte[] packKey, int stickerId, SignalServiceAttachment attachment) {
this.packId = packId;
this.packKey = packKey;
this.stickerId = stickerId;
this.attachment = attachment;
}
public byte[] getPackId() {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public int getStickerId() {
return stickerId;
}
public SignalServiceAttachment getAttachment() {
return attachment;
}
}
}

View File

@ -0,0 +1,331 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* This class represents an encrypted Signal Service envelope.
*
* The envelope contains the wrapping information, such as the sender, the
* message timestamp, the encrypted message type, etc.
*
* @author Moxie Marlinspike
*/
public class SignalServiceEnvelope {
private static final String TAG = SignalServiceEnvelope.class.getSimpleName();
private static final int SUPPORTED_VERSION = 1;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private static final int VERSION_OFFSET = 0;
private static final int VERSION_LENGTH = 1;
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final Envelope envelope;
/**
* Construct an envelope from a serialized, Base64 encoded SignalServiceEnvelope, encrypted
* with a signaling key.
*
* @param message The serialized SignalServiceEnvelope, base64 encoded and encrypted.
* @param signalingKey The signaling key.
* @throws IOException
* @throws InvalidVersionException
*/
public SignalServiceEnvelope(String message, String signalingKey, boolean isSignalingKeyEncrypted)
throws IOException, InvalidVersionException
{
this(Base64.decode(message), signalingKey, isSignalingKeyEncrypted);
}
/**
* Construct an envelope from a serialized SignalServiceEnvelope, encrypted with a signaling key.
*
* @param input The serialized and (optionally) encrypted SignalServiceEnvelope.
* @param signalingKey The signaling key.
* @throws InvalidVersionException
* @throws IOException
*/
public SignalServiceEnvelope(byte[] input, String signalingKey, boolean isSignalingKeyEncrypted)
throws InvalidVersionException, IOException
{
if (!isSignalingKeyEncrypted) {
this.envelope = Envelope.parseFrom(input);
} else {
if (input.length < VERSION_LENGTH || input[VERSION_OFFSET] != SUPPORTED_VERSION) {
throw new InvalidVersionException("Unsupported version!");
}
SecretKeySpec cipherKey = getCipherKey(signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
verifyMac(input, macKey);
this.envelope = Envelope.parseFrom(getPlaintext(input, cipherKey));
}
}
public SignalServiceEnvelope(int type, Optional<SignalServiceAddress> sender, int senderDevice, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type))
.setSourceDevice(senderDevice)
.setTimestamp(timestamp)
.setServerTimestamp(serverTimestamp);
if (sender.isPresent()) {
if (sender.get().getUuid().isPresent()) {
builder.setSourceUuid(sender.get().getUuid().get().toString());
}
if (sender.get().getNumber().isPresent()) {
builder.setSourceE164(sender.get().getNumber().get());
}
}
if (uuid != null) {
builder.setServerGuid(uuid);
}
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
if (content != null) builder.setContent(ByteString.copyFrom(content));
this.envelope = builder.build();
}
public SignalServiceEnvelope(int type, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type))
.setTimestamp(timestamp)
.setServerTimestamp(serverTimestamp);
if (uuid != null) {
builder.setServerGuid(uuid);
}
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
if (content != null) builder.setContent(ByteString.copyFrom(content));
this.envelope = builder.build();
}
public String getUuid() {
return envelope.getServerGuid();
}
public boolean hasUuid() {
return envelope.hasServerGuid();
}
/**
* @return True if either a source E164 or UUID is present.
*/
public boolean hasSource() {
return envelope.hasSourceE164() || envelope.hasSourceUuid();
}
/**
* @return The envelope's sender as an E164 number.
*/
public Optional<String> getSourceE164() {
return Optional.fromNullable(envelope.getSourceE164());
}
/**
* @return The envelope's sender as a UUID.
*/
public Optional<String> getSourceUuid() {
return Optional.fromNullable(envelope.getSourceUuid());
}
public String getSourceIdentifier() {
return getSourceUuid().or(getSourceE164()).orNull();
}
public boolean hasSourceDevice() {
return envelope.hasSourceDevice();
}
/**
* @return The envelope's sender device ID.
*/
public int getSourceDevice() {
return envelope.getSourceDevice();
}
/**
* @return The envelope's sender as a SignalServiceAddress.
*/
public SignalServiceAddress getSourceAddress() {
return new SignalServiceAddress(UuidUtil.parseOrNull(envelope.getSourceUuid()), envelope.getSourceE164());
}
/**
* @return The envelope content type.
*/
public int getType() {
return envelope.getType().getNumber();
}
/**
* @return The timestamp this envelope was sent.
*/
public long getTimestamp() {
return envelope.getTimestamp();
}
public long getServerTimestamp() {
return envelope.getServerTimestamp();
}
/**
* @return Whether the envelope contains a SignalServiceDataMessage
*/
public boolean hasLegacyMessage() {
return envelope.hasLegacyMessage();
}
/**
* @return The envelope's containing SignalService message.
*/
public byte[] getLegacyMessage() {
return envelope.getLegacyMessage().toByteArray();
}
/**
* @return Whether the envelope contains an encrypted SignalServiceContent
*/
public boolean hasContent() {
return envelope.hasContent();
}
/**
* @return The envelope's encrypted SignalServiceContent.
*/
public byte[] getContent() {
return envelope.getContent().toByteArray();
}
/**
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.SignalMessage}
*/
public boolean isSignalMessage() {
return envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE;
}
/**
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.PreKeySignalMessage}
*/
public boolean isPreKeySignalMessage() {
return envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE;
}
/**
* @return true if the containing message is a delivery receipt.
*/
public boolean isReceipt() {
return envelope.getType().getNumber() == Envelope.Type.RECEIPT_VALUE;
}
public boolean isUnidentifiedSender() {
return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
try {
byte[] ivBytes = new byte[IV_LENGTH];
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w(TAG, e);
throw new IOException("Bad padding?");
}
}
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (ciphertext.length < MAC_SIZE + 1)
throw new IOException("Invalid MAC!");
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
byte[] ourMacFull = mac.doFinal();
byte[] ourMacBytes = new byte[MAC_SIZE];
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
throw new IOException("Invalid MAC compare!");
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@ -0,0 +1,143 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
/**
* Group information to include in SignalServiceMessages destined to groups.
*
* This class represents a "context" that is included with Signal Service messages
* to make them group messages. There are three types of context:
*
* 1) Update -- Sent when either creating a group, or updating the properties
* of a group (such as the avatar icon, membership list, or title).
* 2) Deliver -- Sent when a message is to be delivered to an existing group.
* 3) Quit -- Sent when the sender wishes to leave an existing group.
*
* @author Moxie Marlinspike
*/
public class SignalServiceGroup {
public enum Type {
UNKNOWN,
UPDATE,
DELIVER,
QUIT,
REQUEST_INFO
}
private final byte[] groupId;
private final Type type;
private final Optional<String> name;
private final Optional<List<SignalServiceAddress>> members;
private final Optional<SignalServiceAttachment> avatar;
/**
* Construct a DELIVER group context.
* @param groupId
*/
public SignalServiceGroup(byte[] groupId) {
this(Type.DELIVER, groupId, null, null, null);
}
/**
* Construct a group context.
* @param type The group message type (update, deliver, quit).
* @param groupId The group ID.
* @param name The group title.
* @param members The group membership list.
* @param avatar The group avatar icon.
*/
public SignalServiceGroup(Type type, byte[] groupId, String name,
List<SignalServiceAddress> members,
SignalServiceAttachment avatar)
{
this.type = type;
this.groupId = groupId;
this.name = Optional.fromNullable(name);
this.members = Optional.fromNullable(members);
this.avatar = Optional.fromNullable(avatar);
}
public byte[] getGroupId() {
return groupId;
}
public Type getType() {
return type;
}
public Optional<String> getName() {
return name;
}
public Optional<List<SignalServiceAddress>> getMembers() {
return members;
}
public Optional<SignalServiceAttachment> getAvatar() {
return avatar;
}
public static Builder newUpdateBuilder() {
return new Builder(Type.UPDATE);
}
public static Builder newBuilder(Type type) {
return new Builder(type);
}
public static class Builder {
private Type type;
private byte[] id;
private String name;
private List<SignalServiceAddress> members;
private SignalServiceAttachment avatar;
private Builder(Type type) {
this.type = type;
}
public Builder withId(byte[] id) {
this.id = id;
return this;
}
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withMembers(List<SignalServiceAddress> members) {
this.members = members;
return this;
}
public Builder withAvatar(SignalServiceAttachment avatar) {
this.avatar = avatar;
return this;
}
public SignalServiceGroup build() {
if (id == null) throw new IllegalArgumentException("No group ID specified!");
if (type == Type.UPDATE && name == null && members == null && avatar == null) {
throw new IllegalArgumentException("Group update with no updates!");
}
return new SignalServiceGroup(type, id, name, members, avatar);
}
}
}

View File

@ -0,0 +1,41 @@
package org.whispersystems.signalservice.api.messages;
import java.util.List;
public class SignalServiceReceiptMessage {
public enum Type {
UNKNOWN, DELIVERY, READ
}
private final Type type;
private final List<Long> timestamps;
private final long when;
public SignalServiceReceiptMessage(Type type, List<Long> timestamps, long when) {
this.type = type;
this.timestamps = timestamps;
this.when = when;
}
public Type getType() {
return type;
}
public List<Long> getTimestamps() {
return timestamps;
}
public long getWhen() {
return when;
}
public boolean isDeliveryReceipt() {
return type == Type.DELIVERY;
}
public boolean isReadReceipt() {
return type == Type.READ;
}
}

View File

@ -0,0 +1,56 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SignalServiceStickerManifest {
private final Optional<String> title;
private final Optional<String> author;
private final Optional<StickerInfo> cover;
private final List<StickerInfo> stickers;
public SignalServiceStickerManifest(String title, String author, StickerInfo cover, List<StickerInfo> stickers) {
this.title = Optional.of(title);
this.author = Optional.of(author);
this.cover = Optional.of(cover);
this.stickers = (stickers == null) ? Collections.<StickerInfo>emptyList() : new ArrayList<>(stickers);
}
public Optional<String> getTitle() {
return title;
}
public Optional<String> getAuthor() {
return author;
}
public Optional<StickerInfo> getCover() {
return cover;
}
public List<StickerInfo> getStickers() {
return stickers;
}
public static final class StickerInfo {
private final int id;
private final String emoji;
public StickerInfo(int id, String emoji) {
this.id = id;
this.emoji = emoji;
}
public int getId() {
return id;
}
public String getEmoji() {
return emoji;
}
}
}

View File

@ -0,0 +1,40 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
public class SignalServiceTypingMessage {
public enum Action {
UNKNOWN, STARTED, STOPPED
}
private final Action action;
private final long timestamp;
private final Optional<byte[]> groupId;
public SignalServiceTypingMessage(Action action, long timestamp, Optional<byte[]> groupId) {
this.action = action;
this.timestamp = timestamp;
this.groupId = groupId;
}
public Action getAction() {
return action;
}
public long getTimestamp() {
return timestamp;
}
public Optional<byte[]> getGroupId() {
return groupId;
}
public boolean isTypingStarted() {
return action == Action.STARTED;
}
public boolean isTypingStopped() {
return action == Action.STOPPED;
}
}

View File

@ -0,0 +1,21 @@
package org.whispersystems.signalservice.api.messages.calls;
public class AnswerMessage {
private final long id;
private final String description;
public AnswerMessage(long id, String description) {
this.id = id;
this.description = description;
}
public String getDescription() {
return description;
}
public long getId() {
return id;
}
}

View File

@ -0,0 +1,15 @@
package org.whispersystems.signalservice.api.messages.calls;
public class BusyMessage {
private final long id;
public BusyMessage(long id) {
this.id = id;
}
public long getId() {
return id;
}
}

View File

@ -0,0 +1,15 @@
package org.whispersystems.signalservice.api.messages.calls;
public class HangupMessage {
private final long id;
public HangupMessage(long id) {
this.id = id;
}
public long getId() {
return id;
}
}

View File

@ -0,0 +1,33 @@
package org.whispersystems.signalservice.api.messages.calls;
public class IceUpdateMessage {
private final long id;
private final String sdpMid;
private final int sdpMLineIndex;
private final String sdp;
public IceUpdateMessage(long id, String sdpMid, int sdpMLineIndex, String sdp) {
this.id = id;
this.sdpMid = sdpMid;
this.sdpMLineIndex = sdpMLineIndex;
this.sdp = sdp;
}
public String getSdpMid() {
return sdpMid;
}
public int getSdpMLineIndex() {
return sdpMLineIndex;
}
public String getSdp() {
return sdp;
}
public long getId() {
return id;
}
}

View File

@ -0,0 +1,21 @@
package org.whispersystems.signalservice.api.messages.calls;
public class OfferMessage {
private final long id;
private final String description;
public OfferMessage(long id, String description) {
this.id = id;
this.description = description;
}
public String getDescription() {
return description;
}
public long getId() {
return id;
}
}

View File

@ -0,0 +1,108 @@
package org.whispersystems.signalservice.api.messages.calls;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
public class SignalServiceCallMessage {
private final Optional<OfferMessage> offerMessage;
private final Optional<AnswerMessage> answerMessage;
private final Optional<HangupMessage> hangupMessage;
private final Optional<BusyMessage> busyMessage;
private final Optional<List<IceUpdateMessage>> iceUpdateMessages;
private SignalServiceCallMessage(Optional<OfferMessage> offerMessage,
Optional<AnswerMessage> answerMessage,
Optional<List<IceUpdateMessage>> iceUpdateMessages,
Optional<HangupMessage> hangupMessage,
Optional<BusyMessage> busyMessage)
{
this.offerMessage = offerMessage;
this.answerMessage = answerMessage;
this.iceUpdateMessages = iceUpdateMessages;
this.hangupMessage = hangupMessage;
this.busyMessage = busyMessage;
}
public static SignalServiceCallMessage forOffer(OfferMessage offerMessage) {
return new SignalServiceCallMessage(Optional.of(offerMessage),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forAnswer(AnswerMessage answerMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.of(answerMessage),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forIceUpdates(List<IceUpdateMessage> iceUpdateMessages) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.of(iceUpdateMessages),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forIceUpdate(final IceUpdateMessage iceUpdateMessage) {
List<IceUpdateMessage> iceUpdateMessages = new LinkedList<>();
iceUpdateMessages.add(iceUpdateMessage);
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.of(iceUpdateMessages),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forHangup(HangupMessage hangupMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.of(hangupMessage),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forBusy(BusyMessage busyMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.of(busyMessage));
}
public static SignalServiceCallMessage empty() {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public Optional<List<IceUpdateMessage>> getIceUpdateMessages() {
return iceUpdateMessages;
}
public Optional<AnswerMessage> getAnswerMessage() {
return answerMessage;
}
public Optional<OfferMessage> getOfferMessage() {
return offerMessage;
}
public Optional<HangupMessage> getHangupMessage() {
return hangupMessage;
}
public Optional<BusyMessage> getBusyMessage() {
return busyMessage;
}
}

View File

@ -0,0 +1,30 @@
package org.whispersystems.signalservice.api.messages.calls;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class TurnServerInfo {
@JsonProperty
private String username;
@JsonProperty
private String password;
@JsonProperty
private List<String> urls;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public List<String> getUrls() {
return urls;
}
}

View File

@ -0,0 +1,24 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class BlockedListMessage {
private final List<SignalServiceAddress> addresses;
private final List<byte[]> groupIds;
public BlockedListMessage(List<SignalServiceAddress> addresses, List<byte[]> groupIds) {
this.addresses = addresses;
this.groupIds = groupIds;
}
public List<SignalServiceAddress> getAddresses() {
return addresses;
}
public List<byte[]> getGroupIds() {
return groupIds;
}
}

View File

@ -0,0 +1,116 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ChunkedInputStream {
protected final InputStream in;
public ChunkedInputStream(InputStream in) {
this.in = in;
}
protected int readRawVarint32() throws IOException {
byte tmp = (byte)in.read();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7f;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = (byte)in.read()) << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; i++) {
if ((byte)in.read() >= 0) {
return result;
}
}
throw new IOException("Malformed varint!");
}
}
}
}
return result;
}
protected static final class LimitedInputStream extends FilterInputStream {
private long left;
private long mark = -1;
LimitedInputStream(InputStream in, long limit) {
super(in);
left = limit;
}
@Override public int available() throws IOException {
return (int) Math.min(in.available(), left);
}
// it's okay to mark even if mark isn't supported, as reset won't work
@Override public synchronized void mark(int readLimit) {
in.mark(readLimit);
mark = left;
}
@Override public int read() throws IOException {
if (left == 0) {
return -1;
}
int result = in.read();
if (result != -1) {
--left;
}
return result;
}
@Override public int read(byte[] b, int off, int len) throws IOException {
if (left == 0) {
return -1;
}
len = (int) Math.min(len, left);
int result = in.read(b, off, len);
if (result != -1) {
left -= result;
}
return result;
}
@Override public synchronized void reset() throws IOException {
if (!in.markSupported()) {
throw new IOException("Mark not supported");
}
if (mark == -1) {
throw new IOException("Mark not set");
}
in.reset();
left = mark;
}
@Override public long skip(long n) throws IOException {
n = Math.min(n, left);
long skipped = in.skip(n);
left -= skipped;
return skipped;
}
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ChunkedOutputStream {
protected final OutputStream out;
public ChunkedOutputStream(OutputStream out) {
this.out = out;
}
protected void writeVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
out.write(value);
return;
} else {
out.write((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
protected void writeStream(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}

View File

@ -0,0 +1,39 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class ConfigurationMessage {
private final Optional<Boolean> readReceipts;
private final Optional<Boolean> unidentifiedDeliveryIndicators;
private final Optional<Boolean> typingIndicators;
private final Optional<Boolean> linkPreviews;
public ConfigurationMessage(Optional<Boolean> readReceipts,
Optional<Boolean> unidentifiedDeliveryIndicators,
Optional<Boolean> typingIndicators,
Optional<Boolean> linkPreviews)
{
this.readReceipts = readReceipts;
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
this.typingIndicators = typingIndicators;
this.linkPreviews = linkPreviews;
}
public Optional<Boolean> getReadReceipts() {
return readReceipts;
}
public Optional<Boolean> getUnidentifiedDeliveryIndicators() {
return unidentifiedDeliveryIndicators;
}
public Optional<Boolean> getTypingIndicators() {
return typingIndicators;
}
public Optional<Boolean> getLinkPreviews() {
return linkPreviews;
}
}

View File

@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
public class ContactsMessage {
private final SignalServiceAttachment contacts;
private final boolean complete;
public ContactsMessage(SignalServiceAttachment contacts, boolean complete) {
this.contacts = contacts;
this.complete = complete;
}
public SignalServiceAttachment getContactsStream() {
return contacts;
}
public boolean isComplete() {
return complete;
}
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class DeviceContact {
private final SignalServiceAddress address;
private final Optional<String> name;
private final Optional<SignalServiceAttachmentStream> avatar;
private final Optional<String> color;
private final Optional<VerifiedMessage> verified;
private final Optional<byte[]> profileKey;
private final boolean blocked;
private final Optional<Integer> expirationTimer;
public DeviceContact(SignalServiceAddress address, Optional<String> name,
Optional<SignalServiceAttachmentStream> avatar,
Optional<String> color,
Optional<VerifiedMessage> verified,
Optional<byte[]> profileKey,
boolean blocked,
Optional<Integer> expirationTimer)
{
this.address = address;
this.name = name;
this.avatar = avatar;
this.color = color;
this.verified = verified;
this.profileKey = profileKey;
this.blocked = blocked;
this.expirationTimer = expirationTimer;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public SignalServiceAddress getAddress() {
return address;
}
public Optional<String> getColor() {
return color;
}
public Optional<VerifiedMessage> getVerified() {
return verified;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public boolean isBlocked() {
return blocked;
}
public Optional<Integer> getExpirationTimer() {
return expirationTimer;
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.InputStream;
public class DeviceContactsInputStream extends ChunkedInputStream {
private static final String TAG = DeviceContactsInputStream.class.getSimpleName();
public DeviceContactsInputStream(InputStream in) {
super(in);
}
public DeviceContact read() throws IOException {
long detailsLength = readRawVarint32();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
SignalServiceProtos.ContactDetails details = SignalServiceProtos.ContactDetails.parseFrom(detailsSerialized);
if (!SignalServiceAddress.isValidAddress(details.getUuid(), details.getNumber())) {
throw new IOException("Missing contact address!");
}
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(details.getUuid()), details.getNumber());
Optional<String> name = Optional.fromNullable(details.getName());
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
}
if (details.hasVerified()) {
try {
if (!SignalServiceAddress.isValidAddress(details.getVerified().getDestinationUuid(), details.getVerified().getDestinationE164())) {
throw new InvalidMessageException("Missing Verified address!");
}
IdentityKey identityKey = new IdentityKey(details.getVerified().getIdentityKey().toByteArray(), 0);
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(details.getVerified().getDestinationUuid()),
details.getVerified().getDestinationE164());
VerifiedMessage.VerifiedState state;
switch (details.getVerified().getState()) {
case VERIFIED: state = VerifiedMessage.VerifiedState.VERIFIED; break;
case UNVERIFIED:state = VerifiedMessage.VerifiedState.UNVERIFIED; break;
case DEFAULT: state = VerifiedMessage.VerifiedState.DEFAULT; break;
default: throw new InvalidMessageException("Unknown state: " + details.getVerified().getState());
}
verified = Optional.of(new VerifiedMessage(destination, identityKey, state, System.currentTimeMillis()));
} catch (InvalidKeyException | InvalidMessageException e) {
Log.w(TAG, e);
verified = Optional.absent();
}
}
if (details.hasProfileKey()) {
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
expireTimer = Optional.of(details.getExpireTimer());
}
blocked = details.getBlocked();
return new DeviceContact(address, name, avatar, color, verified, profileKey, blocked, expireTimer);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
import java.io.OutputStream;
public class DeviceContactsOutputStream extends ChunkedOutputStream {
public DeviceContactsOutputStream(OutputStream out) {
super(out);
}
public void write(DeviceContact contact) throws IOException {
writeContactDetails(contact);
writeAvatarImage(contact);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceContact contact) throws IOException {
if (contact.getAvatar().isPresent()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
private void writeContactDetails(DeviceContact contact) throws IOException {
SignalServiceProtos.ContactDetails.Builder contactDetails = SignalServiceProtos.ContactDetails.newBuilder();
if (contact.getAddress().getUuid().isPresent()) {
contactDetails.setUuid(contact.getAddress().getUuid().get().toString());
}
if (contact.getAddress().getNumber().isPresent()) {
contactDetails.setNumber(contact.getAddress().getNumber().get());
}
if (contact.getName().isPresent()) {
contactDetails.setName(contact.getName().get());
}
if (contact.getAvatar().isPresent()) {
SignalServiceProtos.ContactDetails.Avatar.Builder avatarBuilder = SignalServiceProtos.ContactDetails.Avatar.newBuilder();
avatarBuilder.setContentType(contact.getAvatar().get().getContentType());
avatarBuilder.setLength((int)contact.getAvatar().get().getLength());
contactDetails.setAvatar(avatarBuilder);
}
if (contact.getColor().isPresent()) {
contactDetails.setColor(contact.getColor().get());
}
if (contact.getVerified().isPresent()) {
SignalServiceProtos.Verified.State state;
switch (contact.getVerified().get().getVerified()) {
case VERIFIED: state = SignalServiceProtos.Verified.State.VERIFIED; break;
case UNVERIFIED: state = SignalServiceProtos.Verified.State.UNVERIFIED; break;
default: state = SignalServiceProtos.Verified.State.DEFAULT; break;
}
SignalServiceProtos.Verified.Builder verifiedBuilder = SignalServiceProtos.Verified.newBuilder()
.setIdentityKey(ByteString.copyFrom(contact.getVerified().get().getIdentityKey().serialize()))
.setState(state);
if (contact.getVerified().get().getDestination().getUuid().isPresent()) {
verifiedBuilder.setDestinationUuid(contact.getVerified().get().getDestination().getUuid().get().toString());
}
if (contact.getVerified().get().getDestination().getNumber().isPresent()) {
verifiedBuilder.setDestinationE164(contact.getVerified().get().getDestination().getNumber().get());
}
contactDetails.setVerified(verifiedBuilder.build());
}
if (contact.getProfileKey().isPresent()) {
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
}
if (contact.getExpirationTimer().isPresent()) {
contactDetails.setExpireTimer(contact.getExpirationTimer().get());
}
contactDetails.setBlocked(contact.isBlocked());
byte[] serializedContactDetails = contactDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class DeviceGroup {
private final byte[] id;
private final Optional<String> name;
private final List<SignalServiceAddress> members;
private final Optional<SignalServiceAttachmentStream> avatar;
private final boolean active;
private final Optional<Integer> expirationTimer;
private final Optional<String> color;
private final boolean blocked;
public DeviceGroup(byte[] id, Optional<String> name, List<SignalServiceAddress> members,
Optional<SignalServiceAttachmentStream> avatar,
boolean active, Optional<Integer> expirationTimer,
Optional<String> color, boolean blocked)
{
this.id = id;
this.name = name;
this.members = members;
this.avatar = avatar;
this.active = active;
this.expirationTimer = expirationTimer;
this.color = color;
this.blocked = blocked;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public byte[] getId() {
return id;
}
public List<SignalServiceAddress> getMembers() {
return members;
}
public boolean isActive() {
return active;
}
public Optional<Integer> getExpirationTimer() {
return expirationTimer;
}
public Optional<String> getColor() {
return color;
}
public boolean isBlocked() {
return blocked;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class DeviceGroupsInputStream extends ChunkedInputStream{
public DeviceGroupsInputStream(InputStream in) {
super(in);
}
public DeviceGroup read() throws IOException {
long detailsLength = readRawVarint32();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
GroupDetails details = GroupDetails.parseFrom(detailsSerialized);
if (!details.hasId()) {
throw new IOException("ID missing on group record!");
}
byte[] id = details.getId().toByteArray();
Optional<String> name = Optional.fromNullable(details.getName());
List<GroupDetails.Member> members = details.getMembersList();
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
boolean active = details.getActive();
Optional<Integer> expirationTimer = Optional.absent();
Optional<String> color = Optional.fromNullable(details.getColor());
boolean blocked = details.getBlocked();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
expirationTimer = Optional.of(details.getExpireTimer());
}
List<SignalServiceAddress> addressMembers = new ArrayList<>(members.size());
for (GroupDetails.Member member : members) {
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
addressMembers.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
} else {
throw new IOException("Missing group member address!");
}
}
return new DeviceGroup(id, name, addressMembers, avatar, active, expirationTimer, color, blocked);
}
}

View File

@ -0,0 +1,94 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class DeviceGroupsOutputStream extends ChunkedOutputStream {
public DeviceGroupsOutputStream(OutputStream out) {
super(out);
}
public void write(DeviceGroup group) throws IOException {
writeGroupDetails(group);
writeAvatarImage(group);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceGroup contact) throws IOException {
if (contact.getAvatar().isPresent()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
private void writeGroupDetails(DeviceGroup group) throws IOException {
GroupDetails.Builder groupDetails = GroupDetails.newBuilder();
groupDetails.setId(ByteString.copyFrom(group.getId()));
if (group.getName().isPresent()) {
groupDetails.setName(group.getName().get());
}
if (group.getAvatar().isPresent()) {
GroupDetails.Avatar.Builder avatarBuilder = GroupDetails.Avatar.newBuilder();
avatarBuilder.setContentType(group.getAvatar().get().getContentType());
avatarBuilder.setLength((int)group.getAvatar().get().getLength());
groupDetails.setAvatar(avatarBuilder);
}
if (group.getExpirationTimer().isPresent()) {
groupDetails.setExpireTimer(group.getExpirationTimer().get());
}
if (group.getColor().isPresent()) {
groupDetails.setColor(group.getColor().get());
}
List<GroupDetails.Member> members = new ArrayList<>(group.getMembers().size());
List<String> membersE164 = new ArrayList<>(group.getMembers().size());
for (SignalServiceAddress address : group.getMembers()) {
GroupDetails.Member.Builder builder = GroupDetails.Member.newBuilder();
if (address.getUuid().isPresent()) {
builder.setUuid(address.getUuid().get().toString());
}
if (address.getNumber().isPresent()) {
builder.setE164(address.getNumber().get());
membersE164.add(address.getNumber().get());
}
members.add(builder.build());
}
groupDetails.addAllMembers(members);
groupDetails.addAllMembersE164(membersE164);
groupDetails.setActive(group.isActive());
groupDetails.setBlocked(group.isBlocked());
byte[] serializedContactDetails = groupDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceInfo {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private long created;
@JsonProperty
private long lastSeen;
public DeviceInfo() {}
public long getId() {
return id;
}
public String getName() {
return name;
}
public long getCreated() {
return created;
}
public long getLastSeen() {
return lastSeen;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class ReadMessage {
private final SignalServiceAddress sender;
private final long timestamp;
public ReadMessage(SignalServiceAddress sender, long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public SignalServiceAddress getSender() {
return sender;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Request;
public class RequestMessage {
private final Request request;
public RequestMessage(Request request) {
this.request = request;
}
public boolean isContactsRequest() {
return request.getType() == Request.Type.CONTACTS;
}
public boolean isGroupsRequest() {
return request.getType() == Request.Type.GROUPS;
}
public boolean isBlockedListRequest() {
return request.getType() == Request.Type.BLOCKED;
}
public boolean isConfigurationRequest() {
return request.getType() == Request.Type.CONFIGURATION;
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class SentTranscriptMessage {
private final Optional<SignalServiceAddress> destination;
private final long timestamp;
private final long expirationStartTimestamp;
private final SignalServiceDataMessage message;
private final Map<String, Boolean> unidentifiedStatusByUuid;
private final Map<String, Boolean> unidentifiedStatusByE164;
private final Set<SignalServiceAddress> recipients;
private final boolean isRecipientUpdate;
public SentTranscriptMessage(Optional<SignalServiceAddress> destination, long timestamp, SignalServiceDataMessage message,
long expirationStartTimestamp, Map<SignalServiceAddress, Boolean> unidentifiedStatus,
boolean isRecipientUpdate)
{
this.destination = destination;
this.timestamp = timestamp;
this.message = message;
this.expirationStartTimestamp = expirationStartTimestamp;
this.unidentifiedStatusByUuid = new HashMap<>();
this.unidentifiedStatusByE164 = new HashMap<>();
this.recipients = unidentifiedStatus.keySet();
this.isRecipientUpdate = isRecipientUpdate;
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
if (entry.getKey().getUuid().isPresent()) {
unidentifiedStatusByUuid.put(entry.getKey().getUuid().get().toString(), entry.getValue());
}
if (entry.getKey().getNumber().isPresent()) {
unidentifiedStatusByE164.put(entry.getKey().getNumber().get(), entry.getValue());
}
}
}
public Optional<SignalServiceAddress> getDestination() {
return destination;
}
public long getTimestamp() {
return timestamp;
}
public long getExpirationStartTimestamp() {
return expirationStartTimestamp;
}
public SignalServiceDataMessage getMessage() {
return message;
}
public boolean isUnidentified(UUID uuid) {
return isUnidentified(uuid.toString());
}
public boolean isUnidentified(String destination) {
if (unidentifiedStatusByUuid.containsKey(destination)) {
return unidentifiedStatusByUuid.get(destination);
} else if (unidentifiedStatusByE164.containsKey(destination)) {
return unidentifiedStatusByE164.get(destination);
} else {
return false;
}
}
public Set<SignalServiceAddress> getRecipients() {
return recipients;
}
public boolean isRecipientUpdate() {
return isRecipientUpdate;
}
}

View File

@ -0,0 +1,287 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import java.util.LinkedList;
import java.util.List;
public class SignalServiceSyncMessage {
private final Optional<SentTranscriptMessage> sent;
private final Optional<ContactsMessage> contacts;
private final Optional<SignalServiceAttachment> groups;
private final Optional<BlockedListMessage> blockedList;
private final Optional<RequestMessage> request;
private final Optional<List<ReadMessage>> reads;
private final Optional<ViewOnceOpenMessage> viewOnceOpen;
private final Optional<VerifiedMessage> verified;
private final Optional<ConfigurationMessage> configuration;
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
private final Optional<FetchType> fetchType;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
Optional<SignalServiceAttachment> groups,
Optional<BlockedListMessage> blockedList,
Optional<RequestMessage> request,
Optional<List<ReadMessage>> reads,
Optional<ViewOnceOpenMessage> viewOnceOpen,
Optional<VerifiedMessage> verified,
Optional<ConfigurationMessage> configuration,
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
Optional<FetchType> fetchType)
{
this.sent = sent;
this.contacts = contacts;
this.groups = groups;
this.blockedList = blockedList;
this.request = request;
this.reads = reads;
this.viewOnceOpen = viewOnceOpen;
this.verified = verified;
this.configuration = configuration;
this.stickerPackOperations = stickerPackOperations;
this.fetchType = fetchType;
}
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
return new SignalServiceSyncMessage(Optional.of(sent),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.of(contacts),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.of(groups),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.of(request),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.of(timerRead),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forRead(ReadMessage read) {
List<ReadMessage> reads = new LinkedList<>();
reads.add(read);
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.of(verifiedMessage),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.of(blocked),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.of(configuration),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.of(stickerPackOperations),
Optional.<FetchType>absent());
}
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.of(fetchType));
}
public static SignalServiceSyncMessage empty() {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent());
}
public Optional<SentTranscriptMessage> getSent() {
return sent;
}
public Optional<SignalServiceAttachment> getGroups() {
return groups;
}
public Optional<ContactsMessage> getContacts() {
return contacts;
}
public Optional<RequestMessage> getRequest() {
return request;
}
public Optional<List<ReadMessage>> getRead() {
return reads;
}
public Optional<ViewOnceOpenMessage> getViewOnceOpen() {
return viewOnceOpen;
}
public Optional<BlockedListMessage> getBlockedList() {
return blockedList;
}
public Optional<VerifiedMessage> getVerified() {
return verified;
}
public Optional<ConfigurationMessage> getConfiguration() {
return configuration;
}
public Optional<List<StickerPackOperationMessage>> getStickerPackOperations() {
return stickerPackOperations;
}
public Optional<FetchType> getFetchType() {
return fetchType;
}
public enum FetchType {
LOCAL_PROFILE,
STORAGE_MANIFEST
}
}

View File

@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class StickerPackOperationMessage {
private final Optional<byte[]> packId;
private final Optional<byte[]> packKey;
private final Optional<Type> type;
public StickerPackOperationMessage(byte[] packId, byte[] packKey, Type type) {
this.packId = Optional.fromNullable(packId);
this.packKey = Optional.fromNullable(packKey);
this.type = Optional.fromNullable(type);
}
public Optional<byte[]> getPackId() {
return packId;
}
public Optional<byte[]> getPackKey() {
return packKey;
}
public Optional<Type> getType() {
return type;
}
public enum Type {
INSTALL, REMOVE
}
}

View File

@ -0,0 +1,40 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class VerifiedMessage {
public enum VerifiedState {
DEFAULT, VERIFIED, UNVERIFIED
}
private final SignalServiceAddress destination;
private final IdentityKey identityKey;
private final VerifiedState verified;
private final long timestamp;
public VerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, VerifiedState verified, long timestamp) {
this.destination = destination;
this.identityKey = identityKey;
this.verified = verified;
this.timestamp = timestamp;
}
public SignalServiceAddress getDestination() {
return destination;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public VerifiedState getVerified() {
return verified;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class ViewOnceOpenMessage {
private final SignalServiceAddress sender;
private final long timestamp;
public ViewOnceOpenMessage(SignalServiceAddress sender, long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public SignalServiceAddress getSender() {
return sender;
}
}

View File

@ -0,0 +1,513 @@
package org.whispersystems.signalservice.api.messages.shared;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import java.util.LinkedList;
import java.util.List;
public class SharedContact {
private final Name name;
private final Optional<Avatar> avatar;
private final Optional<List<Phone>> phone;
private final Optional<List<Email>> email;
private final Optional<List<PostalAddress>> address;
private final Optional<String> organization;
public SharedContact(Name name,
Optional<Avatar> avatar,
Optional<List<Phone>> phone,
Optional<List<Email>> email,
Optional<List<PostalAddress>> address,
Optional<String> organization)
{
this.name = name;
this.avatar = avatar;
this.phone = phone;
this.email = email;
this.address = address;
this.organization = organization;
}
public static Builder newBuilder() {
return new Builder();
}
public Name getName() {
return name;
}
public Optional<Avatar> getAvatar() {
return avatar;
}
public Optional<List<Phone>> getPhone() {
return phone;
}
public Optional<List<Email>> getEmail() {
return email;
}
public Optional<List<PostalAddress>> getAddress() {
return address;
}
public Optional<String> getOrganization() {
return organization;
}
public static class Avatar {
private final SignalServiceAttachment attachment;
private final boolean isProfile;
public Avatar(SignalServiceAttachment attachment, boolean isProfile) {
this.attachment = attachment;
this.isProfile = isProfile;
}
public SignalServiceAttachment getAttachment() {
return attachment;
}
public boolean isProfile() {
return isProfile;
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private SignalServiceAttachment attachment;
private boolean isProfile;
public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachment = attachment;
return this;
}
public Builder withProfileFlag(boolean isProfile) {
this.isProfile = isProfile;
return this;
}
public Avatar build() {
return new Avatar(attachment, isProfile);
}
}
}
public static class Name {
private final Optional<String> display;
private final Optional<String> given;
private final Optional<String> family;
private final Optional<String> prefix;
private final Optional<String> suffix;
private final Optional<String> middle;
public Name(Optional<String> display, Optional<String> given, Optional<String> family, Optional<String> prefix, Optional<String> suffix, Optional<String> middle) {
this.display = display;
this.given = given;
this.family = family;
this.prefix = prefix;
this.suffix = suffix;
this.middle = middle;
}
public static Builder newBuilder() {
return new Builder();
}
public Optional<String> getDisplay() {
return display;
}
public Optional<String> getGiven() {
return given;
}
public Optional<String> getFamily() {
return family;
}
public Optional<String> getPrefix() {
return prefix;
}
public Optional<String> getSuffix() {
return suffix;
}
public Optional<String> getMiddle() {
return middle;
}
public static class Builder {
private String display;
private String given;
private String family;
private String prefix;
private String suffix;
private String middle;
public Builder setDisplay(String display) {
this.display = display;
return this;
}
public Builder setGiven(String given) {
this.given = given;
return this;
}
public Builder setFamily(String family) {
this.family = family;
return this;
}
public Builder setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
public Builder setSuffix(String suffix) {
this.suffix = suffix;
return this;
}
public Builder setMiddle(String middle) {
this.middle = middle;
return this;
}
public Name build() {
return new Name(Optional.fromNullable(display),
Optional.fromNullable(given),
Optional.fromNullable(family),
Optional.fromNullable(prefix),
Optional.fromNullable(suffix),
Optional.fromNullable(middle));
}
}
}
public static class Phone {
public enum Type {
HOME, WORK, MOBILE, CUSTOM
}
private final String value;
private final Type type;
private final Optional<String> label;
public Phone(String value, Type type, Optional<String> label) {
this.value = value;
this.type = type;
this.label = label;
}
public static Builder newBuilder() {
return new Builder();
}
public String getValue() {
return value;
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public static class Builder {
private String value;
private Type type;
private String label;
public Builder setValue(String value) {
this.value = value;
return this;
}
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Phone build() {
return new Phone(value, type, Optional.fromNullable(label));
}
}
}
public static class Email {
public enum Type {
HOME, WORK, MOBILE, CUSTOM
}
private final String value;
private final Type type;
private final Optional<String> label;
public Email(String value, Type type, Optional<String> label) {
this.value = value;
this.type = type;
this.label = label;
}
public static Builder newBuilder() {
return new Builder();
}
public String getValue() {
return value;
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public static class Builder {
private String value;
private Type type;
private String label;
public Builder setValue(String value) {
this.value = value;
return this;
}
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Email build() {
return new Email(value, type, Optional.fromNullable(label));
}
}
}
public static class PostalAddress {
public enum Type {
HOME, WORK, CUSTOM
}
private final Type type;
private final Optional<String> label;
private final Optional<String> street;
private final Optional<String> pobox;
private final Optional<String> neighborhood;
private final Optional<String> city;
private final Optional<String> region;
private final Optional<String> postcode;
private final Optional<String> country;
public PostalAddress(Type type, Optional<String> label, Optional<String> street,
Optional<String> pobox, Optional<String> neighborhood,
Optional<String> city, Optional<String> region,
Optional<String> postcode, Optional<String> country)
{
this.type = type;
this.label = label;
this.street = street;
this.pobox = pobox;
this.neighborhood = neighborhood;
this.city = city;
this.region = region;
this.postcode = postcode;
this.country = country;
}
public static Builder newBuilder() {
return new Builder();
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public Optional<String> getStreet() {
return street;
}
public Optional<String> getPobox() {
return pobox;
}
public Optional<String> getNeighborhood() {
return neighborhood;
}
public Optional<String> getCity() {
return city;
}
public Optional<String> getRegion() {
return region;
}
public Optional<String> getPostcode() {
return postcode;
}
public Optional<String> getCountry() {
return country;
}
public static class Builder {
private Type type;
private String label;
private String street;
private String pobox;
private String neighborhood;
private String city;
private String region;
private String postcode;
private String country;
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Builder setStreet(String street) {
this.street = street;
return this;
}
public Builder setPobox(String pobox) {
this.pobox = pobox;
return this;
}
public Builder setNeighborhood(String neighborhood) {
this.neighborhood = neighborhood;
return this;
}
public Builder setCity(String city) {
this.city = city;
return this;
}
public Builder setRegion(String region) {
this.region = region;
return this;
}
public Builder setPostcode(String postcode) {
this.postcode = postcode;
return this;
}
public Builder setCountry(String country) {
this.country = country;
return this;
}
public PostalAddress build() {
return new PostalAddress(type, Optional.fromNullable(label), Optional.fromNullable(street),
Optional.fromNullable(pobox), Optional.fromNullable(neighborhood),
Optional.fromNullable(city), Optional.fromNullable(region),
Optional.fromNullable(postcode), Optional.fromNullable(country));
}
}
}
public static class Builder {
private Name name;
private Avatar avatar;
private String organization;
private List<Phone> phone = new LinkedList<>();
private List<Email> email = new LinkedList<>();
private List<PostalAddress> address = new LinkedList<>();
public Builder setName(Name name) {
this.name = name;
return this;
}
public Builder withOrganization(String organization) {
this.organization = organization;
return this;
}
public Builder setAvatar(Avatar avatar) {
this.avatar = avatar;
return this;
}
public Builder withPhone(Phone phone) {
this.phone.add(phone);
return this;
}
public Builder withPhones(List<Phone> phones) {
this.phone.addAll(phones);
return this;
}
public Builder withEmail(Email email) {
this.email.add(email);
return this;
}
public Builder withEmails(List<Email> emails) {
this.email.addAll(emails);
return this;
}
public Builder withAddress(PostalAddress address) {
this.address.add(address);
return this;
}
public Builder withAddresses(List<PostalAddress> addresses) {
this.address.addAll(addresses);
return this;
}
public SharedContact build() {
return new SharedContact(name, Optional.fromNullable(avatar),
phone.isEmpty() ? Optional.<List<Phone>>absent() : Optional.of(phone),
email.isEmpty() ? Optional.<List<Email>>absent() : Optional.of(email),
address.isEmpty() ? Optional.<List<PostalAddress>>absent() : Optional.of(address),
Optional.fromNullable(organization));
}
}
}

View File

@ -0,0 +1,62 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SignalServiceProfile {
@JsonProperty
private String identityKey;
@JsonProperty
private String name;
@JsonProperty
private String avatar;
@JsonProperty
private String unidentifiedAccess;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private Capabilities capabilities;
public SignalServiceProfile() {}
public String getIdentityKey() {
return identityKey;
}
public String getName() {
return name;
}
public String getAvatar() {
return avatar;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public Capabilities getCapabilities() {
return capabilities;
}
public static class Capabilities {
@JsonProperty
private boolean uuid;
public Capabilities() {}
public boolean isUuid() {
return uuid;
}
}
}

View File

@ -0,0 +1,69 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* A class that represents a contact's registration state.
*/
public class ContactTokenDetails {
@JsonProperty
private String token;
@JsonProperty
private String relay;
@JsonProperty
private String number;
@JsonProperty
private boolean voice;
@JsonProperty
private boolean video;
public ContactTokenDetails() {}
/**
* @return The "anonymized" token (truncated hash) that's transmitted to the server.
*/
public String getToken() {
return token;
}
/**
* @return The federated server this contact is registered with, or null if on your server.
*/
public String getRelay() {
return relay;
}
/**
* @return Whether this contact supports secure voice calls.
*/
public boolean isVoice() {
return voice;
}
public boolean isVideo() {
return video;
}
public void setNumber(String number) {
this.number = number;
}
/**
* @return This contact's username (e164 formatted number).
*/
public String getNumber() {
return number;
}
}

View File

@ -0,0 +1,119 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
/**
* A class representing a message destination or origin.
*/
public class SignalServiceAddress {
public static final int DEFAULT_DEVICE_ID = 1;
private final Optional<UUID> uuid;
private final Optional<String> e164;
private final Optional<String> relay;
/**
* Construct a PushAddress.
*
* @param uuid The UUID of the user, if available.
* @param e164 The phone number of the user, if available.
* @param relay The Signal service federated server this user is registered with (if not your own server).
*/
public SignalServiceAddress(Optional<UUID> uuid, Optional<String> e164, Optional<String> relay) {
if (!uuid.isPresent() && !e164.isPresent()) {
throw new AssertionError("Must have either a UUID or E164 number!");
}
this.uuid = uuid;
this.e164 = e164;
this.relay = relay;
}
/**
* Convenience constructor that will consider a UUID/E164 string absent if it is null or empty.
*/
public SignalServiceAddress(UUID uuid, String e164) {
this(Optional.fromNullable(uuid),
e164 != null && !e164.isEmpty() ? Optional.of(e164) : Optional.<String>absent());
}
public SignalServiceAddress(Optional<UUID> uuid, Optional<String> e164) {
this(uuid, e164, Optional.<String>absent());
}
public Optional<String> getNumber() {
return e164;
}
public Optional<UUID> getUuid() {
return uuid;
}
public String getIdentifier() {
if (uuid.isPresent()) {
return uuid.get().toString();
} else if (e164.isPresent()) {
return e164.get();
} else {
return null;
}
}
public Optional<String> getRelay() {
return relay;
}
public boolean matches(SignalServiceAddress other) {
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) ||
(e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get()));
}
public static boolean isValidAddress(String rawUuid, String e164) {
return (e164 != null && !e164.isEmpty()) || UuidUtil.parseOrNull(rawUuid) != null;
}
public static Optional<SignalServiceAddress> fromRaw(String rawUuid, String e164) {
if (isValidAddress(rawUuid, e164)) {
return Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(rawUuid), e164));
} else {
return Optional.absent();
}
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof SignalServiceAddress)) return false;
SignalServiceAddress that = (SignalServiceAddress)other;
return equals(this.uuid, that.uuid) &&
equals(this.e164, that.e164) &&
equals(this.relay, that.relay);
}
@Override
public int hashCode() {
int hashCode = 0;
if (this.uuid != null) hashCode ^= this.uuid.hashCode();
if (this.e164 != null) hashCode ^= this.e164.hashCode();
if (this.relay.isPresent()) hashCode ^= this.relay.get().hashCode();
return hashCode;
}
private <T> boolean equals(Optional<T> one, Optional<T> two) {
if (one.isPresent()) return two.isPresent() && one.get().equals(two.get());
else return !two.isPresent();
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.signalservice.internal.push.PreKeyEntity;
import org.whispersystems.util.Base64;
import java.io.IOException;
public class SignedPreKeyEntity extends PreKeyEntity {
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] signature;
public SignedPreKeyEntity() {}
public SignedPreKeyEntity(int keyId, ECPublicKey publicKey, byte[] signature) {
super(keyId, publicKey);
this.signature = signature;
}
public byte[] getSignature() {
return signature;
}
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeBytesWithoutPadding(value));
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Base64.decodeWithoutPadding(p.getValueAsString());
}
}
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push;
import java.io.InputStream;
/**
* A class that represents a Java {@link java.security.KeyStore} and
* its associated password.
*/
public interface TrustStore {
public InputStream getKeyStoreInputStream();
public String getKeyStorePassword();
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@ -0,0 +1,4 @@
package org.whispersystems.signalservice.api.push.exceptions;
public class CaptchaRequiredException extends NonSuccessfulResponseCodeException {
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import java.util.LinkedList;
import java.util.List;
public class EncapsulatedExceptions extends Throwable {
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
private final List<UnregisteredUserException> unregisteredUserExceptions;
private final List<NetworkFailureException> networkExceptions;
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
List<UnregisteredUserException> unregisteredUsers,
List<NetworkFailureException> networkExceptions)
{
this.untrustedIdentityExceptions = untrustedIdentities;
this.unregisteredUserExceptions = unregisteredUsers;
this.networkExceptions = networkExceptions;
}
public EncapsulatedExceptions(UntrustedIdentityException e) {
this.untrustedIdentityExceptions = new LinkedList<>();
this.unregisteredUserExceptions = new LinkedList<>();
this.networkExceptions = new LinkedList<>();
this.untrustedIdentityExceptions.add(e);
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
return untrustedIdentityExceptions;
}
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
return unregisteredUserExceptions;
}
public List<NetworkFailureException> getNetworkExceptions() {
return networkExceptions;
}
}

View File

@ -0,0 +1,9 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class NetworkFailureException extends Exception {
private final String e164number;
public NetworkFailureException(String e164number, Exception nested) {
super(nested);
this.e164number = e164number;
}
public String getE164number() {
return e164number;
}
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
import java.io.IOException;
public class NonSuccessfulResponseCodeException extends IOException {
public NonSuccessfulResponseCodeException() {
super();
}
public NonSuccessfulResponseCodeException(String s) {
super(s);
}
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class NotFoundException extends NonSuccessfulResponseCodeException {
public NotFoundException(String s) {
super(s);
}
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
import java.io.IOException;
public class PushNetworkException extends IOException {
public PushNetworkException(Exception exception) {
super(exception);
}
public PushNetworkException(String s) {
super(s);
}
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class RateLimitException extends NonSuccessfulResponseCodeException {
public RateLimitException(String s) {
super(s);
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class RemoteAttestationResponseExpiredException extends NonSuccessfulResponseCodeException {
public RemoteAttestationResponseExpiredException(String message) {
super(message);
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
import java.io.IOException;
public class UnregisteredUserException extends IOException {
private final String e164number;
public UnregisteredUserException(String e164number, Exception exception) {
super(exception);
this.e164number = e164number;
}
public String getE164Number() {
return e164number;
}
}

View File

@ -0,0 +1,16 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.util;
import java.util.UUID;
public interface CredentialsProvider {
public UUID getUuid();
public String getE164();
public String getPassword();
public String getSignalingKey();
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.util;
public class InvalidNumberException extends Throwable {
public InvalidNumberException(String s) {
super(s);
}
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.util;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import org.whispersystems.libsignal.logging.Log;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* Phone number formats are a pain.
*
* @author Moxie Marlinspike
*
*/
public class PhoneNumberFormatter {
private static final String TAG = PhoneNumberFormatter.class.getSimpleName();
private static final String COUNTRY_CODE_BR = "55";
private static final String COUNTRY_CODE_US = "1";
public static boolean isValidNumber(String e164Number, String countryCode) {
if (!PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode)) {
Log.w(TAG, "Failed isPossibleNumber()");
return false;
}
if (COUNTRY_CODE_US.equals(countryCode) && !Pattern.matches("^\\+1[0-9]{10}$", e164Number)) {
Log.w(TAG, "Failed US number format check");
return false;
}
if (COUNTRY_CODE_BR.equals(countryCode) && !Pattern.matches("^\\+55[0-9]{2}9?[0-9]{8}$", e164Number)) {
Log.w(TAG, "Failed Brazil number format check");
return false;
}
return e164Number.matches("^\\+[1-9][0-9]{6,14}$");
}
private static String impreciseFormatNumber(String number, String localNumber)
throws InvalidNumberException
{
number = number.replaceAll("[^0-9+]", "");
if (number.charAt(0) == '+')
return number;
if (localNumber.charAt(0) == '+')
localNumber = localNumber.substring(1);
if (localNumber.length() == number.length() || number.length() > localNumber.length())
return "+" + number;
int difference = localNumber.length() - number.length();
return "+" + localNumber.substring(0, difference) + number;
}
public static String formatNumberInternational(String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w(TAG, e);
return number;
}
}
public static String formatNumber(String number, String localNumber)
throws InvalidNumberException
{
if (number == null) {
throw new InvalidNumberException("Null String passed as number.");
}
if (number.contains("@")) {
throw new InvalidNumberException("Possible attempt to use email address.");
}
number = number.replaceAll("[^0-9+]", "");
if (number.length() == 0) {
throw new InvalidNumberException("No valid characters found.");
}
// if (number.charAt(0) == '+')
// return number;
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber localNumberObject = util.parse(localNumber, null);
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
Log.w(TAG, "Got local CC: " + localCountryCode);
PhoneNumber numberObject = util.parse(number, localCountryCode);
return util.format(numberObject, PhoneNumberFormat.E164);
} catch (NumberParseException e) {
Log.w(TAG, e);
return impreciseFormatNumber(number, localNumber);
}
}
public static String getRegionDisplayName(String regionCode) {
return (regionCode == null || regionCode.equals("ZZ") || regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
? "Unknown country" : new Locale("", regionCode).getDisplayCountry(Locale.getDefault());
}
public static String formatE164(String countryCode, String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
int parsedCountryCode = Integer.parseInt(countryCode);
PhoneNumber parsedNumber = util.parse(number,
util.getRegionCodeForCountryCode(parsedCountryCode));
return util.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (NumberParseException | NumberFormatException npe) {
Log.w(TAG, npe);
}
return "+" +
countryCode.replaceAll("[^0-9]", "").replaceAll("^0*", "") +
number.replaceAll("[^0-9]", "");
}
public static String getInternationalFormatFromE164(String e164number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(e164number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w(TAG, e);
return e164number;
}
}
}

View File

@ -0,0 +1,5 @@
package org.whispersystems.signalservice.api.util;
public interface SleepTimer {
public void sleep(long millis) throws InterruptedException;
}

View File

@ -0,0 +1,29 @@
package org.whispersystems.signalservice.api.util;
import java.io.InputStream;
public class StreamDetails {
private final InputStream stream;
private final String contentType;
private final long length;
public StreamDetails(InputStream stream, String contentType, long length) {
this.stream = stream;
this.contentType = contentType;
this.length = length;
}
public InputStream getStream() {
return stream;
}
public String getContentType() {
return contentType;
}
public long getLength() {
return length;
}
}

View File

@ -0,0 +1,69 @@
package org.whispersystems.signalservice.api.util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* Enables TLS v1.2 when creating SSLSockets.
* <p/>
* For some reason, android supports TLS v1.2 from API 16, but enables it by
* default only from API 20.
* @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
* @see SSLSocketFactory
*/
public class Tls12SocketFactory extends SSLSocketFactory {
private static final String[] TLS_V12_V13_ONLY = {"TLSv1.3", "TLSv1.2"};
final SSLSocketFactory delegate;
public Tls12SocketFactory(SSLSocketFactory base) {
this.delegate = base;
}
@Override
public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return patch(delegate.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return patch(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return patch(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return patch(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return patch(delegate.createSocket(address, port, localAddress, localPort));
}
private Socket patch(Socket s) {
if (s instanceof SSLSocket) {
((SSLSocket) s).setEnabledProtocols(TLS_V12_V13_ONLY);
}
return s;
}
}

View File

@ -0,0 +1,16 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.signalservice.api.util.SleepTimer;
/**
* A simle sleep timer. Since Thread.sleep is based on uptime
* this will not work properly in low-power sleep modes, when
* the CPU is suspended and uptime does not elapse.
*
*/
public class UptimeSleepTimer implements SleepTimer {
@Override
public void sleep(long millis) throws InterruptedException {
Thread.sleep(millis);
}
}

View File

@ -0,0 +1,46 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.nio.ByteBuffer;
import java.util.UUID;
import java.util.regex.Pattern;
public final class UuidUtil {
private static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE);
private UuidUtil() { }
public static Optional<UUID> parse(String uuid) {
return Optional.fromNullable(parseOrNull(uuid));
}
public static UUID parseOrNull(String uuid) {
return isUuid(uuid) ? parseOrThrow(uuid) : null;
}
public static UUID parseOrThrow(String uuid) {
return UUID.fromString(uuid);
}
public static UUID parseOrThrow(byte[] bytes) {
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
long high = byteBuffer.getLong();
long low = byteBuffer.getLong();
return new UUID(high, low);
}
public static boolean isUuid(String uuid) {
return uuid != null && UUID_PATTERN.matcher(uuid).matches();
}
public static byte[] toByteArray(UUID uuid) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
}

View File

@ -0,0 +1,9 @@
package org.whispersystems.signalservice.api.websocket;
public interface ConnectivityListener {
void onConnected();
void onConnecting();
void onDisconnected();
void onAuthenticationFailure();
}

View File

@ -0,0 +1,16 @@
package org.whispersystems.signalservice.internal.configuration;
import org.whispersystems.signalservice.api.push.TrustStore;
import okhttp3.ConnectionSpec;
public class SignalCdnUrl extends SignalUrl {
public SignalCdnUrl(String url, TrustStore trustStore) {
super(url, trustStore);
}
public SignalCdnUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
super(url, hostHeader, trustStore, connectionSpec);
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.configuration;
import org.whispersystems.signalservice.api.push.TrustStore;
import okhttp3.ConnectionSpec;
public class SignalContactDiscoveryUrl extends SignalUrl {
public SignalContactDiscoveryUrl(String url, TrustStore trustStore) {
super(url, trustStore);
}
public SignalContactDiscoveryUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
super(url, hostHeader, trustStore, connectionSpec);
}
}

View File

@ -0,0 +1,27 @@
package org.whispersystems.signalservice.internal.configuration;
public class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final SignalCdnUrl[] signalCdnUrls;
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls) {
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrls = signalCdnUrls;
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
}
public SignalServiceUrl[] getSignalServiceUrls() {
return signalServiceUrls;
}
public SignalCdnUrl[] getSignalCdnUrls() {
return signalCdnUrls;
}
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
return signalContactDiscoveryUrls;
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.configuration;
import org.whispersystems.signalservice.api.push.TrustStore;
import okhttp3.ConnectionSpec;
public class SignalServiceUrl extends SignalUrl {
public SignalServiceUrl(String url, TrustStore trustStore) {
super(url, trustStore);
}
public SignalServiceUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
super(url, hostHeader, trustStore, connectionSpec);
}
}

View File

@ -0,0 +1,53 @@
package org.whispersystems.signalservice.internal.configuration;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.TrustManager;
import okhttp3.ConnectionSpec;
public class SignalUrl {
private final String url;
private final Optional<String> hostHeader;
private final Optional<ConnectionSpec> connectionSpec;
private TrustStore trustStore;
public SignalUrl(String url, TrustStore trustStore) {
this(url, null, trustStore, null);
}
public SignalUrl(String url, String hostHeader,
TrustStore trustStore,
ConnectionSpec connectionSpec)
{
this.url = url;
this.hostHeader = Optional.fromNullable(hostHeader);
this.trustStore = trustStore;
this.connectionSpec = Optional.fromNullable(connectionSpec);
}
public Optional<String> getHostHeader() {
return hostHeader;
}
public String getUrl() {
return url;
}
public TrustStore getTrustStore() {
return trustStore;
}
public Optional<List<ConnectionSpec>> getConnectionSpecs() {
return connectionSpec.isPresent() ? Optional.of(Collections.singletonList(connectionSpec.get())) : Optional.<List<ConnectionSpec>>absent();
}
}

View File

@ -0,0 +1,148 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.threeten.bp.Instant;
import org.threeten.bp.LocalDateTime;
import org.threeten.bp.Period;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ContactDiscoveryCipher {
private static final int TAG_LENGTH_BYTES = 16;
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
private static final long SIGNATURE_BODY_VERSION = 3L;
public DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
try {
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
for (String address : addressBook) {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
byte[] requestData = requestDataStream.toByteArray();
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce));
cipher.updateAAD(remoteAttestation.getRequestId());
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]);
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
}
public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
throws UnauthenticatedQuoteException
{
try {
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
}
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
}
if (quote.isDebugQuote()) {
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
}
} catch (IOException e) {
throw new UnauthenticatedQuoteException(e);
}
}
public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
throws SignatureException
{
if (certificates == null || certificates.isEmpty()) {
throw new SignatureException("No certificates.");
}
try {
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
signingCertificate.verifySignature(signatureBody, signature);
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
}
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
}
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
}
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
.plus(Period.ofDays(1))
.isBefore(Instant.now()))
{
throw new SignatureException("Signature is expired");
}
} catch (CertificateException | CertPathValidatorException | IOException e) {
throw new SignatureException(e);
}
}
private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
}

View File

@ -0,0 +1,136 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class Quote {
private static final long SGX_FLAGS_INITTED = 0x0000_0000_0000_0001L;
private static final long SGX_FLAGS_DEBUG = 0x0000_0000_0000_0002L;
private static final long SGX_FLAGS_MODE64BIT = 0x0000_0000_0000_0004L;
private static final long SGX_FLAGS_PROVISION_KEY = 0x0000_0000_0000_0004L;
private static final long SGX_FLAGS_EINITTOKEN_KEY = 0x0000_0000_0000_0004L;
private static final long SGX_FLAGS_RESERVED = 0xFFFF_FFFF_FFFF_FFC8L;
private static final long SGX_XFRM_LEGACY = 0x0000_0000_0000_0003L;
private static final long SGX_XFRM_AVX = 0x0000_0000_0000_0006L;
private static final long SGX_XFRM_RESERVED = 0xFFFF_FFFF_FFFF_FFF8L;
private final int version;
private final boolean isSigLinkable;
private final long gid;
private final int qeSvn;
private final int pceSvn;
private final byte[] basename = new byte[32];
private final byte[] cpuSvn = new byte[16];
private final long flags;
private final long xfrm;
private final byte[] mrenclave = new byte[32];
private final byte[] mrsigner = new byte[32];
private final int isvProdId;
private final int isvSvn;
private final byte[] reportData = new byte[64];
private final byte[] signature;
private final byte[] quoteBytes;
public Quote(byte[] quoteBytes) throws InvalidQuoteFormatException {
this.quoteBytes = quoteBytes;
ByteBuffer quoteBuf = ByteBuffer.wrap(quoteBytes);
quoteBuf.order(ByteOrder.LITTLE_ENDIAN);
this.version = quoteBuf.getShort(0) & 0xFFFF;
if (!(version >= 1 && version <= 2)) {
throw new InvalidQuoteFormatException("unknown_quote_version "+version);
}
int sign_type = quoteBuf.getShort(2) & 0xFFFF;
if ((sign_type & ~1) != 0) {
throw new InvalidQuoteFormatException("unknown_quote_sign_type "+sign_type);
}
this.isSigLinkable = sign_type == 1;
this.gid = quoteBuf.getInt(4) & 0xFFFF_FFFF;
this.qeSvn = quoteBuf.getShort(8) & 0xFFFF;
if (version > 1) {
this.pceSvn = quoteBuf.getShort(10) & 0xFFFF;
} else {
readZero(quoteBuf, 10, 2);
this.pceSvn = 0;
}
readZero(quoteBuf, 12, 4); // xeid (reserved)
read(quoteBuf, 16, basename);
//
// report_body
//
read(quoteBuf, 48, cpuSvn);
readZero(quoteBuf, 64, 4); // misc_select (reserved)
readZero(quoteBuf, 68, 28); // reserved1
this.flags = quoteBuf.getLong(96);
if ((flags & SGX_FLAGS_RESERVED ) != 0 ||
(flags & SGX_FLAGS_INITTED ) == 0 ||
(flags & SGX_FLAGS_MODE64BIT) == 0) {
throw new InvalidQuoteFormatException("bad_quote_flags "+flags);
}
this.xfrm = quoteBuf.getLong(104);
if ((xfrm & SGX_XFRM_RESERVED) != 0) {
throw new InvalidQuoteFormatException("bad_quote_xfrm "+xfrm);
}
read(quoteBuf, 112, mrenclave);
readZero(quoteBuf, 144, 32); // reserved2
read(quoteBuf, 176, mrsigner);
readZero(quoteBuf, 208, 96); // reserved3
this.isvProdId = quoteBuf.getShort(304) & 0xFFFF;
this.isvSvn = quoteBuf.getShort(306) & 0xFFFF;
readZero(quoteBuf, 308, 60); // reserved4
read(quoteBuf, 368, reportData);
// quote signature
int sig_len = quoteBuf.getInt(432) & 0xFFFF_FFFF;
if (sig_len != quoteBytes.length - 436) {
throw new InvalidQuoteFormatException("bad_quote_sig_len "+sig_len);
}
this.signature = new byte[sig_len];
read(quoteBuf, 436, signature);
}
public byte[] getReportData() {
return reportData;
}
private void read(ByteBuffer quoteBuf, int pos, byte[] buf) {
quoteBuf.position(pos);
quoteBuf.get(buf);
}
private void readZero(ByteBuffer quoteBuf, int pos, int count) {
byte[] zeroBuf = new byte[count];
read(quoteBuf, pos, zeroBuf);
for (int zeroBufIdx = 0; zeroBufIdx < count; zeroBufIdx++) {
if (zeroBuf[zeroBufIdx] != 0) {
throw new IllegalArgumentException("quote_reserved_mismatch "+pos);
}
}
}
public byte[] getQuoteBytes() {
return quoteBytes;
}
public byte[] getMrenclave() {
return mrenclave;
}
public boolean isDebugQuote() {
return (flags & SGX_FLAGS_DEBUG) != 0;
}
public static class InvalidQuoteFormatException extends Exception {
public InvalidQuoteFormatException(String value) {
super(value);
}
}
}

View File

@ -0,0 +1,20 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
public class RemoteAttestation {
private final byte[] requestId;
private final RemoteAttestationKeys keys;
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
this.requestId = requestId;
this.keys = keys;
}
public byte[] getRequestId() {
return requestId;
}
public RemoteAttestationKeys getKeys() {
return keys;
}
}

View File

@ -0,0 +1,35 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
public class RemoteAttestationKeys {
private final byte[] clientKey = new byte[32];
private final byte[] serverKey = new byte[32];
public RemoteAttestationKeys(Curve25519KeyPair keyPair, byte[] serverPublicEphemeral, byte[] serverPublicStatic) {
byte[] ephemeralToEphemeral = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(serverPublicEphemeral, keyPair.getPrivateKey());
byte[] ephemeralToStatic = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(serverPublicStatic, keyPair.getPrivateKey());
byte[] masterSecret = ByteUtil.combine(ephemeralToEphemeral, ephemeralToStatic );
byte[] publicKeys = ByteUtil.combine(keyPair.getPublicKey(), serverPublicEphemeral, serverPublicStatic);
HKDFv3 generator = new HKDFv3();
byte[] keys = generator.deriveSecrets(masterSecret, publicKeys, null, clientKey.length + serverKey.length);
System.arraycopy(keys, 0, clientKey, 0, clientKey.length);
System.arraycopy(keys, clientKey.length, serverKey, 0, serverKey.length);
}
public byte[] getClientKey() {
return clientKey;
}
public byte[] getServerKey() {
return serverKey;
}
}

View File

@ -0,0 +1,34 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SignatureBodyEntity {
@JsonProperty
private byte[] isvEnclaveQuoteBody;
@JsonProperty
private String isvEnclaveQuoteStatus;
@JsonProperty
private Long version;
@JsonProperty
private String timestamp;
public byte[] getIsvEnclaveQuoteBody() {
return isvEnclaveQuoteBody;
}
public String getIsvEnclaveQuoteStatus() {
return isvEnclaveQuoteStatus;
}
public Long getVersion() {
return version;
}
public String getTimestamp() {
return timestamp;
}
}

View File

@ -0,0 +1,73 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.net.URLDecoder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class SigningCertificate {
private final CertPath path;
public SigningCertificate(String certificateChain, KeyStore trustStore)
throws CertificateException, CertPathValidatorException
{
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
List<X509Certificate> certificates = new LinkedList<>(certificatesCollection);
PKIXParameters pkixParameters = new PKIXParameters(trustStore);
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
this.path = certificateFactory.generateCertPath(certificates);
pkixParameters.setRevocationEnabled(false);
validator.validate(path, pkixParameters);
verifyDistinguishedName(path);
} catch (KeyStoreException | InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public void verifySignature(String body, String encodedSignature)
throws SignatureException
{
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(path.getCertificates().get(0));
signature.update(body.getBytes());
if (!signature.verify(Base64.decode(encodedSignature.getBytes()))) {
throw new SignatureException("Signature verification failed.");
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private void verifyDistinguishedName(CertPath path) throws CertificateException {
X509Certificate leaf = (X509Certificate) path.getCertificates().get(0);
String distinguishedName = leaf.getSubjectX500Principal().getName();
if (!"CN=Intel SGX Attestation Report Signing,O=Intel Corporation,L=Santa Clara,ST=CA,C=US".equals(distinguishedName)) {
throw new CertificateException("Bad DN: " + distinguishedName);
}
}
}

View File

@ -0,0 +1,12 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
public class UnauthenticatedQuoteException extends Exception {
public UnauthenticatedQuoteException(String s) {
super(s);
}
public UnauthenticatedQuoteException(Exception nested) {
super(nested);
}
}

View File

@ -0,0 +1,11 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
public class UnauthenticatedResponseException extends Exception {
public UnauthenticatedResponseException(Exception e) {
super(e);
}
public UnauthenticatedResponseException(String s) {
super(s);
}
}

Some files were not shown because too many files have changed in this diff Show More