mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-28 18:10:38 +00:00
Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
|
f6d81e3c05 | ||
|
42f10670b6 | ||
|
b453d7a0b9 | ||
|
f9a36c6e04 | ||
|
be48afb2b5 | ||
|
a0960fcabd | ||
|
dbc454ba9e | ||
|
2225e69277 | ||
|
783201d12e | ||
|
3e981d66e9 | ||
|
7c7fc76a64 | ||
|
c924d5c03a | ||
|
dc787be17b | ||
|
3d4070a139 | ||
|
dbdff83132 | ||
|
4ce194afe2 | ||
|
ca33249170 | ||
|
a96626c468 | ||
|
d54be747da | ||
|
ff846bc678 | ||
|
1b7f755590 | ||
|
887ed3bb44 | ||
|
3180eba836 | ||
|
cb06cbdcca | ||
|
069325af47 | ||
|
e7ca02f1fb | ||
|
fa9bb3c210 | ||
|
e6113d4d96 | ||
|
6cc3a6f561 | ||
|
70c79eac01 | ||
|
5dc66f839d | ||
|
a0d5744c49 | ||
|
6b60a6d5a5 | ||
|
0257344940 | ||
|
17cd99be59 | ||
|
2f8328847c | ||
|
7e9727aa38 | ||
|
bf87fcc652 | ||
|
6b46314eab | ||
|
e89803464b | ||
|
a9bb8d9aae | ||
|
74909408c4 | ||
|
bb124a922d | ||
|
56e11d0857 | ||
|
d0d0021f57 | ||
|
7aafb05995 | ||
|
e594f3b237 | ||
|
bb86830a61 | ||
|
bcc1eadc7d | ||
|
4fd9e55c3c | ||
|
a2900085c9 | ||
|
5e11cf1c50 | ||
|
4e455d85d6 | ||
|
1e685c7cab | ||
|
ce813e4529 | ||
|
bd7948e246 |
70 changed files with 936 additions and 547 deletions
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '21', '23' ]
|
||||
java: [ '21', '24' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -69,3 +69,28 @@ jobs:
|
|||
with:
|
||||
name: signal-cli-native
|
||||
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
|
||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -57,4 +57,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -182,7 +182,7 @@ jobs:
|
|||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
rm -r signal-cli-archive-* signal-cli-native
|
||||
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
|
||||
id: build_image
|
||||
|
|
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -1,5 +1,60 @@
|
|||
# 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
|
||||
|
||||
Requires libsignal-client version 0.70.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix native access warning with Java 24
|
||||
- Fix storage sync loop due to old removed e164 field
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased compatibility of native build with older/virtual CPUs
|
||||
|
||||
## [0.13.14] - 2025-04-06
|
||||
|
||||
Requires libsignal-client version 0.68.1.
|
||||
|
|
|
@ -83,6 +83,12 @@ of all country codes.)
|
|||
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.
|
||||
|
||||
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
||||
|
|
|
@ -8,7 +8,7 @@ plugins {
|
|||
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.13.14"
|
||||
version = "0.13.19-SNAPSHOT"
|
||||
}
|
||||
|
||||
java {
|
||||
|
@ -24,6 +24,7 @@ java {
|
|||
|
||||
application {
|
||||
mainClass.set("org.asamk.signal.Main")
|
||||
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
graalvmNative {
|
||||
|
@ -32,6 +33,7 @@ graalvmNative {
|
|||
buildArgs.add("--install-exit-handlers")
|
||||
buildArgs.add("-Dfile.encoding=UTF-8")
|
||||
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
||||
buildArgs.add("-march=compatibility")
|
||||
resources.autodetect()
|
||||
configurationFileDirectories.from(file("graalvm-config-dir"))
|
||||
if (System.getenv("GRAALVM_HOME") == null) {
|
||||
|
@ -112,7 +114,8 @@ tasks.withType<Jar> {
|
|||
attributes(
|
||||
"Implementation-Title" to project.name,
|
||||
"Implementation-Version" to project.version,
|
||||
"Main-Class" to application.mainClass.get()
|
||||
"Main-Class" to application.mainClass.get(),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
@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.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
|
@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
|||
val name = dependency.name
|
||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
||||
try {
|
||||
val url = ResourceGroovyMethods.toURL(metaDataUrl)
|
||||
val metaDataText = ResourceGroovyMethods.getText(url)
|
||||
val metadata = XmlSlurper().parseText(metaDataText)
|
||||
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
|
||||
val dbf = DocumentBuilderFactory.newInstance()
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val doc = db.parse(metaDataUrl);
|
||||
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||
if (version != newest.toString()) {
|
||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||
}
|
||||
|
|
798
client/Cargo.lock
generated
798
client/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,17 @@
|
|||
[package]
|
||||
name = "signal-cli-client"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
||||
log = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
||||
jsonrpsee = { version = "0.24", features = [
|
||||
jsonrpsee = { version = "0.25", features = [
|
||||
"macros",
|
||||
"async-client",
|
||||
"http-client",
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct Cli {
|
|||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||
|
||||
/// UNIX socket address and port of signal-cli daemon
|
||||
#[cfg(unix)]
|
||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||
pub json_rpc_socket: Option<Option<OsString>>,
|
||||
|
||||
|
@ -84,6 +85,8 @@ pub enum CliCommands {
|
|||
},
|
||||
GetUserStatus {
|
||||
recipient: Vec<String>,
|
||||
#[arg(long)]
|
||||
username: Vec<String>,
|
||||
},
|
||||
JoinGroup {
|
||||
#[arg(long)]
|
||||
|
@ -176,6 +179,9 @@ pub enum CliCommands {
|
|||
#[arg(short = 'a', long)]
|
||||
attachment: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
view_once: bool,
|
||||
|
||||
#[arg(long)]
|
||||
mention: Vec<String>,
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ pub trait Rpc {
|
|||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
usernames: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[method(name = "joinGroup", param_kind = map)]
|
||||
|
@ -182,6 +183,7 @@ pub trait Rpc {
|
|||
endSession: bool,
|
||||
message: String,
|
||||
attachments: Vec<String>,
|
||||
viewOnce: bool,
|
||||
mentions: Vec<String>,
|
||||
textStyle: Vec<String>,
|
||||
quoteTimestamp: Option<u64>,
|
||||
|
@ -190,10 +192,10 @@ pub trait Rpc {
|
|||
quoteMention: Vec<String>,
|
||||
quoteTextStyle: Vec<String>,
|
||||
quoteAttachment: Vec<String>,
|
||||
preview_url: Option<String>,
|
||||
preview_title: Option<String>,
|
||||
preview_description: Option<String>,
|
||||
preview_image: Option<String>,
|
||||
previewUrl: Option<String>,
|
||||
previewTitle: Option<String>,
|
||||
previewDescription: Option<String>,
|
||||
previewImage: Option<String>,
|
||||
sticker: Option<String>,
|
||||
storyTimestamp: Option<u64>,
|
||||
storyAuthor: Option<String>,
|
||||
|
@ -409,6 +411,7 @@ pub async fn connect_tcp(
|
|||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn connect_unix(
|
||||
socket_path: impl AsRef<Path>,
|
||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||
|
@ -417,6 +420,6 @@ pub async fn connect_unix(
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -60,8 +60,13 @@ async fn handle_command(
|
|||
.delete_local_account_data(cli.account, ignore_registered)
|
||||
.await
|
||||
}
|
||||
CliCommands::GetUserStatus { recipient } => {
|
||||
client.get_user_status(cli.account, recipient).await
|
||||
CliCommands::GetUserStatus {
|
||||
recipient,
|
||||
username,
|
||||
} => {
|
||||
client
|
||||
.get_user_status(cli.account, recipient, username)
|
||||
.await
|
||||
}
|
||||
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||
CliCommands::Link { name } => {
|
||||
|
@ -70,7 +75,7 @@ async fn handle_command(
|
|||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||
.device_link_uri;
|
||||
println!("{}", url);
|
||||
println!("{url}");
|
||||
client.finish_link(url, name).await
|
||||
}
|
||||
CliCommands::ListAccounts => client.list_accounts().await,
|
||||
|
@ -139,6 +144,7 @@ async fn handle_command(
|
|||
end_session,
|
||||
message,
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
|
@ -165,6 +171,7 @@ async fn handle_command(
|
|||
end_session,
|
||||
message.unwrap_or_default(),
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
|
@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
|||
|
||||
handle_command(cli, client).await
|
||||
} else {
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Err(RpcError::Custom("Invalid socket".into()))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
handle_command(cli, client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||
use jsonrpsee::core::{
|
||||
async_trait,
|
||||
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
|
||||
};
|
||||
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod ipc;
|
||||
mod stream_codec;
|
||||
pub mod tcp;
|
||||
|
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
|
|||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||
for Sender<T>
|
||||
{
|
||||
|
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||
self.inner
|
||||
.send(body)
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||
self.inner
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
|
|||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||
for Receiver<T>
|
||||
{
|
||||
|
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
|
|||
match self.inner.next().await {
|
||||
None => Err(Errors::Closed),
|
||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
|
|||
|
||||
match str::from_utf8(line.as_ref()) {
|
||||
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 {
|
||||
Ok(None)
|
||||
|
|
|
@ -45,6 +45,18 @@
|
|||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<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">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
|
||||
</release>
|
||||
<release version="0.13.14" date="2025-04-06">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
|
||||
</release>
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
{
|
||||
"name":"java.lang.ClassNotFoundException"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Enum",
|
||||
"methods":[{"name":"ordinal","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.IllegalArgumentException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
|
@ -48,9 +52,13 @@
|
|||
{
|
||||
"name":"java.lang.String"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Thread",
|
||||
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
@ -88,7 +96,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
|
@ -118,6 +126,10 @@
|
|||
"name":"org.signal.libsignal.net.NetworkException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.net.RetryLaterException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
|
@ -195,6 +207,9 @@
|
|||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
|
||||
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
|
||||
"fields":[{"name":"unsafeHandle"}]
|
||||
|
|
|
@ -671,6 +671,10 @@
|
|||
{
|
||||
"name":"long[]"
|
||||
},
|
||||
{
|
||||
"name":"okhttp3.internal.connection.RealConnectionPool",
|
||||
"fields":[{"name":"addressStates"}]
|
||||
},
|
||||
{
|
||||
"name":"okio.BufferedSink"
|
||||
},
|
||||
|
@ -1409,6 +1413,12 @@
|
|||
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
|
||||
"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",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -1620,6 +1630,10 @@
|
|||
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
|
@ -2988,9 +3002,11 @@
|
|||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
|
||||
"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":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory"
|
||||
},
|
||||
{
|
||||
"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$NotificationProfileManualOverride"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
|
||||
},
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
slf4j = "2.0.17"
|
||||
|
||||
[libraries]
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80"
|
||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.18.3"
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
|
||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
|
||||
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||
logback = "ch.qos.logback:logback-classic:1.5.18"
|
||||
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_121"
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.49.1.0"
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
|
||||
hikari = "com.zaxxer:HikariCP:6.3.0"
|
||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0"
|
||||
junit-launcher = "org.junit.platform:junit-platform-launcher:1.12.0"
|
||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
|
||||
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"
|
||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -114,7 +114,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
4
gradlew.bat
vendored
4
gradlew.bat
vendored
|
@ -70,11 +70,11 @@ goto fail
|
|||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
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.RateLimitException;
|
||||
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, UsernameStatus> getUsernameStatus(Set<String> usernames);
|
||||
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
|
||||
|
||||
void updateAccountAttributes(
|
||||
String deviceName,
|
||||
|
@ -140,7 +141,7 @@ public interface Manager extends Closeable {
|
|||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException;
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
|
||||
|
||||
void unregister() throws IOException;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.asamk.signal.manager;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
@ -21,7 +22,7 @@ public interface RegistrationManager extends Closeable {
|
|||
void verifyAccount(
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException;
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
||||
|
||||
void deleteLocalAccountData() throws IOException;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager;
|
|||
|
||||
import org.asamk.signal.manager.api.AccountCheckException;
|
||||
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.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
|
@ -63,19 +64,28 @@ public class SignalAccountFiles {
|
|||
return accountsStore.getAllNumbers();
|
||||
}
|
||||
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
|
||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
try {
|
||||
return initManager(a.number(), a.path());
|
||||
} catch (NotRegisteredException | IOException | AccountCheckException e) {
|
||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
} catch (AccountCheckException | IOException e) {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
|
|||
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
|
||||
import java.net.URI;
|
||||
|
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
|||
}
|
||||
ECPublicKey deviceKey;
|
||||
try {
|
||||
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
|
||||
deviceKey = new ECPublicKey(publicKeyBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Optional;
|
|||
public record Message(
|
||||
String messageText,
|
||||
List<String> attachments,
|
||||
boolean viewOnce,
|
||||
List<Mention> mentions,
|
||||
Optional<Quote> quote,
|
||||
Optional<Sticker> sticker,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package org.asamk.signal.manager.api;
|
||||
|
||||
public class PinLockMissingException extends Exception {}
|
|
@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
|
|||
|
||||
import org.signal.libsignal.net.Network.Environment;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
|
@ -28,7 +28,9 @@ class LiveConfig {
|
|||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
||||
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 CDN_URL = "https://cdn.signal.org";
|
||||
|
@ -41,6 +43,7 @@ class LiveConfig {
|
|||
|
||||
private static final Optional<Dns> dns = Optional.empty();
|
||||
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
|
||||
|
@ -68,6 +71,7 @@ class LiveConfig {
|
|||
interceptors,
|
||||
dns,
|
||||
proxy,
|
||||
systemProxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams,
|
||||
|
@ -76,7 +80,7 @@ class LiveConfig {
|
|||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
try {
|
||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
||||
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ class LiveConfig {
|
|||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoot(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE));
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||
}
|
||||
|
||||
private LiveConfig() {
|
||||
|
|
|
@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
|
|||
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
|
@ -28,7 +28,9 @@ class StagingConfig {
|
|||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
||||
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 CDN_URL = "https://cdn-staging.signal.org";
|
||||
|
@ -41,6 +43,7 @@ class StagingConfig {
|
|||
|
||||
private static final Optional<Dns> dns = Optional.empty();
|
||||
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
|
||||
|
@ -68,6 +71,7 @@ class StagingConfig {
|
|||
interceptors,
|
||||
dns,
|
||||
proxy,
|
||||
systemProxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams,
|
||||
|
@ -76,7 +80,7 @@ class StagingConfig {
|
|||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
try {
|
||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
||||
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ class StagingConfig {
|
|||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoot(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE));
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||
}
|
||||
|
||||
private StagingConfig() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
@ -105,7 +106,7 @@ public class AccountHelper {
|
|||
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||
throw new IOException("Missing PNI identity key, relinking required");
|
||||
}
|
||||
if (account.getPreviousStorageVersion() < 4
|
||||
if (account.getPreviousStorageVersion() < 10
|
||||
&& account.isPrimaryDevice()
|
||||
&& account.getRegistrationLockPin() != null) {
|
||||
migrateRegistrationPin();
|
||||
|
@ -185,7 +186,7 @@ public class AccountHelper {
|
|||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException {
|
||||
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||
for (var attempts = 0; attempts < 5; attempts++) {
|
||||
try {
|
||||
finishChangeNumberInternal(newNumber, verificationCode, pin);
|
||||
|
@ -205,7 +206,7 @@ public class AccountHelper {
|
|||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException {
|
||||
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
|
||||
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
|
||||
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
|
||||
|
@ -535,9 +536,9 @@ public class AccountHelper {
|
|||
account.getAciIdentityKeyPair(),
|
||||
account.getPniIdentityKeyPair(),
|
||||
account.getProfileKey(),
|
||||
account.getOrCreateAccountEntropyPool(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateMediaRootBackupKey(),
|
||||
account.getOrCreateAccountEntropyPool(),
|
||||
verificationCode.getVerificationCode(),
|
||||
null));
|
||||
account.setMultiDevice(true);
|
||||
|
@ -595,7 +596,7 @@ public class AccountHelper {
|
|||
}
|
||||
account.setRegistrationLockPin(null);
|
||||
|
||||
dependencies.getAccountManager().deleteAccount();
|
||||
handleResponseException(dependencies.getAccountApi().deleteAccount());
|
||||
|
||||
account.setRegistered(false);
|
||||
unregisteredListener.call();
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
|
|||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
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 {
|
||||
var attachmentStreams = createAttachmentStreams(attachments);
|
||||
final var attachmentStreams = createAttachmentStreams(attachments);
|
||||
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
||||
for (var attachmentStream : attachmentStreams) {
|
||||
attachmentPointers.add(uploadAttachment(attachmentStream));
|
||||
try {
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
||||
for (final var attachmentStream : attachmentStreams) {
|
||||
attachmentPointers.add(uploadAttachment(attachmentStream));
|
||||
}
|
||||
return attachmentPointers;
|
||||
} finally {
|
||||
for (final var attachmentStream : attachmentStreams) {
|
||||
attachmentStream.close();
|
||||
}
|
||||
}
|
||||
return attachmentPointers;
|
||||
}
|
||||
|
||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||
|
@ -132,9 +139,15 @@ public class AttachmentHelper {
|
|||
SignalServiceAttachmentPointer pointer,
|
||||
File tmpFile
|
||||
) throws IOException {
|
||||
if (pointer.getDigest().isEmpty()) {
|
||||
throw new IOException("Attachment pointer has no digest.");
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
|
|
@ -551,6 +551,9 @@ public class GroupHelper {
|
|||
while (true) {
|
||||
final var page = context.getGroupV2Helper()
|
||||
.getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
|
||||
if (page == null) {
|
||||
break;
|
||||
}
|
||||
page.getChangeLogs()
|
||||
.stream()
|
||||
.map(DecryptedGroupChangeLog::getChange)
|
||||
|
|
|
@ -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.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -119,6 +120,8 @@ class GroupV2Helper {
|
|||
groupsV2AuthorizationString,
|
||||
false,
|
||||
sendEndorsementsExpirationMs);
|
||||
} catch (NotInGroupException e) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.code == 403) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
|||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
|
@ -105,7 +106,7 @@ public final class IncomingMessageHandler {
|
|||
try {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| 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());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
|
@ -143,7 +144,7 @@ public final class IncomingMessageHandler {
|
|||
try {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| 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());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
|
|
|
@ -88,7 +88,11 @@ public class PinHelper {
|
|||
IOException exception = null;
|
||||
for (final var secureValueRecovery : secureValueRecoveries) {
|
||||
try {
|
||||
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
|
||||
final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
|
||||
if (lockData == null) {
|
||||
continue;
|
||||
}
|
||||
return lockData;
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
|
|
|
@ -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.exceptions.CdsiInvalidArgumentException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
@ -68,7 +69,7 @@ public class RecipientHelper {
|
|||
.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());
|
||||
for (var number : recipients) {
|
||||
final var recipientId = resolveRecipient(number);
|
||||
|
@ -91,7 +92,11 @@ public class RecipientHelper {
|
|||
}
|
||||
});
|
||||
} else if (recipient instanceof RecipientIdentifier.Username(String username)) {
|
||||
return resolveRecipientByUsernameOrLink(username, false);
|
||||
try {
|
||||
return resolveRecipientByUsernameOrLink(username, false);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
|
||||
}
|
||||
|
@ -99,7 +104,7 @@ public class RecipientHelper {
|
|||
public RecipientId resolveRecipientByUsernameOrLink(
|
||||
String username,
|
||||
boolean forceRefresh
|
||||
) throws UnregisteredRecipientException {
|
||||
) throws UnregisteredRecipientException, IOException {
|
||||
final Username finalUsername;
|
||||
try {
|
||||
finalUsername = getUsernameFromUsernameOrLink(username);
|
||||
|
@ -110,11 +115,15 @@ public class RecipientHelper {
|
|||
try {
|
||||
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
||||
} catch (IOException e) {
|
||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||
null,
|
||||
null,
|
||||
username));
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.code == 404) {
|
||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||
null,
|
||||
null,
|
||||
username));
|
||||
}
|
||||
logger.debug("Failed to get uuid for username: {}", username, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
||||
|
@ -241,7 +250,6 @@ public class RecipientHelper {
|
|||
token,
|
||||
null,
|
||||
dependencies.getLibSignalNetwork(),
|
||||
false,
|
||||
newToken -> {
|
||||
if (isPartialRefresh) {
|
||||
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
|||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||
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.Profile;
|
||||
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.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
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> addressChangedListeners = new ArrayList<>();
|
||||
private final CompositeDisposable disposable = new CompositeDisposable();
|
||||
private final AtomicLong lastMessageTimestamp = new AtomicLong();
|
||||
|
||||
public ManagerImpl(
|
||||
SignalAccount account,
|
||||
|
@ -282,7 +285,7 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
@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>();
|
||||
for (final var username : usernames) {
|
||||
try {
|
||||
|
@ -426,7 +429,7 @@ public class ManagerImpl implements Manager {
|
|||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
throw new NotPrimaryDeviceException();
|
||||
}
|
||||
|
@ -598,6 +601,24 @@ public class ManagerImpl implements Manager {
|
|||
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(
|
||||
SignalServiceDataMessage.Builder messageBuilder,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
|
@ -613,7 +634,7 @@ public class ManagerImpl implements Manager {
|
|||
Optional<Long> editTargetTimestamp
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
long timestamp = getNextMessageTimestamp();
|
||||
messageBuilder.withTimestamp(timestamp);
|
||||
for (final var recipient : recipients) {
|
||||
if (recipient instanceof RecipientIdentifier.NoteToSelf || (
|
||||
|
@ -653,7 +674,7 @@ public class ManagerImpl implements Manager {
|
|||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
for (var recipient : recipients) {
|
||||
if (recipient instanceof RecipientIdentifier.Single single) {
|
||||
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
|
||||
|
@ -685,7 +706,7 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
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,
|
||||
messageIds,
|
||||
timestamp);
|
||||
|
@ -695,7 +716,7 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
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,
|
||||
messageIds,
|
||||
timestamp);
|
||||
|
@ -789,6 +810,7 @@ public class ManagerImpl implements Manager {
|
|||
} else if (!additionalAttachments.isEmpty()) {
|
||||
messageBuilder.withAttachments(additionalAttachments);
|
||||
}
|
||||
messageBuilder.withViewOnce(message.viewOnce());
|
||||
if (!message.mentions().isEmpty()) {
|
||||
messageBuilder.withMentions(resolveMentions(message.mentions()));
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
|
@ -76,7 +75,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
|
||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
password = KeyUtils.createPassword();
|
||||
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
||||
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||
null,
|
||||
null,
|
||||
|
@ -85,7 +83,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
clientZkOperations.getProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
userAgent);
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.asamk.signal.manager.RegistrationManager;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
|
@ -129,12 +130,15 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
}
|
||||
|
||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||
logger.trace("Creating verification session");
|
||||
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
|
||||
account.getSessionId(account.getNumber()),
|
||||
id -> account.setSessionId(account.getNumber(), id),
|
||||
voiceVerification,
|
||||
captcha);
|
||||
logger.trace("Requesting verification code");
|
||||
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
|
||||
logger.debug("Successfully requested verification code");
|
||||
account.setRegistered(false);
|
||||
} catch (DeprecatedVersionException e) {
|
||||
logger.debug("Signal-Server returned deprecated version exception", e);
|
||||
|
@ -146,7 +150,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
public void verifyAccount(
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
|
||||
if (account.isRegistered()) {
|
||||
throw new IOException("Account is already registered");
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
|||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -147,7 +148,6 @@ public class SignalDependencies {
|
|||
() -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
getClientZkProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||
}
|
||||
|
||||
|
@ -290,7 +290,7 @@ public class SignalDependencies {
|
|||
Optional.of(credentialsProvider),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories), timer, TimeUnit.SECONDS.toMillis(10));
|
||||
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
|
@ -306,7 +306,7 @@ public class SignalDependencies {
|
|||
Optional.empty(),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories), timer, TimeUnit.SECONDS.toMillis(10));
|
||||
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
|
@ -326,7 +326,9 @@ public class SignalDependencies {
|
|||
getKeysApi(),
|
||||
Optional.empty(),
|
||||
executor,
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE));
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||
() -> true,
|
||||
UsePqRatchet.NO));
|
||||
}
|
||||
|
||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||
|
@ -339,7 +341,10 @@ public class SignalDependencies {
|
|||
|
||||
public ProfileApi getProfileApi() {
|
||||
return getOrCreate(() -> profileApi,
|
||||
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
|
||||
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
|
||||
getUnauthenticatedSignalWebSocket(),
|
||||
getPushServiceSocket(),
|
||||
getClientZkProfileOperations()));
|
||||
}
|
||||
|
||||
public ProfileService getProfileService() {
|
||||
|
|
|
@ -56,7 +56,10 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
|||
.distinctUntilChanged()
|
||||
.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) {
|
||||
}
|
||||
|
||||
private Unit updateKeepAliveSenderStatus() {
|
||||
private void updateKeepAliveSenderStatus() {
|
||||
if (keepAliveSender == null && sendKeepAlives()) {
|
||||
keepAliveSender = new KeepAliveSender();
|
||||
keepAliveSender.start();
|
||||
|
@ -86,7 +89,6 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
|||
keepAliveSender.shutdown();
|
||||
keepAliveSender = null;
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
private boolean sendKeepAlives() {
|
||||
|
|
|
@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
|
|||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
|||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
@ -62,11 +63,11 @@ public class IdentityKeyStore {
|
|||
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);
|
||||
}
|
||||
|
||||
public boolean saveIdentity(
|
||||
public IdentityChange saveIdentity(
|
||||
final Connection connection,
|
||||
final ServiceId serviceId,
|
||||
final IdentityKey identityKey
|
||||
|
@ -74,9 +75,9 @@ public class IdentityKeyStore {
|
|||
return saveIdentity(connection, serviceId.toString(), identityKey);
|
||||
}
|
||||
|
||||
boolean saveIdentity(final String address, final IdentityKey identityKey) {
|
||||
IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
|
||||
if (isRetryingDecryption) {
|
||||
return false;
|
||||
return IdentityChange.NEW_OR_UNCHANGED;
|
||||
}
|
||||
try (final var connection = database.getConnection()) {
|
||||
return saveIdentity(connection, address, identityKey);
|
||||
|
@ -85,20 +86,24 @@ public class IdentityKeyStore {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean saveIdentity(
|
||||
private IdentityChange saveIdentity(
|
||||
final Connection connection,
|
||||
final String address,
|
||||
final IdentityKey identityKey
|
||||
) throws SQLException {
|
||||
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
|
||||
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);
|
||||
return true;
|
||||
saveNewIdentity(connection, address, identityKey, false);
|
||||
return IdentityChange.REPLACED_EXISTING;
|
||||
}
|
||||
|
||||
public void setRetryingDecryption(final boolean retryingDecryption) {
|
||||
|
|
|
@ -33,7 +33,7 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address.getName(), identityKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
|
|||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
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.ECPrivateKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -176,8 +177,8 @@ public class PreKeyStore implements SignalServicePreKeyStore {
|
|||
private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
try {
|
||||
final var keyId = resultSet.getInt("key_id");
|
||||
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0);
|
||||
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key"));
|
||||
final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
|
||||
final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
|
||||
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
|
||||
} catch (InvalidKeyException e) {
|
||||
return null;
|
||||
|
|
|
@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
|
|||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
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.ECPrivateKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.slf4j.Logger;
|
||||
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 {
|
||||
try {
|
||||
final var keyId = resultSet.getInt("key_id");
|
||||
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0);
|
||||
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key"));
|
||||
final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
|
||||
final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
|
||||
final var signature = resultSet.getBytes("signature");
|
||||
final var timestamp = resultSet.getLong("timestamp");
|
||||
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);
|
||||
|
|
|
@ -65,7 +65,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address, identityKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,10 +83,11 @@ public class MergeRecipientHelper {
|
|||
recipientsToBeStripped.add(recipient);
|
||||
}
|
||||
|
||||
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}) and strip ({})",
|
||||
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}, {}) and strip ({})",
|
||||
address,
|
||||
recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")),
|
||||
recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")));
|
||||
resultingRecipient.map(RecipientWithAddress::address),
|
||||
recipientsToBeMerged.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")),
|
||||
recipientsToBeStripped.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")));
|
||||
|
||||
RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null);
|
||||
for (final var recipient : recipientsToBeMerged) {
|
||||
|
|
|
@ -994,7 +994,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
) throws SQLException {
|
||||
markUnregistered(connection, recipientId);
|
||||
final var address = resolveRecipientAddress(connection, recipientId);
|
||||
if (address.aci().isPresent() && address.pni().isPresent()) {
|
||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||
recipientId,
|
||||
needSplit,
|
||||
address);
|
||||
if (needSplit) {
|
||||
final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null));
|
||||
updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress));
|
||||
addNewRecipient(connection, numberAddress);
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.asamk.signal.manager.syncStorage;
|
|||
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.internal.JobExecutor;
|
||||
import org.asamk.signal.manager.jobs.CheckWhoAmIJob;
|
||||
import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
|
@ -112,7 +111,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData);
|
||||
}
|
||||
|
||||
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
final var mergedBuilder = remote.newBuilder()
|
||||
.givenName(givenName)
|
||||
.familyName(familyName)
|
||||
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
|
||||
|
@ -146,7 +145,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
: remote.storyViewReceiptsEnabled)
|
||||
.username(remote.username)
|
||||
.usernameLink(remote.usernameLink)
|
||||
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
|
||||
.avatarColor(remote.avatarColor);
|
||||
safeSetPayments(mergedBuilder,
|
||||
payments != null && payments.enabled,
|
||||
payments == null ? null : payments.entropy.toByteArray());
|
||||
|
@ -179,10 +178,6 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
final var accountRecord = update.newRecord();
|
||||
final var accountProto = accountRecord.getProto();
|
||||
|
||||
if (!accountProto.e164.equals(account.getNumber())) {
|
||||
jobExecutor.enqueueJob(new CheckWhoAmIJob());
|
||||
}
|
||||
|
||||
account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
|
||||
account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
|
||||
account.getConfigurationStore()
|
||||
|
|
|
@ -37,7 +37,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
|
||||
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 PNI selfPni;
|
||||
|
@ -172,7 +172,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
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)
|
||||
.e164(e164)
|
||||
.pni(pni)
|
||||
|
|
|
@ -74,7 +74,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
final var mergedBuilder = remote.newBuilder()
|
||||
.id(remote.id)
|
||||
.blocked(remote.blocked)
|
||||
.whitelisted(remote.whitelisted)
|
||||
|
|
|
@ -53,7 +53,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
final var mergedBuilder = remote.newBuilder()
|
||||
.masterKey(remote.masterKey)
|
||||
.blocked(remote.blocked)
|
||||
.whitelisted(remote.whitelisted)
|
||||
|
|
|
@ -74,7 +74,6 @@ public final class StorageSyncModels {
|
|||
.phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode(connection))
|
||||
.map(StorageSyncModels::localToRemote)
|
||||
.orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
|
||||
.e164(self.getAddress().number().orElse(""))
|
||||
.username(self.getAddress().username().orElse(""));
|
||||
if (usernameLinkComponents != null) {
|
||||
final var linkColor = configStore.getUsernameLinkColor(connection);
|
||||
|
|
|
@ -4,7 +4,7 @@ import org.asamk.signal.manager.storage.SignalAccount;
|
|||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
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.kem.KEMKeyPair;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType;
|
||||
|
@ -33,8 +33,8 @@ public class KeyUtils {
|
|||
|
||||
public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
|
||||
try {
|
||||
IdentityKey publicKey = new IdentityKey(publicKeyBytes);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes);
|
||||
final var publicKey = new IdentityKey(publicKeyBytes);
|
||||
final var privateKey = new ECPrivateKey(privateKeyBytes);
|
||||
|
||||
return new IdentityKeyPair(publicKey, privateKey);
|
||||
} catch (InvalidKeyException e) {
|
||||
|
@ -43,7 +43,7 @@ public class KeyUtils {
|
|||
}
|
||||
|
||||
public static IdentityKeyPair generateIdentityKeyPair() {
|
||||
var djbKeyPair = Curve.generateKeyPair();
|
||||
var djbKeyPair = ECKeyPair.generate();
|
||||
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||
var djbPrivateKey = djbKeyPair.getPrivateKey();
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class KeyUtils {
|
|||
var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE);
|
||||
for (var i = 0; i < PREKEY_BATCH_SIZE; i++) {
|
||||
var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID;
|
||||
var keyPair = Curve.generateKeyPair();
|
||||
var keyPair = ECKeyPair.generate();
|
||||
var record = new PreKeyRecord(preKeyId, keyPair);
|
||||
|
||||
records.add(record);
|
||||
|
@ -66,13 +66,9 @@ public class KeyUtils {
|
|||
final int signedPreKeyId,
|
||||
final ECPrivateKey privateKey
|
||||
) {
|
||||
var keyPair = Curve.generateKeyPair();
|
||||
var keyPair = ECKeyPair.generate();
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Curve.calculateSignature(privateKey, keyPair.getPublicKey().serialize());
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize());
|
||||
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
@ -114,7 +115,7 @@ public class NumberVerificationUtils {
|
|||
String pin,
|
||||
PinHelper pinHelper,
|
||||
Verifier verifier
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
|
||||
verificationCode = verificationCode.replace("-", "");
|
||||
try {
|
||||
final var response = verifier.verify(sessionId, verificationCode, null);
|
||||
|
@ -127,7 +128,7 @@ public class NumberVerificationUtils {
|
|||
|
||||
final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
|
||||
if (registrationLockData == null) {
|
||||
throw e;
|
||||
throw new PinLockMissingException();
|
||||
}
|
||||
|
||||
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
|
||||
|
|
|
@ -316,6 +316,11 @@ 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>`
|
||||
|
||||
*--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::
|
||||
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.
|
||||
|
|
15
run_tests.sh
15
run_tests.sh
|
@ -176,6 +176,17 @@ run_main -a "$NUMBER_2" receive
|
|||
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
|
||||
run_main -a "$NUMBER_1" 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
|
||||
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
|
||||
|
@ -238,7 +249,9 @@ for OUTPUT in "plain-text" "json"; do
|
|||
run_linked -a "$NUMBER_1" --output="$OUTPUT" receive
|
||||
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
|
||||
if [ "$TEST_REGISTER" -eq 1 ]; then
|
||||
|
|
|
@ -291,6 +291,8 @@ public class App {
|
|||
commandHandler.handleMultiLocalCommand(command, multiAccountManager);
|
||||
} catch (IOException e) {
|
||||
throw new IOErrorException("Failed to load local accounts file", e);
|
||||
} catch (AccountCheckException e) {
|
||||
throw new UnexpectedErrorException("Failed to load account file", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ public class BaseConfig {
|
|||
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
|
||||
|
||||
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
|
||||
.orElse("Signal-Android/7.35.0");
|
||||
.orElse("Signal-Android/7.47.1");
|
||||
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
|
||||
? "signal-cli"
|
||||
: PROJECT_NAME + "/" + PROJECT_VERSION;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.asamk.signal;
|
||||
|
||||
import org.asamk.signal.commands.exceptions.CommandException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -11,7 +12,7 @@ import sun.misc.Signal;
|
|||
public class Shutdown {
|
||||
|
||||
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 boolean initialized = false;
|
||||
|
||||
|
@ -43,9 +44,17 @@ public class Shutdown {
|
|||
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 {
|
||||
shutdown.get();
|
||||
final var result = shutdown.get();
|
||||
if (result instanceof CommandException e) {
|
||||
throw e;
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
|
|||
final OutputWriter outputWriter
|
||||
) throws CommandException {
|
||||
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 receiveMode = ns.<ReceiveMode>get("receive-mode");
|
||||
final var receiveConfig = getReceiveConfig(ns);
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.asamk.signal.commands.exceptions.UserErrorException;
|
|||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.output.OutputWriter;
|
||||
|
||||
|
@ -50,6 +51,8 @@ public class FinishChangeNumberCommand implements JsonRpcLocalCommand {
|
|||
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
|
||||
} catch (IncorrectPinException e) {
|
||||
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) {
|
||||
throw new UserErrorException("This command doesn't work on linked devices.");
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -64,9 +64,16 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
|
|||
}
|
||||
|
||||
final var usernames = ns.<String>getList("username");
|
||||
final var registeredUsernames = usernames == null
|
||||
? Map.<String, UsernameStatus>of()
|
||||
: m.getUsernameStatus(new HashSet<>(usernames));
|
||||
final Map<String, UsernameStatus> registeredUsernames;
|
||||
try {
|
||||
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
|
||||
switch (outputWriter) {
|
||||
|
|
|
@ -66,6 +66,9 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||
.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. "
|
||||
+ "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")
|
||||
.help("Clear session state and send end session message.")
|
||||
.action(Arguments.storeTrue());
|
||||
|
@ -164,6 +167,7 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||
if (attachments == null) {
|
||||
attachments = List.of();
|
||||
}
|
||||
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
|
||||
|
||||
final var selfNumber = m.getSelfNumber();
|
||||
|
||||
|
@ -239,6 +243,7 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
viewOnce,
|
||||
mentions,
|
||||
Optional.ofNullable(quote),
|
||||
Optional.ofNullable(sticker),
|
||||
|
@ -250,8 +255,14 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||
: m.sendMessage(message, recipientIdentifiers, notifySelf);
|
||||
outputResult(outputWriter, results);
|
||||
} catch (AttachmentInvalidException | IOException e) {
|
||||
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
|
||||
.getSimpleName() + ")", 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()
|
||||
.getSimpleName() + ")", e);
|
||||
}
|
||||
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
||||
throw new UserErrorException(e.getMessage());
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException;
|
|||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
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.output.JsonWriter;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -76,6 +77,8 @@ public class VerifyCommand implements RegistrationCommand, JsonRpcRegistrationCo
|
|||
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
|
||||
} catch (IncorrectPinException e) {
|
||||
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) {
|
||||
throw new IOErrorException("Verify error: " + e.getMessage(), e);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
package org.asamk.signal.dbus;
|
||||
|
||||
import org.asamk.signal.DbusConfig;
|
||||
import org.asamk.signal.Shutdown;
|
||||
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.UserErrorException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
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.DBusConnectionBuilder;
|
||||
import org.freedesktop.dbus.exceptions.DBusException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -94,7 +98,9 @@ public class DbusHandler implements AutoCloseable {
|
|||
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
|
||||
logger.debug("Starting DBus server on {} bus: {}", busType, busname);
|
||||
try {
|
||||
dBusConnection = DBusConnectionBuilder.forType(busType).build();
|
||||
dBusConnection = DBusConnectionBuilder.forType(busType)
|
||||
.withDisconnectCallback(new DisconnectCallback())
|
||||
.build();
|
||||
dbusRunner.run(dBusConnection);
|
||||
} catch (DBusException 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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.asamk.signal.manager.RegistrationManager;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
||||
|
@ -105,6 +106,8 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
|
|||
+ (e.getTimeRemaining() / 1000 / 60 / 60));
|
||||
} catch (IncorrectPinException e) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -236,6 +236,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
false,
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
|
@ -399,6 +400,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
false,
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
|
@ -444,6 +446,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||
try {
|
||||
final var message = new Message(messageText,
|
||||
attachments,
|
||||
false,
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue