Compare commits

...

43 commits

Author SHA1 Message Date
AsamK
f6d81e3c05 Update gradle
Some checks failed
signal-cli CI / build (21) (push) Has been cancelled
signal-cli CI / build (24) (push) Has been cancelled
signal-cli CI / build-graalvm (push) Has been cancelled
signal-cli CI / build-client (macos) (push) Has been cancelled
signal-cli CI / build-client (ubuntu) (push) Has been cancelled
signal-cli CI / build-client (windows) (push) Has been cancelled
CodeQL / Analyse (push) Has been cancelled
2025-08-17 17:35:59 +02:00
AsamK
42f10670b6 Replace deprecated groovy utils 2025-08-17 17:35:14 +02:00
AsamK
b453d7a0b9 Add new svr2 mrenclave 2025-08-02 12:05:01 +02:00
AsamK
f9a36c6e04 Fix send parameters to be all camel case
Fixes #1814
2025-07-16 20:59:26 +02:00
AsamK
be48afb2b5 Fix container build 2025-07-16 20:56:02 +02:00
AsamK
a0960fcabd Prepare next release 2025-07-16 20:55:50 +02:00
AsamK
dbc454ba9e Bump version to 0.13.18 2025-07-16 19:40:10 +02:00
AsamK
2225e69277 Update sqlite-jdbc 2025-07-16 19:37:03 +02:00
AsamK
783201d12e Fix incorrect error message 2025-07-16 19:17:21 +02:00
AsamK
3e981d66e9 Fix null pointer regression 2025-07-14 18:52:41 +02:00
AsamK
7c7fc76a64 Add support for sending view once messages
Closes #1812
2025-07-14 16:42:06 +02:00
AsamK
c924d5c03a Update libsignal-service-java 2025-07-14 16:21:47 +02:00
AsamK
dc787be17b Build rust json-rpc client in CI 2025-07-12 11:57:23 +02:00
AsamK
3d4070a139 Compile UnixStream support only on unix systems 2025-07-12 11:42:12 +02:00
AsamK
dbdff83132 Update README
Fixes #1803
2025-07-12 11:09:24 +02:00
AsamK
4ce194afe2 Add missing username parameter to getUserStatus command in json-rpc client 2025-07-12 11:03:54 +02:00
AsamK
ca33249170 Handle rate limit exception correctly when querying usernames
Fixes #1797
2025-07-12 11:03:28 +02:00
AsamK
a96626c468 Update to rust 2024 edition 2025-07-12 10:16:57 +02:00
AsamK
d54be747da Remove unused dependency 2025-07-12 10:15:27 +02:00
AsamK
ff846bc678 Fix clippy warnings 2025-07-12 10:05:57 +02:00
AsamK
1b7f755590 Update dependencies 2025-07-12 10:05:14 +02:00
AsamK
887ed3bb44 Show better error message when sending fails due to missing pre keys 2025-07-08 17:35:17 +02:00
AsamK
3180eba836 Exit if account check fails at startup
Fixes #1804
2025-07-08 17:34:04 +02:00
AsamK
cb06cbdcca Shut down when dbus daemon connection goes away unexpectedly
Fixes #1800
2025-06-29 11:22:30 +02:00
AsamK
069325af47 Extend shutdown request with optional error 2025-06-29 11:22:30 +02:00
AsamK
e7ca02f1fb Prepare next release 2025-06-29 11:22:30 +02:00
AsamK
fa9bb3c210 Bump version to 0.13.17 2025-06-28 14:57:20 +02:00
AsamK
e6113d4d96 Update libsignal-service-java 2025-06-28 14:35:56 +02:00
AsamK
6cc3a6f561 Update dependencies 2025-06-25 00:20:42 +02:00
AsamK
70c79eac01 Keep all unhandled fields of remote storage record
Fixes #1792
2025-06-24 23:13:00 +02:00
AsamK
5dc66f839d Close attachment input streams after upload
Fixes #1790
2025-06-10 19:36:52 +02:00
AsamK
a0d5744c49 Improve behavior when pin data doesn't exist on the server 2025-06-08 16:22:03 +02:00
AsamK
6b60a6d5a5 Fix NPR when loading an inactive group
Fixes #1786
2025-06-08 14:48:25 +02:00
AsamK
0257344940 Prepare next release 2025-06-08 14:47:20 +02:00
AsamK
17cd99be59 Bump version to 0.13.16 2025-06-07 16:58:00 +02:00
AsamK
2f8328847c Update dependencies 2025-06-07 16:14:55 +02:00
AsamK
7e9727aa38 Update tests 2025-06-07 16:14:55 +02:00
AsamK
bf87fcc652 Ensure messages are created with a unique timestamp
Fixes #1783
2025-06-03 22:22:51 +02:00
AsamK
6b46314eab Update dependencies 2025-06-03 21:59:38 +02:00
AsamK
e89803464b Update libsignal-service 2025-06-01 21:51:03 +02:00
AsamK
a9bb8d9aae Update gradle 2025-06-01 16:11:21 +02:00
AsamK
74909408c4 Add missing reflect config
Fixes #1768
2025-05-10 10:18:03 +02:00
AsamK
bb124a922d Prepare next release 2025-05-08 22:56:55 +02:00
64 changed files with 741 additions and 377 deletions

View file

@ -69,3 +69,28 @@ jobs:
with: with:
name: signal-cli-native name: signal-cli-native
path: build/native/nativeCompile/signal-cli path: build/native/nativeCompile/signal-cli
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v4
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View file

@ -182,7 +182,7 @@ jobs:
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/ mkdir -p build/install/
mv ./signal-cli-*/ build/install/signal-cli mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image

View file

@ -1,5 +1,47 @@
# Changelog # Changelog
## [Unreleased]
## [0.13.18] - 2025-07-16
Requires libsignal-client version 0.76.3.
### Added
- Added `--view-once` parameter to send command to send view once images
### Fixed
- Handle rate limit exception correctly when querying usernames
### Improved
- Shut down when dbus daemon connection goes away unexpectedly
- In daemon mode, exit immediately if account check fails at startup
- Improve behavior when sending to devices that have no available prekeys
## [0.13.17] - 2025-06-28
Requires libsignal-client version 0.76.0.
### Fixed
- Fix issue when loading an older inactive group
- Close attachment input streams after upload
- Fix storage sync behavior with unhandled fields
### Changed
- Improve behavior when pin data doesn't exist on the server
## [0.13.16] - 2025-06-07
Requires libsignal-client version 0.73.2.
### Changed
- Ensure every sent message gets a unique timestamp
## [0.13.15] - 2025-05-08 ## [0.13.15] - 2025-05-08
Requires libsignal-client version 0.70.0. Requires libsignal-client version 0.70.0.

