mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 01:18:04 +01:00
Introduce encrypted device creation timestamps
This commit is contained in:
@@ -7,7 +7,10 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter;
|
||||
|
||||
public record DeviceInfo(long id,
|
||||
@@ -17,9 +20,27 @@ public record DeviceInfo(long id,
|
||||
byte[] name,
|
||||
|
||||
long lastSeen,
|
||||
long created) {
|
||||
|
||||
@Deprecated
|
||||
@Schema(description = """
|
||||
The time in milliseconds since epoch when the device was linked.
|
||||
Deprecated in favor of `createdAtCiphertext`.
|
||||
""", deprecated = true)
|
||||
long created,
|
||||
|
||||
@Schema(description = "The registration ID of the given device.")
|
||||
int registrationId,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@Schema(description = """
|
||||
The ciphertext of the time in milliseconds since epoch when the device was attached
|
||||
to the parent account, encoded in standard base64 without padding.
|
||||
""")
|
||||
byte[] createdAtCiphertext) {
|
||||
|
||||
public static DeviceInfo forDevice(final Device device) {
|
||||
return new DeviceInfo(device.getId(), device.getName(), device.getLastSeen(), device.getCreated());
|
||||
return new DeviceInfo(device.getId(), device.getName(), device.getLastSeen(), device.getCreated(), device.getRegistrationId(
|
||||
IdentityType.ACI), device.getCreatedAtCiphertext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.signal.chat.device.SetPushTokenRequest;
|
||||
import org.signal.chat.device.SetPushTokenResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DeviceCapability;
|
||||
@@ -61,6 +62,8 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
.setId(device.getId())
|
||||
.setCreated(device.getCreated())
|
||||
.setLastSeen(device.getLastSeen())
|
||||
.setRegistrationId(device.getRegistrationId(IdentityType.ACI))
|
||||
.setCreatedAtCiphertext(ByteString.copyFrom(device.getCreatedAtCiphertext()))
|
||||
.build());
|
||||
})
|
||||
.map(GetDevicesResponse.Builder::build);
|
||||
|
||||
@@ -308,7 +308,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
account.setNumber(number, pni);
|
||||
account.setIdentityKey(aciIdentityKey);
|
||||
account.setPhoneNumberIdentityKey(pniIdentityKey);
|
||||
account.addDevice(primaryDeviceSpec.toDevice(Device.PRIMARY_ID, clock));
|
||||
account.addDevice(primaryDeviceSpec.toDevice(Device.PRIMARY_ID, clock, aciIdentityKey));
|
||||
account.setRegistrationLockFromAttributes(accountAttributes);
|
||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
|
||||
@@ -436,7 +436,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
final Account account = accountAndNextDeviceId.first();
|
||||
final byte nextDeviceId = accountAndNextDeviceId.second();
|
||||
|
||||
account.addDevice(deviceSpec.toDevice(nextDeviceId, clock));
|
||||
account.addDevice(deviceSpec.toDevice(nextDeviceId, clock, account.getIdentityKey(IdentityType.ACI)));
|
||||
|
||||
final List<TransactWriteItem> additionalWriteItems = new ArrayList<>(keysManager.buildWriteItemsForNewDevice(
|
||||
account.getIdentifier(IdentityType.ACI),
|
||||
|
||||
@@ -13,7 +13,6 @@ import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -21,6 +20,7 @@ import javax.annotation.Nullable;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.DeviceCapabilityAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.DeviceNameByteArrayAdapter;
|
||||
|
||||
@@ -44,6 +44,11 @@ public class Device {
|
||||
@JsonDeserialize(using = DeviceNameByteArrayAdapter.Deserializer.class)
|
||||
private byte[] name;
|
||||
|
||||
@JsonProperty("createdAt")
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
private byte[] createdAtCiphertext;
|
||||
|
||||
@JsonProperty
|
||||
private String authToken;
|
||||
|
||||
@@ -110,6 +115,14 @@ public class Device {
|
||||
return this.created;
|
||||
}
|
||||
|
||||
public void setCreatedAtCiphertext(byte[] createdAtCiphertext) {
|
||||
this.createdAtCiphertext = createdAtCiphertext;
|
||||
}
|
||||
|
||||
public byte[] getCreatedAtCiphertext() {
|
||||
return this.createdAtCiphertext;
|
||||
}
|
||||
|
||||
public String getGcmId() {
|
||||
return gcmId;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.EncryptDeviceCreationTimestampUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public record DeviceSpec(
|
||||
@@ -27,7 +29,9 @@ public record DeviceSpec(
|
||||
KEMSignedPreKey aciPqLastResortPreKey,
|
||||
KEMSignedPreKey pniPqLastResortPreKey) {
|
||||
|
||||
public Device toDevice(final byte deviceId, final Clock clock) {
|
||||
public Device toDevice(final byte deviceId, final Clock clock, final IdentityKey aciIdentityKey) {
|
||||
final long created = clock.millis();
|
||||
|
||||
final Device device = new Device();
|
||||
device.setId(deviceId);
|
||||
device.setAuthTokenHash(SaltedTokenHash.generateFor(password()));
|
||||
@@ -36,7 +40,9 @@ public record DeviceSpec(
|
||||
device.setPhoneNumberIdentityRegistrationId(pniRegistrationId());
|
||||
device.setName(deviceNameCiphertext());
|
||||
device.setCapabilities(capabilities());
|
||||
device.setCreated(clock.millis());
|
||||
device.setCreated(created);
|
||||
device.setCreatedAtCiphertext(
|
||||
EncryptDeviceCreationTimestampUtil.encrypt(created, aciIdentityKey, deviceId, aciRegistrationId()));
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(signalAgent());
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class EncryptDeviceCreationTimestampUtil {
|
||||
@VisibleForTesting
|
||||
final static String ENCRYPTION_INFO = "deviceCreatedAt";
|
||||
|
||||
/**
|
||||
* Encrypts the provided timestamp with the ACI identity key using the
|
||||
* Hybrid Public Key Encryption scheme as defined in (<a href="https://www.rfc-editor.org/rfc/rfc9180.html">RFC 9180</a>).
|
||||
*
|
||||
* @param createdAt The timestamp in milliseconds since epoch when a given device was linked to the account.
|
||||
* @param aciIdentityKey The ACI identity key associated with the account.
|
||||
* @param deviceId The ID of the given device.
|
||||
* @param registrationId The registration ID of the given device.
|
||||
*
|
||||
* @return The timestamp ciphertext
|
||||
*/
|
||||
public static byte[] encrypt(
|
||||
final long createdAt,
|
||||
final IdentityKey aciIdentityKey,
|
||||
final byte deviceId,
|
||||
final int registrationId) {
|
||||
final ByteBuffer timestampBytes = ByteBuffer.allocate(8);
|
||||
timestampBytes.putLong(createdAt);
|
||||
|
||||
// "Associated data" is metadata that ties the ciphertext to a specific context to prevent an adversary from
|
||||
// swapping the ciphertext of two devices on the same account.
|
||||
final ByteBuffer associatedData = ByteBuffer.allocate(5);
|
||||
associatedData.put(deviceId);
|
||||
associatedData.putInt(registrationId);
|
||||
|
||||
return aciIdentityKey.getPublicKey().seal(timestampBytes.array(), ENCRYPTION_INFO, associatedData.array());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user