Use upstream provisioning functionality

This commit is contained in:
AsamK 2026-07-30 21:44:34 +02:00
parent 1e59c6f6cc
commit b3e3eda4f7
10 changed files with 302 additions and 116 deletions

View File

@ -1,9 +1,10 @@
[versions]
slf4j = "2.0.18"
coroutines = "1.10.2"
junit = "6.1.2"
micronaut-json-schema = "2.1.0"
micronaut-core = "5.1.10"
signal-service = "2.15.3_unofficial_150"
signal-service = "2.15.3_unofficial_151"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
@ -19,6 +20,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.6.1"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
hikari = "com.zaxxer:HikariCP:7.1.0"

View File

@ -30,6 +30,7 @@ dependencies {
implementation(libs.slf4j.api)
implementation(libs.sqlite)
implementation(libs.hikari)
compileOnly(libs.kotlinx.coroutines.core)
testImplementation(libs.junit.jupiter)
testImplementation(platform(libs.junit.jupiter.bom))

View File

@ -17,6 +17,7 @@ import org.asamk.signal.manager.util.Utils;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.Base64;
import org.signal.core.util.crypto.DeviceNameCipher;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.NoSessionException;
@ -40,7 +41,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
@ -48,6 +48,7 @@ import org.whispersystems.signalservice.internal.push.SyncMessage;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -519,18 +520,22 @@ public class AccountHelper {
}
public void setDeviceName(String deviceName) {
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
account.setEncryptedDeviceName(encryptedDeviceName);
}
public void setDeviceName(int deviceId, String deviceName) throws IOException {
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, deviceId));
context.getSyncHelper().sendDeviceNameChange(deviceId);
}
private String getEncryptedDeviceName(final String deviceName) {
final var identityKey = account.getAciIdentityKeyPair();
return Base64.encodeWithoutPadding(DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8),
identityKey));
}
public void refreshDeviceName() throws IOException {
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.getDevices(cont));

View File

