Delay processing FCM uninstalled feedback

Check to make sure client is not still active before unregistering,
since FCM feedback seems to be often erroneous
This commit is contained in:
Moxie Marlinspike 2019-05-06 21:06:18 -07:00
parent 92ca8862e1
commit 4d9c9206cf
13 changed files with 259 additions and 63 deletions

View File

@ -210,7 +210,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ActiveUserCounter activeUserCounter = new ActiveUserCounter(config.getMetricsFactory(), cacheClient);
DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryReconciliationClient, directory);
AccountCleaner accountCleaner = new AccountCleaner(accountsManager, directoryQueue);
List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = Arrays.asList(activeUserCounter, directoryReconciler, accountCleaner);
PushFeedbackProcessor pushFeedbackProcessor = new PushFeedbackProcessor(accountsManager, directoryQueue);
List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = List.of(pushFeedbackProcessor, activeUserCounter, directoryReconciler, accountCleaner);
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheClient);
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accounts, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs());

View File

@ -17,6 +17,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.HashMap;
@ -111,19 +112,20 @@ public class GCMSender implements Managed {
GcmMessage message = (GcmMessage)result.getContext();
logger.warn("Got GCM unregistered notice! " + message.getGcmId());
// Optional<Account> account = getAccountForEvent(message);
//
// if (account.isPresent()) {
// Device device = account.get().getDevice(message.getDeviceId()).get();
Optional<Account> account = getAccountForEvent(message);
if (account.isPresent()) {
Device device = account.get().getDevice(message.getDeviceId()).get();
device.setUninstalledFeedbackTimestamp(Util.todayInMillis());
// device.setGcmId(null);
// device.setFetchesMessages(false);
//
// accountsManager.update(account.get());
//
accountsManager.update(account.get());
// if (!account.get().isActive()) {
// directoryQueue.deleteRegisteredUser(account.get().getNumber());
// }
// }
}
unregistered.mark();
}

View File

@ -93,7 +93,7 @@ public class Account implements Principal {
}
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false));
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false, 0));
}
public Set<Device> getDevices() {

View File

@ -55,6 +55,9 @@ public class Device {
@JsonProperty
private long pushTimestamp;
@JsonProperty
private long uninstalledFeedback;
@JsonProperty
private boolean fetchesMessages;
@ -83,7 +86,7 @@ public class Device {
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, String userAgent,
boolean unauthenticatedDelivery)
boolean unauthenticatedDelivery, long uninstalledFeedback)
{
this.id = id;
this.name = name;
@ -100,6 +103,7 @@ public class Device {
this.created = created;
this.userAgent = userAgent;
this.unauthenticatedDelivery = unauthenticatedDelivery;
this.uninstalledFeedback = uninstalledFeedback;
}
public String getApnId() {
@ -122,6 +126,14 @@ public class Device {
this.voipApnId = voipApnId;
}
public void setUninstalledFeedbackTimestamp(long uninstalledFeedback) {
this.uninstalledFeedback = uninstalledFeedback;
}
public long getUninstalledFeedbackTimestamp() {
return uninstalledFeedback;
}
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
}

View File

@ -0,0 +1,69 @@
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
public class PushFeedbackProcessor implements AccountDatabaseCrawlerListener {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter expired = metricRegistry.meter(name(getClass(), "unregistered", "expired"));
private final Meter recovered = metricRegistry.meter(name(getClass(), "unregistered", "recovered"));
private final AccountsManager accountsManager;
private final DirectoryQueue directoryQueue;
public PushFeedbackProcessor(AccountsManager accountsManager, DirectoryQueue directoryQueue) {
this.accountsManager = accountsManager;
this.directoryQueue = directoryQueue;
}
@Override
public void onCrawlStart() {}
@Override
public void onCrawlChunk(Optional<String> fromNumber, List<Account> chunkAccounts) {
for (Account account : chunkAccounts) {
boolean update = false;
for (Device device : account.getDevices()) {
if (device.getUninstalledFeedbackTimestamp() != 0 &&
device.getUninstalledFeedbackTimestamp() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis())
{
if (device.getLastSeen() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis()) {
device.setGcmId(null);
device.setApnId(null);
device.setVoipApnId(null);
device.setFetchesMessages(false);
expired.mark();
} else {
device.setUninstalledFeedbackTimestamp(0);
recovered.mark();
}
update = true;
}
}
if (update) {
accountsManager.update(account);
if (!account.isEnabled()) {
directoryQueue.deleteRegisteredUser(account.getNumber());
}
}
}
}
@Override
public void onCrawlEnd(Optional<String> fromNumber) {}
}

View File

@ -80,13 +80,13 @@ public class MessageControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true, 0));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList, "1234".getBytes());

View File

@ -1,9 +1,7 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.hamcrest.MatcherAssert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -25,7 +23,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.*;
@ -55,8 +52,8 @@ public class TransparentDataControllerTest {
@Before
public void setup() {
Account accountOne = new Account("+14151231111", Collections.singleton(new Device(1, "foo", "bar", "salt", "keykey", "gcm-id", "apn-id", "voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]);
Account accountTwo = new Account("+14151232222", Collections.singleton(new Device(1, "2foo", "2bar", "2salt", "2keykey", "2gcm-id", "2apn-id", "2voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]);
Account accountOne = new Account("+14151231111", Collections.singleton(new Device(1, "foo", "bar", "salt", "keykey", "gcm-id", "apn-id", "voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true, 0)), new byte[16]);
Account accountTwo = new Account("+14151232222", Collections.singleton(new Device(1, "2foo", "2bar", "2salt", "2keykey", "2gcm-id", "2apn-id", "2voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true, 0)), new byte[16]);
accountOne.setProfileName("OneProfileName");
accountOne.setIdentityKey("identity_key_value");

View File

@ -13,6 +13,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Optional;
@ -48,45 +49,45 @@ public class GCMSenderTest {
verify(sender, times(1)).send(any(Message.class), eq(message));
}
// @Test
// public void testSendError() {
// String destinationNumber = "+12223334444";
// String gcmId = "foo";
//
// AccountsManager accountsManager = mock(AccountsManager.class);
// Sender sender = mock(Sender.class );
// Result invalidResult = mock(Result.class );
// DirectoryQueue directoryQueue = mock(DirectoryQueue.class );
// SynchronousExecutorService executorService = new SynchronousExecutorService();
//
// Account destinationAccount = mock(Account.class);
// Device destinationDevice = mock(Device.class );
//
// when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
// when(accountsManager.get(destinationNumber)).thenReturn(Optional.of(destinationAccount));
// when(destinationDevice.getGcmId()).thenReturn(gcmId);
//
// when(invalidResult.isInvalidRegistrationId()).thenReturn(true);
// when(invalidResult.isUnregistered()).thenReturn(false);
// when(invalidResult.hasCanonicalRegistrationId()).thenReturn(false);
// when(invalidResult.isSuccess()).thenReturn(true);
//
// GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false);
// GCMSender gcmSender = new GCMSender(accountsManager, sender, directoryQueue, executorService);
//
// SettableFuture<Result> invalidFuture = SettableFuture.create();
// invalidFuture.set(invalidResult);
//
// when(sender.send(any(Message.class), Matchers.anyObject())).thenReturn(invalidFuture);
// when(invalidResult.getContext()).thenReturn(message);
//
// gcmSender.sendMessage(message);
//
// verify(sender, times(1)).send(any(Message.class), eq(message));
// verify(accountsManager, times(1)).get(eq(destinationNumber));
// verify(accountsManager, times(1)).update(eq(destinationAccount));
// verify(destinationDevice, times(1)).setGcmId(eq((String)null));
// }
@Test
public void testSendUninstalled() {
String destinationNumber = "+12223334444";
String gcmId = "foo";
AccountsManager accountsManager = mock(AccountsManager.class);
Sender sender = mock(Sender.class );
Result invalidResult = mock(Result.class );
DirectoryQueue directoryQueue = mock(DirectoryQueue.class );
SynchronousExecutorService executorService = new SynchronousExecutorService();
Account destinationAccount = mock(Account.class);
Device destinationDevice = mock(Device.class );
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
when(accountsManager.get(destinationNumber)).thenReturn(Optional.of(destinationAccount));
when(destinationDevice.getGcmId()).thenReturn(gcmId);
when(invalidResult.isInvalidRegistrationId()).thenReturn(true);
when(invalidResult.isUnregistered()).thenReturn(false);
when(invalidResult.hasCanonicalRegistrationId()).thenReturn(false);
when(invalidResult.isSuccess()).thenReturn(true);
GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false);
GCMSender gcmSender = new GCMSender(accountsManager, sender, directoryQueue, executorService);
SettableFuture<Result> invalidFuture = SettableFuture.create();
invalidFuture.set(invalidResult);
when(sender.send(any(Message.class), Matchers.anyObject())).thenReturn(invalidFuture);
when(invalidResult.getContext()).thenReturn(message);
gcmSender.sendMessage(message);
verify(sender, times(1)).send(any(Message.class), eq(message));
verify(accountsManager, times(1)).get(eq(destinationNumber));
verify(accountsManager, times(1)).update(eq(destinationAccount));
verify(destinationDevice, times(1)).setUninstalledFeedbackTimestamp(eq(Util.todayInMillis()));
}
@Test
public void testCanonicalId() {

View File

@ -241,7 +241,7 @@ public class AccountsTest {
private Device generateDevice(long id) {
Random random = new Random(System.currentTimeMillis());
SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt());
return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt(), random.nextBoolean());
return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt(), random.nextBoolean(), 0);
}
private Account generateAccount(String number) {

View File

@ -21,7 +21,7 @@ public class PublicAccountTest {
@Test
public void testPinSanitation() throws IOException {
Set<Device> devices = Collections.singleton(new Device(1, "foo", "bar", "12345", null, "gcm-1234", null, null, true, 1234, new SignedPreKey(1, "public-foo", "signature-foo"), 31337, 31336, "Android4Life", true));
Set<Device> devices = Collections.singleton(new Device(1, "foo", "bar", "12345", null, "gcm-1234", null, null, true, 1234, new SignedPreKey(1, "public-foo", "signature-foo"), 31337, 31336, "Android4Life", true, 0));
Account account = new Account("+14151231234", devices, new byte[16]);
account.setPin("123456");

View File

@ -0,0 +1,114 @@
package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Before;
import org.junit.Test;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.mockito.Mockito.*;
public class PushFeedbackProcessorTest {
private AccountsManager accountsManager = mock(AccountsManager.class);
private DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
private Account uninstalledAccount = mock(Account.class);
private Account mixedAccount = mock(Account.class);
private Account freshAccount = mock(Account.class);
private Account cleanAccount = mock(Account.class);
private Account stillActiveAccount = mock(Account.class);
private Device uninstalledDevice = mock(Device.class);
private Device uninstalledDeviceTwo = mock(Device.class);
private Device installedDevice = mock(Device.class);
private Device installedDeviceTwo = mock(Device.class);
private Device recentUninstalledDevice = mock(Device.class);
private Device stillActiveDevice = mock(Device.class);
@Before
public void setup() {
when(uninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(uninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(uninstalledDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(3));
when(uninstalledDeviceTwo.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(3));
when(installedDevice.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(installedDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(recentUninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(1));
when(recentUninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis());
when(stillActiveDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(stillActiveDevice.getLastSeen()).thenReturn(Util.todayInMillis());
when(uninstalledAccount.getDevices()).thenReturn(Set.of(uninstalledDevice));
when(mixedAccount.getDevices()).thenReturn(Set.of(installedDevice, uninstalledDeviceTwo));
when(freshAccount.getDevices()).thenReturn(Set.of(recentUninstalledDevice));
when(cleanAccount.getDevices()).thenReturn(Set.of(installedDeviceTwo));
when(stillActiveAccount.getDevices()).thenReturn(Set.of(stillActiveDevice));
}
@Test
public void testEmpty() {
PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, directoryQueue);
processor.onCrawlChunk(Optional.of("+14152222222"), Collections.emptyList());
verifyZeroInteractions(accountsManager);
verifyZeroInteractions(directoryQueue);
}
@Test
public void testUpdate() {
PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, directoryQueue);
processor.onCrawlChunk(Optional.of("+14153333333"), List.of(uninstalledAccount, mixedAccount, stillActiveAccount, freshAccount, cleanAccount));
verify(uninstalledDevice).setApnId(isNull());
verify(uninstalledDevice).setGcmId(isNull());
verify(uninstalledDevice).setFetchesMessages(eq(false));
verify(accountsManager).update(eq(uninstalledAccount));
verify(uninstalledDeviceTwo).setApnId(isNull());
verify(uninstalledDeviceTwo).setGcmId(isNull());
verify(uninstalledDeviceTwo).setFetchesMessages(eq(false));
verify(installedDevice, never()).setApnId(any());
verify(installedDevice, never()).setGcmId(any());
verify(installedDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager).update(eq(mixedAccount));
verify(recentUninstalledDevice, never()).setApnId(any());
verify(recentUninstalledDevice, never()).setGcmId(any());
verify(recentUninstalledDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager, never()).update(eq(freshAccount));
verify(installedDeviceTwo, never()).setApnId(any());
verify(installedDeviceTwo, never()).setGcmId(any());
verify(installedDeviceTwo, never()).setFetchesMessages(anyBoolean());
verify(accountsManager, never()).update(eq(cleanAccount));
verify(stillActiveDevice).setUninstalledFeedbackTimestamp(eq(0L));
verify(stillActiveDevice, never()).setApnId(any());
verify(stillActiveDevice, never()).setGcmId(any());
verify(stillActiveDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager).update(eq(stillActiveAccount));
}
}

View File

@ -1 +1 @@
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}

View File

@ -1 +1 @@
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}