View file

@ -83,6 +83,12 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
``` ```
* Send a message to a username, usernames need to be prefixed with `u:`
```sh
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
```
* Pipe the message content from another process. * Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT

View file

@ -8,7 +8,7 @@ plugins {
allprojects { allprojects {
group = "org.asamk" group = "org.asamk"
version = "0.13.15" version = "0.13.19-SNAPSHOT"
} }
java { java {

View file

@ -1,12 +1,10 @@
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
import groovy.util.XmlSlurper
import groovy.util.slurpersupport.GPathResult
import org.codehaus.groovy.runtime.ResourceGroovyMethods
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import javax.xml.parsers.DocumentBuilderFactory
class CheckLibVersionsPlugin : Plugin<Project> { class CheckLibVersionsPlugin : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin<Project> {
val name = dependency.name val name = dependency.name
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try { try {
val url = ResourceGroovyMethods.toURL(metaDataUrl) val dbf = DocumentBuilderFactory.newInstance()
val metaDataText = ResourceGroovyMethods.getText(url) val db = dbf.newDocumentBuilder()
val metadata = XmlSlurper().parseText(metaDataText) val doc = db.parse(metaDataUrl);
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest") val newest = doc.getElementsByTagName("latest").item(0).textContent
if (version != newest.toString()) { if (version != newest.toString()) {
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}") println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
} }

513
client/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
[package] [package]
name = "signal-cli-client" name = "signal-cli-client"
version = "0.0.1" version = "0.0.1"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
log = "0.4"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
jsonrpsee = { version = "0.24", features = [ jsonrpsee = { version = "0.25", features = [
"macros", "macros",
"async-client", "async-client",
"http-client", "http-client",

View file

@ -15,6 +15,7 @@ pub struct Cli {
pub json_rpc_tcp: Option<Option<SocketAddr>>, pub json_rpc_tcp: Option<Option<SocketAddr>>,
/// UNIX socket address and port of signal-cli daemon /// UNIX socket address and port of signal-cli daemon
#[cfg(unix)]
#[arg(long, conflicts_with = "json_rpc_tcp")] #[arg(long, conflicts_with = "json_rpc_tcp")]
pub json_rpc_socket: Option<Option<OsString>>, pub json_rpc_socket: Option<Option<OsString>>,
@ -84,6 +85,8 @@ pub enum CliCommands {
}, },
GetUserStatus { GetUserStatus {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(long)]
username: Vec<String>,
}, },
JoinGroup { JoinGroup {
#[arg(long)] #[arg(long)]
@ -176,6 +179,9 @@ pub enum CliCommands {
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
attachment: Vec<String>, attachment: Vec<String>,
#[arg(long)]
view_once: bool,
#[arg(long)] #[arg(long)]
mention: Vec<String>, mention: Vec<String>,

View file

@ -70,6 +70,7 @@ pub trait Rpc {
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
usernames: Vec<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "joinGroup", param_kind = map)] #[method(name = "joinGroup", param_kind = map)]
@ -182,6 +183,7 @@ pub trait Rpc {
endSession: bool, endSession: bool,
message: String, message: String,
attachments: Vec<String>, attachments: Vec<String>,
viewOnce: bool,
mentions: Vec<String>, mentions: Vec<String>,
textStyle: Vec<String>, textStyle: Vec<String>,
quoteTimestamp: Option<u64>, quoteTimestamp: Option<u64>,
@ -190,10 +192,10 @@ pub trait Rpc {
quoteMention: Vec<String>, quoteMention: Vec<String>,
quoteTextStyle: Vec<String>, quoteTextStyle: Vec<String>,
quoteAttachment: Vec<String>, quoteAttachment: Vec<String>,
preview_url: Option<String>, previewUrl: Option<String>,
preview_title: Option<String>, previewTitle: Option<String>,
preview_description: Option<String>, previewDescription: Option<String>,
preview_image: Option<String>, previewImage: Option<String>,
sticker: Option<String>, sticker: Option<String>,
storyTimestamp: Option<u64>, storyTimestamp: Option<u64>,
storyAuthor: Option<String>, storyAuthor: Option<String>,
@ -409,6 +411,7 @@ pub async fn connect_tcp(
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
#[cfg(unix)]
pub async fn connect_unix( pub async fn connect_unix(
socket_path: impl AsRef<Path>, socket_path: impl AsRef<Path>,
) -> Result<impl SubscriptionClientT, std::io::Error> { ) -> Result<impl SubscriptionClientT, std::io::Error> {
@ -417,6 +420,6 @@ pub async fn connect_unix(
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> { pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
HttpClientBuilder::default().build(uri) HttpClientBuilder::default().build(uri)
} }

View file

@ -60,8 +60,13 @@ async fn handle_command(
.delete_local_account_data(cli.account, ignore_registered) .delete_local_account_data(cli.account, ignore_registered)
.await .await
} }
CliCommands::GetUserStatus { recipient } => { CliCommands::GetUserStatus {
client.get_user_status(cli.account, recipient).await recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
} }
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => { CliCommands::Link { name } => {
@ -70,7 +75,7 @@ async fn handle_command(
.await .await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri; .device_link_uri;
println!("{}", url); println!("{url}");
client.finish_link(url, name).await client.finish_link(url, name).await
} }
CliCommands::ListAccounts => client.list_accounts().await, CliCommands::ListAccounts => client.list_accounts().await,
@ -139,6 +144,7 @@ async fn handle_command(
end_session, end_session,
message, message,
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -165,6 +171,7 @@ async fn handle_command(
end_session, end_session,
message.unwrap_or_default(), message.unwrap_or_default(),
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -477,6 +484,12 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} else { } else {
#[cfg(windows)]
{
Err(RpcError::Custom("Invalid socket".into()))
}
#[cfg(unix)]
{
let socket_path = cli let socket_path = cli
.json_rpc_socket .json_rpc_socket
.clone() .clone()
@ -496,6 +509,7 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} }
} }
}
async fn stream_next( async fn stream_next(
timeout: f64, timeout: f64,

View file

@ -1,10 +1,8 @@
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream}; use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
use jsonrpsee::core::{ use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
async_trait,
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
};
use thiserror::Error; use thiserror::Error;
#[cfg(unix)]
pub mod ipc; pub mod ipc;
mod stream_codec; mod stream_codec;
pub mod tcp; pub mod tcp;
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T> for Sender<T>
{ {
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.send(body) .send(body)
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.close() .close()
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
} }
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T> for Receiver<T>
{ {
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
match self.inner.next().await { match self.inner.next().await {
None => Err(Errors::Closed), None => Err(Errors::Closed),
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)), Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))), Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
} }
} }
} }

View file

@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
match str::from_utf8(line.as_ref()) { match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())), Ok(s) => Ok(Some(s.to_string())),
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")), Err(_) => Err(io::Error::other("invalid UTF-8")),
} }
} else { } else {
Ok(None) Ok(None)

View file

@ -45,6 +45,15 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="0.13.18" date="2025-07-16">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
</release>
<release version="0.13.17" date="2025-06-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
</release>
<release version="0.13.16" date="2025-06-07">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
</release>
<release version="0.13.15" date="2025-05-08"> <release version="0.13.15" date="2025-05-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url> <url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
</release> </release>

View file

@ -27,6 +27,10 @@
{ {
"name":"java.lang.ClassNotFoundException" "name":"java.lang.ClassNotFoundException"
}, },
{
"name":"java.lang.Enum",
"methods":[{"name":"ordinal","parameterTypes":[] }]
},
{ {
"name":"java.lang.IllegalArgumentException", "name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -48,9 +52,13 @@
{ {
"name":"java.lang.String" "name":"java.lang.String"
}, },
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{ {
"name":"java.lang.Throwable", "name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] "methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.UnsatisfiedLinkError", "name":"java.lang.UnsatisfiedLinkError",
@ -88,7 +96,7 @@
}, },
{ {
"name":"org.signal.libsignal.internal.CompletableFuture", "name":"org.signal.libsignal.internal.CompletableFuture",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }] "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }]
}, },
{ {
"name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner", "name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
@ -118,6 +126,10 @@
"name":"org.signal.libsignal.net.NetworkException", "name":"org.signal.libsignal.net.NetworkException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
}, },
{
"name":"org.signal.libsignal.net.RetryLaterException",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{ {
"name":"org.signal.libsignal.protocol.DuplicateMessageException", "name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -195,6 +207,9 @@
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction", "name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}] "fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
}, },
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
},
{ {
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord", "name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}] "fields":[{"name":"unsafeHandle"}]

View file

@ -671,6 +671,10 @@
{ {
"name":"long[]" "name":"long[]"
}, },
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{ {
"name":"okio.BufferedSink" "name":"okio.BufferedSink"
}, },
@ -1409,6 +1413,12 @@
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer", "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{ {
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry", "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -1620,6 +1630,10 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@ -2988,9 +3002,11 @@
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord", "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
"allDeclaredFields":true, "allDeclaredFields":true,
"fields":[{"name":"avatarColor"}, {"name":"avatarUrlPath"}, {"name":"backupSubscriberData"}, {"name":"backupTier"}, {"name":"displayBadgesOnProfile"}, {"name":"e164"}, {"name":"familyName"}, {"name":"givenName"}, {"name":"hasBackup"}, {"name":"hasCompletedUsernameOnboarding"}, {"name":"hasSeenGroupStoryEducationSheet"}, {"name":"hasSetMyStoriesPrivacy"}, {"name":"hasViewedOnboardingStory"}, {"name":"keepMutedChatsArchived"}, {"name":"linkPreviews"}, {"name":"noteToSelfArchived"}, {"name":"noteToSelfMarkedUnread"}, {"name":"payments"}, {"name":"phoneNumberSharingMode"}, {"name":"pinnedConversations"}, {"name":"preferContactAvatars"}, {"name":"preferredReactionEmoji"}, {"name":"primarySendsSms"}, {"name":"profileKey"}, {"name":"readReceipts"}, {"name":"sealedSenderIndicators"}, {"name":"storiesDisabled"}, {"name":"storyViewReceiptsEnabled"}, {"name":"subscriberCurrencyCode"}, {"name":"subscriberId"}, {"name":"subscriptionManuallyCancelled"}, {"name":"typingIndicators"}, {"name":"universalExpireTimer"}, {"name":"unlistedPhoneNumber"}, {"name":"username"}, {"name":"usernameLink"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder"
}, },
@ -3000,6 +3016,9 @@
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData"
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$NotificationProfileManualOverride"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
}, },
@ -3021,7 +3040,6 @@
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
"allDeclaredFields":true, "allDeclaredFields":true,
"fields":[{"name":"aci"}, {"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"e164"}, {"name":"familyName"}, {"name":"givenName"}, {"name":"hidden"}, {"name":"hideStory"}, {"name":"identityKey"}, {"name":"identityState"}, {"name":"markedUnread"}, {"name":"mutedUntilTimestamp"}, {"name":"nickname"}, {"name":"note"}, {"name":"pni"}, {"name":"pniSignatureVerified"}, {"name":"profileKey"}, {"name":"systemFamilyName"}, {"name":"systemGivenName"}, {"name":"systemNickname"}, {"name":"unregisteredAtTimestamp"}, {"name":"username"}, {"name":"whitelisted"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
}, },
{ {

View file

@ -2,16 +2,16 @@
slf4j = "2.0.17" slf4j = "2.0.17"
[libraries] [libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.0" jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.18" logback = "ch.qos.logback:logback-classic:1.5.18"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_122" signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
sqlite = "org.xerial:sqlite-jdbc:3.49.1.0" sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
hikari = "com.zaxxer:HikariCP:6.3.0" hikari = "com.zaxxer:HikariCP:6.3.0"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0" junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
junit-launcher = "org.junit.platform:junit-platform-launcher:1.12.0" junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

2
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.

View file

@ -30,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
@ -95,7 +96,7 @@ public interface Manager extends Closeable {
*/ */
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException; Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames); Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
void updateAccountAttributes( void updateAccountAttributes(
String deviceName, String deviceName,
@ -140,7 +141,7 @@ public interface Manager extends Closeable {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException; ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException; void unregister() throws IOException;

View file

@ -3,6 +3,7 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -21,7 +22,7 @@ public interface RegistrationManager extends Closeable {
void verifyAccount( void verifyAccount(
String verificationCode, String verificationCode,
String pin String pin
) throws IOException, PinLockedException, IncorrectPinException; ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException; void deleteLocalAccountData() throws IOException;

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException; import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
@ -63,19 +64,28 @@ public class SignalAccountFiles {
return accountsStore.getAllNumbers(); return accountsStore.getAllNumbers();
} }
public MultiAccountManager initMultiAccountManager() throws IOException { public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> { final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
try { try {
return initManager(a.number(), a.path()); return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
} catch (NotRegisteredException | IOException | AccountCheckException e) { } catch (NotRegisteredException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null; return null;
} catch (Throwable e) { } catch (AccountCheckException | IOException e) {
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
throw e; return new Pair<Manager, Throwable>(null, e);
} }
}).filter(Objects::nonNull).toList(); }).filter(Objects::nonNull).toList();
for (final var pair : managerPairs) {
if (pair.second() instanceof IOException e) {
throw e;
} else if (pair.second() instanceof AccountCheckException e) {
throw e;
}
}
final var managers = managerPairs.stream().map(Pair::first).toList();
return new MultiAccountManagerImpl(managers, this); return new MultiAccountManagerImpl(managers, this);
} }

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI; import java.net.URI;
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
} }
ECPublicKey deviceKey; ECPublicKey deviceKey;
try { try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0); deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e); throw new InvalidDeviceLinkException("Invalid device link", e);
} }

View file

@ -6,6 +6,7 @@ import java.util.Optional;
public record Message( public record Message(
String messageText, String messageText,
List<String> attachments, List<String> attachments,
boolean viewOnce,
List<Mention> mentions, List<Mention> mentions,
Optional<Quote> quote, Optional<Quote> quote,
Optional<Sticker> sticker, Optional<Sticker> sticker,

View file

@ -0,0 +1,3 @@
package org.asamk.signal.manager.api;
public class PinLockMissingException extends Exception {}

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network.Environment; import org.signal.libsignal.net.Network.Environment;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy; import org.whispersystems.signalservice.internal.configuration.HttpProxy;
@ -29,7 +28,9 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
private static final String URL = "https://chat.signal.org"; private static final String URL = "https://chat.signal.org";
private static final String CDN_URL = "https://cdn.signal.org"; private static final String CDN_URL = "https://cdn.signal.org";
@ -79,7 +80,7 @@ class LiveConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -91,7 +92,7 @@ class LiveConfig {
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private LiveConfig() { private LiveConfig() {

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy; import org.whispersystems.signalservice.internal.configuration.HttpProxy;
@ -29,7 +28,9 @@ class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
private static final String URL = "https://chat.staging.signal.org"; private static final String URL = "https://chat.staging.signal.org";
private static final String CDN_URL = "https://cdn-staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org";
@ -79,7 +80,7 @@ class StagingConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -91,7 +92,7 @@ class StagingConfig {
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private StagingConfig() { private StagingConfig() {

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -105,7 +106,7 @@ public class AccountHelper {
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) { if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
throw new IOException("Missing PNI identity key, relinking required"); throw new IOException("Missing PNI identity key, relinking required");
} }
if (account.getPreviousStorageVersion() < 4 if (account.getPreviousStorageVersion() < 10
&& account.isPrimaryDevice() && account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) { && account.getRegistrationLockPin() != null) {
migrateRegistrationPin(); migrateRegistrationPin();
@ -185,7 +186,7 @@ public class AccountHelper {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException { ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
for (var attempts = 0; attempts < 5; attempts++) { for (var attempts = 0; attempts < 5; attempts++) {
try { try {
finishChangeNumberInternal(newNumber, verificationCode, pin); finishChangeNumberInternal(newNumber, verificationCode, pin);
@ -205,7 +206,7 @@ public class AccountHelper {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException { ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair(); final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>(); final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>(); final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();

View file

@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
@ -44,14 +45,20 @@ public class AttachmentHelper {
} }
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException { public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
var attachmentStreams = createAttachmentStreams(attachments); final var attachmentStreams = createAttachmentStreams(attachments);
try {
// Upload attachments here, so we only upload once even for multiple recipients // Upload attachments here, so we only upload once even for multiple recipients
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachmentStream : attachmentStreams) { for (final var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream)); attachmentPointers.add(uploadAttachment(attachmentStream));
} }
return attachmentPointers; return attachmentPointers;
} finally {
for (final var attachmentStream : attachmentStreams) {
attachmentStream.close();
}
}
} }
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException { private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
@ -132,9 +139,15 @@ public class AttachmentHelper {
SignalServiceAttachmentPointer pointer, SignalServiceAttachmentPointer pointer,
File tmpFile File tmpFile
) throws IOException { ) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try { try {
return dependencies.getMessageReceiver() return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); .retrieveAttachment(pointer,
tmpFile,
ServiceConfig.MAX_ATTACHMENT_SIZE,
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
} catch (MissingConfigurationException | InvalidMessageException e) { } catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e); throw new IOException(e);
} }

View file

@ -551,6 +551,9 @@ public class GroupHelper {
while (true) { while (true) {
final var page = context.getGroupV2Helper() final var page = context.getGroupV2Helper()
.getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs); .getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
if (page == null) {
break;
}
page.getChangeLogs() page.getChangeLogs()
.stream() .stream()
.map(DecryptedGroupChangeLog::getChange) .map(DecryptedGroupChangeLog::getChange)

View file

@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -119,6 +120,8 @@ class GroupV2Helper {
groupsV2AuthorizationString, groupsV2AuthorizationString,
false, false,
sendEndorsementsExpirationMs); sendEndorsementsExpirationMs);
} catch (NotInGroupException e) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.code == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);

View file

@ -41,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
@ -105,7 +106,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);
@ -143,7 +144,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);

View file

@ -88,7 +88,11 @@ public class PinHelper {
IOException exception = null; IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) { for (final var secureValueRecovery : secureValueRecoveries) {
try { try {
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
if (lockData == null) {
continue;
}
return lockData;
} catch (IOException e) { } catch (IOException e) {
exception = e; exception = e;
} }

View file

@ -17,6 +17,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
@ -68,7 +69,7 @@ public class RecipientHelper {
.toSignalServiceAddress(); .toSignalServiceAddress();
} }
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException { public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
final var recipientIds = new HashSet<RecipientId>(recipients.size()); final var recipientIds = new HashSet<RecipientId>(recipients.size());
for (var number : recipients) { for (var number : recipients) {
final var recipientId = resolveRecipient(number); final var recipientId = resolveRecipient(number);
@ -91,7 +92,11 @@ public class RecipientHelper {
} }
}); });
} else if (recipient instanceof RecipientIdentifier.Username(String username)) { } else if (recipient instanceof RecipientIdentifier.Username(String username)) {
try {
return resolveRecipientByUsernameOrLink(username, false); return resolveRecipientByUsernameOrLink(username, false);
} catch (Exception e) {
return null;
}
} }
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient); throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
} }
@ -99,7 +104,7 @@ public class RecipientHelper {
public RecipientId resolveRecipientByUsernameOrLink( public RecipientId resolveRecipientByUsernameOrLink(
String username, String username,
boolean forceRefresh boolean forceRefresh
) throws UnregisteredRecipientException { ) throws UnregisteredRecipientException, IOException {
final Username finalUsername; final Username finalUsername;
try { try {
finalUsername = getUsernameFromUsernameOrLink(username); finalUsername = getUsernameFromUsernameOrLink(username);
@ -110,12 +115,16 @@ public class RecipientHelper {
try { try {
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername)); final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername()); return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
} catch (IOException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.code == 404) {
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
null, null,
null, null,
username)); username));
} }
logger.debug("Failed to get uuid for username: {}", username, e);
throw e;
}
} }
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> { return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
try { try {

View file

@ -48,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.api.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
@ -131,6 +132,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -160,6 +162,7 @@ public class ManagerImpl implements Manager {
private final List<Runnable> closedListeners = new ArrayList<>(); private final List<Runnable> closedListeners = new ArrayList<>();
private final List<Runnable> addressChangedListeners = new ArrayList<>(); private final List<Runnable> addressChangedListeners = new ArrayList<>();
private final CompositeDisposable disposable = new CompositeDisposable(); private final CompositeDisposable disposable = new CompositeDisposable();
private final AtomicLong lastMessageTimestamp = new AtomicLong();
public ManagerImpl( public ManagerImpl(
SignalAccount account, SignalAccount account,
@ -282,7 +285,7 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) { public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
final var registeredUsers = new HashMap<String, RecipientAddress>(); final var registeredUsers = new HashMap<String, RecipientAddress>();
for (final var username : usernames) { for (final var username : usernames) {
try { try {
@ -426,7 +429,7 @@ public class ManagerImpl implements Manager {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException { ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
@ -598,6 +601,24 @@ public class ManagerImpl implements Manager {
return context.getGroupHelper().joinGroup(inviteLinkUrl); return context.getGroupHelper().joinGroup(inviteLinkUrl);
} }
private long getNextMessageTimestamp() {
while (true) {
final var last = lastMessageTimestamp.get();
final var timestamp = System.currentTimeMillis();
if (last == timestamp) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
continue;
}
if (lastMessageTimestamp.compareAndSet(last, timestamp)) {
return timestamp;
}
}
}
private SendMessageResults sendMessage( private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients, Set<RecipientIdentifier> recipients,
@ -613,7 +634,7 @@ public class ManagerImpl implements Manager {
Optional<Long> editTargetTimestamp Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis(); long timestamp = getNextMessageTimestamp();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) { for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.NoteToSelf || ( if (recipient instanceof RecipientIdentifier.NoteToSelf || (
@ -653,7 +674,7 @@ public class ManagerImpl implements Manager {
Set<RecipientIdentifier> recipients Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
for (var recipient : recipients) { for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) { if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty()); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
@ -685,7 +706,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) { public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds, messageIds,
timestamp); timestamp);
@ -695,7 +716,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) { public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds, messageIds,
timestamp); timestamp);
@ -789,6 +810,7 @@ public class ManagerImpl implements Manager {
} else if (!additionalAttachments.isEmpty()) { } else if (!additionalAttachments.isEmpty()) {
messageBuilder.withAttachments(additionalAttachments); messageBuilder.withAttachments(additionalAttachments);
} }
messageBuilder.withViewOnce(message.viewOnce());
if (!message.mentions().isEmpty()) { if (!message.mentions().isEmpty()) {
messageBuilder.withMentions(resolveMentions(message.mentions())); messageBuilder.withMentions(resolveMentions(message.mentions()));
} }

View file

@ -21,6 +21,7 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
@ -149,7 +150,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
public void verifyAccount( public void verifyAccount(
String verificationCode, String verificationCode,
String pin String pin
) throws IOException, PinLockedException, IncorrectPinException { ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) { if (account.isRegistered()) {
throw new IOException("Account is already registered"); throw new IOException("Account is already registered");
} }

View file

@ -5,6 +5,7 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -325,7 +326,9 @@ public class SignalDependencies {
getKeysApi(), getKeysApi(),
Optional.empty(), Optional.empty(),
executor, executor,
ServiceConfig.MAX_ENVELOPE_SIZE)); ServiceConfig.MAX_ENVELOPE_SIZE,
() -> true,
UsePqRatchet.NO));
} }
public List<SecureValueRecovery> getSecureValueRecovery() { public List<SecureValueRecovery> getSecureValueRecovery() {
@ -338,7 +341,10 @@ public class SignalDependencies {
public ProfileApi getProfileApi() { public ProfileApi getProfileApi() {
return getOrCreate(() -> profileApi, return getOrCreate(() -> profileApi,
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket())); () -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket(),
getClientZkProfileOperations()));
} }
public ProfileService getProfileService() { public ProfileService getProfileService() {

View file

@ -56,7 +56,10 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
.distinctUntilChanged() .distinctUntilChanged()
.subscribe(this::onStateChanged); .subscribe(this::onStateChanged);
webSocket.setKeepAliveChangedListener(this::updateKeepAliveSenderStatus); webSocket.addKeepAliveChangeListener(() -> {
executor.execute(this::updateKeepAliveSenderStatus);
return Unit.INSTANCE;
});
}); });
} }
@ -78,7 +81,7 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
public void onMessageError(int status, boolean isIdentifiedWebSocket) { public void onMessageError(int status, boolean isIdentifiedWebSocket) {
} }
private Unit updateKeepAliveSenderStatus() { private void updateKeepAliveSenderStatus() {
if (keepAliveSender == null && sendKeepAlives()) { if (keepAliveSender == null && sendKeepAlives()) {
keepAliveSender = new KeepAliveSender(); keepAliveSender = new KeepAliveSender();
keepAliveSender.start(); keepAliveSender.start();
@ -86,7 +89,6 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
keepAliveSender.shutdown(); keepAliveSender.shutdown();
keepAliveSender = null; keepAliveSender = null;
} }
return Unit.INSTANCE;
} }
private boolean sendKeepAlives() { private boolean sendKeepAlives() {

View file

@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class); private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private static final int MINIMUM_STORAGE_VERSION = 1; private static final int MINIMUM_STORAGE_VERSION = 1;
private static final int CURRENT_STORAGE_VERSION = 9; private static final int CURRENT_STORAGE_VERSION = 10;
private final Object LOCK = new Object(); private final Object LOCK = new Object();

View file

@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction; import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
@ -62,11 +63,11 @@ public class IdentityKeyStore {
return identityChanges; return identityChanges;
} }
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) { public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
return saveIdentity(serviceId.toString(), identityKey); return saveIdentity(serviceId.toString(), identityKey);
} }
public boolean saveIdentity( public IdentityChange saveIdentity(
final Connection connection, final Connection connection,
final ServiceId serviceId, final ServiceId serviceId,
final IdentityKey identityKey final IdentityKey identityKey
@ -74,9 +75,9 @@ public class IdentityKeyStore {
return saveIdentity(connection, serviceId.toString(), identityKey); return saveIdentity(connection, serviceId.toString(), identityKey);
} }
boolean saveIdentity(final String address, final IdentityKey identityKey) { IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
if (isRetryingDecryption) { if (isRetryingDecryption) {
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
return saveIdentity(connection, address, identityKey); return saveIdentity(connection, address, identityKey);
@ -85,20 +86,24 @@ public class IdentityKeyStore {
} }
} }
private boolean saveIdentity( private IdentityChange saveIdentity(
final Connection connection, final Connection connection,
final String address, final String address,
final IdentityKey identityKey final IdentityKey identityKey
) throws SQLException { ) throws SQLException {
final var identityInfo = loadIdentity(connection, address); final var identityInfo = loadIdentity(connection, address);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { if (identityInfo == null) {
saveNewIdentity(connection, address, identityKey, true);
return IdentityChange.NEW_OR_UNCHANGED;
}
if (identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level // Identity already exists, not updating the trust level
logger.trace("Not storing new identity for recipient {}, identity already stored", address); logger.trace("Not storing new identity for recipient {}, identity already stored", address);
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
saveNewIdentity(connection, address, identityKey, identityInfo == null); saveNewIdentity(connection, address, identityKey, false);
return true; return IdentityChange.REPLACED_EXISTING;
} }
public void setRetryingDecryption(final boolean retryingDecryption) { public void setRetryingDecryption(final boolean retryingDecryption) {

View file

@ -33,7 +33,7 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
} }
@Override @Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address.getName(), identityKey); return identityKeyStore.saveIdentity(address.getName(), identityKey);
} }

View file

@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils; import org.asamk.signal.manager.storage.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.InvalidKeyIdException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -176,8 +177,8 @@ public class PreKeyStore implements SignalServicePreKeyStore {
private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
try { try {
final var keyId = resultSet.getInt("key_id"); final var keyId = resultSet.getInt("key_id");
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey)); return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
return null; return null;

View file

@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils; import org.asamk.signal.manager.storage.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.InvalidKeyIdException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -238,8 +239,8 @@ public class SignedPreKeyStore implements org.signal.libsignal.protocol.state.Si
private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
try { try {
final var keyId = resultSet.getInt("key_id"); final var keyId = resultSet.getInt("key_id");
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
final var signature = resultSet.getBytes("signature"); final var signature = resultSet.getBytes("signature");
final var timestamp = resultSet.getLong("timestamp"); final var timestamp = resultSet.getLong("timestamp");
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature); return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);

View file

@ -65,7 +65,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
} }
@Override @Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey); return identityKeyStore.saveIdentity(address, identityKey);
} }