@ -98,8 +98,10 @@ import org.asamk.signal.manager.util.StickerUtils;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.Base64;
import org.signal.core.util.Hex;
import org.signal.core.util.crypto.DeviceName;
import org.signal.core.util.crypto.DeviceNameCipher;
import org.signal.libsignal.net.LinkedDevice;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.usernames.BaseUsernameException;
@ -120,10 +122,8 @@ 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.DeviceInfo;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Util;
@ -484,24 +484,26 @@ public class ManagerImpl implements Manager {
@Override
public List<Device> getLinkedDevices() throws IOException {
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
final List<LinkedDevice> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.getDevices(cont));
account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
var identityKey = account.getAciIdentityKeyPair();
return devices.stream().map(d -> {
String deviceName = d.getName();
if (deviceName != null) {
String deviceName = null;
if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) {
try {
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
} catch (IOException e) {
deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()),
identityKey), StandardCharsets.UTF_8);
} catch (Exception e) {
logger.debug("Failed to decrypt device name, maybe plain text?", e);
deviceName = new String(d.getEncryptedName(), StandardCharsets.UTF_8);
}
}
final var createdAt = getPlaintextCreatedAt(d);
return new Device(d.getId(),
deviceName,
createdAt == null ? 0 : createdAt,
d.getLastSeen(),
d.getLastSeen().toEpochMilli(),
d.getId() == account.getDeviceId());
}).toList();
}
@ -521,7 +523,7 @@ public class ManagerImpl implements Manager {
}
}
private Long getPlaintextCreatedAt(DeviceInfo d) {
private Long getPlaintextCreatedAt(LinkedDevice d) {
final var DECRYPTION_INFO = "deviceCreatedAt";
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
try {
@ -529,8 +531,9 @@ public class ManagerImpl implements Manager {
var associatedData = new ByteArrayOutputStream();
associatedData.write(d.getId());
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext()
.getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray());
var createdAtPlaintext = identityKey.open(d.getCreatedAtCiphertext(),
DECRYPTION_INFO,
associatedData.toByteArray());
return ByteBuffer.wrap(createdAtPlaintext).getLong();
} catch (Exception e) {
logger.warn("Failed while reading the protobuf.", e);

View File

@ -19,34 +19,57 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.Settings;
import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.UserAlreadyExistsException;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.accounts.AccountsStore;
import org.asamk.signal.manager.util.KeyUtils;
import org.signal.core.models.AccountEntropyPool;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.models.backup.MediaRootBackupKey;
import org.signal.core.util.crypto.DeviceNameCipher;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.DeviceAttributes;
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import kotlin.ResultKt;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import kotlin.jvm.functions.Function3;
import kotlinx.coroutines.BuildersKt;
import kotlinx.coroutines.CoroutineScope;
public class ProvisioningManagerImpl implements ProvisioningManager {
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
@ -56,9 +79,10 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore;
private final ProvisioningApi provisioningApi;
private final IdentityKeyPair tempIdentityKey;
private final String password;
private final CompletableFuture<String> urlFuture = new CompletableFuture<>();
private final CompletableFuture<SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage>> messageFuture = new CompletableFuture<>();
private final Closeable socketHandle;
public ProvisioningManagerImpl(
PathConfig pathConfig,
@ -73,35 +97,58 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
this.newManagerListener = newManagerListener;
this.accountsStore = accountsStore;
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair();
password = KeyUtils.createPassword();
final var credentialsProvider = new DynamicCredentialsProvider(null,
null,
null,
password,
SignalServiceAddress.DEFAULT_DEVICE_ID);
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider,
userAgent,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
userAgent);
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false),
tempIdentityKey,
serviceEnvironmentConfig.signalServiceConfiguration(),
(id, t) -> {
urlFuture.completeExceptionally(t);
messageFuture.completeExceptionally(t);
},
new ProvisioningBlock());
}
@Override
public URI getDeviceLinkUri() throws TimeoutException, IOException {
var deviceUuid = provisioningApi.getNewDeviceUuid();
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
try {
var url = urlFuture.get(30, TimeUnit.SECONDS);
return new URI(url);
} catch (java.util.concurrent.TimeoutException e) {
throw new TimeoutException("Timed out waiting for provisioning URL");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for provisioning URL", e);
} catch (ExecutionException e) {
throw new IOException("Failed to get provisioning URL", e.getCause());
} catch (URISyntaxException e) {
throw new IOException("Invalid provisioning URL", e);
}
}
@Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
var aci = ret.getAci();
var pni = ret.getPni();
SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage> decryptResult;
try {
decryptResult = messageFuture.get(120, TimeUnit.SECONDS);
} catch (java.util.concurrent.TimeoutException e) {
throw new TimeoutException("Timed out waiting for provisioning message");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for provisioning message", e);
} catch (ExecutionException e) {
throw new IOException("Failed to receive provisioning message", e.getCause());
}
if (!(decryptResult instanceof SecondaryProvisioningCipher.ProvisioningDecryptResult.Success<ProvisionMessage> success)) {
throw new IOException("Failed to decrypt provisioning message");
}
var msg = success.getMessage();
var number = msg.number;
var aci = ACI.parseOrThrow(msg.aci, msg.aciBinary);
var pni = PNI.parseOrThrow(msg.pni, msg.pniBinary);
logger.info("Received link information from {}, linking in progress ...", number);
@ -120,11 +167,28 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
accountsStore.updateAccount(accountPath, number, aci);
}
final IdentityKeyPair aciIdentity;
final IdentityKeyPair pniIdentity;
final ProfileKey profileKey;
try {
aciIdentity = new IdentityKeyPair(new IdentityKey(msg.aciIdentityKeyPublic.toByteArray()),
new ECPrivateKey(msg.aciIdentityKeyPrivate.toByteArray()));
pniIdentity = new IdentityKeyPair(new IdentityKey(msg.pniIdentityKeyPublic.toByteArray()),
new ECPrivateKey(msg.pniIdentityKeyPrivate.toByteArray()));
profileKey = msg.profileKey == null
? KeyUtils.createProfileKey()
: new ProfileKey(msg.profileKey.toByteArray());
} catch (Exception e) {
throw new IOException("Invalid key material in provisioning message", e);
}
var encryptedDeviceName = deviceName == null
? null
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
// Create new account with the synced identity
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
: DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity);
var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool);
var mediaRootBackupKey = msg.mediaRootBackupKey == null
? null
: new MediaRootBackupKey(msg.mediaRootBackupKey.toByteArray());
SignalAccount account = null;
try {
@ -142,22 +206,39 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
pni,
password,
encryptedDeviceName,
ret.getAciIdentity(),
ret.getPniIdentity(),
aciIdentity,
pniIdentity,
profileKey,
ret.getAccountEntropyPool(),
ret.getMediaRootBackupKey());
accountEntropyPool,
mediaRootBackupKey);
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
if (Boolean.TRUE.equals(msg.readReceipts)) {
account.getConfigurationStore().setReadReceipts(true);
}
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
logger.debug("Finishing new device registration");
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
account.getAccountAttributes(null),
aciPreKeys,
pniPreKeys);
final var attrs = account.getAccountAttributes(null);
final var deviceAttributes = new DeviceAttributes(attrs.getFetchesMessages(),
attrs.getRegistrationId(),
attrs.getPniRegistrationId(),
attrs.getName(),
attrs.getCapabilities());
final var unauthAccountManager = SignalServiceAccountManager.createWithStaticCredentials(
serviceEnvironmentConfig.signalServiceConfiguration(),
null,
null,
number,
SignalServiceAddress.DEFAULT_DEVICE_ID,
password,
userAgent,
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
ServiceConfig.GROUP_MAX_SIZE);
final var registerResponse = handleResponseException(unauthAccountManager.getRegistrationApi()
.registerAsSecondaryDevice(msg.provisioningCode, deviceAttributes, aciPreKeys, pniPreKeys, null));
final var deviceId = Integer.parseInt(registerResponse.getDeviceId());
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
@ -203,6 +284,11 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
}
}
@Override
public void close() throws IOException {
socketHandle.close();
}
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
final SignalAccount signalAccount;
try {
@ -243,4 +329,36 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
return false;
}
}
private class ProvisioningBlock implements Function3<CoroutineScope, ProvisioningSocket<ProvisionMessage>, Continuation<? super Unit>, Object> {
@Override
public Object invoke(
CoroutineScope scope,
ProvisioningSocket<ProvisionMessage> socket,
Continuation<? super Unit> cont
) {
Thread.ofVirtual().start(() -> {
try {
urlFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(s, c) -> socket.getProvisioningUrl(c)));
messageFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(s, c) -> socket.getProvisioningMessageDecryptResult(c)));
cont.resumeWith(Unit.INSTANCE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
failBoth(new RuntimeException(e), cont);
} catch (Throwable t) {
failBoth(t, cont);
}
});
return IntrinsicsKt.getCOROUTINE_SUSPENDED();
}
private void failBoth(Throwable t, Continuation<? super Unit> cont) {
urlFuture.completeExceptionally(t);
messageFuture.completeExceptionally(t);
cont.resumeWith(ResultKt.createFailure(t));
}
}
}

View File

@ -292,7 +292,7 @@ public class SignalAccount implements Closeable {
final ACI aci,
final PNI pni,
final String password,
final String encryptedDeviceName,
final byte[] encryptedDeviceName,
final IdentityKeyPair aciIdentity,
final IdentityKeyPair pniIdentity,
final ProfileKey profileKey,
@ -307,7 +307,7 @@ public class SignalAccount implements Closeable {
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
this.password = password;
this.profileKey = profileKey;
this.encryptedDeviceName = encryptedDeviceName;
this.encryptedDeviceName = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName);
this.aciAccountData.setIdentityKeyPair(aciIdentity);
this.pniAccountData.setIdentityKeyPair(pniIdentity);
this.registered = false;

View File

@ -31,8 +31,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
@ -40,11 +38,9 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import kotlin.ResultKt;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import kotlinx.coroutines.BuildersKt;
import okio.ByteString;
public class Utils {
@ -167,58 +163,15 @@ public class Utils {
return NetworkResultUtil.toBasicLegacy(response);
}
/**
* Bridge Kotlin suspend functions for Java callers.
*/
@SuppressWarnings("unchecked")
public static <T> T runSuspendBlocking(final Function<Continuation<? super T>, Object> call) {
final var value = new AtomicReference<T>();
final var throwable = new AtomicReference<Throwable>();
final var done = new CountDownLatch(1);
final var immediate = call.apply(new Continuation<>() {
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(final Object result) {
try {
ResultKt.throwOnFailure(result);
value.set((T) result);
} catch (Throwable t) {
throwable.set(t);
} finally {
done.countDown();
}
}
});
if (immediate != IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
ResultKt.throwOnFailure(immediate);
return (T) immediate;
}
try {
done.await();
return (T) BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
(scope, cont) -> call.apply((Continuation<? super T>) cont));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for suspend function", e);
}
final var t = throwable.get();
if (t instanceof RuntimeException e) {
throw e;
}
if (t instanceof Error e) {
throw e;
}
if (t != null) {
throw new RuntimeException(t);
}
return value.get();
}
@SuppressWarnings("unchecked")

View File

@ -1 +1 @@
0.96.3
0.99.1

View File

@ -8,7 +8,7 @@ public class BaseConfig {
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
.orElse("Signal-Android/8.15.0");
.orElse("Signal-Android/8.21.1");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION;

View File

@ -1163,7 +1163,8 @@
"type": "java.lang.Integer",
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
"allDeclaredConstructors": true,
"jniAccessible": true
},
{
"type": "java.lang.Iterable",
@ -1198,6 +1199,7 @@
},
{
"type": "java.lang.Object",
"jniAccessible": true,
"methods": [
{
"name": "equals",
@ -6142,9 +6144,30 @@
}
]
},
{
"type": "org.signal.libsignal.internal.LinkedDeviceInternal",
"jniAccessible": true,
"methods": [
{
"name": "fromNative",
"parameterTypes": [
"java.lang.Object",
"java.lang.Object",
"java.lang.Object",
"java.lang.Object",
"java.lang.Object"
]
}
]
},
{
"type": "org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
"jniAccessible": true,
"fields": [
{
"name": "nativeHandle"
}
],
"methods": [
{
"name": "unsafeNativeHandleWithoutGuard",
@ -6152,6 +6175,9 @@
}
]
},
{
"type": "org.signal.libsignal.net.AuthenticatedChatConnection"
},
{
"type": "org.signal.libsignal.net.CdsiLookupResponse",
"jniAccessible": true,
@ -8053,6 +8079,31 @@
}
]
},
{
"type": "org.whispersystems.signalservice.api.account.DeviceAttributes",
"methods": [
{
"name": "getCapabilities",
"parameterTypes": []
},
{
"name": "getFetchesMessages",
"parameterTypes": []
},
{
"name": "getName",
"parameterTypes": []
},
{
"name": "getPniRegistrationId",
"parameterTypes": []
},
{
"name": "getRegistrationId",
"parameterTypes": []
}
]
},
{
"type": "org.whispersystems.signalservice.api.groupsv2.CredentialResponse",
"allDeclaredFields": true,
@ -8179,6 +8230,26 @@
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{
"type": "org.whispersystems.signalservice.api.messages.multidevice.RegisterAsSecondaryDeviceResponse",
"fields": [
{
"name": "deviceId"
},
{
"name": "pni"
},
{
"name": "uuid"
}
],
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.whispersystems.signalservice.api.profiles.SignalServiceProfile",
"allDeclaredFields": true,
@ -9079,6 +9150,39 @@
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{
"type": "org.whispersystems.signalservice.internal.push.RegisterAsSecondaryDeviceRequest",
"methods": [
{
"name": "getAccountAttributes",
"parameterTypes": []
},
{
"name": "getAciPqLastResortPreKey",
"parameterTypes": []
},
{
"name": "getAciSignedPreKey",
"parameterTypes": []
},
{
"name": "getGcmToken",
"parameterTypes": []
},
{
"name": "getPniPqLastResortPreKey",
"parameterTypes": []
},
{
"name": "getPniSignedPreKey",
"parameterTypes": []
},
{
"name": "getVerificationCode",
"parameterTypes": []
}
]
},
{
"type": "org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson",
"allDeclaredFields": true,