View file

@ -111,7 +111,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData); backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData);
} }
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.givenName(givenName) .givenName(givenName)
.familyName(familyName) .familyName(familyName)
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath)) .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))

View file

@ -37,7 +37,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$"); private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{6,18}$");
private final ACI selfAci; private final ACI selfAci;
private final PNI selfPni; private final PNI selfPni;
@ -172,7 +172,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
e164 = firstNonEmpty(remote.e164, local.e164); e164 = firstNonEmpty(remote.e164, local.e164);
} }
final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.aci(local.aci.isEmpty() ? remote.aci : local.aci) .aci(local.aci.isEmpty() ? remote.aci : local.aci)
.e164(e164) .e164(e164)
.pni(pni) .pni(pni)

View file

@ -74,7 +74,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
final var remote = remoteRecord.getProto(); final var remote = remoteRecord.getProto();
final var local = localRecord.getProto(); final var local = localRecord.getProto();
final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.id(remote.id) .id(remote.id)
.blocked(remote.blocked) .blocked(remote.blocked)
.whitelisted(remote.whitelisted) .whitelisted(remote.whitelisted)

View file

@ -53,7 +53,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
final var remote = remoteRecord.getProto(); final var remote = remoteRecord.getProto();
final var local = localRecord.getProto(); final var local = localRecord.getProto();
final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.masterKey(remote.masterKey) .masterKey(remote.masterKey)
.blocked(remote.blocked) .blocked(remote.blocked)
.whitelisted(remote.whitelisted) .whitelisted(remote.whitelisted)

View file

@ -4,7 +4,7 @@ import org.asamk.signal.manager.storage.SignalAccount;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey; import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.kem.KEMKeyPair; import org.signal.libsignal.protocol.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType; import org.signal.libsignal.protocol.kem.KEMKeyType;
@ -33,8 +33,8 @@ public class KeyUtils {
public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) { public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
try { try {
IdentityKey publicKey = new IdentityKey(publicKeyBytes); final var publicKey = new IdentityKey(publicKeyBytes);
ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes); final var privateKey = new ECPrivateKey(privateKeyBytes);
return new IdentityKeyPair(publicKey, privateKey); return new IdentityKeyPair(publicKey, privateKey);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -43,7 +43,7 @@ public class KeyUtils {
} }
public static IdentityKeyPair generateIdentityKeyPair() { public static IdentityKeyPair generateIdentityKeyPair() {
var djbKeyPair = Curve.generateKeyPair(); var djbKeyPair = ECKeyPair.generate();
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
var djbPrivateKey = djbKeyPair.getPrivateKey(); var djbPrivateKey = djbKeyPair.getPrivateKey();
@ -54,7 +54,7 @@ public class KeyUtils {
var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE); var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE);
for (var i = 0; i < PREKEY_BATCH_SIZE; i++) { for (var i = 0; i < PREKEY_BATCH_SIZE; i++) {
var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID; var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID;
var keyPair = Curve.generateKeyPair(); var keyPair = ECKeyPair.generate();
var record = new PreKeyRecord(preKeyId, keyPair); var record = new PreKeyRecord(preKeyId, keyPair);
records.add(record); records.add(record);
@ -66,13 +66,9 @@ public class KeyUtils {
final int signedPreKeyId, final int signedPreKeyId,
final ECPrivateKey privateKey final ECPrivateKey privateKey
) { ) {
var keyPair = Curve.generateKeyPair(); var keyPair = ECKeyPair.generate();
byte[] signature; byte[] signature;
try { signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize());
signature = Curve.calculateSignature(privateKey, keyPair.getPublicKey().serialize());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature); return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
} }

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -114,7 +115,7 @@ public class NumberVerificationUtils {
String pin, String pin,
PinHelper pinHelper, PinHelper pinHelper,
Verifier verifier Verifier verifier
) throws IOException, PinLockedException, IncorrectPinException { ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
verificationCode = verificationCode.replace("-", ""); verificationCode = verificationCode.replace("-", "");
try { try {
final var response = verifier.verify(sessionId, verificationCode, null); final var response = verifier.verify(sessionId, verificationCode, null);
@ -127,7 +128,7 @@ public class NumberVerificationUtils {
final var registrationLockData = pinHelper.getRegistrationLockData(pin, e); final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
if (registrationLockData == null) { if (registrationLockData == null) {
throw e; throw new PinLockMissingException();
} }
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();

View file

@ -316,6 +316,11 @@ Data URI encoded attachments must follow the RFC 2397.
Additionally a file name can be added: Additionally a file name can be added:
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>` e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
*--view-once*::
Send the message as a view once message.
A conformant client will only allow the receiver to view the message once.
View Once is only supported for messages that include an image attachment.
*--sticker* STICKER:: *--sticker* STICKER::
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId). Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
Shouldn't be used together with `-m` as the official clients don't support this. Shouldn't be used together with `-m` as the official clients don't support this.

View file

@ -176,6 +176,17 @@ run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_1" receive run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=true
run_main -a "$NUMBER_2" removeContact --forget "$NUMBER_1"
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=false
run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive
## Groups ## Groups
GROUP_ID=$(run_main -a "$NUMBER_1" --output=json updateGroup -n GRUPPE -a LICENSE -m "$NUMBER_1" | jq -r '.groupId') GROUP_ID=$(run_main -a "$NUMBER_1" --output=json updateGroup -n GRUPPE -a LICENSE -m "$NUMBER_1" | jq -r '.groupId')
run_main -a "$NUMBER_1" send "$NUMBER_2" -m first run_main -a "$NUMBER_1" send "$NUMBER_2" -m first
@ -238,7 +249,9 @@ for OUTPUT in "plain-text" "json"; do
run_linked -a "$NUMBER_1" --output="$OUTPUT" receive run_linked -a "$NUMBER_1" --output="$OUTPUT" receive
done done
run_main -a "$NUMBER_1" removeDevice -d 2 run_main -a "$NUMBER_1" --output="$OUTPUT" receive
run_main -a "$NUMBER_1" removeDevice -d 2 || true
run_main -a "$NUMBER_1" removeDevice -d 2 || true
## Unregister ## Unregister
if [ "$TEST_REGISTER" -eq 1 ]; then if [ "$TEST_REGISTER" -eq 1 ]; then

View file

@ -291,6 +291,8 @@ public class App {
commandHandler.handleMultiLocalCommand(command, multiAccountManager); commandHandler.handleMultiLocalCommand(command, multiAccountManager);
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("Failed to load local accounts file", e); throw new IOErrorException("Failed to load local accounts file", e);
} catch (AccountCheckException e) {
throw new UnexpectedErrorException("Failed to load account file", e);
} }
} }

View file

@ -8,7 +8,7 @@ public class BaseConfig {
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); 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")) static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
.orElse("Signal-Android/7.41.3"); .orElse("Signal-Android/7.47.1");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli" ? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION; : PROJECT_NAME + "/" + PROJECT_VERSION;

View file

@ -1,5 +1,6 @@
package org.asamk.signal; package org.asamk.signal;
import org.asamk.signal.commands.exceptions.CommandException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -11,7 +12,7 @@ import sun.misc.Signal;
public class Shutdown { public class Shutdown {
private static final Logger logger = LoggerFactory.getLogger(Shutdown.class); private static final Logger logger = LoggerFactory.getLogger(Shutdown.class);
private static final CompletableFuture<Void> shutdown = new CompletableFuture<>(); private static final CompletableFuture<Object> shutdown = new CompletableFuture<>();
private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>(); private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>();
private static boolean initialized = false; private static boolean initialized = false;
@ -43,9 +44,17 @@ public class Shutdown {
shutdown.complete(null); shutdown.complete(null);
} }
public static void waitForShutdown() throws InterruptedException { public static void triggerShutdown(CommandException exception) {
logger.debug("Triggering shutdown with exception.", exception);
shutdown.complete(exception);
}
public static void waitForShutdown() throws InterruptedException, CommandException {
try { try {
shutdown.get(); final var result = shutdown.get();
if (result instanceof CommandException e) {
throw e;
}
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -97,7 +97,7 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
final OutputWriter outputWriter final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
Shutdown.installHandler(); Shutdown.installHandler();
logger.info("Starting daemon in single-account mode for " + m.getSelfNumber()); logger.info("Starting daemon in single-account mode for {}", m.getSelfNumber());
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout")); final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
final var receiveMode = ns.<ReceiveMode>get("receive-mode"); final var receiveMode = ns.<ReceiveMode>get("receive-mode");
final var receiveConfig = getReceiveConfig(ns); final var receiveConfig = getReceiveConfig(ns);

View file

@ -9,6 +9,7 @@ import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.output.OutputWriter; import org.asamk.signal.output.OutputWriter;
@ -50,6 +51,8 @@ public class FinishChangeNumberCommand implements JsonRpcLocalCommand {
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN"); + "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new UserErrorException("Account is pin locked, but pin data has been deleted on the server.");
} catch (NotPrimaryDeviceException e) { } catch (NotPrimaryDeviceException e) {
throw new UserErrorException("This command doesn't work on linked devices."); throw new UserErrorException("This command doesn't work on linked devices.");
} catch (IOException e) { } catch (IOException e) {

View file

@ -64,9 +64,16 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
} }
final var usernames = ns.<String>getList("username"); final var usernames = ns.<String>getList("username");
final var registeredUsernames = usernames == null final Map<String, UsernameStatus> registeredUsernames;
? Map.<String, UsernameStatus>of() try {
: m.getUsernameStatus(new HashSet<>(usernames)); registeredUsernames = usernames == null ? Map.of() : m.getUsernameStatus(new HashSet<>(usernames));
} catch (IOException e) {
throw new IOErrorException("Unable to check if users are registered: "
+ e.getMessage()
+ " ("
+ e.getClass().getSimpleName()
+ ")", e);
}
// Output // Output
switch (outputWriter) { switch (outputWriter) {

View file

@ -66,6 +66,9 @@ public class SendCommand implements JsonRpcLocalCommand {
.help("Add an attachment. " .help("Add an attachment. "
+ "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. " + "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. "
+ "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>."); + "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
subparser.addArgument("--view-once")
.action(Arguments.storeTrue())
.help("Send the message as a view once message");
subparser.addArgument("-e", "--end-session", "--endsession") subparser.addArgument("-e", "--end-session", "--endsession")
.help("Clear session state and send end session message.") .help("Clear session state and send end session message.")
.action(Arguments.storeTrue()); .action(Arguments.storeTrue());
@ -164,6 +167,7 @@ public class SendCommand implements JsonRpcLocalCommand {
if (attachments == null) { if (attachments == null) {
attachments = List.of(); attachments = List.of();
} }
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
final var selfNumber = m.getSelfNumber(); final var selfNumber = m.getSelfNumber();
@ -239,6 +243,7 @@ public class SendCommand implements JsonRpcLocalCommand {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
viewOnce,
mentions, mentions,
Optional.ofNullable(quote), Optional.ofNullable(quote),
Optional.ofNullable(sticker), Optional.ofNullable(sticker),
@ -250,8 +255,14 @@ public class SendCommand implements JsonRpcLocalCommand {
: m.sendMessage(message, recipientIdentifiers, notifySelf); : m.sendMessage(message, recipientIdentifiers, notifySelf);
outputResult(outputWriter, results); outputResult(outputWriter, results);
} catch (AttachmentInvalidException | IOException e) { } catch (AttachmentInvalidException | IOException e) {
if (e instanceof IOException io && io.getMessage().contains("No prekeys available")) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.",
e);
} else {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e); .getSimpleName() + ")", e);
}
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
throw new UserErrorException(e.getMessage()); throw new UserErrorException(e.getMessage());
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {

View file

@ -11,6 +11,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.JsonWriter;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -76,6 +77,8 @@ public class VerifyCommand implements RegistrationCommand, JsonRpcRegistrationCo
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN"); + "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new UserErrorException("Account is pin locked, but pin data has been deleted on the server.");
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("Verify error: " + e.getMessage(), e); throw new IOErrorException("Verify error: " + e.getMessage(), e);
} }

View file

@ -1,17 +1,21 @@
package org.asamk.signal.dbus; package org.asamk.signal.dbus;
import org.asamk.signal.DbusConfig; import org.asamk.signal.DbusConfig;
import org.asamk.signal.Shutdown;
import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.MultiAccountManager; import org.asamk.signal.manager.MultiAccountManager;
import org.freedesktop.dbus.connections.IDisconnectCallback;
import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -94,7 +98,9 @@ public class DbusHandler implements AutoCloseable {
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION; final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
logger.debug("Starting DBus server on {} bus: {}", busType, busname); logger.debug("Starting DBus server on {} bus: {}", busType, busname);
try { try {
dBusConnection = DBusConnectionBuilder.forType(busType).build(); dBusConnection = DBusConnectionBuilder.forType(busType)
.withDisconnectCallback(new DisconnectCallback())
.build();
dbusRunner.run(dBusConnection); dbusRunner.run(dBusConnection);
} catch (DBusException e) { } catch (DBusException e) {
throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e); throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e);
@ -141,4 +147,13 @@ public class DbusHandler implements AutoCloseable {
void run(DBusConnection connection) throws DBusException; void run(DBusConnection connection) throws DBusException;
} }
private static final class DisconnectCallback implements IDisconnectCallback {
@Override
public void disconnectOnError(IOException ex) {
logger.debug("DBus daemon disconnected unexpectedly, shutting down");
Shutdown.triggerShutdown(new IOErrorException("Unexpected dbus daemon disconnect", ex));
}
}
} }

View file

@ -10,6 +10,7 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UserAlreadyExistsException; import org.asamk.signal.manager.api.UserAlreadyExistsException;
@ -105,6 +106,8 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
+ (e.getTimeRemaining() / 1000 / 60 / 60)); + (e.getTimeRemaining() / 1000 / 60 / 60));
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new Error.Failure("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new Error.Failure("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new Error.Failure("Account is pin locked, but pin data has been deleted on the server.");
} }
} }

View file

@ -236,6 +236,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
@ -399,6 +400,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
@ -444,6 +446,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),