Compare commits
423 Commits
andrew/tes
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8cb0c5fc | ||
|
|
c41e917d4e | ||
|
|
4d43a6270a | ||
|
|
7543c3d35b | ||
|
|
bd383e51f0 | ||
|
|
fb9407cbcb | ||
|
|
9903175a51 | ||
|
|
af55da7bbd | ||
|
|
875f93019b | ||
|
|
73bcc78e12 | ||
|
|
d0b3edc0f1 | ||
|
|
ec67c55017 | ||
|
|
ee47959258 | ||
|
|
7b399f26d8 | ||
|
|
f70d1faaa0 | ||
|
|
b8f2aaf5dc | ||
|
|
2af375875b | ||
|
|
6e5a0466b3 | ||
|
|
2486ffe4e2 | ||
|
|
9adf4191f0 | ||
|
|
4fe3cbf6b6 | ||
|
|
a360c14a58 | ||
|
|
d33445757d | ||
|
|
2c7c0d16dc | ||
|
|
9be982cbf3 | ||
|
|
8b4eff395e | ||
|
|
a84b3560d1 | ||
|
|
36add9ba9b | ||
|
|
bbc16886ca | ||
|
|
29adeae754 | ||
|
|
df8cf83114 | ||
|
|
0929fa7cc7 | ||
|
|
b6ae27f259 | ||
|
|
16a1b80a82 | ||
|
|
191f78b8e2 | ||
|
|
dc72d43c21 | ||
|
|
3e616bfca4 | ||
|
|
4c460615cd | ||
|
|
309a220fe3 | ||
|
|
ce7b55a3af | ||
|
|
12228b31e7 | ||
|
|
7c6ef18903 | ||
|
|
fc0fb6fd5c | ||
|
|
12617f69b7 | ||
|
|
7d5eebee89 | ||
|
|
202df549ef | ||
|
|
bbc6c37c1a | ||
|
|
8582eb70d6 | ||
|
|
3ae7241bc1 | ||
|
|
bddcb4e15f | ||
|
|
14f4cee71f | ||
|
|
7962de2f9a | ||
|
|
67a8a50c7a | ||
|
|
ded51f5bdb | ||
|
|
b41aeb1fda | ||
|
|
9abd8a52d2 | ||
|
|
8044869780 | ||
|
|
91932c20c5 | ||
|
|
cac69c686a | ||
|
|
ccd54a28f9 | ||
|
|
aa6769e2e2 | ||
|
|
5967b6a9b1 | ||
|
|
98915c442d | ||
|
|
a110fe85c7 | ||
|
|
5cf8b0d717 | ||
|
|
c975a83e8a | ||
|
|
c683a45242 | ||
|
|
00709fe70b | ||
|
|
f556ac7b1f | ||
|
|
9178fbc04e | ||
|
|
b58bd7d5df | ||
|
|
a8083dee6e | ||
|
|
3d333fa3e6 | ||
|
|
5289445a4e | ||
|
|
480021a260 | ||
|
|
5835078f50 | ||
|
|
c235197dad | ||
|
|
1a347324e4 | ||
|
|
bd5e21217a | ||
|
|
cf9a7445c6 | ||
|
|
939690b070 | ||
|
|
4cafee67b0 | ||
|
|
50a7742adf | ||
|
|
87ba49f645 | ||
|
|
8418be45db | ||
|
|
81afdfe2fa | ||
|
|
3d4e950848 | ||
|
|
f645178020 | ||
|
|
c077c48393 | ||
|
|
400a021c2b | ||
|
|
1bca413376 | ||
|
|
a7a24ed517 | ||
|
|
c18509b108 | ||
|
|
6f48650cea | ||
|
|
49d53a6506 | ||
|
|
8df85d3c0b | ||
|
|
1feca0e32e | ||
|
|
407d7e7a27 | ||
|
|
98794de745 | ||
|
|
955e4c9f1d | ||
|
|
e9ec8dd431 | ||
|
|
c7e696c536 | ||
|
|
ec675c52a6 | ||
|
|
0a58e80bbc | ||
|
|
c2db79042d | ||
|
|
467cd795e5 | ||
|
|
e06ff35e1a | ||
|
|
7ca4f738fb | ||
|
|
e3d9c09bec | ||
|
|
703dafe239 | ||
|
|
c3479fabe1 | ||
|
|
daaea0a9e9 | ||
|
|
a4a1191309 | ||
|
|
bd3ee636b8 | ||
|
|
ab73bbf262 | ||
|
|
cb2990f318 | ||
|
|
1f5f12f1fc | ||
|
|
da00edb1ec | ||
|
|
d3eaaa46b7 | ||
|
|
1283a8a00b | ||
|
|
75925e46d7 | ||
|
|
2c0b3c9984 | ||
|
|
f0babf1537 | ||
|
|
c706b7f5ce | ||
|
|
1c9a428c2c | ||
|
|
b9b9cf0684 | ||
|
|
68f33f488d | ||
|
|
db33599cad | ||
|
|
3e0c17541f | ||
|
|
227dc23941 | ||
|
|
e667bd6c03 | ||
|
|
a5e7667488 | ||
|
|
63fd295b9d | ||
|
|
84b2718fd1 | ||
|
|
8abb863cca | ||
|
|
18f47d2cb4 | ||
|
|
358dd5cb80 | ||
|
|
c5b3a53c9c | ||
|
|
8a1663387b | ||
|
|
2936450819 | ||
|
|
1ca00428e0 | ||
|
|
8cc2832763 | ||
|
|
a47ba487a7 | ||
|
|
8bfbd12323 | ||
|
|
863219012f | ||
|
|
260c46ce61 | ||
|
|
f6c4ff2e8d | ||
|
|
7cfb75ea1e | ||
|
|
71183a9043 | ||
|
|
0e51bb356b | ||
|
|
657d185fb8 | ||
|
|
fbe0e69889 | ||
|
|
895b079448 | ||
|
|
fe9d0e761b | ||
|
|
c5edd3fa38 | ||
|
|
be5c8df415 | ||
|
|
19e0b3d34f | ||
|
|
77a04db08e | ||
|
|
6c2bf65989 | ||
|
|
f7c4aceebd | ||
|
|
aad9131f5d | ||
|
|
68019908f8 | ||
|
|
5efc009a63 | ||
|
|
9cf78b7509 | ||
|
|
c8a9b64590 | ||
|
|
8607a91ef6 | ||
|
|
5c4c5435e2 | ||
|
|
55b233d43c | ||
|
|
6a7cc67173 | ||
|
|
4bc22d2216 | ||
|
|
23918f2601 | ||
|
|
39197348f0 | ||
|
|
d604dbd076 | ||
|
|
32bef826ac | ||
|
|
d390508da5 | ||
|
|
06f30cb23d | ||
|
|
d7fcde47d6 | ||
|
|
017c6ec8d3 | ||
|
|
5e5f0ece94 | ||
|
|
cdc158dfdb | ||
|
|
86e4175ce1 | ||
|
|
5e97729155 | ||
|
|
83c83c36a7 | ||
|
|
44bd39743a | ||
|
|
d0d4fdde88 | ||
|
|
42f426fc94 | ||
|
|
36ab93849a | ||
|
|
83f1b335ab | ||
|
|
3f92b94484 | ||
|
|
83ab6d3eec | ||
|
|
f80e6cc647 | ||
|
|
ea62515452 | ||
|
|
352d170876 | ||
|
|
cd28805329 | ||
|
|
bd4ec2bee1 | ||
|
|
ffaa9f0435 | ||
|
|
56e3330a9a | ||
|
|
9c04f9b74c | ||
|
|
d645c48aaa | ||
|
|
db0152418a | ||
|
|
954306ac7f | ||
|
|
46611def16 | ||
|
|
c5845300e9 | ||
|
|
3a82ed8e0b | ||
|
|
1b27f27feb | ||
|
|
d25b710d62 | ||
|
|
2d3f4158ae | ||
|
|
e5647239d8 | ||
|
|
bed6f5e5ae | ||
|
|
38514bdc8a | ||
|
|
5bb92bfe13 | ||
|
|
49513ab592 | ||
|
|
ba9f75383f | ||
|
|
b13f76c8dd | ||
|
|
16be17b4c6 | ||
|
|
c17dc6ca7b | ||
|
|
e65993fdc2 | ||
|
|
c489f3f393 | ||
|
|
67ea4cc351 | ||
|
|
cc3f031eaa | ||
|
|
1e1f808844 | ||
|
|
12d487ffb9 | ||
|
|
c0e19d7d4a | ||
|
|
82f1633d6a | ||
|
|
d1795c244b | ||
|
|
55b71ff92d | ||
|
|
606072ab92 | ||
|
|
bf48c34d23 | ||
|
|
203d8412a1 | ||
|
|
dce9c0d30a | ||
|
|
9bbe720882 | ||
|
|
9e5f68e3fa | ||
|
|
32b5163c2e | ||
|
|
2e6b194ca7 | ||
|
|
7908e504fa | ||
|
|
ae3e04f978 | ||
|
|
a0859c0268 | ||
|
|
8ca5252980 | ||
|
|
0e402a8c95 | ||
|
|
58e916940c | ||
|
|
8406795667 | ||
|
|
f08390b0e2 | ||
|
|
3d150964c2 | ||
|
|
ac903f0d28 | ||
|
|
d73d695bb6 | ||
|
|
91e36ddc64 | ||
|
|
79db90a5d8 | ||
|
|
845c4e3fc2 | ||
|
|
ec3aa0827f | ||
|
|
6dd67d52e8 | ||
|
|
e419b9cf01 | ||
|
|
d7f99838a6 | ||
|
|
cc9d60a8a8 | ||
|
|
6a9f73b998 | ||
|
|
7e84d2d7d8 | ||
|
|
68c9a0616b | ||
|
|
f92938d492 | ||
|
|
53d4f5a3fe | ||
|
|
f8f821e9ea | ||
|
|
a6edef3ad0 | ||
|
|
f758cf9794 | ||
|
|
6fdb2d5166 | ||
|
|
70c00c8dc5 | ||
|
|
69735455dd | ||
|
|
77f41bbaeb | ||
|
|
0c874690a0 | ||
|
|
1346a40e34 | ||
|
|
2d4b7b9b3b | ||
|
|
632d32c5fe | ||
|
|
479f044cd6 | ||
|
|
143ca1ce08 | ||
|
|
618f6c96ab | ||
|
|
48c1120601 | ||
|
|
53380fca8e | ||
|
|
b1281d60f9 | ||
|
|
3c5e82b1c6 | ||
|
|
28ffbcaed1 | ||
|
|
c5ba215e3f | ||
|
|
9d9122c154 | ||
|
|
52f4f99c4c | ||
|
|
85bf014f1d | ||
|
|
f5ab0fe616 | ||
|
|
40955292f9 | ||
|
|
67e624edca | ||
|
|
a982a115e4 | ||
|
|
88b7fe66fc | ||
|
|
3de0430b8b | ||
|
|
a95feb9c5d | ||
|
|
bc7f5719d4 | ||
|
|
dff54c1af2 | ||
|
|
0b712a1d0a | ||
|
|
a24341b044 | ||
|
|
bc7d2af953 | ||
|
|
b4bee1c214 | ||
|
|
e11318dbdb | ||
|
|
a7e0cd9be0 | ||
|
|
0480b16ac3 | ||
|
|
d6b61b46d2 | ||
|
|
282fa41534 | ||
|
|
74cb077238 | ||
|
|
f8ba33c1ab | ||
|
|
13649374f5 | ||
|
|
a1a5f0f51e | ||
|
|
73071eee1b | ||
|
|
504af9001c | ||
|
|
c86b4a1f0c | ||
|
|
184b5b9000 | ||
|
|
f2eafbe6f8 | ||
|
|
81e47eb31c | ||
|
|
50e2743229 | ||
|
|
fc1de1e454 | ||
|
|
d9283bf53c | ||
|
|
5aad667adb | ||
|
|
cb685ceb80 | ||
|
|
fce7213e40 | ||
|
|
1d8500621d | ||
|
|
78ca29b493 | ||
|
|
636d6479c9 | ||
|
|
37dda46866 | ||
|
|
a06877a8e8 | ||
|
|
ec420b3102 | ||
|
|
fe149679fa | ||
|
|
70366e86c8 | ||
|
|
d3a4a47177 | ||
|
|
4cd2c507d3 | ||
|
|
e551e2444a | ||
|
|
2abe7c1845 | ||
|
|
f629f724cb | ||
|
|
bea4305cbf | ||
|
|
72fda058ae | ||
|
|
a777b8c520 | ||
|
|
2006368c10 | ||
|
|
f2142df7c0 | ||
|
|
1c54596e66 | ||
|
|
cfaf27f3a2 | ||
|
|
64ba0fcf26 | ||
|
|
5b4fc4850b | ||
|
|
fbdafc0222 | ||
|
|
558bea8229 | ||
|
|
40940e4c31 | ||
|
|
3103b37faf | ||
|
|
0ec05213b9 | ||
|
|
4a0afd11e5 | ||
|
|
46306b2b6b | ||
|
|
55123ec3f3 | ||
|
|
5d1433cedd | ||
|
|
c9115d1955 | ||
|
|
869ae954bb | ||
|
|
2478642135 | ||
|
|
4d743dc447 | ||
|
|
382e138b92 | ||
|
|
7e4c3a7e8f | ||
|
|
2d76abfc38 | ||
|
|
ace404879f | ||
|
|
cd9a196648 | ||
|
|
4c308862a4 | ||
|
|
3929708a59 | ||
|
|
4906c42c5f | ||
|
|
85638b1fee | ||
|
|
6b4770bbf2 | ||
|
|
cd27e75694 | ||
|
|
f91624ac14 | ||
|
|
cbf6220cd8 | ||
|
|
8191a48f15 | ||
|
|
123fae5b45 | ||
|
|
f39f510b56 | ||
|
|
f33376ac03 | ||
|
|
494495504f | ||
|
|
02d51454f2 | ||
|
|
f5a10a78f8 | ||
|
|
8f30203f3d | ||
|
|
1bf4a3c6f5 | ||
|
|
a0b94cf25d | ||
|
|
8cd1c0b12f | ||
|
|
0800506d3d | ||
|
|
81499b2d93 | ||
|
|
c83cae8fd8 | ||
|
|
ebc4724f6c | ||
|
|
6554a83d18 | ||
|
|
94f030cdb7 | ||
|
|
9b63526808 | ||
|
|
f15981ae78 | ||
|
|
ddcbfa560b | ||
|
|
95ee094c0f | ||
|
|
de5bbb992b | ||
|
|
b39e93f1a5 | ||
|
|
8894050176 | ||
|
|
89e3d4df8f | ||
|
|
5a64e17ed4 | ||
|
|
db7d35eddf | ||
|
|
03433189b6 | ||
|
|
c0a2099948 | ||
|
|
42935e5c5a | ||
|
|
de881ddde4 | ||
|
|
92adc95346 | ||
|
|
8c3e1bff2b | ||
|
|
acbe6822c5 | ||
|
|
2dcd1e0b79 | ||
|
|
cc7a670e91 | ||
|
|
76cea46935 | ||
|
|
bb7451a55c | ||
|
|
ef0001108a | ||
|
|
3f0d2b1384 | ||
|
|
8af11e52ea | ||
|
|
7475974889 | ||
|
|
b2c5685080 | ||
|
|
11e8353843 | ||
|
|
776cf0601e | ||
|
|
26f46d3d68 | ||
|
|
177a495eba | ||
|
|
1b2304022a | ||
|
|
56ccf4c38b | ||
|
|
298dd979d7 | ||
|
|
6289924c4c | ||
|
|
6e0057636c | ||
|
|
cbbd9c3281 | ||
|
|
334e1e7962 | ||
|
|
afbda11358 | ||
|
|
38edb37326 | ||
|
|
4154a0c2f0 | ||
|
|
ff8baf8b33 | ||
|
|
ca5612f309 | ||
|
|
be62a177b1 |
@ -1,6 +1,6 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained, used by libcrux
|
||||
"RUSTSEC-2024-0381", # pqcrypto-kyber is unmaintained
|
||||
"RUSTSEC-2024-0436", # paste is unmaintained, used by libsignal-bridge
|
||||
"RUSTSEC-2025-0141", # bincode is unmaintained, used by zkgroup
|
||||
]
|
||||
|
||||
@ -7,5 +7,5 @@ disallowed-methods = [
|
||||
{ path = "jni::JNIEnv::call_static_method_unchecked", reason = "use helper method instead" },
|
||||
{ path = "jni::JNIEnv::new_object", reason = "use helper method instead" },
|
||||
{ path = "jni::JNIEnv::new_object_unchecked", reason = "use helper method instead" },
|
||||
".." # keep any defaults
|
||||
]
|
||||
]
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
3
.github/actionlint.yaml
vendored
3
.github/actionlint.yaml
vendored
@ -3,6 +3,9 @@ self-hosted-runner:
|
||||
labels:
|
||||
# Used in Slow Tests' AArch64 Linux Tests.
|
||||
- ubuntu-24.04-arm64-4-cores
|
||||
# This is... not a custom worker label, but it's not included in actionlint for some reason.
|
||||
# See: https://github.com/rhysd/actionlint/blob/f9408506b4c7f9cda1263bca8166271f65e65c3d/rule_runner_label.go#L29
|
||||
- windows-latest-4-cores
|
||||
|
||||
# Configuration variables in array of strings defined in your repository or
|
||||
# organization. `null` means disabling configuration variables check.
|
||||
|
||||
16
.github/actions/restore-cargo-cache/action.yml
vendored
16
.github/actions/restore-cargo-cache/action.yml
vendored
@ -18,6 +18,9 @@ outputs:
|
||||
cache-key-current:
|
||||
description: 'Hash of current working tree'
|
||||
value: ${{ steps.calculate.outputs['cache-key-current'] }}
|
||||
cache-key:
|
||||
description: 'Full cache key used for cargo artifacts'
|
||||
value: ${{ steps.calculate-primary-cache-key.outputs['cache-primary-key'] }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@ -26,8 +29,15 @@ runs:
|
||||
shell: bash
|
||||
run: python3 "${{ github.action_path }}/calculate_cache_keys.py" --toolchain "${{ inputs.toolchain }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Calculate primary cache key
|
||||
id: calculate-primary-cache-key
|
||||
shell: bash
|
||||
run: echo "cache-primary-key=${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: runs-on/cache@3a15256b3556fbc5ae15f7f04598e4c7680e9c25 # v4
|
||||
id: cache
|
||||
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE != 'true' }}
|
||||
uses: runs-on/cache/restore@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
|
||||
with:
|
||||
# The special handling for the Windows target path comes because we overwrite
|
||||
# $CARGO_BUILD_TARGET_DIR in build_node_bridge.py because Visual Studio's CLI
|
||||
@ -45,6 +55,8 @@ runs:
|
||||
# - We use the working tree hash as the final key component to ensure uniqueness per commit.
|
||||
# - On cache miss, we fall back, in order, to:
|
||||
# 1. Most recent cache from the last common ancestor with main.
|
||||
# 1.1. Most recent cache from the last common ancestor with main's parent.
|
||||
# 1.2. Most recent cache from the last common ancestor with main's grandparent.
|
||||
# 2. Most recent cache for this job/OS/rustc combination.
|
||||
# 3. Most recent cache for this job/OS combination.
|
||||
# This yields perfect hits on reruns while still warming cold builds with close matches.
|
||||
@ -52,5 +64,7 @@ runs:
|
||||
key: ${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}
|
||||
restore-keys: |
|
||||
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-
|
||||
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-parent'] }}-
|
||||
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-grandparent'] }}-
|
||||
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-
|
||||
${{ inputs['job-name'] }}-${{ runner.os }}-
|
||||
|
||||
@ -42,6 +42,17 @@ def get_merge_base() -> str:
|
||||
return output
|
||||
|
||||
|
||||
def get_merge_base_parent(commit: str) -> str:
|
||||
"""Get the parent commit of the given commit."""
|
||||
returncode, output, _ = run_command(
|
||||
['git', 'rev-parse', f'{commit}^'],
|
||||
check=False,
|
||||
)
|
||||
if returncode != 0 or not output:
|
||||
return 'none'
|
||||
return output
|
||||
|
||||
|
||||
def get_working_tree_hash() -> str:
|
||||
"""Get a hash representing the current working tree state."""
|
||||
returncode, working_tree_hash, _ = run_command(
|
||||
@ -109,16 +120,22 @@ def main() -> None:
|
||||
rustc_version_hash = hashlib.sha256(rustc_version.encode()).hexdigest()[:32]
|
||||
|
||||
lca = get_merge_base()
|
||||
lca_parent = get_merge_base_parent(lca)
|
||||
lca_grandparent = get_merge_base_parent(lca_parent)
|
||||
current = get_working_tree_hash()
|
||||
|
||||
print(f'rustc-version={rustc_version_hash}')
|
||||
print(f'cache-key-merge-base={lca}')
|
||||
print(f'cache-key-merge-base-parent={lca_parent}')
|
||||
print(f'cache-key-merge-base-grandparent={lca_grandparent}')
|
||||
print(f'cache-key-current={current}')
|
||||
|
||||
debug_parts = [
|
||||
f'Toolchain={toolchain}',
|
||||
f'RustcVersion={rustc_version}',
|
||||
f'LCA={lca}',
|
||||
f'LCAParent={lca_parent}',
|
||||
f'LCAGrandparent={lca_grandparent}',
|
||||
f'Current={current}',
|
||||
]
|
||||
print('Debug: ' + '; '.join(debug_parts), file=sys.stderr)
|
||||
|
||||
23
.github/actions/save-cargo-cache/action.yml
vendored
Normal file
23
.github/actions/save-cargo-cache/action.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: 'Save Cargo Cache'
|
||||
description: 'Save cargo and build cache artifacts with appropriate keys'
|
||||
inputs:
|
||||
key:
|
||||
description: 'The cache key to save under'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE == 'true' }}
|
||||
uses: runs-on/cache/save@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
|
||||
with:
|
||||
# Keep this path list in sync with restore-cargo-cache/action.yml.
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/registry/src
|
||||
~/.cargo/git/db
|
||||
~/.cargo/git/checkouts
|
||||
target
|
||||
${{ runner.os == 'Windows' && format('{0}\\libsignal', runner.temp) || '' }}
|
||||
key: ${{ inputs.key }}
|
||||
8
.github/workflows/android_integration.yml
vendored
8
.github/workflows/android_integration.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Android Integration Test
|
||||
name: "Integration - Android"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -23,13 +23,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout libsignal
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: libsignal
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Signal-Android
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: signalapp/Signal-Android
|
||||
ref: ${{ inputs.signal_android_branch }}
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
|
||||
177
.github/workflows/build_and_test.yml
vendored
177
.github/workflows/build_and_test.yml
vendored
@ -1,10 +1,15 @@
|
||||
name: Build and Test
|
||||
name: "[CI] Build and Test"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request: # all target branches
|
||||
workflow_dispatch: {}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_cargo_cache:
|
||||
description: Skip cargo cache restore/save steps
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
# On PRs, "head_ref" is defined and is consistent across updates. On
|
||||
# pushes, it's not defined, so we use "run_id", which is unique across
|
||||
@ -22,7 +27,8 @@ env:
|
||||
# For dev builds, include limited debug info in the output. See
|
||||
# https://doc.rust-lang.org/cargo/reference/profiles.html#debug
|
||||
CARGO_PROFILE_DEV_DEBUG: limited
|
||||
SHOULD_USE_CARGO_CACHE: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ENDPOINT != '' }}
|
||||
DO_CLEAN_BUILD_AND_POPULATE_CACHE: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
|
||||
SHOULD_USE_CARGO_CACHE: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ENDPOINT != '' && secrets.R2_BUCKET_NAME != '' && inputs.skip_cargo_cache != true }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
@ -43,11 +49,12 @@ jobs:
|
||||
rust_ios: ${{ steps.filter.outputs.rust_ios }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0
|
||||
- uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0
|
||||
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@ -131,7 +138,7 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -150,7 +157,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust-${{ matrix.version }}
|
||||
@ -174,11 +181,23 @@ jobs:
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Rust docs
|
||||
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features --keep-going
|
||||
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features --no-deps --document-private-items --keep-going
|
||||
if: matrix.version == 'stable'
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
rust32:
|
||||
name: Rust (32-bit testing)
|
||||
|
||||
@ -202,11 +221,11 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib
|
||||
- run: sudo apt-get install -U gcc-multilib g++-multilib
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
@ -223,7 +242,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust32-${{ matrix.version }}
|
||||
@ -232,6 +251,21 @@ jobs:
|
||||
- name: Run tests (32-bit)
|
||||
# Exclude signal-neon-futures because those tests run Node
|
||||
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
|
||||
env:
|
||||
CFLAGS: "-msse2" # for BoringSSL
|
||||
CXXFLAGS: "-msse2" # for BoringSSL
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
rust-fuzz-build:
|
||||
name: Rust (Fuzz Targets)
|
||||
@ -243,7 +277,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -261,12 +295,13 @@ jobs:
|
||||
|
||||
- name: Restore cargo cache
|
||||
id: rust-cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/restore-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust-fuzz-build
|
||||
@ -287,6 +322,18 @@ jobs:
|
||||
env:
|
||||
RUSTFLAGS: --cfg fuzzing
|
||||
|
||||
- name: Save cargo cache
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
rust-fmt:
|
||||
name: Rust (Formatting and Acknowledgments)
|
||||
|
||||
@ -299,7 +346,7 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -316,7 +363,7 @@ jobs:
|
||||
run: rustup toolchain install "${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }}" --profile minimal --component rustfmt
|
||||
|
||||
- name: Cache locally-built tools
|
||||
uses: runs-on/cache@3a15256b3556fbc5ae15f7f04598e4c7680e9c25 # v4
|
||||
uses: runs-on/cache@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
|
||||
with:
|
||||
path: local-tools
|
||||
key: local-tools-${{ runner.os }}-infra-${{ hashFiles('acknowledgments/cargo-about-version', '.taplo-cli-version') }}
|
||||
@ -343,7 +390,7 @@ jobs:
|
||||
working-directory: rust/protocol/cross-version-testing
|
||||
|
||||
- name: Check acknowledgments
|
||||
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh && git diff --exit-code acknowledgments
|
||||
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh --check
|
||||
|
||||
java_android:
|
||||
name: Java Android
|
||||
@ -364,7 +411,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
# Download all commits so we can search for the merge base with origin/main.
|
||||
@ -391,12 +438,12 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: java-android
|
||||
|
||||
- run: ./gradlew --dependency-verification strict :android:build :android:assembleAndroidTest :android:lintDebug :android:packaging-test:assembleDebugAndroidTest :android:benchmarks:assembleReleaseAndroidTest -PandroidArchs=arm,arm64 -x :makeJniLibrariesDesktop | tee ./gradle-output.txt
|
||||
- run: ./gradlew --dependency-verification strict --warning-mode fail :android:build :android:assembleAndroidTest :android:lintDebug :android:packaging-test:assembleDebugAndroidTest :android:benchmarks:assembleReleaseAndroidTest -PandroidArchs=arm,arm64 -x :makeJniLibrariesDesktop | tee ./gradle-output.txt
|
||||
working-directory: java
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
@ -410,6 +457,18 @@ jobs:
|
||||
|
||||
- run: grep -v -F '***' ./check_code_size-output.txt >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
java_jvm:
|
||||
name: Java JVM
|
||||
|
||||
@ -427,7 +486,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@ -449,7 +508,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: java-jvm
|
||||
@ -457,9 +516,21 @@ jobs:
|
||||
- name: Verify that the JNI bindings are up to date
|
||||
run: rust/bridge/jni/bin/gen_java_decl.py --verify
|
||||
|
||||
- run: ./gradlew --dependency-verification strict build -PskipAndroid
|
||||
- run: ./gradlew --dependency-verification strict --warning-mode fail build -PskipAndroid
|
||||
working-directory: java
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
node:
|
||||
name: Node
|
||||
|
||||
@ -467,7 +538,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest-4-cores, windows-latest, macos-15]
|
||||
os: [ubuntu-latest-4-cores, windows-latest-4-cores, macos-15-xlarge]
|
||||
|
||||
needs: changes
|
||||
|
||||
@ -476,7 +547,7 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -490,7 +561,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: node
|
||||
@ -508,17 +579,17 @@ jobs:
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
||||
- run: choco install protoc
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
||||
- run: npm ci
|
||||
@ -541,6 +612,18 @@ jobs:
|
||||
- run: npm run test
|
||||
working-directory: node
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
swift_package:
|
||||
name: Swift Package
|
||||
|
||||
@ -553,7 +636,7 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -567,7 +650,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: swift-package
|
||||
@ -592,6 +675,18 @@ jobs:
|
||||
run: swift run -v Benchmarks --allow-debug-build
|
||||
working-directory: swift/Benchmarks
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
# Disabled for now, broken on Linux in the Swift 6.0 release.
|
||||
# See https://forums.swift.org/t/generate-documentation-failing-for-swift-6-pre-release/74534
|
||||
# - name: Build Swift package documentation
|
||||
@ -601,7 +696,7 @@ jobs:
|
||||
swift_cocoapod:
|
||||
name: Swift CocoaPod
|
||||
|
||||
runs-on: macos-15
|
||||
runs-on: macos-15-xlarge
|
||||
|
||||
needs: changes
|
||||
|
||||
@ -611,9 +706,11 @@ jobs:
|
||||
|
||||
env:
|
||||
LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH: 1
|
||||
# For Swift 6.2. Can be removed when advancing to the macos-26 runner.
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.3.app
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@ -637,7 +734,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: swift-cocoapod
|
||||
@ -649,3 +746,17 @@ jobs:
|
||||
|
||||
- name: Run pod lint
|
||||
run: pod lib lint --verbose --platforms=ios --skip-tests
|
||||
env:
|
||||
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
|
||||
|
||||
- name: Save cargo cache
|
||||
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
|
||||
uses: ./.github/actions/save-cargo-cache
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: us-east-1
|
||||
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
|
||||
|
||||
4
.github/workflows/check_versions.yml
vendored
4
.github/workflows/check_versions.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Check Versions
|
||||
name: "[CI] Check Versions"
|
||||
# We want to run this job on all changes, so that we do not have to risk breakage slipping
|
||||
# through due to the set of files included in the version consistency check getting out of sync
|
||||
# with the set of files checked by the test dispatch logic.
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Docs
|
||||
name: "[CI] Docs"
|
||||
|
||||
env:
|
||||
MDBOOK_VERSION: "0.4.43"
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
20
.github/workflows/ios_artifacts.yml
vendored
20
.github/workflows/ios_artifacts.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Build iOS Artifacts
|
||||
name: "Release - iOS"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -22,24 +22,24 @@ jobs:
|
||||
# Needed for google-github-actions/auth.
|
||||
id-token: 'write'
|
||||
|
||||
runs-on: macos-15
|
||||
runs-on: macos-15-xlarge
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const dryRun = ${{ inputs.dry_run }};
|
||||
const refType = '${{ github.ref_type }}';
|
||||
const refName = '${{ github.ref_name }}';
|
||||
console.log(dryRun
|
||||
? `Running in 'dry run' mode on '${refName}' ${refType}`
|
||||
console.log(dryRun
|
||||
? `Running in 'dry run' mode on '${refName}' ${refType}`
|
||||
: `Running on '${refName}' ${refType}`);
|
||||
if (refType !== 'tag' && !dryRun) {
|
||||
core.setFailed("the action should either be launched on a tag or with a 'dry run' switch");
|
||||
@ -75,18 +75,18 @@ jobs:
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
- name: Attach artifact to the run
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
path: ${{ steps.archive-name.outputs.name }}
|
||||
name: libsignal-client-ios
|
||||
|
||||
- uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
|
||||
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
|
||||
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
|
||||
|
||||
- uses: google-github-actions/upload-cloud-storage@22121cd842b0d185e042e28d969925b538c33d77 # v2.1.0
|
||||
- uses: google-github-actions/upload-cloud-storage@6397bd7208e18d13ba2619ee21b9873edc94427a # v3.0.0
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
path: ${{ steps.archive-name.outputs.name }}
|
||||
@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
# This step is expected to fail if not run on a tag.
|
||||
- name: Upload checksum to release
|
||||
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
allowUpdates: true
|
||||
|
||||
52
.github/workflows/jni_artifacts.yml
vendored
52
.github/workflows/jni_artifacts.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Upload Java libraries to Sonatype
|
||||
name: "Release - Java"
|
||||
run-name: ${{ github.workflow }} (${{ github.ref_name }})
|
||||
|
||||
on:
|
||||
@ -21,22 +21,22 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-15]
|
||||
os: [windows-latest-8-cores, macos-15-xlarge]
|
||||
include:
|
||||
- os: windows-latest
|
||||
- os: macos-15
|
||||
- os: windows-latest-8-cores
|
||||
- os: macos-15-xlarge
|
||||
additional-rust-target: x86_64-apple-darwin
|
||||
# Ubuntu binaries are built using Docker, below
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const dryRun = ${{ inputs.dry_run }};
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- run: choco install protoc
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
@ -87,7 +87,7 @@ jobs:
|
||||
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
|
||||
|
||||
- name: Upload client libraries
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-client libraries (${{matrix.os}})
|
||||
path: |
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
java/client/src/main/resources/*.dylib
|
||||
|
||||
- name: Upload server libraries
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-server libraries (${{matrix.os}})
|
||||
path: |
|
||||
@ -109,13 +109,13 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
|
||||
|
||||
@ -125,26 +125,31 @@ jobs:
|
||||
publish:
|
||||
name: Build for production and publish
|
||||
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed for google-github-actions/auth.
|
||||
id-token: write
|
||||
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
needs: [build, verify-rust]
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download built client libraries
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: java/client/src/main/resources
|
||||
pattern: libsignal-client*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download built server libraries
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: java/server/src/main/resources
|
||||
pattern: libsignal-server*
|
||||
@ -156,31 +161,38 @@ jobs:
|
||||
|
||||
- name: Upload libsignal-android
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-android
|
||||
path: java/android/build/outputs/aar/libsignal-android-release.aar
|
||||
|
||||
- name: Upload libsignal-client
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-client
|
||||
path: java/client/build/libs/libsignal-client-*.jar
|
||||
|
||||
- name: Upload libsignal-server
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-server
|
||||
path: java/server/build/libs/libsignal-server-*.jar
|
||||
|
||||
- id: gcp-auth
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
|
||||
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
|
||||
token_format: 'access_token'
|
||||
|
||||
- run: make publish_java
|
||||
if: ${{ !inputs.dry_run }}
|
||||
working-directory: java
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
|
||||
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||
CLOUDSDK_AUTH_ACCESS_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
|
||||
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||
# ASCII-armored PGP secret key
|
||||
|
||||
6
.github/workflows/lints.yml
vendored
6
.github/workflows/lints.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Lints
|
||||
name: "[CI] Lints"
|
||||
# This is in a separate job because we have shell scripts scattered across all our targets,
|
||||
# *and* some of them have common dependencies.
|
||||
|
||||
@ -16,10 +16,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: sudo apt-get update && sudo apt-get install python3-flake8 python3-flake8-comprehensions python3-flake8-deprecated python3-flake8-import-order python3-flake8-quotes python3-mypy
|
||||
- run: sudo apt-get install -U python3-flake8 python3-flake8-comprehensions python3-flake8-deprecated python3-flake8-import-order python3-flake8-quotes python3-mypy
|
||||
- run: |
|
||||
shopt -s globstar
|
||||
shellcheck -- **/*.sh bin/verify_duplicate_crates bin/adb-run-test
|
||||
|
||||
44
.github/workflows/npm.yml
vendored
44
.github/workflows/npm.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Publish to NPM
|
||||
name: "Release - NPM"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -24,23 +24,23 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-15]
|
||||
os: [windows-latest-8-cores, macos-15-xlarge]
|
||||
include:
|
||||
- os: macos-15
|
||||
- os: macos-15-xlarge
|
||||
rust-cross-target: x86_64-apple-darwin
|
||||
- os: windows-latest
|
||||
- os: windows-latest-8-cores
|
||||
rust-cross-target: aarch64-pc-windows-msvc
|
||||
# Ubuntu binaries are built using Docker, below
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const dryRun = ${{ inputs.dry_run }};
|
||||
@ -67,14 +67,14 @@ jobs:
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
|
||||
- run: cargo +stable install dump_syms --no-default-features --features cli
|
||||
- run: cargo +stable install dump_syms --locked --no-default-features --features cli
|
||||
|
||||
- name: Get Node version from .nvmrc
|
||||
id: get-nvm-version
|
||||
shell: bash
|
||||
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.24
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@ -97,13 +97,13 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Upload library
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal_client (${{matrix.os}})
|
||||
path: node/prebuilds/*
|
||||
|
||||
- name: Upload debug info
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: Debug info (${{matrix.os}})
|
||||
path: |
|
||||
@ -113,25 +113,25 @@ jobs:
|
||||
build-docker:
|
||||
name: Build (Ubuntu via Docker)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: node/docker-prebuildify.sh
|
||||
|
||||
- name: Upload library
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal_client (ubuntu-docker)
|
||||
path: node/prebuilds/*
|
||||
|
||||
- name: Upload debug info
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: Debug info (ubuntu-docker)
|
||||
path: |
|
||||
@ -145,16 +145,16 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
@ -172,24 +172,24 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Download built libraries
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: libsignal_client*
|
||||
path: node/prebuilds
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download debug info
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Debug info*
|
||||
path: debuginfo
|
||||
@ -220,7 +220,7 @@ jobs:
|
||||
|
||||
# This step is expected to fail if not run on a tag.
|
||||
- name: Upload debug info to release
|
||||
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
allowUpdates: true
|
||||
|
||||
6
.github/workflows/release_notes.yml
vendored
6
.github/workflows/release_notes.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Check release notes
|
||||
name: "[CI] Check release notes"
|
||||
# This is in a separate job because it only runs on pull requests and triggers
|
||||
# on label changes in addition to code changes.
|
||||
|
||||
@ -13,8 +13,8 @@ env:
|
||||
jobs:
|
||||
check:
|
||||
name: Check for release notes
|
||||
# Don't check draft PRs
|
||||
if: github.event.pull_request.draft == false
|
||||
# Only check non-draft PRs in Signal's private repo.
|
||||
if: (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private') && github.event.pull_request.draft == false)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
131
.github/workflows/slow_tests.yml
vendored
131
.github/workflows/slow_tests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Slow Tests
|
||||
name: "Integration - Slow Tests"
|
||||
|
||||
env:
|
||||
ANDROID_NDK_VERSION: 28.0.13004108
|
||||
@ -27,16 +27,20 @@ on:
|
||||
type: boolean
|
||||
description: 'Skip Key Transparency tests (sets LIBSIGNAL_TESTING_IGNORE_KT_TESTS)'
|
||||
default: false
|
||||
bigger_workers:
|
||||
type: boolean
|
||||
description: 'Run on larger, more expensive workers for faster results'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
java-docker:
|
||||
name: Java (Docker)
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
@ -47,19 +51,19 @@ jobs:
|
||||
echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=${LIBSIGNAL_TESTING_IGNORE_KT_TESTS:-<unset>}"
|
||||
- run: make -C java
|
||||
- name: Upload JNI libraries
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jniLibs
|
||||
path: java/android/src/main/jniLibs/*
|
||||
retention-days: 2
|
||||
- name: Upload full JARs
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jars
|
||||
path: java/*/build/libs/*
|
||||
retention-days: 2
|
||||
- name: Upload full AARs
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: aars
|
||||
path: java/android/build/outputs/aar/*
|
||||
@ -67,12 +71,12 @@ jobs:
|
||||
|
||||
java-docker-secondary:
|
||||
name: Java (Secondary Docker)
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
@ -80,13 +84,13 @@ jobs:
|
||||
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
|
||||
- run: make -C java
|
||||
- name: Upload full JARs
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jars-secondary
|
||||
path: java/*/build/libs/*
|
||||
retention-days: 2
|
||||
- name: Upload full AARs
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: aars-secondary
|
||||
path: java/android/build/outputs/aar/*
|
||||
@ -100,22 +104,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download jars
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: jars
|
||||
path: a/jars/
|
||||
- name: Download jars (secondary)
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: jars-secondary
|
||||
path: b/jars/
|
||||
- name: Download aars
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: aars
|
||||
path: a/aars/
|
||||
- name: Download aars (secondary)
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: aars-secondary
|
||||
path: b/aars/
|
||||
@ -128,14 +132,14 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
|
||||
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
|
||||
|
||||
- run: sudo apt-get install protobuf-compiler
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: ./gradlew :client:test :server:test -PskipAndroid -PjniTypeTagging -PjniCheckAnnotations
|
||||
working-directory: java
|
||||
@ -143,7 +147,7 @@ jobs:
|
||||
android-emulator-tests:
|
||||
name: Android Emulator Tests
|
||||
# For hardware acceleration; see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
|
||||
env:
|
||||
# Proxy server tests will fail on Android API prior to 28
|
||||
LIBSIGNAL_TESTING_PROXY_SERVER: ${{ fromJSON(matrix.api_level) >= 28 && secrets.LIBSIGNAL_TESTING_PROXY_SERVER || '' }}
|
||||
@ -154,12 +158,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 21 is our minimal API level
|
||||
# 23 is our minimal API level
|
||||
# 33 is our target API level
|
||||
include:
|
||||
- api_level: 21
|
||||
- api_level: 23
|
||||
arch: x86
|
||||
- api_level: 21
|
||||
- api_level: 23
|
||||
arch: x86_64
|
||||
- api_level: 33
|
||||
arch: x86_64
|
||||
@ -178,20 +182,20 @@ jobs:
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download JNI libraries
|
||||
id: download
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: jniLibs
|
||||
path: java/android/src/main/jniLibs/
|
||||
|
||||
# From reactivecircus/android-emulator-runner
|
||||
- name: AVD cache
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
@ -201,23 +205,23 @@ jobs:
|
||||
|
||||
- name: Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api_level }}
|
||||
ndk: ${{ env.ANDROID_NDK_VERSION }}
|
||||
force-avd-creation: false
|
||||
disk-size: 1024M
|
||||
disk-size: 4096M
|
||||
emulator-options: -no-window -noaudio -no-boot-anim -no-metrics
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Run tests
|
||||
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api_level }}
|
||||
ndk: ${{ env.ANDROID_NDK_VERSION }}
|
||||
disk-size: 1024M
|
||||
disk-size: 4096M
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim -no-metrics
|
||||
script: |
|
||||
@ -234,7 +238,7 @@ jobs:
|
||||
|
||||
- name: Upload logcat logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: logcat-logs-api${{ matrix.api_level }}-${{ matrix.arch }}
|
||||
path: java/logcat.log
|
||||
@ -247,14 +251,14 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
|
||||
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
@ -269,12 +273,12 @@ jobs:
|
||||
|
||||
node-docker:
|
||||
name: Node (Ubuntu via Docker)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
@ -286,7 +290,7 @@ jobs:
|
||||
env:
|
||||
PREBUILDS_ONLY: 1
|
||||
- name: Upload prebuilds
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: node-prebuilds
|
||||
path: node/prebuilds
|
||||
@ -294,17 +298,17 @@ jobs:
|
||||
|
||||
node-docker-secondary:
|
||||
name: Node (Secondary Ubuntu via Docker)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: node/docker-prebuildify.sh
|
||||
- name: Upload prebuilds
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: node-prebuilds-secondary
|
||||
path: node/prebuilds
|
||||
@ -318,12 +322,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download prebuilds
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: node-prebuilds
|
||||
path: a/prebuilds/
|
||||
- name: Download prebuilds (secondary)
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: node-prebuilds-secondary
|
||||
path: b/prebuilds/
|
||||
@ -336,7 +340,7 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-pc-windows-msvc
|
||||
@ -351,7 +355,7 @@ jobs:
|
||||
id: get-nvm-version
|
||||
shell: bash
|
||||
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Build for arm64
|
||||
@ -364,14 +368,15 @@ jobs:
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
|
||||
# Uncomment this to select a specific version of Xcode to build and test with. Check the runner
|
||||
# Selects a specific version of Xcode to build and test with. Check the runner
|
||||
# image (in https://github.com/actions/runner-images/) to see which ones are available. You can
|
||||
# also set this on specific steps if necessary.
|
||||
# env:
|
||||
# DEVELOPER_DIR: /Applications/Xcode_16.2.app
|
||||
env:
|
||||
# For Swift 6.2. Can be commented out again when the default runner moves to macos-26.
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.3.app
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
@ -401,6 +406,8 @@ jobs:
|
||||
# We run this for the non-hermetic tests; it's otherwise the same as regular CI.
|
||||
- name: Run pod lint
|
||||
run: pod lib lint --verbose --platforms=ios
|
||||
env:
|
||||
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
|
||||
|
||||
# Make sure we can build for device, just for completeness.
|
||||
- name: Set up testing workspace
|
||||
@ -422,14 +429,14 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Enable KT skip flag
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
|
||||
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
|
||||
- run: sudo apt-get install -U gcc-multilib g++-multilib protobuf-compiler
|
||||
|
||||
- run: rustup +stable target add i686-unknown-linux-gnu
|
||||
|
||||
@ -469,6 +476,8 @@ jobs:
|
||||
run: cargo +stable test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
CFLAGS: "-msse2" # for BoringSSL
|
||||
CXXFLAGS: "-msse2" # for BoringSSL
|
||||
|
||||
- name: Run libsignal-protocol cross-version tests
|
||||
run: cargo +stable test --no-fail-fast
|
||||
@ -497,16 +506,16 @@ jobs:
|
||||
FUZZ_JOBS: 4 # because this is a "4-cores" runner
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- name: Cache cargo-fuzz
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: local-tools
|
||||
key: ${{ runner.os }}-fuzzing-local-tools-${{ env.CARGO_FUZZ_VERSION }}
|
||||
@ -524,10 +533,13 @@ jobs:
|
||||
- run: cargo fuzz build sealed_sender_v2 && cargo fuzz run sealed_sender_v2 -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
|
||||
working-directory: rust/protocol
|
||||
|
||||
- run: cargo fuzz build session_management && cargo fuzz run session_management -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
|
||||
working-directory: rust/protocol
|
||||
|
||||
- run: cargo fuzz build dcap && cargo fuzz run dcap -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
|
||||
working-directory: rust/attest
|
||||
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: fuzzing-artifacts-${{ github.sha }}
|
||||
@ -535,12 +547,23 @@ jobs:
|
||||
|
||||
# This isn't fuzzing, but we have to do it with a nightly compiler, so we're going to tack it on to this job.
|
||||
- name: Build everything with no lockfile and -Zdirect-minimal-versions
|
||||
run: mkdir minimal-versions && bin/without_building_boring.sh cargo check --workspace --all-targets --all-features --verbose --keep-going -Zdirect-minimal-versions -Zunstable-options --lockfile-path minimal-versions/Cargo.lock
|
||||
run: mkdir minimal-versions && CARGO_RESOLVER_LOCKFILE_PATH=minimal-versions/Cargo.lock bin/without_building_boring.sh cargo check --workspace --all-targets --all-features --verbose --keep-going -Zdirect-minimal-versions -Zlockfile-path
|
||||
|
||||
report_failures:
|
||||
report-failures:
|
||||
name: Report Failures
|
||||
runs-on: ubuntu-latest
|
||||
needs: [java-docker, java-reproducibility, android-emulator-tests, aarch64, node-docker, node-reproducibility, node-windows-arm64, swift-cocoapod, rust-stable-testing, rust-fuzzing]
|
||||
needs:
|
||||
- java-docker
|
||||
- java-reproducibility
|
||||
- java-extra-bridging-checks
|
||||
- android-emulator-tests
|
||||
- aarch64
|
||||
- node-docker
|
||||
- node-reproducibility
|
||||
- node-windows-arm64
|
||||
- swift-cocoapod
|
||||
- rust-stable-testing
|
||||
- rust-fuzzing
|
||||
if: ${{ failure() && github.event_name == 'schedule' }}
|
||||
|
||||
permissions:
|
||||
@ -549,7 +572,7 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitComment({
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
name: "[auto] Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 12 * * *' # 7:15 EST, early in a workday
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
/.idea
|
||||
.idea
|
||||
*.iml
|
||||
/target
|
||||
/swift/**/.build
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
include = ["Cargo.toml", "rust/**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
reorder_keys = false
|
||||
align_comments = false
|
||||
indent_string = ' '
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "workspace.dependencies", "dev-dependencies", "build-dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = true
|
||||
inline_table_expand = false
|
||||
reorder_keys = true
|
||||
|
||||
@ -9,7 +9,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
# General
|
||||
|
||||
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintainence, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
|
||||
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintenance, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
|
||||
|
||||
(Not that you should be sloppy in the bridging layer. Maintainability is still a priority!)
|
||||
|
||||
@ -45,7 +45,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
- **Prefer `expect()` to `unwrap()`.** As noted, we don't have a no-panics policy, but `expect()` forces you to write down why you believe something should *never* happen except for programmer errors. In particular, untrusted input that fails to validate should *not* panic.
|
||||
|
||||
(Yes, there's a Clippy lint for this, but we also have a lot of code that predates this guideline.)
|
||||
As an exception, it's okay to use `unwrap()` in tests, though `expect()` is still preferred if it's for the thing you're actively testing.
|
||||
|
||||
- You don't have to write doc comments on everything, but **if you do write a comment, make it a doc comment**, because they show up more nicely in IDEs.
|
||||
|
||||
@ -53,7 +53,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
- Crate-level Cargo.tomls don't usually inherit the workspace `rust-version`, because many crates are relatively stable and may continue working for external folks using earlier versions of Rust even though we no longer test for them; picking up the top-level MSRV update would therefore be unnecessarily breaking. Instead, they have a `rust-version` that indicates a known minimum at some point in the past; it may be too low, but it will never be overly high. The exceptions are the `bridge` crates, which are not intended to be used for anything but the app language libraries.
|
||||
|
||||
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases).
|
||||
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases). Unreleased changes are collected in [RELEASE_NOTES.md][], which is reset after each release.
|
||||
|
||||
- **Avoid `cargo add`**, or fix up the Cargo.toml afterwards. Some of our dependency lists are organized and `cargo add` doesn't respect that.
|
||||
|
||||
@ -65,6 +65,10 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
These are automatically detected on x86_64, but will require an opt-in for aarch64 until we can update to `aes 0.9` or newer (not out yet at the time of this writing). All our app library build scripts set this themselves, but doing a manual `cargo build --release` will not.
|
||||
|
||||
- Our bridging logic uses code generation tools for the app-language interface files (C header for Swift, wrapper APIs for Java/Kotlin and TypeScript). These tools, or the macros used with them, depend on how types are written in `#[bridge_fn]` and other bridged APIs. Therefore, **use qualified names for non-std, non-libsignal types** in bridged signatures, so that they can be matched specifically and without ambiguity.
|
||||
|
||||
(There is one exception: `uuid::Uuid` has been `Uuid` for a long time, and is sufficiently unique to justify leaving it that way.)
|
||||
|
||||
|
||||
## Async
|
||||
|
||||
@ -81,7 +85,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
# Java
|
||||
|
||||
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 21, at the time of this writing), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
|
||||
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 23, at the time of this update), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
|
||||
|
||||
- **Put server-specific APIs in the server/ folder if they're not needed to test client features**, so they don't add code size for Android.
|
||||
|
||||
|
||||
2284
Cargo.lock
generated
2284
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
103
Cargo.toml
103
Cargo.toml
@ -3,6 +3,7 @@
|
||||
members = [
|
||||
"rust/attest",
|
||||
"rust/crypto",
|
||||
"rust/debug",
|
||||
"rust/device-transfer",
|
||||
"rust/keytrans",
|
||||
"rust/media",
|
||||
@ -21,6 +22,7 @@ members = [
|
||||
"rust/bridge/jni/impl",
|
||||
"rust/bridge/jni/testing",
|
||||
"rust/bridge/node",
|
||||
"rust/bridge/node/native_ts",
|
||||
]
|
||||
default-members = [
|
||||
"rust/crypto",
|
||||
@ -37,10 +39,10 @@ default-members = [
|
||||
resolver = "2" # so that our dev-dependency features don't leak into products
|
||||
|
||||
[workspace.package]
|
||||
version = "0.85.6"
|
||||
version = "0.94.1"
|
||||
authors = ["Signal Messenger LLC"]
|
||||
license = "AGPL-3.0-only"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Prefer TryFrom between integers unless truncation is desired.
|
||||
@ -60,11 +62,13 @@ device-transfer = { path = "rust/device-transfer" }
|
||||
libsignal-account-keys = { path = "rust/account-keys" }
|
||||
libsignal-cli-utils = { path = "rust/cli-utils" }
|
||||
libsignal-core = { path = "rust/core" }
|
||||
libsignal-debug = { path = "rust/debug" }
|
||||
libsignal-keytrans = { path = "rust/keytrans" }
|
||||
libsignal-message-backup = { path = "rust/message-backup" }
|
||||
libsignal-net = { path = "rust/net" }
|
||||
libsignal-net-chat = { path = "rust/net/chat" }
|
||||
libsignal-net-grpc = { path = "rust/net/grpc" }
|
||||
libsignal-node = { path = "rust/bridge/node" }
|
||||
libsignal-protocol = { path = "rust/protocol" }
|
||||
libsignal-svrb = { path = "rust/svrb" }
|
||||
poksho = { path = "rust/poksho" }
|
||||
@ -84,17 +88,18 @@ signal-neon-futures = { path = "rust/bridge/node/futures" }
|
||||
# Our forks of some dependencies, accessible as xxx_signal so that usages of them are obvious in source code. Crates
|
||||
# that want to use the real things can depend on those directly.
|
||||
|
||||
boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v4.18.0", package = "boring", default-features = false }
|
||||
boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "boring", default-features = false }
|
||||
curve25519-dalek-signal = { git = 'https://github.com/signalapp/curve25519-dalek', package = "curve25519-dalek", tag = 'signal-curve25519-4.1.3' }
|
||||
spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.2.0" }
|
||||
tokio-boring-signal = { git = "https://github.com/signalapp/boring", package = "tokio-boring", tag = "signal-v4.18.0" }
|
||||
spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.5.1" }
|
||||
tokio-boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "tokio-boring" }
|
||||
|
||||
aes = "0.8.3"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
anyhow = "1.0.97"
|
||||
arbitrary = "1.4.2"
|
||||
argon2 = "0.5.0"
|
||||
arrayvec = "0.7.4"
|
||||
asn1 = "0.21.0"
|
||||
asn1 = "0.23.0"
|
||||
assert_cmd = "2.0.13"
|
||||
assert_matches = "1.5"
|
||||
async-compression = "0.4.5"
|
||||
@ -106,21 +111,21 @@ bincode = "1.3.2"
|
||||
bitflags = "2.9"
|
||||
bitstream-io = "1.10.0"
|
||||
blake2 = "0.10.6"
|
||||
boring = { version = "4.6.0", default-features = false }
|
||||
boring-sys = { version = "4.6.0", default-features = false }
|
||||
bytes = "1.9.0"
|
||||
boring = { version = "5.0", default-features = false }
|
||||
boring-sys = { version = "5.0", default-features = false }
|
||||
bytes = "1.11.1"
|
||||
cbc = "0.1.2"
|
||||
cfg-if = "1.0.0"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
chrono = "0.4.23"
|
||||
chrono = "0.4.42"
|
||||
clap = "4.4.11"
|
||||
clap-stdin = "0.6.0"
|
||||
const-str = "0.6.2"
|
||||
clap-stdin = "0.8.0"
|
||||
const-str = "1.0"
|
||||
criterion = "0.5"
|
||||
ctr = "0.9.2"
|
||||
curve25519-dalek = "4.1.3"
|
||||
data-encoding-macro = "0.1.18"
|
||||
derive-where = "1.2.7"
|
||||
derive-where = "1.6.1"
|
||||
derive_more = "2.0.0"
|
||||
dir-test = "0.4.1"
|
||||
displaydoc = "0.2.5"
|
||||
@ -128,29 +133,27 @@ ed25519-dalek = "2.1.0"
|
||||
either = "1.13.0"
|
||||
env_logger = "0.11.7"
|
||||
flate2 = { version = "1.1.1", default-features = false }
|
||||
foreign-types = "0.5"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
ghash = "0.5.0"
|
||||
heck = "0.5"
|
||||
hex = "0.4.3"
|
||||
hickory-proto = "0.24.1"
|
||||
hickory-proto = "0.26.1"
|
||||
hkdf = "0.12"
|
||||
hmac = "0.12.0"
|
||||
hpke-rs = "0.3.0"
|
||||
hpke-rs-crypto = "0.3.0"
|
||||
hpke-rs = "0.6.1"
|
||||
hpke-rs-crypto = "0.6.1"
|
||||
http = "1.3.0"
|
||||
http-body = "1.0.1"
|
||||
http-body-util = "0.1.3"
|
||||
hyper = "1.7"
|
||||
hyper-util = "0.1.17"
|
||||
indexmap = "2.1.0"
|
||||
indexmap = "2.7.0"
|
||||
intmap = "3.1.2"
|
||||
itertools = "0.14.0"
|
||||
jni = "0.21"
|
||||
json5 = "0.4.1"
|
||||
libc = "0.2.175"
|
||||
libcrux-ml-kem = { version = "0.0.2", default-features = false }
|
||||
libc = "0.2.186"
|
||||
libcrux-ml-kem = { version = "0.0.8", default-features = false }
|
||||
linkme = "0.3.33"
|
||||
log = "0.4.21"
|
||||
log-panics = "2.1.0"
|
||||
@ -159,34 +162,41 @@ mediasan-common = "0.5.3"
|
||||
minidump = { version = "0.22.1", default-features = false }
|
||||
minidump-processor = { version = "0.22.1", default-features = false }
|
||||
minidump-unwind = { version = "0.22.1", default-features = false }
|
||||
minijinja = "2.19.0"
|
||||
mp4san = "0.5.3"
|
||||
neon = { version = "1.1.0", default-features = false }
|
||||
nonzero_ext = "0.3.0"
|
||||
once_cell = "1.19.0"
|
||||
once_cell = "1.20.0"
|
||||
partial-default = "0.1.0"
|
||||
paste = "1.0.15"
|
||||
pbjson = "0.9.0"
|
||||
pbjson-build = "0.9.0"
|
||||
pbjson-types = "0.9.0"
|
||||
pin-project = "1.1.5"
|
||||
pretty_assertions = "1.4.0"
|
||||
proc-macro2 = "1.0.93"
|
||||
proptest = "1.7"
|
||||
proptest-arbitrary-interop = "0.1.0"
|
||||
proptest-state-machine = "0.4"
|
||||
prost = "0.13.5"
|
||||
prost-build = "0.13.5"
|
||||
prost = "0.14"
|
||||
prost-build = "0.14"
|
||||
prost-types = "0.14"
|
||||
protobuf = "3.7.2"
|
||||
protobuf-codegen = "3.7.2"
|
||||
protobuf-json-mapping = "3.7.2"
|
||||
quote = "1.0.38"
|
||||
rand = "0.9"
|
||||
quote = "1.0.40"
|
||||
rand = "0.9.4"
|
||||
rand_chacha = "0.9"
|
||||
rand_core = "0.9"
|
||||
rangemap = "1.5.1"
|
||||
rayon = "1.8.0"
|
||||
rcgen = "0.13.0"
|
||||
rcgen = "0.14.0"
|
||||
ref-cast = "1.0.25"
|
||||
rustls = { version = "0.23.25", default-features = false }
|
||||
rustls-platform-verifier = "0.5.1"
|
||||
scopeguard = "1.0"
|
||||
serde = "1.0.203"
|
||||
serde_json = "1.0.45"
|
||||
serde_json5 = "0.2.1"
|
||||
serde_with = "3.1.0"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
@ -202,31 +212,44 @@ test-log = "0.2.16"
|
||||
testing_logger = "0.1.1"
|
||||
thiserror = "2.0.11"
|
||||
tls-parser = "0.12.2"
|
||||
tokio = "1.45"
|
||||
tokio = "1.52.2"
|
||||
tokio-socks = "0.5.2"
|
||||
tokio-stream = "0.1.16"
|
||||
tokio-tungstenite = "0.27.0"
|
||||
tokio-util = "0.7.11"
|
||||
tonic = "0.13.1"
|
||||
tonic-build = "0.13.1"
|
||||
tower = "0.5.2"
|
||||
tungstenite = "0.27.0"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
tokio-util = "0.7.18"
|
||||
tonic = { version = "0.14", default-features = false }
|
||||
tonic-prost = "0.14"
|
||||
tonic-prost-build = { version = "0.14", default-features = false }
|
||||
tower-service = "0.3.3"
|
||||
tungstenite = "0.28.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
url = "2.4.1"
|
||||
url = "2.5.4"
|
||||
uuid = "1.5"
|
||||
visibility = "0.1.1"
|
||||
warp = "0.4.2"
|
||||
webpsan = { version = "0.5.3", default-features = false }
|
||||
x25519-dalek = "2.0.0"
|
||||
zerocopy = "0.8.24"
|
||||
zerocopy = "0.8.33"
|
||||
zeroize = "1.8.2"
|
||||
|
||||
[patch.crates-io]
|
||||
# When building libsignal, just use our forks so we don't end up with two different versions of the libraries.
|
||||
|
||||
boring = { git = 'https://github.com/signalapp/boring', tag = 'signal-v4.18.0' }
|
||||
boring-sys = { git = 'https://github.com/signalapp/boring', tag = 'signal-v4.18.0' }
|
||||
boring = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
|
||||
boring-sys = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
|
||||
curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' }
|
||||
tungstenite = { git = 'https://github.com/signalapp/tungstenite-rs', tag = 'signal-v0.27.0' }
|
||||
|
||||
[profile.dev.package.argon2]
|
||||
opt-level = 2 # libsignal-account-keys unit tests are too slow with an unoptimized argon2
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
|
||||
[profile.release.package.curve25519-dalek]
|
||||
overflow-checks = false
|
||||
|
||||
[profile.release.package.sha2]
|
||||
overflow-checks = false
|
||||
|
||||
[profile.release.package.hmac]
|
||||
overflow-checks = false
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'LibSignalClient'
|
||||
s.version = '0.85.6'
|
||||
s.version = '0.94.1'
|
||||
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
|
||||
|
||||
s.homepage = 'https://github.com/signalapp/libsignal'
|
||||
@ -15,7 +15,6 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.swift_version = '5'
|
||||
s.platform = :ios, '15.0'
|
||||
s.libraries = ['z']
|
||||
|
||||
s.source_files = ['swift/Sources/**/*.swift', 'swift/Sources/**/*.m']
|
||||
s.preserve_paths = [
|
||||
@ -58,14 +57,20 @@ Pod::Spec.new do |s|
|
||||
'ARCHS[sdk=iphonesimulator*]' => 'x86_64 arm64',
|
||||
'ARCHS[sdk=iphoneos*]' => 'arm64',
|
||||
}
|
||||
user_target_xcconfig = {}
|
||||
|
||||
if ENV['LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH']
|
||||
pod_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
|
||||
|
||||
s.user_target_xcconfig = { 'ONLY_ACTIVE_ARCH' => 'YES' }
|
||||
user_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
|
||||
end
|
||||
# This pod does not currently support explicit modules, but clients should specify that explicitly.
|
||||
# `pod lib lint` doesn't provide that opportunity though.
|
||||
if ENV['LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES']
|
||||
user_target_xcconfig['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO'
|
||||
end
|
||||
|
||||
s.pod_target_xcconfig = pod_target_xcconfig
|
||||
s.user_target_xcconfig = user_target_xcconfig
|
||||
|
||||
s.script_phases = [
|
||||
{ name: 'Download libsignal-ffi if not in cache',
|
||||
|
||||
48
README.md
48
README.md
@ -43,15 +43,15 @@ increases to the minimum supported tools versions.
|
||||
|
||||
### Toolchain Installation
|
||||
|
||||
To build anything in this repository you must have [Rust](https://rust-lang.org) installed,
|
||||
as well as Clang, libclang, [CMake](https://cmake.org), Make, protoc, and git.
|
||||
To build anything in this repository you must have [Rust](https://rust-lang.org) installed, as well
|
||||
as recent versions of Clang, libclang, [CMake](https://cmake.org), Make, protoc, Python (3.9+), and git.
|
||||
|
||||
#### Linux/Debian
|
||||
|
||||
On a Debian-like system, you can get these extra dependencies through `apt`:
|
||||
|
||||
```shell
|
||||
$ apt-get install clang libclang-dev cmake make protobuf-compiler git
|
||||
$ apt-get install clang libclang-dev cmake make protobuf-compiler libprotobuf-dev python3 git
|
||||
```
|
||||
|
||||
#### macOS
|
||||
@ -145,22 +145,34 @@ $ make
|
||||
When exposing new APIs to Java, you will need to run `rust/bridge/jni/bin/gen_java_decl.py` in
|
||||
addition to rebuilding. This requires installing the `cbindgen` Rust tool, as detailed above.
|
||||
|
||||
### Maven Central
|
||||
### Use as a library
|
||||
|
||||
Signal publishes Java packages on [Maven Central](https://central.sonatype.org) for its own use,
|
||||
under the names org.signal:libsignal-server, org.signal:libsignal-client, and
|
||||
org.signal:libsignal-android. libsignal-client and libsignal-server contain native libraries for
|
||||
Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS (x86_64 and arm64).
|
||||
libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and x86_64 Android.
|
||||
Signal publishes Java packages for its own use, under the names org.signal:libsignal-server,
|
||||
org.signal:libsignal-client, and org.signal:libsignal-android. libsignal-client and libsignal-server
|
||||
contain native libraries for Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS
|
||||
(x86_64 and arm64). libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and
|
||||
x86_64 Android. These are located in a Maven repository at
|
||||
https://build-artifacts.signal.org/libraries/maven/; for use from Gradle, add the following to your
|
||||
`repositories` block:
|
||||
|
||||
```
|
||||
maven {
|
||||
name = "SignalBuildArtifacts"
|
||||
// The "uri()" part is only necessary for Kotlin Gradle; Groovy Gradle accepts a bare string here.
|
||||
url = uri("https://build-artifacts.signal.org/libraries/maven/")
|
||||
}
|
||||
```
|
||||
|
||||
Older builds were published to [Maven Central](https://central.sonatype.org) instead.
|
||||
|
||||
When building for Android you need *both* libsignal-android and libsignal-client, but the Windows
|
||||
and macOS libraries in libsignal-client won't automatically be excluded from your final app. You can
|
||||
explicitly exclude them using `packagingOptions`:
|
||||
explicitly exclude them using `packaging`:
|
||||
|
||||
```
|
||||
android {
|
||||
// ...
|
||||
packagingOptions {
|
||||
packaging {
|
||||
resources {
|
||||
excludes += setOf("libsignal_jni*.dylib", "signal_jni*.dll")
|
||||
}
|
||||
@ -172,6 +184,12 @@ android {
|
||||
You can additionally exclude `libsignal_jni_testing.so` if you do not plan to use any of the APIs
|
||||
intended for client testing.
|
||||
|
||||
### Testing a local build with Signal-Android
|
||||
|
||||
The Signal-Android gradle.properties file has a commented-out line to include libsignal as part of the build. Uncomment that and adjust the path; optionally, you can restrict the architectures you want to build for by adding `androidArchs=aarch64` to *libsignal's* gradle.properties. (The set of recognized architectures is in java/build_jni.sh.) If you're using an IDE, you'll need to re-import the Gradle structure at this point. When you're done, revert the changes to the Android app's gradle.properties and re-import once more.
|
||||
|
||||
Note that this does not import the *Rust* parts of the project into the IDE. Doing that in a multi-language IDE like IDEA is possible, but finicky; as of 2025 the most reliable way to do it is to open the Android project first, add the libsignal repo root directory as a Rust project second (only including the top-level directory), and only then make the changes to gradle.properties.
|
||||
|
||||
|
||||
## Swift
|
||||
|
||||
@ -196,7 +214,7 @@ $ npm run test
|
||||
|
||||
When testing changes locally, you can use `npm run build` to do an incremental rebuild of the Rust library. Alternately, `npm run build-with-debug-level-logs` will rebuild without filtering out debug- and verbose-level logs.
|
||||
|
||||
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
|
||||
When exposing new APIs to Node, you will need to run `just generate-node` in
|
||||
addition to rebuilding.
|
||||
|
||||
[nvm]: https://github.com/nvm-sh/nvm
|
||||
@ -208,6 +226,10 @@ libraries for Windows, macOS, and Debian-flavored Linux. Both x64 and arm64 buil
|
||||
all three platforms, but the arm64 builds for Windows and Linux are considered experimental, since
|
||||
there are no official builds of Signal for those architectures.
|
||||
|
||||
### Testing a local build with Signal-Desktop
|
||||
|
||||
After running all the build commands above, adjust the `@signalapp/libsignal-client` dependency in the Desktop app's package.json to "link:path/to/libsignal/node" and run `pnpm install`. When you're done, revert the changes to package.json and run `pnpm install` again.
|
||||
|
||||
|
||||
# Contributions
|
||||
|
||||
@ -254,6 +276,6 @@ Administration Regulations, Section 740.13) for both object code and source code
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2020-2024 Signal Messenger, LLC
|
||||
Copyright 2020-2026 Signal Messenger, LLC
|
||||
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
10
RELEASE.md
10
RELEASE.md
@ -54,19 +54,17 @@ git push <remote> HEAD:main <release tag, e.g. v0.x.y>
|
||||
|
||||
## 3. Submit to package repositories as needed
|
||||
|
||||
### Android and Server: Sonatype
|
||||
### Android and Server: Maven
|
||||
|
||||
In the signalapp/libsignal repository on GitHub, run the "Upload Java libraries to Sonatype" action on the tag you just made. Then go to [Maven Central][] and wait for the build to show up (it can take up to an hour).
|
||||
|
||||
[Maven Central]: https://central.sonatype.com/artifact/org.signal/libsignal-client/versions
|
||||
In the signalapp/libsignal repository on GitHub, run the "Release - Java" action on the tag you just made.
|
||||
|
||||
### Node: NPM
|
||||
|
||||
In the signalapp/libsignal repository on GitHub, run the "Publish to NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
|
||||
In the signalapp/libsignal repository on GitHub, run the "Release - NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
|
||||
|
||||
### iOS: Build Artifacts
|
||||
|
||||
In the signalapp/libsignal repository on GitHub, run the "Build iOS Artifacts" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.
|
||||
In the signalapp/libsignal repository on GitHub, run the "Release - iOS" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.
|
||||
|
||||
## Appendix: Release Standards and Information
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
v0.85.6
|
||||
v0.94.1
|
||||
|
||||
- Node: Update package.json with repository link
|
||||
- Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads.
|
||||
|
||||
- keytrans: Add reset account data field functionality for all platforms.
|
||||
|
||||
@ -42,7 +42,7 @@ Rust allows running tests with cross-compiled targets, but normally that only wo
|
||||
ANDROID_NDK_HOME=path/to/ndk
|
||||
CARGO_PROFILE_TEST_STRIP=debuginfo # make the "push" step take less time
|
||||
CARGO_PROFILE_BENCH_STRIP=debuginfo # same for benchmarks
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android21-clang
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android23-clang
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=bin/adb-run-test # in the repo root
|
||||
```
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ accepted = [
|
||||
"ISC",
|
||||
"MPL-2.0",
|
||||
"AGPL-3.0-only",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
@ -29,9 +28,33 @@ workarounds = [
|
||||
"chrono",
|
||||
"prost",
|
||||
"ring",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
|
||||
# The async-compression crates are embedded in a larger repo
|
||||
[async-compression.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[async-compression.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
|
||||
|
||||
[compression-codecs.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[compression-codecs.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
|
||||
|
||||
[compression-core.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[compression-core.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
|
||||
|
||||
|
||||
# Boring's main license isn't at the root of the repo
|
||||
[boring.clarify]
|
||||
license = "Apache-2.0"
|
||||
@ -46,7 +69,7 @@ checksum = "48e488ce333f8a1e86a68b2a1df454464037f1ff580b5bff926053c56dbadc2d"
|
||||
# and the similar configuration for 'ring' in https://github.com/EmbarkStudios/cargo-about/blob/3bcd3380f606fd468b2836e04cdcf7997d1f3ff8/src/licenses/workarounds/ring.rs
|
||||
|
||||
[boring-sys.clarify]
|
||||
license = "MIT AND ISC AND OpenSSL"
|
||||
license = "MIT AND Apache-2.0"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The MIT license of the Rust code
|
||||
@ -55,28 +78,10 @@ license = "MIT"
|
||||
checksum = "ad2e7bdef7c00b92eaf4f657a472c7d3f8b36aac3cdc270e65bb0c287eec0d4e"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The original OpenSSL license
|
||||
# The Apache 2.0 license of BoringSSL
|
||||
path = "deps/boringssl/LICENSE"
|
||||
license = "OpenSSL"
|
||||
start = "/* ===================================================================="
|
||||
end = "*/"
|
||||
checksum = "53552a9b197cd0db29bd085d81253e67097eedd713706e8cd2a3cc6c29850ceb"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The ISC license of the Google-written BoringSSL code
|
||||
path = "deps/boringssl/LICENSE"
|
||||
license = "ISC"
|
||||
start = "/* Copyright (c) 2015, Google Inc."
|
||||
end = "* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */"
|
||||
checksum = "19c779f8bbc141fa15c14e0a15aacaee2da917f7043af883c90cbef3cd6f4847"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The MIT license of the BoringSSL code in third_party/fiat
|
||||
path = "deps/boringssl/LICENSE"
|
||||
license = "MIT"
|
||||
start = "Copyright (c) 2015-2016 the fiat-crypto authors"
|
||||
end = "SOFTWARE."
|
||||
checksum = "7d5e1fb4bbd5e89a687f94c3d3826db50e26bd6f4ade136a025dc2080c5bdc85"
|
||||
license = "Apache-2.0"
|
||||
checksum = "827c8d8fc207c2392794eef9e00fe246f9f61fdcc132556c275be3dd8c3cd97f"
|
||||
|
||||
|
||||
# Newer versions of convert_case have a LICENSE file, we'll use that one
|
||||
@ -194,6 +199,13 @@ license = "Apache-2.0"
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-secrets.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-secrets.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-sha2.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
@ -302,6 +314,22 @@ path = "LICENSE"
|
||||
checksum = "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
|
||||
|
||||
|
||||
# The tonic-prost crates are embedded in a larger repo
|
||||
[tonic-prost.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[tonic-prost.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
|
||||
|
||||
[tonic-prost-build.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[tonic-prost-build.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
|
||||
|
||||
|
||||
# webpsan is embedded in a larger repo
|
||||
[webpsan.clarify]
|
||||
license = "MIT"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
20
bin/benchmark-criterion
Executable file
20
bin/benchmark-criterion
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Workaround for https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
|
||||
# Invoke this like: ./bin/benchmark-criterion -p foo -- --save-baseline my_baseline
|
||||
# Any arguments after the "--" will _only_ be passed to criterion binaries.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
cd "${SCRIPT_DIR}"/..
|
||||
declare -a CARGO_ARGS
|
||||
while [[ "${1:-}" != "--" ]] && (! [[ -z "${1:-}" ]]); do
|
||||
CARGO_ARGS+=("$1")
|
||||
shift 1
|
||||
done
|
||||
if [[ "${1:-}" == "--" ]]; then
|
||||
shift 1
|
||||
fi
|
||||
export LIBSIGNAL_BENCHMARK_ARGS="$@"
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
|
||||
export CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
|
||||
exec cargo bench "${CARGO_ARGS[@]}"
|
||||
12
bin/benchmark-criterion-helper.sh
Executable file
12
bin/benchmark-criterion-helper.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Don't invoke this directly. See ./benchmark-criterion
|
||||
# This script is intended to be invoked as a cargo runner.
|
||||
# Add $LIBSIGNAL_BENCHMARK_ARGS to the arguments if the benchmark is criterion.
|
||||
if "$@" --help 2>&1 | grep criterion > /dev/null; then
|
||||
# We intentionally want to expand the args here
|
||||
# shellcheck disable=SC2086
|
||||
exec "$@" $LIBSIGNAL_BENCHMARK_ARGS
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
@ -21,15 +21,14 @@ brew "rustup"
|
||||
brew "shellcheck"
|
||||
brew "swiftlint"
|
||||
brew "taplo"
|
||||
brew "terraform"
|
||||
brew "yamllint"
|
||||
cask "google-cloud-sdk"
|
||||
cask "gcloud-cli"
|
||||
EOF
|
||||
|
||||
# Install Python tools using pipx.
|
||||
# This keeps their dependencies isolated from other things on your system,
|
||||
# but is still global state for each tool. We may some day want to switch this to a venv instead.
|
||||
"$(brew --prefix pipx)/bin/pipx" install mypy
|
||||
"$(brew --prefix pipx)/bin/pipx" install "mypy<2.0"
|
||||
"$(brew --prefix pipx)/bin/pipx" install flake8
|
||||
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
|
||||
flake8-comprehensions \
|
||||
|
||||
@ -30,6 +30,7 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
|
||||
@ -45,6 +46,18 @@ class ReleaseFailedException(Exception):
|
||||
# execute the rest while performing a rollback.
|
||||
on_failure_rollback_commands: list[list[str]] = []
|
||||
|
||||
# The following ids can be obtained by running:
|
||||
# gh workflow list
|
||||
# or, more programmatically friendly,
|
||||
# gh workflow list --json id,name
|
||||
BUILD_AND_TEST_WORKFLOW_ID = 6587503
|
||||
SLOW_TEST_WORKFLOW_ID = 30989402
|
||||
RELEASE_WORKFLOW_IDS = [
|
||||
10143338, # Node
|
||||
15104239, # Android
|
||||
46287777, # iOS
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
@ -87,7 +100,7 @@ def main() -> None:
|
||||
print('User interrupted execution! Aborting...')
|
||||
exit_code = 1
|
||||
except Exception as ex:
|
||||
print(f'Unexpected error: {ex}')
|
||||
traceback.print_exception(None, value=ex, tb=ex.__traceback__)
|
||||
exit_code = 1
|
||||
|
||||
if exit_code != 0:
|
||||
@ -101,11 +114,27 @@ def main() -> None:
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def get_workflow_name_mapping(repo_name: str) -> dict[int, str]:
|
||||
"""Gets a mapping of workflow ids to their names from github."""
|
||||
list_workflows_cmd = [
|
||||
'gh', 'workflow', 'list',
|
||||
'--repo', f'signalapp/{repo_name}',
|
||||
'--json', 'name,id'
|
||||
]
|
||||
|
||||
raw_json = run_command(list_workflows_cmd)
|
||||
data = json.loads(raw_json)
|
||||
return {d['id']: d['name'] for d in data}
|
||||
|
||||
|
||||
def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: bool = False, skip_worktree_clean_check: bool = False, dry_run: bool = False) -> None:
|
||||
setup_and_check_env(skip_main_check, skip_worktree_clean_check)
|
||||
REPO_NAME = get_repo_name()
|
||||
RELEASE_NOTES_FILE_PATH = Path('RELEASE_NOTES.md')
|
||||
|
||||
# Obtain the workflow ids once
|
||||
workflows = get_workflow_name_mapping(REPO_NAME)
|
||||
|
||||
# Get the commit sha of the commit we intend to mark as the release.
|
||||
head_sha = run_command(['git', 'rev-parse', 'HEAD']).strip()
|
||||
short_sha = head_sha[:9]
|
||||
@ -117,8 +146,8 @@ def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: boo
|
||||
# If needed, you can run the Slow Tests manually under the repository Actions tab on GitHub.
|
||||
# You should run the Slow Tests before running this script.
|
||||
if not skip_tests_pass_check:
|
||||
build_and_test_run_id = check_workflow_success(REPO_NAME, 'Build and Test', head_sha)
|
||||
slow_test_run_id = check_workflow_success(REPO_NAME, 'Slow Tests', head_sha)
|
||||
build_and_test_run_id = check_workflow_success(REPO_NAME, workflows[BUILD_AND_TEST_WORKFLOW_ID], head_sha)
|
||||
slow_test_run_id = check_workflow_success(REPO_NAME, workflows[SLOW_TEST_WORKFLOW_ID], head_sha)
|
||||
|
||||
print('Found GitHub Actions runs! They look good, but please double check manually as well.')
|
||||
print(f'Build and Test: https://github.com/signalapp/{REPO_NAME}/actions/runs/{build_and_test_run_id}')
|
||||
@ -221,9 +250,14 @@ def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: boo
|
||||
print('Next steps:')
|
||||
print('1) Verify the GitHub Actions runs above passed.')
|
||||
print('2) If they passed, push to the proper remote(s), e.g.:')
|
||||
print(f' git push {upstream} HEAD~1:main {head_release_version} && git push {origin} HEAD:main {head_release_version}')
|
||||
print(f'\tgit push {upstream} HEAD~1:main {head_release_version} && git push {origin} HEAD:main {head_release_version}')
|
||||
print('3) To review the reset commit, you can run:')
|
||||
print(' git show')
|
||||
print('\tgit show')
|
||||
print('4) To run post-release actions, you can run:')
|
||||
for id in RELEASE_WORKFLOW_IDS:
|
||||
name = workflows[id]
|
||||
raw_field = ' --raw-field dry_run=true' if dry_run else ''
|
||||
print(f'\tgh workflow run "{name}" --repo signalapp/libsignal --ref {head_release_version}{raw_field}')
|
||||
|
||||
|
||||
def setup_and_check_env(skip_main_check: bool = False, skip_worktree_clean_check: bool = False) -> None:
|
||||
@ -388,6 +422,8 @@ def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) ->
|
||||
if workflow_name == 'Slow Tests':
|
||||
print('Note that Slow Tests do not run automatically.')
|
||||
print(f'You must kick them off automatically at: https://github.com/signalapp/{repo_name}/actions/workflows/slow_tests.yml')
|
||||
print('Or by running')
|
||||
print(f'\tgh workflow run "{workflow_name}" --repo signalapp/{repo_name} --ref main')
|
||||
print('If tests have actually passed, you can skip this check by re-running with --skip-ci-tests-pass-check')
|
||||
raise ReleaseFailedException
|
||||
|
||||
@ -413,6 +449,8 @@ def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) ->
|
||||
if status != 'completed' or conclusion != 'success':
|
||||
print(f"Error: '{workflow_name}' did not succeed (status={status}, conclusion={conclusion}).")
|
||||
print('Please ensure all CI checks have passed before releasing.')
|
||||
print('You can watch the run using:')
|
||||
print(f'\tgh run watch {selected_run_id}')
|
||||
raise ReleaseFailedException
|
||||
|
||||
return selected_run_id
|
||||
|
||||
@ -11,6 +11,24 @@ SCRIPT_DIR=$(dirname "$0")
|
||||
cd "${SCRIPT_DIR}"/..
|
||||
. bin/build_helpers.sh
|
||||
|
||||
CHECK=0
|
||||
case "${1:-}" in
|
||||
--check)
|
||||
CHECK=1
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$#" -ne 0 ]; then
|
||||
echo "usage: $0 [--check]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ "$CHECK" -eq 1 ]; then
|
||||
OUTPUT_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$OUTPUT_DIR"' EXIT
|
||||
fi
|
||||
|
||||
echo "Checking cargo-about version"
|
||||
VERSION=$(cargo about --version)
|
||||
echo "Found $VERSION"
|
||||
@ -32,6 +50,20 @@ generate() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
generate_and_maybe_check() {
|
||||
template="$1"
|
||||
tracked_output="$2"
|
||||
shift 2
|
||||
|
||||
if [ "$CHECK" -eq 1 ]; then
|
||||
generated_output="${OUTPUT_DIR}/$(basename "$tracked_output")"
|
||||
generate "$template" "$generated_output" "$@"
|
||||
diff -u "$tracked_output" "$generated_output"
|
||||
else
|
||||
generate "$template" "$tracked_output" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# List every target we ship, just in case some dependencies are platform-gated.
|
||||
ANDROID_TARGETS=(
|
||||
aarch64-linux-android
|
||||
@ -50,12 +82,12 @@ DESKTOP_TARGETS=(
|
||||
IOS_TARGETS=(aarch64-apple-ios)
|
||||
|
||||
# shellcheck disable=SC2068 # We want "--target" to end up as a separate argument.
|
||||
generate acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
|
||||
# shellcheck disable=SC2068
|
||||
generate acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
|
||||
# shellcheck disable=SC2068
|
||||
generate acknowledgments/acknowledgments{.md.hbs,-android-testing.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/testing/Cargo.toml
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android-testing.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/testing/Cargo.toml
|
||||
# shellcheck disable=SC2068
|
||||
generate acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
|
||||
# shellcheck disable=SC2068
|
||||
generate acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml
|
||||
|
||||
@ -12,22 +12,14 @@
|
||||
# and then document them here.
|
||||
#
|
||||
# thiserror: minimal and highly inlinable, most of the code is synthesized at the use site
|
||||
# rand_core, getrandom: waiting on snow to support rand_core 0.9
|
||||
# hax-lib: highly inlinable, mostly annotated versions of simple operations
|
||||
# libcrux-sha3, libcrux-intrinsics: v0.0.3 is referenced by hpke-rs, but only needed if you use X-Wing KEM
|
||||
# rand_core, getrandom: waiting to update all the RustCrypto crates together
|
||||
EXPECTED="
|
||||
getrandom v0.2.16
|
||||
getrandom v0.3.3
|
||||
hax-lib v0.2.0
|
||||
hax-lib v0.3.4
|
||||
libcrux-intrinsics v0.0.2
|
||||
libcrux-intrinsics v0.0.3
|
||||
libcrux-sha3 v0.0.2
|
||||
libcrux-sha3 v0.0.3
|
||||
getrandom v0.3.4
|
||||
rand_core v0.6.4
|
||||
rand_core v0.9.3
|
||||
thiserror v1.0.69
|
||||
thiserror v2.0.16"
|
||||
thiserror v2.0.17"
|
||||
|
||||
check_cargo_tree() {
|
||||
# Only check the mobile targets, where we care most about code size.
|
||||
@ -41,7 +33,16 @@ check_cargo_tree() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
if [[ "$(check_cargo_tree --depth 0 | sort -u -V)" != "${EXPECTED}" ]]; then
|
||||
ACTUAL="$(check_cargo_tree --depth 0 | sort -u -V)"
|
||||
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
|
||||
cat <<EOF
|
||||
----- EXPECTED -----
|
||||
${EXPECTED}
|
||||
|
||||
------ ACTUAL ------
|
||||
${ACTUAL}
|
||||
|
||||
EOF
|
||||
check_cargo_tree
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
# This is expected to be the path to a BoringSSL *build* directory,
|
||||
# but if we set it to the *source* directory it's enough for bindgen to run.
|
||||
# And we can use a relative path because build scripts run from the package root).
|
||||
export BORING_BSSL_PATH=deps/boringssl/src
|
||||
export BORING_BSSL_PATH=deps/boringssl
|
||||
command "$@"
|
||||
|
||||
1
java/.gitignore
vendored
1
java/.gitignore
vendored
@ -5,3 +5,4 @@ out
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.kotlin
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
FROM ubuntu:jammy-20230624@sha256:b060fffe8e1561c9c3e6dea6db487b900100fc26830b9ea2ec966c151ab4c020
|
||||
FROM ubuntu:jammy-20260109@sha256:c7eb020043d8fc2ae0793fb35a37bff1cf33f156d4d4b12ccc7f3ef8706c38b1
|
||||
|
||||
COPY java/docker/ docker/
|
||||
COPY java/docker/apt.conf java/docker/sources.list /etc/apt/
|
||||
@ -61,17 +61,24 @@ RUN ./gradlew --version
|
||||
# Rust setup...
|
||||
|
||||
COPY rust-toolchain rust-toolchain
|
||||
ARG RUSTUP_SHA=ad1f8b5199b3b9e231472ed7aa08d2e5d1d539198a15c5b1e53c746aad81d27b
|
||||
ARG RUSTUP_SHA=20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c
|
||||
ENV PATH="/home/libsignal/.cargo/bin:${PATH}"
|
||||
|
||||
ADD --chown=libsignal --chmod=755 --checksum=sha256:${RUSTUP_SHA} \
|
||||
https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-unknown-linux-gnu/rustup-init /tmp/rustup-init
|
||||
https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init /tmp/rustup-init
|
||||
|
||||
RUN /tmp/rustup-init -y --profile minimal --default-toolchain "$(cat rust-toolchain)" \
|
||||
&& rm -rf /tmp/rustup-init
|
||||
|
||||
RUN rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android aarch64-unknown-linux-gnu
|
||||
|
||||
# Manually install a newer protoc
|
||||
ADD --chown=libsignal https://github.com/protocolbuffers/protobuf/releases/download/v29.3/protoc-29.3-linux-x86_64.zip protoc.zip
|
||||
|
||||
RUN unzip protoc.zip -d proto && rm -f protoc.zip
|
||||
|
||||
ENV PATH="/home/libsignal/proto/bin:${PATH}"
|
||||
|
||||
# Install the full set of tools now that the long setup steps are done.
|
||||
# Note that we temporarily hop back to root to do this.
|
||||
USER root
|
||||
@ -85,7 +92,6 @@ RUN apt-get install -y \
|
||||
gpg-agent \
|
||||
libclang-dev \
|
||||
make \
|
||||
protobuf-compiler \
|
||||
python3
|
||||
USER libsignal
|
||||
|
||||
|
||||
@ -11,11 +11,12 @@ default: java_build
|
||||
|
||||
DOCKER_IMAGE := libsignal-builder
|
||||
DOCKER_TTY_FLAG := $$(test -t 0 && echo -it)
|
||||
DOCKER_PLATFORM ?= linux/amd64
|
||||
GRADLE_OPTIONS ?= --dependency-verification strict
|
||||
CROSS_COMPILE_SERVER ?= -PcrossCompileServer
|
||||
|
||||
docker_image:
|
||||
cd .. && $(DOCKER) build --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
|
||||
cd .. && $(DOCKER) build --platform=$(DOCKER_PLATFORM) --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
|
||||
|
||||
java_build: DOCKER_EXTRA=$(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
|
||||
java_build: docker_image
|
||||
@ -29,14 +30,13 @@ java_build: docker_image
|
||||
publish_java: DOCKER_EXTRA = $(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
|
||||
publish_java: docker_image
|
||||
$(DOCKER) run --rm --user $$(id -u):$$(id -g) \
|
||||
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
|
||||
-e ORG_GRADLE_PROJECT_sonatypeUsername \
|
||||
-e ORG_GRADLE_PROJECT_sonatypePassword \
|
||||
-e ORG_GRADLE_PROJECT_signingKeyId \
|
||||
-e ORG_GRADLE_PROJECT_signingPassword \
|
||||
-e ORG_GRADLE_PROJECT_signingKey \
|
||||
-e CLOUDSDK_AUTH_ACCESS_TOKEN \
|
||||
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
|
||||
$(DOCKER_IMAGE) \
|
||||
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) publish closeAndReleaseStagingRepositories $(CROSS_COMPILE_SERVER)"
|
||||
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) publish $(CROSS_COMPILE_SERVER)"
|
||||
|
||||
# We could run these through Docker, but they would have the same result anyway.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.library' version '8.10.1'
|
||||
id 'com.android.library'
|
||||
id 'androidx.benchmark' version '1.1.1'
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
multiDexEnabled true
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
|
||||
|
||||
@ -27,15 +27,15 @@ android {
|
||||
// }
|
||||
}
|
||||
|
||||
testBuildType "release"
|
||||
testBuildType = "release"
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
coreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
namespace "org.signal.libsignal.benchmarks"
|
||||
namespace = "org.signal.libsignal.benchmarks"
|
||||
|
||||
packagingOptions {
|
||||
doNotStrip '**/*.so'
|
||||
|
||||
@ -52,8 +52,10 @@ public class SealedSender {
|
||||
InMemorySignalProtocolStore bobStore =
|
||||
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xBB);
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
@ -66,10 +68,7 @@ public class SealedSender {
|
||||
31337);
|
||||
SealedSessionCipher aliceCipher =
|
||||
new SealedSessionCipher(
|
||||
aliceStore,
|
||||
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
|
||||
"+14151111111",
|
||||
1);
|
||||
aliceStore, UUID.fromString(aliceAddress.getName()), "+14151111111", 1);
|
||||
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
while (state.keepRunning()) {
|
||||
@ -99,7 +98,7 @@ public class SealedSender {
|
||||
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), i);
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress(UUID.randomUUID().toString(), i % 127 + 1);
|
||||
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress));
|
||||
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress));
|
||||
recipients.add(bobAddress);
|
||||
}
|
||||
}
|
||||
@ -187,7 +186,8 @@ public class SealedSender {
|
||||
private static void initializeSessions(
|
||||
InMemorySignalProtocolStore aliceStore,
|
||||
InMemorySignalProtocolStore bobStore,
|
||||
SignalProtocolAddress bobAddress)
|
||||
SignalProtocolAddress bobAddress,
|
||||
SignalProtocolAddress aliceAddress)
|
||||
throws InvalidKeyException, UntrustedIdentityException {
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
@ -207,7 +207,7 @@ public class SealedSender {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilder.process(bobBundle);
|
||||
|
||||
bobStore.storeSignedPreKey(2, bobSignedPreKey);
|
||||
|
||||
@ -7,7 +7,9 @@ plugins {
|
||||
id 'signing'
|
||||
}
|
||||
|
||||
archivesBaseName = "libsignal-android"
|
||||
base {
|
||||
archivesName = "libsignal-android"
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
@ -16,15 +18,15 @@ repositories {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'org.signal.libsignal'
|
||||
namespace = 'org.signal.libsignal'
|
||||
|
||||
compileSdk 34
|
||||
ndkVersion '28.0.13004108'
|
||||
ndkVersion = '28.0.13004108'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
multiDexEnabled true
|
||||
multiDexEnabled = true
|
||||
testInstrumentationRunner "org.signal.libsignal.util.AndroidJUnitRunner"
|
||||
// Automatically propagate matching environment variables into Java properties.
|
||||
// See the custom AndroidJUnitRunner and TestEnvironment classes for more details.
|
||||
@ -32,7 +34,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
coreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
@ -69,14 +71,14 @@ kotlin {
|
||||
}
|
||||
|
||||
task dokkaHtmlJar(type: Jar) {
|
||||
dependsOn(dokkaHtml)
|
||||
from(dokkaHtml)
|
||||
dependsOn(dokkaGeneratePublicationHtml)
|
||||
from(dokkaGeneratePublicationHtml)
|
||||
archiveClassifier.set("dokka")
|
||||
}
|
||||
|
||||
task dokkaJavadocJar(type: Jar) {
|
||||
dependsOn(dokkaJavadoc)
|
||||
from(dokkaJavadoc)
|
||||
dependsOn(dokkaGeneratePublicationJavadoc)
|
||||
from(dokkaGeneratePublicationJavadoc)
|
||||
archiveClassifier.set("javadoc")
|
||||
}
|
||||
|
||||
@ -120,6 +122,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'com.googlecode.json-simple:json-simple:1.1'
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
|
||||
androidTestImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
api project(':client')
|
||||
@ -135,16 +138,17 @@ String[] archsFromProperty(String prop) {
|
||||
}
|
||||
|
||||
task makeJniLibraries(type:Exec) {
|
||||
group 'Rust'
|
||||
description 'Build the JNI libraries for Android'
|
||||
group = 'Rust'
|
||||
description = 'Build the JNI libraries for Android'
|
||||
|
||||
def archs = archsFromProperty('androidArchs') ?: ['android']
|
||||
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
|
||||
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
|
||||
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
|
||||
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
|
||||
def libsignalDebugFlag = project.hasProperty('libsignalDebug') ? ['--libsignal-debug'] : []
|
||||
// Explicitly specify 'bash' for Windows compatibility.
|
||||
commandLine 'bash', '../build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
|
||||
commandLine 'bash', '../build_jni.sh', *libsignalDebugFlag, *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
}
|
||||
|
||||
@ -163,13 +167,13 @@ afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = archivesBaseName
|
||||
artifactId = base.archivesName.get()
|
||||
from components.release
|
||||
artifact dokkaHtmlJar
|
||||
artifact dokkaJavadocJar
|
||||
|
||||
pom {
|
||||
name = archivesBaseName
|
||||
name = base.archivesName.get()
|
||||
packaging = 'aar'
|
||||
description = 'Signal Protocol cryptography library for Android'
|
||||
url = 'https://github.com/signalapp/libsignal'
|
||||
@ -199,7 +203,7 @@ afterEvaluate {
|
||||
|
||||
setUpSigningKey(signing)
|
||||
signing {
|
||||
required { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
|
||||
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.library' version '8.10.1'
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@ -12,15 +12,15 @@ android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
multiDexEnabled true
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
coreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
@ -29,7 +29,7 @@ android {
|
||||
jniLibs.excludes.add("**/libsignal_jni_testing.so")
|
||||
}
|
||||
|
||||
namespace "org.signal.libsignal.packagingtest"
|
||||
namespace = "org.signal.libsignal.packagingtest"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
package org.signal.libsignal.util
|
||||
|
||||
import android.util.Base64
|
||||
|
||||
public class AndroidBase64 : org.signal.libsignal.util.Base64.Impl {
|
||||
public override fun decode(encoded: ByteArray): ByteArray = Base64.decode(encoded, 0)
|
||||
|
||||
public override fun decodeUrl(encoded: ByteArray): ByteArray = Base64.decode(encoded, Base64.URL_SAFE)
|
||||
|
||||
public override fun encode(raw: ByteArray): String = Base64.encodeToString(raw, Base64.NO_WRAP)
|
||||
|
||||
public override fun encodeUrl(raw: ByteArray): String =
|
||||
Base64.encodeToString(
|
||||
raw,
|
||||
Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE,
|
||||
)
|
||||
}
|
||||
@ -17,7 +17,8 @@ public class AndroidJUnitRunner extends androidx.test.runner.AndroidJUnitRunner
|
||||
super.onCreate(bundle);
|
||||
|
||||
// Make sure libsignal logs get caught correctly.
|
||||
SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
|
||||
SignalProtocolLoggerProvider.setProvider(
|
||||
new TestLoggerDecorator(new AndroidSignalProtocolLogger()));
|
||||
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.VERBOSE);
|
||||
|
||||
// Propagate any "environment variables" the test might need into System properties.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import org.gradle.api.publish.PublishingExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@ -5,14 +6,14 @@ plugins {
|
||||
id "base"
|
||||
id "signing"
|
||||
id "com.diffplug.spotless" version "7.2.1"
|
||||
id "io.github.gradle-nexus.publish-plugin" version "2.0.0"
|
||||
|
||||
id "org.jetbrains.kotlin.jvm" version "2.1.0" apply false
|
||||
id "org.jetbrains.dokka" version "2.0.0" apply false
|
||||
id "org.jetbrains.kotlin.jvm" version "2.2.20" apply false
|
||||
id "org.jetbrains.dokka" version "2.1.0" apply false
|
||||
id "org.jetbrains.dokka-javadoc" version "2.1.0" apply false
|
||||
|
||||
// These plugins need to be loaded together, so we must declare them up front.
|
||||
id 'com.android.library' version "8.10.1" apply false
|
||||
id 'org.jetbrains.kotlin.android' version "2.1.0" apply false
|
||||
id 'com.android.library' version "8.13.2" apply false
|
||||
id 'org.jetbrains.kotlin.android' version "2.2.20" apply false
|
||||
}
|
||||
|
||||
repositories {
|
||||
@ -22,7 +23,7 @@ repositories {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.85.6"
|
||||
version = "0.94.1"
|
||||
group = "org.signal"
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
@ -42,11 +43,12 @@ allprojects {
|
||||
}
|
||||
|
||||
apply plugin: "org.jetbrains.dokka"
|
||||
apply plugin: "org.jetbrains.dokka-javadoc"
|
||||
}
|
||||
|
||||
task makeJniLibrariesDesktop(type:Exec) {
|
||||
group 'Rust'
|
||||
description 'Build the JNI libraries'
|
||||
group = 'Rust'
|
||||
description = 'Build the JNI libraries'
|
||||
|
||||
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
|
||||
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
|
||||
@ -57,8 +59,8 @@ task makeJniLibrariesDesktop(type:Exec) {
|
||||
}
|
||||
|
||||
task makeJniLibrariesServer(type:Exec) {
|
||||
group 'Rust'
|
||||
description 'Build the JNI libraries'
|
||||
group = 'Rust'
|
||||
description = 'Build the JNI libraries'
|
||||
|
||||
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
|
||||
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
|
||||
@ -70,12 +72,12 @@ task makeJniLibrariesServer(type:Exec) {
|
||||
}
|
||||
|
||||
task cargoClean(type:Exec) {
|
||||
group 'Rust'
|
||||
group = 'Rust'
|
||||
commandLine 'cargo', 'clean'
|
||||
}
|
||||
|
||||
task cleanJni(type: Delete) {
|
||||
description 'Clean JNI libs'
|
||||
description = 'Clean JNI libs'
|
||||
delete fileTree('./android/src/main/jniLibs') {
|
||||
include '**/*.so'
|
||||
}
|
||||
@ -104,15 +106,25 @@ ext.setUpSigningKey = { signingExt ->
|
||||
}
|
||||
}
|
||||
|
||||
nexusPublishing {
|
||||
repositories {
|
||||
sonatype {
|
||||
username = project.findProperty('sonatypeUsername') ?: ""
|
||||
password = project.findProperty('sonatypePassword') ?: ""
|
||||
// This is the recommended configuration from the README for the plugin we use, gradle-nexus/publish-plugin.
|
||||
// The URLs are from https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration
|
||||
nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/"))
|
||||
snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/"))
|
||||
subprojects { subproject ->
|
||||
subproject.plugins.withId('maven-publish') {
|
||||
subproject.extensions.configure(PublishingExtension) { publishing ->
|
||||
publishing.repositories {
|
||||
maven {
|
||||
name = "SignalBuildArtifacts"
|
||||
// We can't use Gradle's built-in GCS support with the way we authenticate
|
||||
// GitHub Actions. Fortunately, GCS's REST APIs are basically just normal HTTP
|
||||
// GET/PUT with an auth token, which is compatible with what Gradle will do.
|
||||
url = subproject.uri("https://storage.googleapis.com/build-artifacts.signal.org/libraries/maven")
|
||||
credentials(HttpHeaderCredentials) {
|
||||
name = "Authorization"
|
||||
value = "Bearer ${System.getenv("CLOUDSDK_AUTH_ACCESS_TOKEN") ?: ""}"
|
||||
}
|
||||
authentication {
|
||||
header(HttpHeaderAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,8 @@ DESKTOP_LIB_DIR=java/client/src/main/resources
|
||||
SERVER_LIB_DIR=java/server/src/main/resources
|
||||
|
||||
# Fetch dependencies first, so we can use them in computing later options.
|
||||
cargo fetch
|
||||
# But allow this to fail in case we're offline.
|
||||
cargo fetch || true
|
||||
|
||||
export CARGO_PROFILE_RELEASE_DEBUG=1 # Enable line tables
|
||||
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
|
||||
@ -34,6 +35,10 @@ while [ "${1:-}" != "" ]; do
|
||||
DEBUG_LEVEL_LOGS=1
|
||||
shift
|
||||
;;
|
||||
--libsignal-debug )
|
||||
LIBSIGNAL_DEBUG=1
|
||||
shift
|
||||
;;
|
||||
--jni-type-tagging )
|
||||
JNI_TYPE_TAGGING=1
|
||||
shift
|
||||
@ -64,6 +69,9 @@ fi
|
||||
if [[ -n "${JNI_CHECK_ANNOTATIONS:-}" ]]; then
|
||||
FEATURES+=("libsignal-bridge-types/jni-invoke-annotated")
|
||||
fi
|
||||
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
|
||||
FEATURES+=("libsignal-debug/enabled")
|
||||
fi
|
||||
|
||||
# usage: check_for_debug_level_logs_if_needed lib_dir
|
||||
check_for_debug_level_logs_if_needed () {
|
||||
@ -82,6 +90,18 @@ check_for_debug_level_logs_if_needed () {
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
# See libsignal-debug for the strings matched by this pattern.
|
||||
if grep -q -- 'LIBSIGNAL-DEBUG IS ENABLED' "$1"/*; then
|
||||
if [[ -z "${LIBSIGNAL_DEBUG:-}" ]]; then
|
||||
echo 'error: libsignal-debug found in build that should not have it!' >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
|
||||
echo 'error: libsignal-debug NOT found in build that SHOULD have it!' >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# usage: build_desktop_for_arch target_triple host_triple output_dir
|
||||
@ -186,12 +206,24 @@ export CXXFLAGS="-DOPENSSL_SMALL -flto=full ${CXXFLAGS:-}"
|
||||
export CARGO_PROFILE_RELEASE_LTO=fat
|
||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1
|
||||
|
||||
# Instruct boring-sys to autolink the *static* libc++, and to delay that linking until the final
|
||||
# build product (the -bundle part). This is consistent with Google's advice for Android JNI
|
||||
# libraries that are not part of the main app build[1], elaborated on in a GitHub thread[2]. It's
|
||||
# also what we're doing with WebRTC. The syntax comes from rustc[3] via Cargo's rustc-link-lib build
|
||||
# script feature.
|
||||
#
|
||||
# [1]: https://developer.android.com/ndk/guides/middleware-vendors#using_the_stl
|
||||
# [2]: https://github.com/android/ndk/issues/796
|
||||
# [3]: https://doc.rust-lang.org/rustc/command-line-arguments.html#-l-link-the-generated-crate-to-a-native-library
|
||||
export BORING_BSSL_RUST_CPPLIB="static:-bundle=c++"
|
||||
|
||||
# Use the Android NDK's prebuilt Clang+lld as pqcrypto's compiler and Rust's linker.
|
||||
ANDROID_TOOLCHAIN_DIR=$(echo "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt"/*/bin/)
|
||||
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android21-clang"
|
||||
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi21-clang"
|
||||
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android21-clang"
|
||||
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android21-clang"
|
||||
ANDROID_MIN_SDK_VERSION=23
|
||||
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
|
||||
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi${ANDROID_MIN_SDK_VERSION}-clang"
|
||||
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
|
||||
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
|
||||
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${CC_aarch64_linux_android}"
|
||||
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${CC_armv7_linux_androideabi}"
|
||||
@ -225,6 +257,7 @@ target_for_abi() {
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
for abi in "${android_abis[@]}"; do
|
||||
rust_target=$(target_for_abi "$abi")
|
||||
echo_then_run cargo build -p libsignal-jni -p libsignal-jni-testing ${RUST_RELEASE:+--release} ${FEATURES:+--features "${FEATURES[*]}"} -Z unstable-options --target "$rust_target" --artifact-dir "${ANDROID_LIB_DIR}/$abi" --timings
|
||||
|
||||
@ -67,19 +67,18 @@ def current_origin_main_entry() -> Optional[Mapping[str, Any]]:
|
||||
runs_info = subprocess.run(['gh', 'api', '--method=GET', f'repos/{repo_path}/actions/runs', '-f', f'head_sha={most_recent_commit}'], capture_output=True, check=True).stdout
|
||||
runs_json = json.loads(runs_info)
|
||||
|
||||
run_id = [run['id'] for run in runs_json['workflow_runs'] if run['name'] == 'Build and Test'][0]
|
||||
run_id = [run['id'] for run in runs_json['workflow_runs'] if 'Build and Test' in run['name']][0]
|
||||
|
||||
run_jobs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, f'{run_id}', '--json', 'jobs'], capture_output=True, check=True).stdout
|
||||
jobs_json = json.loads(run_jobs)
|
||||
|
||||
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == 'Java'][0]
|
||||
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == 'Java Android'][0]
|
||||
|
||||
job_logs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, '--job', f'{job_id}', '--log'], capture_output=True, check=True).stdout.decode()
|
||||
|
||||
for line in job_logs.splitlines():
|
||||
if 'check_code_size.py' in line and 'current: *' in line:
|
||||
(_, after) = line.split('(', maxsplit=1)
|
||||
(bytes_count, _) = after.split(' bytes)', maxsplit=1)
|
||||
if 'update code_size.json with' in line:
|
||||
(_, bytes_count) = line.rsplit(' ', maxsplit=1)
|
||||
return {'size': int(bytes_count), 'version': f'{most_recent_commit[:6]} ({base_ref})'}
|
||||
|
||||
print(f'skipping checking current {base_ref} (most recent run did not include check_code_size.py)', file=sys.stderr)
|
||||
|
||||
@ -14,8 +14,9 @@ plugins {
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
}
|
||||
|
||||
sourceCompatibility = 17
|
||||
archivesBaseName = "libsignal-client"
|
||||
base {
|
||||
archivesName = "libsignal-client"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -52,6 +53,7 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'com.googlecode.json-simple:json-simple:1.1'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
|
||||
}
|
||||
@ -61,14 +63,15 @@ test {
|
||||
testLogging {
|
||||
events 'passed','skipped','failed'
|
||||
showStandardStreams = true
|
||||
showExceptions true
|
||||
exceptionFormat 'full'
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
showExceptions = true
|
||||
exceptionFormat = 'full'
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
@ -84,14 +87,14 @@ sourcesJar {
|
||||
}
|
||||
|
||||
task dokkaHtmlJar(type: Jar) {
|
||||
dependsOn(dokkaHtml)
|
||||
from(dokkaHtml)
|
||||
dependsOn(dokkaGeneratePublicationHtml)
|
||||
from(dokkaGeneratePublicationHtml)
|
||||
archiveClassifier.set("dokka")
|
||||
}
|
||||
|
||||
task dokkaJavadocJar(type: Jar) {
|
||||
dependsOn(dokkaJavadoc)
|
||||
from(dokkaJavadoc)
|
||||
dependsOn(dokkaGeneratePublicationJavadoc)
|
||||
from(dokkaGeneratePublicationJavadoc)
|
||||
archiveClassifier.set("javadoc")
|
||||
}
|
||||
|
||||
@ -110,13 +113,13 @@ processResources {
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = archivesBaseName
|
||||
artifactId = base.archivesName.get()
|
||||
from components.java
|
||||
artifact dokkaHtmlJar
|
||||
artifact dokkaJavadocJar
|
||||
|
||||
pom {
|
||||
name = archivesBaseName
|
||||
name = base.archivesName.get()
|
||||
description = 'Signal Protocol cryptography library for Java'
|
||||
url = 'https://github.com/signalapp/libsignal'
|
||||
|
||||
@ -145,6 +148,6 @@ publishing {
|
||||
|
||||
setUpSigningKey(signing)
|
||||
signing {
|
||||
required { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
|
||||
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
@ -11,6 +11,13 @@ import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
|
||||
/**
|
||||
* Implements the <a
|
||||
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)">AES-256-CTR</a>
|
||||
* stream cipher with a 12-byte nonce and an initial counter.
|
||||
*
|
||||
* <p>CTR mode is built on XOR, so encrypting and decrypting are the same operation.
|
||||
*/
|
||||
public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
|
||||
public Aes256Ctr32(byte[] key, byte[] nonce, int initialCtr) throws InvalidKeyException {
|
||||
super(
|
||||
@ -23,10 +30,20 @@ public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
|
||||
Native.Aes256Ctr32_Destroy(nativeHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
|
||||
* state of the cipher.
|
||||
*/
|
||||
public void process(byte[] data) {
|
||||
this.process(data, 0, data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
|
||||
* state of the cipher.
|
||||
*
|
||||
* <p>Bytes outside the designated offset/length are unchanged.
|
||||
*/
|
||||
public void process(byte[] data, int offset, int length) {
|
||||
guardedRun((nativeHandle) -> Native.Aes256Ctr32_Process(nativeHandle, data, offset, length));
|
||||
}
|
||||
|
||||
@ -11,9 +11,27 @@ import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
|
||||
/**
|
||||
* Implements the <a
|
||||
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
|
||||
* authenticated stream cipher with a 12-byte nonce.
|
||||
*
|
||||
* <p>This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
|
||||
* resident in memory all at once. You <strong>must</strong> call {@link #verifyTag} when the
|
||||
* decryption is complete, or else you have no authenticity guarantees.
|
||||
*
|
||||
* @see Aes256GcmEncryption
|
||||
*/
|
||||
public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
|
||||
/** The size of the authentication tag, as used by {@link #verifyTag} */
|
||||
public static final int TAG_SIZE_IN_BYTES = 16;
|
||||
|
||||
/**
|
||||
* Initializes the cipher with the given inputs.
|
||||
*
|
||||
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
|
||||
* between the encrypter and decrypter.
|
||||
*/
|
||||
public Aes256GcmDecryption(byte[] key, byte[] nonce, byte[] associatedData)
|
||||
throws InvalidKeyException {
|
||||
super(
|
||||
@ -27,18 +45,35 @@ public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
|
||||
Native.Aes256GcmDecryption_Destroy(nativeHandle);
|
||||
}
|
||||
|
||||
public void decrypt(byte[] plaintext) {
|
||||
/**
|
||||
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
|
||||
*
|
||||
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
|
||||
*/
|
||||
public void decrypt(byte[] ciphertext) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, 0, plaintext.length));
|
||||
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, 0, ciphertext.length));
|
||||
}
|
||||
|
||||
public void decrypt(byte[] plaintext, int offset, int length) {
|
||||
/**
|
||||
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
|
||||
*
|
||||
* <p>Bytes outside the designated offset/length are unchanged.
|
||||
*
|
||||
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
|
||||
*/
|
||||
public void decrypt(byte[] ciphertext, int offset, int length) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, offset, length));
|
||||
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, offset, length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if and only if {@code tag} matches the ciphertext that has been processed.
|
||||
*
|
||||
* <p>After calling {@code verifyTag}, this object may not be used anymore.
|
||||
*/
|
||||
public boolean verifyTag(byte[] tag) {
|
||||
return guardedMap(
|
||||
(nativeHandle) ->
|
||||
|
||||
@ -11,7 +11,25 @@ import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
|
||||
/**
|
||||
* Implements the <a
|
||||
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
|
||||
* authenticated stream cipher with a 12-byte nonce.
|
||||
*
|
||||
* <p>This API exposes the streaming nature of AES-GCM to allow encrypting data without having it
|
||||
* resident in memory all at once. You must call {@link #computeTag} when the encryption is complete
|
||||
* to produce the authentication tag for the ciphertext, and then make sure the tag makes it to the
|
||||
* decrypter.
|
||||
*
|
||||
* @see Aes256GcmDecryption
|
||||
*/
|
||||
public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
|
||||
/**
|
||||
* Initializes the cipher with the given inputs.
|
||||
*
|
||||
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
|
||||
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
|
||||
*/
|
||||
public Aes256GcmEncryption(byte[] key, byte[] nonce, byte[] associatedData)
|
||||
throws InvalidKeyException {
|
||||
super(
|
||||
@ -25,18 +43,35 @@ public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
|
||||
Native.Aes256GcmEncryption_Destroy(nativeHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts {@code plaintext} in place and advances the state of the cipher.
|
||||
*
|
||||
* <p>Bytes outside the designated offset/length are unchanged.
|
||||
*
|
||||
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
|
||||
*/
|
||||
public void encrypt(byte[] plaintext, int offset, int length) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, offset, length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts {@code plaintext} in place and advances the state of the cipher.
|
||||
*
|
||||
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
|
||||
*/
|
||||
public void encrypt(byte[] plaintext) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, 0, plaintext.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces an authentication tag for the plaintext that has been processed.
|
||||
*
|
||||
* <p>After calling {@code computeTag}, this object may not be used anymore.
|
||||
*/
|
||||
public byte[] computeTag() {
|
||||
return guardedMap(Native::Aes256GcmEncryption_ComputeTag);
|
||||
}
|
||||
|
||||
@ -12,6 +12,13 @@ import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
|
||||
/**
|
||||
* Implements the <a href="https://en.wikipedia.org/wiki/AES-GCM-SIV">AES-256-GCM-SIV</a>
|
||||
* authenticated stream cipher with a 12-byte nonce.
|
||||
*
|
||||
* <p>AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
|
||||
* this API does not expose a streaming form.
|
||||
*/
|
||||
public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
|
||||
|
||||
public Aes256GcmSiv(byte[] key) throws InvalidKeyException {
|
||||
@ -23,6 +30,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
|
||||
Native.Aes256GcmSiv_Destroy(nativeHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
|
||||
* associated data.
|
||||
*
|
||||
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
|
||||
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
|
||||
*
|
||||
* @return The encrypted data, including an appended 16-byte authentication tag.
|
||||
*/
|
||||
public byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
|
||||
return filterExceptions(
|
||||
() ->
|
||||
@ -31,6 +47,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
|
||||
Native.Aes256GcmSiv_Encrypt(nativeHandle, plaintext, nonce, associated_data)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and
|
||||
* given associated data.
|
||||
*
|
||||
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
|
||||
* between the encrypter and decrypter.
|
||||
*
|
||||
* @return The decrypted data
|
||||
*/
|
||||
public byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
|
||||
throws InvalidMessageException {
|
||||
return filterExceptions(
|
||||
|
||||
@ -60,9 +60,19 @@ public fun <T, R> CompletableFuture<T>.mapWithCancellation(
|
||||
|
||||
this.whenComplete { value, err ->
|
||||
when (err) {
|
||||
null -> outer.complete(onSuccess(value))
|
||||
null ->
|
||||
try {
|
||||
outer.complete(onSuccess(value))
|
||||
} catch (e: Exception) {
|
||||
outer.completeExceptionally(e)
|
||||
}
|
||||
is CancellationException -> outer.cancel(true)
|
||||
else -> outer.complete(onError(err))
|
||||
else ->
|
||||
try {
|
||||
outer.complete(onError(err))
|
||||
} catch (e: Exception) {
|
||||
outer.completeExceptionally(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
|
||||
package org.signal.libsignal.keytrans;
|
||||
|
||||
import org.signal.libsignal.net.BadRequestError;
|
||||
|
||||
/** Key transparency operation failed. */
|
||||
public class KeyTransparencyException extends Exception {
|
||||
public class KeyTransparencyException extends Exception implements BadRequestError {
|
||||
public KeyTransparencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.messagebackup
|
||||
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.NativeHandleGuard
|
||||
|
||||
/**
|
||||
* Result of exporting a single backup frame to JSON.
|
||||
*
|
||||
* @property line The JSON line for this frame, or null if the frame was filtered out (e.g.
|
||||
* disappearing messages).
|
||||
* @property errorMessage A validation error message, or null if the frame validated cleanly.
|
||||
*/
|
||||
public data class FrameExportResult(
|
||||
val line: String?,
|
||||
val errorMessage: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Exports a backup to newline-delimited JSON (JSONL), frame by frame.
|
||||
*
|
||||
* Optionally validates each frame and the whole backup during export. Sanitization (filtering
|
||||
* disappearing messages, stripping view-once attachments) is always applied.
|
||||
*
|
||||
* This class is not thread-safe.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* val (exporter, initialChunk) = BackupJsonExporter.start(backupInfoBytes)
|
||||
* exporter.use {
|
||||
* output.write(initialChunk)
|
||||
* output.write("\n")
|
||||
* while (hasMoreFrames) {
|
||||
* val results = exporter.exportFrames(framedBytes)
|
||||
* for (result in results) {
|
||||
* result.line?.let { output.write(it); output.write("\n") }
|
||||
* result.errorMessage?.let { log.warn(it) }
|
||||
* }
|
||||
* }
|
||||
* val finishError = exporter.finishExport()
|
||||
* finishError?.let { log.warn(it) }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public class BackupJsonExporter private constructor(
|
||||
private val handleOwner: NativeHandleGuard.CloseableOwner,
|
||||
) : AutoCloseable {
|
||||
private var closed = false
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* Creates a new exporter from serialized BackupInfo protobuf bytes.
|
||||
*
|
||||
* @param backupInfo serialized BackupInfo protobuf (without varint length prefix)
|
||||
* @param validate whether to run semantic validation during export (default true)
|
||||
* @return a pair of the exporter and the initial JSON chunk (serialized BackupInfo);
|
||||
* caller must [close] the exporter when done
|
||||
* @throws ValidationError if the BackupInfo is malformed
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Throws(ValidationError::class)
|
||||
public fun start(
|
||||
backupInfo: ByteArray,
|
||||
validate: Boolean = true,
|
||||
): Pair<BackupJsonExporter, String> {
|
||||
val owner =
|
||||
object : NativeHandleGuard.CloseableOwner(
|
||||
Native.BackupJsonExporter_New(backupInfo, validate),
|
||||
) {
|
||||
override fun release(nativeHandle: Long) {
|
||||
Native.BackupJsonExporter_Destroy(nativeHandle)
|
||||
}
|
||||
}
|
||||
val initialChunk =
|
||||
owner.guardedMap { h ->
|
||||
Native.BackupJsonExporter_GetInitialChunk(h)
|
||||
}
|
||||
return Pair(BackupJsonExporter(owner), initialChunk)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports one or more varint-delimited Frame protobuf messages to JSON lines.
|
||||
*
|
||||
* Can be called repeatedly to stream frames through the exporter.
|
||||
*
|
||||
* @param frames one or more varint-delimited serialized Frame protobufs
|
||||
* @return one result per frame, in order
|
||||
* @throws ValidationError if the frame bytes cannot be parsed at all
|
||||
* @throws IllegalStateException if the exporter has already been closed
|
||||
*/
|
||||
@Throws(ValidationError::class)
|
||||
public fun exportFrames(frames: ByteArray): List<FrameExportResult> {
|
||||
check(!closed) { "BackupJsonExporter is already closed" }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val pairs =
|
||||
handleOwner.guardedMapChecked { h -> Native.BackupJsonExporter_ExportFrames(h, frames) }
|
||||
as Array<Pair<String?, String?>>
|
||||
return pairs.map { (line, errorMessage) -> FrameExportResult(line, errorMessage) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the export and runs any final whole-backup validation checks.
|
||||
*
|
||||
* Must be called before [close]. Calling this on a closed exporter throws
|
||||
* [IllegalStateException].
|
||||
*
|
||||
* @return a validation error message if whole-backup checks failed, or null if clean
|
||||
* @throws IllegalStateException if the exporter has already been closed
|
||||
*/
|
||||
public fun finishExport(): String? {
|
||||
check(!closed) { "BackupJsonExporter is already closed" }
|
||||
return try {
|
||||
handleOwner.guardedRunChecked { h -> Native.BackupJsonExporter_Finish(h) }
|
||||
null
|
||||
} catch (e: ValidationError) {
|
||||
// All of our ValidationError instances should have a message, but we'll be defensive
|
||||
// and provide a default message if one is not available.
|
||||
e.message ?: "Backup export validation failed for unknown reason"
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the exporter, releasing native resources. Safe to call multiple times. */
|
||||
override fun close() {
|
||||
closed = true
|
||||
handleOwner.close()
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,9 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.function.Supplier;
|
||||
import kotlin.Pair;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
|
||||
/** Message-backup-related functionality. */
|
||||
public class MessageBackup {
|
||||
@ -73,11 +73,11 @@ public class MessageBackup {
|
||||
result = outputPair;
|
||||
}
|
||||
|
||||
String errorMessage = result.first();
|
||||
String errorMessage = result.getFirst();
|
||||
if (errorMessage != null) {
|
||||
throw new ValidationError(errorMessage, result.second());
|
||||
throw new ValidationError(errorMessage, result.getSecond());
|
||||
}
|
||||
|
||||
return new ValidationResult(result.second());
|
||||
return new ValidationResult(result.getSecond());
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ package org.signal.libsignal.metadata;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -60,27 +59,19 @@ public class SealedSessionCipher {
|
||||
SignalProtocolAddress destinationAddress,
|
||||
SenderCertificate senderCertificate,
|
||||
byte[] paddedPlaintext)
|
||||
throws InvalidKeyException, UntrustedIdentityException {
|
||||
try (NativeHandleGuard addressGuard = new NativeHandleGuard(destinationAddress)) {
|
||||
CiphertextMessage message =
|
||||
filterExceptions(
|
||||
InvalidKeyException.class,
|
||||
UntrustedIdentityException.class,
|
||||
() ->
|
||||
Native.SessionCipher_EncryptMessage(
|
||||
paddedPlaintext,
|
||||
addressGuard.nativeHandle(),
|
||||
this.signalProtocolStore,
|
||||
this.signalProtocolStore,
|
||||
Instant.now().toEpochMilli()));
|
||||
UnidentifiedSenderMessageContent content =
|
||||
new UnidentifiedSenderMessageContent(
|
||||
message,
|
||||
senderCertificate,
|
||||
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
|
||||
Optional.<byte[]>empty());
|
||||
return encrypt(destinationAddress, content);
|
||||
}
|
||||
throws InvalidKeyException, NoSessionException, UntrustedIdentityException {
|
||||
SignalProtocolAddress localAddress =
|
||||
new SignalProtocolAddress(this.localUuidAddress, this.localDeviceId);
|
||||
CiphertextMessage message =
|
||||
new SessionCipher(signalProtocolStore, localAddress, destinationAddress)
|
||||
.encrypt(paddedPlaintext);
|
||||
UnidentifiedSenderMessageContent content =
|
||||
new UnidentifiedSenderMessageContent(
|
||||
message,
|
||||
senderCertificate,
|
||||
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
|
||||
Optional.<byte[]>empty());
|
||||
return encrypt(destinationAddress, content);
|
||||
}
|
||||
|
||||
public byte[] encrypt(
|
||||
@ -95,7 +86,7 @@ public class SealedSessionCipher {
|
||||
Native.SealedSessionCipher_Encrypt(
|
||||
addressGuard.nativeHandle(),
|
||||
contentGuard.nativeHandle(),
|
||||
this.signalProtocolStore));
|
||||
SessionCipher._bridge(this.signalProtocolStore)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +164,7 @@ public class SealedSessionCipher {
|
||||
recipientSessionHandles,
|
||||
ServiceId.toConcatenatedFixedWidthBinary(excludedRecipients),
|
||||
contentGuard.nativeHandle(),
|
||||
this.signalProtocolStore));
|
||||
SessionCipher._bridge(this.signalProtocolStore)));
|
||||
// Manually keep the lists of recipients and sessions from being garbage collected
|
||||
// while we're using their native handles.
|
||||
Native.keepAlive(recipients);
|
||||
@ -204,7 +195,8 @@ public class SealedSessionCipher {
|
||||
try {
|
||||
content =
|
||||
new UnidentifiedSenderMessageContent(
|
||||
Native.SealedSessionCipher_DecryptToUsmc(ciphertext, this.signalProtocolStore));
|
||||
Native.SealedSessionCipher_DecryptToUsmc(
|
||||
ciphertext, SessionCipher._bridge(this.signalProtocolStore)));
|
||||
validator.validate(content.getSenderCertificate(), timestamp);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidMetadataMessageException(e);
|
||||
@ -248,11 +240,13 @@ public class SealedSessionCipher {
|
||||
}
|
||||
|
||||
public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
||||
return new SessionCipher(signalProtocolStore, remoteAddress).getSessionVersion();
|
||||
return new SessionCipher(signalProtocolStore, localAddress(), remoteAddress)
|
||||
.getSessionVersion();
|
||||
}
|
||||
|
||||
public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
||||
return new SessionCipher(signalProtocolStore, remoteAddress).getRemoteRegistrationId();
|
||||
return new SessionCipher(signalProtocolStore, localAddress(), remoteAddress)
|
||||
.getRemoteRegistrationId();
|
||||
}
|
||||
|
||||
private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
||||
@ -271,10 +265,10 @@ public class SealedSessionCipher {
|
||||
|
||||
switch (message.getType()) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return new SessionCipher(signalProtocolStore, sender)
|
||||
return new SessionCipher(signalProtocolStore, localAddress(), sender)
|
||||
.decrypt(new SignalMessage(message.getContent()));
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return new SessionCipher(signalProtocolStore, sender)
|
||||
return new SessionCipher(signalProtocolStore, localAddress(), sender)
|
||||
.decrypt(new PreKeySignalMessage(message.getContent()));
|
||||
case CiphertextMessage.SENDERKEY_TYPE:
|
||||
return new GroupCipher(signalProtocolStore, sender).decrypt(message.getContent());
|
||||
@ -348,4 +342,8 @@ public class SealedSessionCipher {
|
||||
return groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private SignalProtocolAddress localAddress() {
|
||||
return new SignalProtocolAddress(this.localUuidAddress, this.localDeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public class AuthMessagesService(
|
||||
private val connection: AuthenticatedChatConnection,
|
||||
) {
|
||||
/**
|
||||
* Get an attachment upload form
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
|
||||
* too large.
|
||||
*/
|
||||
public fun getUploadForm(uploadSize: Long): CompletableFuture<RequestResult<UploadForm, UploadTooLargeException>> =
|
||||
try {
|
||||
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native
|
||||
.AuthenticatedChatConnection_get_upload_form(
|
||||
asyncCtx,
|
||||
conn,
|
||||
uploadSize,
|
||||
).mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(it as UploadForm) },
|
||||
onError = { err -> err.toRequestResult<UploadTooLargeException>() },
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an unsealed 1:1 message.
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [MismatchedDeviceException] indicates the recipient
|
||||
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
|
||||
* result; the message has not been sent to anybody.) A [ServiceIdNotFoundException] indicates the
|
||||
* destination account has been unregistered. A [RateLimitChallengeException] must be handled
|
||||
* before the client can send this message.
|
||||
*/
|
||||
public fun sendMessage(
|
||||
destination: ServiceId,
|
||||
timestamp: Long,
|
||||
contents: List<SingleOutboundUnsealedMessage>,
|
||||
onlineOnly: Boolean,
|
||||
urgent: Boolean,
|
||||
): CompletableFuture<RequestResult<Unit, UnsealedSendFailure>> =
|
||||
try {
|
||||
val deviceIds = IntArray(contents.size)
|
||||
val registrationIds = IntArray(contents.size)
|
||||
val messages = arrayOfNulls<Object>(contents.size)
|
||||
|
||||
contents.forEachIndexed { i, next ->
|
||||
deviceIds[i] = next.deviceId
|
||||
registrationIds[i] = next.registrationId
|
||||
messages[i] = next.message as Object
|
||||
}
|
||||
|
||||
connection
|
||||
.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native.AuthenticatedChatConnection_send_message_java(
|
||||
asyncCtx,
|
||||
conn,
|
||||
destination.toServiceIdFixedWidthBinary(),
|
||||
timestamp,
|
||||
deviceIds,
|
||||
registrationIds,
|
||||
messages.requireNoNulls(),
|
||||
onlineOnly,
|
||||
urgent,
|
||||
)
|
||||
}.mapWithCancellation(
|
||||
onSuccess = { _ -> RequestResult.Success(Unit) },
|
||||
onError = { err -> err.toRequestResult<UnsealedSendFailure>() },
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 1:1 message to linked devices.
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [MismatchedDeviceException] indicates the recipient
|
||||
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
|
||||
* result; the message has not been sent to anybody.) A [RateLimitChallengeException] must be
|
||||
* handled before the client can send this message.
|
||||
*/
|
||||
public fun sendSyncMessage(
|
||||
timestamp: Long,
|
||||
contents: List<SingleOutboundUnsealedMessage>,
|
||||
urgent: Boolean,
|
||||
): CompletableFuture<RequestResult<Unit, SyncSendFailure>> =
|
||||
try {
|
||||
val deviceIds = IntArray(contents.size)
|
||||
val registrationIds = IntArray(contents.size)
|
||||
val messages = arrayOfNulls<Object>(contents.size)
|
||||
|
||||
contents.forEachIndexed { i, next ->
|
||||
deviceIds[i] = next.deviceId
|
||||
registrationIds[i] = next.registrationId
|
||||
messages[i] = next.message as Object
|
||||
}
|
||||
|
||||
connection
|
||||
.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native.AuthenticatedChatConnection_send_sync_message_java(
|
||||
asyncCtx,
|
||||
conn,
|
||||
timestamp,
|
||||
deviceIds,
|
||||
registrationIds,
|
||||
messages.requireNoNulls(),
|
||||
urgent,
|
||||
)
|
||||
}.mapWithCancellation(
|
||||
onSuccess = { _ -> RequestResult.Success(Unit) },
|
||||
onError = { err -> err.toRequestResult<SyncSendFailure>() },
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
|
||||
/** Either [ServiceIdNotFoundException], [RateLimitChallengeException], or [MismatchedDeviceException]. */
|
||||
public sealed interface UnsealedSendFailure : BadRequestError
|
||||
|
||||
/** Either [RateLimitChallengeException] or [MismatchedDeviceException]. */
|
||||
public sealed interface SyncSendFailure : BadRequestError
|
||||
@ -6,12 +6,12 @@
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import java.util.Locale;
|
||||
import kotlin.Pair;
|
||||
import org.signal.libsignal.internal.CompletableFuture;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
import org.signal.libsignal.net.internal.BridgeChatListener;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
|
||||
/**
|
||||
* Represents an authenticated communication channel with the ChatConnection.
|
||||
@ -63,7 +63,7 @@ public class AuthenticatedChatConnection extends ChatConnection {
|
||||
*/
|
||||
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener) {
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0]);
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0], new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,14 +72,20 @@ public class AuthenticatedChatConnection extends ChatConnection {
|
||||
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
|
||||
*/
|
||||
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener, String[] alerts) {
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
String[] grpcOverrides,
|
||||
String[] alerts) {
|
||||
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle -> {
|
||||
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
|
||||
long fakeChatConnection =
|
||||
NativeTesting.TESTING_FakeChatConnection_Create(
|
||||
asyncContextHandle, bridgeListener, String.join("\n", alerts));
|
||||
asyncContextHandle,
|
||||
bridgeListener,
|
||||
String.join("\n", grpcOverrides),
|
||||
String.join("\n", alerts));
|
||||
AuthenticatedChatConnection chat =
|
||||
new AuthenticatedChatConnection(
|
||||
tokioAsyncContext,
|
||||
|
||||
@ -64,21 +64,18 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
|
||||
this.chat = new WeakReference<>(chat);
|
||||
}
|
||||
|
||||
public void onIncomingMessage(
|
||||
public void receivedIncomingMessage(
|
||||
byte[] envelope, long serverDeliveryTimestamp, long sendAckHandle) {
|
||||
|
||||
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
|
||||
ChatConnection chat = this.chat.get();
|
||||
if (chat == null) return;
|
||||
if (chat.chatListener == null) return;
|
||||
|
||||
chat.chatListener.onIncomingMessage(
|
||||
chat,
|
||||
envelope,
|
||||
serverDeliveryTimestamp,
|
||||
new ChatConnectionListener.ServerMessageAck(chat.tokioAsyncContext, sendAckHandle));
|
||||
chat.chatListener.onIncomingMessage(chat, envelope, serverDeliveryTimestamp, ack);
|
||||
}
|
||||
|
||||
public void onQueueEmpty() {
|
||||
public void receivedQueueEmpty() {
|
||||
ChatConnection chat = this.chat.get();
|
||||
if (chat == null) return;
|
||||
if (chat.chatListener == null) return;
|
||||
@ -86,7 +83,7 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
|
||||
chat.chatListener.onQueueEmpty(chat);
|
||||
}
|
||||
|
||||
public void onReceivedAlerts(String[] alerts) {
|
||||
public void receivedAlerts(String[] alerts) {
|
||||
ChatConnection chat = this.chat.get();
|
||||
if (chat == null) return;
|
||||
if (chat.chatListener == null) return;
|
||||
@ -94,7 +91,7 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
|
||||
chat.chatListener.onReceivedAlerts(chat, alerts);
|
||||
}
|
||||
|
||||
public void onConnectionInterrupted(Throwable disconnectReason) {
|
||||
public void connectionInterrupted(Throwable disconnectReason) {
|
||||
ChatConnection chat = this.chat.get();
|
||||
if (chat == null) return;
|
||||
if (chat.chatListener == null) return;
|
||||
@ -119,19 +116,20 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
|
||||
void setChat(ChatConnection chat) {
|
||||
this.chat = new WeakReference<>(chat);
|
||||
if (savedAlerts != null) {
|
||||
super.onReceivedAlerts(savedAlerts);
|
||||
super.receivedAlerts(savedAlerts);
|
||||
savedAlerts = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onReceivedAlerts(String[] alerts) {
|
||||
@Override
|
||||
public void receivedAlerts(String[] alerts) {
|
||||
// This callback can happen before setChat, so we might need to replay it later.
|
||||
if (this.chat.get() == null) {
|
||||
savedAlerts = alerts;
|
||||
return;
|
||||
}
|
||||
|
||||
super.onReceivedAlerts(alerts);
|
||||
super.receivedAlerts(alerts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ package org.signal.libsignal.net;
|
||||
import org.signal.libsignal.internal.FilterExceptions;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
|
||||
public interface ChatConnectionListener {
|
||||
/**
|
||||
@ -53,11 +52,8 @@ public interface ChatConnectionListener {
|
||||
ChatConnection chat, ChatServiceException disconnectReason) {}
|
||||
|
||||
public static class ServerMessageAck extends NativeHandleGuard.SimpleOwner {
|
||||
private final TokioAsyncContext asyncContext;
|
||||
|
||||
ServerMessageAck(TokioAsyncContext context, long nativeHandle) {
|
||||
ServerMessageAck(long nativeHandle) {
|
||||
super(nativeHandle);
|
||||
asyncContext = context;
|
||||
}
|
||||
|
||||
protected void release(long nativeHandle) {
|
||||
|
||||
@ -5,14 +5,17 @@
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import java.util.UUID;
|
||||
import kotlin.Pair;
|
||||
import org.signal.libsignal.internal.CompletableFuture;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
import org.signal.libsignal.net.ChatConnection.InternalRequest;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
|
||||
class FakeChatRemote extends NativeHandleGuard.SimpleOwner {
|
||||
public static UUID FAKE_AUTH_CONNECT_SELF_UUID = new UUID(~0, ~0);
|
||||
|
||||
private TokioAsyncContext tokioContext;
|
||||
|
||||
FakeChatRemote(TokioAsyncContext tokioContext, long nativeHandle) {
|
||||
@ -32,7 +35,7 @@ class FakeChatRemote extends NativeHandleGuard.SimpleOwner {
|
||||
.thenApply(
|
||||
rawRequest -> {
|
||||
var sentRequest = (Pair<Long, Long>) rawRequest;
|
||||
return new Pair(new InternalRequest(sentRequest.first()), sentRequest.second());
|
||||
return new Pair(new InternalRequest(sentRequest.getFirst()), sentRequest.getSecond());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
public abstract class KeyTransparency {
|
||||
/**
|
||||
* Mode of the monitor operation.
|
||||
*
|
||||
* <p>If the newer version of account data is found in the key transparency log, self-monitor will
|
||||
* terminate with an error, but monitor for other account will fall back to a full search and
|
||||
* update the locally stored data.
|
||||
*/
|
||||
public enum MonitorMode {
|
||||
SELF,
|
||||
OTHER
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.keytrans.Store
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public object KeyTransparency {
|
||||
/**
|
||||
* Mode of the key transparency operation.
|
||||
*
|
||||
* The behavior of [KeyTransparencyClient.check] differs depending on whether it is
|
||||
* performed for the owner of the account or contact and in the former case whether
|
||||
* the phone number discoverability is enabled.
|
||||
*
|
||||
* For example, if the newer version of account data is found in the key
|
||||
* transparency log while monitoring "self", it will terminate with an error.
|
||||
* However, the same check for a "contact" will result in a follow-up search
|
||||
* operation.
|
||||
*/
|
||||
public sealed class CheckMode {
|
||||
public data class Self(
|
||||
val isE164Discoverable: Boolean,
|
||||
) : CheckMode()
|
||||
|
||||
public object Contact : CheckMode()
|
||||
|
||||
public fun isE164Discoverable(): Boolean? =
|
||||
when (this) {
|
||||
is Self -> isE164Discoverable
|
||||
is Contact -> null
|
||||
}
|
||||
|
||||
public fun isSelf(): Boolean = this is Self
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag identifying an optional field of the account data.
|
||||
*
|
||||
* (Must be in sync with the Rust counterpart)
|
||||
*/
|
||||
public enum class AccountDataField(
|
||||
public val value: Int,
|
||||
) {
|
||||
E164(0),
|
||||
USERNAME_HASH(1),
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a particular field in the data associated with given ACI.
|
||||
*
|
||||
* Must only be called for the "self" account when either E.164 or username change is performed.
|
||||
*
|
||||
* Upon successful completion the data associated with the account will be updated in the store, if it
|
||||
* was present to begin with, noop if it was not.
|
||||
*
|
||||
* @param aci An ACI of "self" account.
|
||||
* @param field Account data field to be reset (E.164 or username hash)
|
||||
* @param store local persistent storage for key transparency-related data.
|
||||
* @throws IllegalArgumentException if the stored data cannot be decoded correctly, which means data corruption.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun resetField(
|
||||
aci: ServiceId.Aci,
|
||||
field: AccountDataField,
|
||||
store: Store,
|
||||
) {
|
||||
store.getAccountData(aci).map {
|
||||
val updated = Native.KeyTransparency_ResetDataField(it, field.value)
|
||||
if (updated.isEmpty()) {
|
||||
throw IllegalArgumentException("failed to decode account data")
|
||||
}
|
||||
store.setAccountData(aci, updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,298 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.signal.libsignal.internal.CompletableFuture;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
import org.signal.libsignal.keytrans.Store;
|
||||
import org.signal.libsignal.net.KeyTransparency.MonitorMode;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
|
||||
/**
|
||||
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
|
||||
* connection.
|
||||
*
|
||||
* <p>Unlike {@link ChatConnection}, key transparency client does not export "raw" send/receive
|
||||
* APIs, and instead uses them internally to implement high-level operations.
|
||||
*
|
||||
* <p>Note: {@code Store} APIs may be invoked concurrently. Here are possible strategies to make
|
||||
* sure there are no thread safety violations:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Types implementing {@code Store} can be made thread safe
|
||||
* <li>{@link KeyTransparencyClient} operations-completed asynchronous calls-can be serialized.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* var net = new Network(Network.Environment.STAGING, "key-transparency-example");
|
||||
* var chat = net.connectUnauthChat(new Listener()).get();
|
||||
* chat.start();
|
||||
*
|
||||
* KeyTransparencyClient client = chat.keyTransparencyClient();
|
||||
*
|
||||
* client.search(aci, identityKey, null, null, null, KT_DATA_STORE).get();
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class KeyTransparencyClient {
|
||||
private final TokioAsyncContext tokioAsyncContext;
|
||||
private final UnauthenticatedChatConnection chatConnection;
|
||||
private final Network.Environment environment;
|
||||
|
||||
KeyTransparencyClient(
|
||||
UnauthenticatedChatConnection chat,
|
||||
TokioAsyncContext tokioAsyncContext,
|
||||
Network.Environment environment) {
|
||||
this.chatConnection = chat;
|
||||
this.tokioAsyncContext = tokioAsyncContext;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for account information in the key transparency tree.
|
||||
*
|
||||
* <p>Only ACI and ACI identity key are required to identify the account.
|
||||
*
|
||||
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
|
||||
* the server prior to performing the search via {@link #updateDistinguished}.
|
||||
*
|
||||
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
|
||||
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
|
||||
*
|
||||
* <p>Possible exceptions include:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link ChatServiceException} for errors related to communication with the server.
|
||||
* Depending on the concrete subclass, client can retry the operation. See also {@link
|
||||
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
|
||||
* {@link AppExpiredException},
|
||||
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
|
||||
* amount of time before making new requests.
|
||||
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
|
||||
* after a recommended period.
|
||||
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
|
||||
* failure to establish a websocket connection.
|
||||
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
|
||||
* transparency logic, which includes missing required fields in the serialized data.
|
||||
* Retrying the search without changing any of the arguments (including the state of the
|
||||
* store) is unlikely to yield a different result.
|
||||
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
|
||||
* verify the data in key transparency server response, such as an incorrect proof or a
|
||||
* wrong signature.
|
||||
* </ul>
|
||||
*
|
||||
* @param aci the ACI of the account to be searched for. Required.
|
||||
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
|
||||
* @param e164 string representation of an E.164 number associated with the account. Optional.
|
||||
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
|
||||
* same optionality as the E.164 parameter.
|
||||
* @param usernameHash hash of the username associated with the account. Optional.
|
||||
* @param store local persistent storage for key transparency-related data, such as the latest
|
||||
* tree heads and account monitoring data. It will be queried for data before performing the
|
||||
* server request and updated with the latest information from the server response if it
|
||||
* succeeds.
|
||||
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
|
||||
* that the search request has succeeded and store has been updated with the latest account
|
||||
* data.
|
||||
* @throws IllegalArgumentException if the store contains corrupted data.
|
||||
*/
|
||||
public CompletableFuture<Void> search(
|
||||
/* @NotNull */ final ServiceId.Aci aci,
|
||||
/* @NotNull */ final IdentityKey aciIdentityKey,
|
||||
final String e164,
|
||||
final byte[] unidentifiedAccessKey,
|
||||
final byte[] usernameHash,
|
||||
final Store store) {
|
||||
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
|
||||
if (lastDistinguishedTreeHead.isEmpty()) {
|
||||
return this.updateDistinguished(store)
|
||||
.thenCompose(
|
||||
(ignored) ->
|
||||
this.search(
|
||||
aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
|
||||
}
|
||||
// Decoding of the last distinguished tree head happens "eagerly" before making any network
|
||||
// requests.
|
||||
// It may result in an IllegalArgumentException.
|
||||
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
|
||||
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
|
||||
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection); ) {
|
||||
return Native.KeyTransparency_Search(
|
||||
tokioContextGuard.nativeHandle(),
|
||||
this.environment.value,
|
||||
chatConnectionGuard.nativeHandle(),
|
||||
aci.toServiceIdFixedWidthBinary(),
|
||||
identityKeyGuard.nativeHandle(),
|
||||
e164,
|
||||
unidentifiedAccessKey,
|
||||
usernameHash,
|
||||
store.getAccountData(aci).orElse(null),
|
||||
lastDistinguishedTreeHead.get())
|
||||
.thenApply(
|
||||
(accountData) -> {
|
||||
store.setAccountData(aci, accountData);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the latest distinguished tree head from the server and update it in the local store.
|
||||
*
|
||||
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
|
||||
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
|
||||
*
|
||||
* <p>Possible exceptions include:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link ChatServiceException} for errors related to communication with the server.
|
||||
* Depending on the concrete subclass, client can retry the operation. See also {@link
|
||||
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
|
||||
* {@link AppExpiredException},
|
||||
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
|
||||
* amount of time before making new requests.
|
||||
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
|
||||
* after a recommended period.
|
||||
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
|
||||
* failure to establish a websocket connection.
|
||||
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
|
||||
* transparency logic. Retrying the search without changing any of the arguments (including
|
||||
* the state of the store) is unlikely to yield a different result.
|
||||
* </ul>
|
||||
*
|
||||
* @param store local persistent storage for key transparency related data, such as the latest
|
||||
* tree heads and account monitoring data. It will be queried for the latest distinguished
|
||||
* tree head before performing the server request and updated with data from the server
|
||||
* response if it succeeds. Distinguished tree does not have to be present in the store prior
|
||||
* to the call.
|
||||
* @return An instance of {@link CompletableFuture} representing the asynchronous operation, which
|
||||
* does not produce any value. Successful completion of the operation results in an updated
|
||||
* state of the store.
|
||||
* @throws IllegalArgumentException if the store contains corrupted data.
|
||||
*/
|
||||
public CompletableFuture<Void> updateDistinguished(final Store store) {
|
||||
byte[] lastDistinguished = store.getLastDistinguishedTreeHead().orElse(null);
|
||||
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
|
||||
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
|
||||
return Native.KeyTransparency_Distinguished(
|
||||
tokioContextGuard.nativeHandle(),
|
||||
this.environment.value,
|
||||
chatConnectionGuard.nativeHandle(),
|
||||
lastDistinguished)
|
||||
.thenApply(
|
||||
bytes -> {
|
||||
store.setLastDistinguishedTreeHead(bytes);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a monitor request to the key transparency service.
|
||||
*
|
||||
* <p>Store must contain data associated with the account being requested prior to making this
|
||||
* call. Another way of putting this is: monitor cannot be called before {@link #search}.
|
||||
*
|
||||
* <p>If any of the monitored fields in the server response contain a version that is higher than
|
||||
* the one currently in the store, the behavior depends on the mode parameter value.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code MonitorMode.SELF} - An exception will be thrown, no search request will be issued.
|
||||
* <li>{@code MonitorMode.OTHER} - A search request will be performed automatically and, if it
|
||||
* succeeds, the updated account data will be stored.
|
||||
* </ul>
|
||||
*
|
||||
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
|
||||
* the server prior to performing the search via {@link #updateDistinguished}.
|
||||
*
|
||||
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
|
||||
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
|
||||
*
|
||||
* <p>Possible exceptions include:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link ChatServiceException} for errors related to communication with the server.
|
||||
* Depending on the concrete subclass, client can retry the operation. See also {@link
|
||||
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
|
||||
* {@link AppExpiredException},
|
||||
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
|
||||
* amount of time before making new requests.
|
||||
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
|
||||
* after a recommended period.
|
||||
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
|
||||
* failure to establish a websocket connection.
|
||||
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
|
||||
* transparency logic, which includes missing required fields in the serialized data.
|
||||
* Retrying the search without changing any of the arguments (including the state of the
|
||||
* store) is unlikely to yield a different result.
|
||||
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
|
||||
* verify the data in key transparency server response, such as an incorrect proof or a
|
||||
* wrong signature.
|
||||
* </ul>
|
||||
*
|
||||
* @param mode Mode of the monitor operation. See {@link MonitorMode}.
|
||||
* @param aci the ACI of the account to be searched for. Required.
|
||||
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
|
||||
* @param e164 string representation of an E.164 number associated with the account. Optional.
|
||||
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
|
||||
* same optionality as the E.164 parameter.
|
||||
* @param usernameHash hash of the username associated with the account. Optional.
|
||||
* @param store local persistent storage for key transparency-related data, such as the latest
|
||||
* tree heads and account monitoring data. It will be queried for data before performing the
|
||||
* server request and updated with the latest information from the server response if it
|
||||
* succeeds.
|
||||
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
|
||||
* that the monitor request has succeeded and store has been updated with the latest account
|
||||
* data.
|
||||
* @throws IllegalArgumentException if the store contains corrupted data.
|
||||
*/
|
||||
public CompletableFuture<Void> monitor(
|
||||
/* @NotNull */ final MonitorMode mode,
|
||||
final ServiceId.Aci aci,
|
||||
/* @NotNull */ final IdentityKey aciIdentityKey,
|
||||
final String e164,
|
||||
final byte[] unidentifiedAccessKey,
|
||||
final byte[] usernameHash,
|
||||
final Store store) {
|
||||
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
|
||||
if (lastDistinguishedTreeHead.isEmpty()) {
|
||||
return this.updateDistinguished(store)
|
||||
.thenCompose(
|
||||
(ignored) ->
|
||||
this.monitor(
|
||||
mode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
|
||||
}
|
||||
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
|
||||
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
|
||||
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
|
||||
return Native.KeyTransparency_Monitor(
|
||||
tokioContextGuard.nativeHandle(),
|
||||
this.environment.value,
|
||||
chatConnectionGuard.nativeHandle(),
|
||||
aci.toServiceIdFixedWidthBinary(),
|
||||
identityKeyGuard.nativeHandle(),
|
||||
e164,
|
||||
unidentifiedAccessKey,
|
||||
usernameHash,
|
||||
// Technically this is a required parameter, but passing null
|
||||
// to generate the error on the Rust side.
|
||||
store.getAccountData(aci).orElse(null),
|
||||
lastDistinguishedTreeHead.get(),
|
||||
mode == MonitorMode.SELF)
|
||||
.thenApply(
|
||||
(updatedAccountData) -> {
|
||||
store.setAccountData(aci, updatedAccountData);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.NativeHandleGuard
|
||||
import org.signal.libsignal.internal.TokioAsyncContext
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.keytrans.KeyTransparencyException
|
||||
import org.signal.libsignal.keytrans.Store
|
||||
import org.signal.libsignal.keytrans.VerificationFailedException
|
||||
import org.signal.libsignal.net.KeyTransparency.CheckMode
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
/**
|
||||
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
|
||||
* connection.
|
||||
*
|
||||
* Unlike [ChatConnection], key transparency client does not export "raw" send/receive APIs, and
|
||||
* instead uses them internally to implement high-level operations.
|
||||
*
|
||||
* All operations return [RequestResult]. Request-specific failures are represented as
|
||||
* [RequestResult.NonSuccess] with [KeyTransparencyException]; retryable network errors as
|
||||
* [RequestResult.RetryableNetworkError].
|
||||
*
|
||||
* Note: [Store] APIs may be invoked concurrently. Here are possible strategies to make sure there
|
||||
* are no thread safety violations:
|
||||
* - Types implementing [Store] can be made thread safe
|
||||
* - [KeyTransparencyClient] operations-completed asynchronous calls-can be serialized.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* val net = Network(Network.Environment.STAGING, "key-transparency-example")
|
||||
* val chat = net.connectUnauthChat(Listener()).get()
|
||||
* chat.start()
|
||||
*
|
||||
* val client = chat.keyTransparencyClient()
|
||||
*
|
||||
* val result = client.check(CheckMode.Contact, aci, identityKey, null, null, null, KT_DATA_STORE).get()
|
||||
* ```
|
||||
*/
|
||||
public class KeyTransparencyClient internal constructor(
|
||||
private val chatConnection: UnauthenticatedChatConnection,
|
||||
private val tokioAsyncContext: TokioAsyncContext,
|
||||
private val environment: Network.Environment,
|
||||
) {
|
||||
/**
|
||||
* A unified key transparency operation that performs a search, a monitor, or both.
|
||||
*
|
||||
* Caller should pass latest known values of all identifiers (ACI, E.164, username hash) associated
|
||||
* with the account, along with a correct value of [CheckMode].
|
||||
*
|
||||
* If there is no data in the store for the account, the search operation will be performed. Following
|
||||
* this initial search, the monitor operation will be used.
|
||||
*
|
||||
* If any of the fields in the monitor response contain a version that is higher than the one
|
||||
* currently in the store, the behavior depends on the mode parameter value.
|
||||
* - [CheckMode.Self] - A [KeyTransparencyException] will be returned, no search request will
|
||||
* be issued.
|
||||
* - [CheckMode.Contact] - Another search request will be performed automatically and, if it succeeds,
|
||||
* the updated account data will be stored.
|
||||
*
|
||||
* Possible non-success results include:
|
||||
* - [RequestResult.RetryableNetworkError] for errors related to communication with the server,
|
||||
* including [RetryLaterException] when the client is being throttled,
|
||||
* [ServerSideErrorException], [NetworkException], [NetworkProtocolException], and
|
||||
* [TimeoutException].
|
||||
* - [RequestResult.NonSuccess] with [KeyTransparencyException] for errors related to key
|
||||
* transparency logic, which includes missing required fields in the serialized data.
|
||||
* Retrying without changing any of the arguments (including the state of the store) is
|
||||
* unlikely to yield a different result.
|
||||
* - [RequestResult.NonSuccess] with [VerificationFailedException] (a subclass of
|
||||
* [KeyTransparencyException]) indicating a failure to verify the data in key transparency
|
||||
* server response, such as an incorrect proof or a wrong signature.
|
||||
* - [RequestResult.ApplicationError] for invalid arguments or other caller errors that could have
|
||||
* been avoided, such as providing an [unidentifiedAccessKey] without an [e164].
|
||||
*
|
||||
* @param mode Mode of the key transparency operation being performed. See [CheckMode].
|
||||
* @param aci the ACI of the account to be checked. Required.
|
||||
* @param aciIdentityKey [IdentityKey] associated with the ACI. Required.
|
||||
* @param e164 string representation of an E.164 number associated with the account. Optional.
|
||||
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
|
||||
* same optionality as the E.164 parameter.
|
||||
* @param usernameHash hash of the username associated with the account. Optional.
|
||||
* @param store local persistent storage for key transparency-related data, such as the latest
|
||||
* tree heads and account monitoring data. It will be queried for data before performing the
|
||||
* server request and updated with the latest information from the server response if it
|
||||
* succeeds.
|
||||
* @return an instance of [CompletableFuture] that completes with a [RequestResult] indicating
|
||||
* success or containing the error details.
|
||||
*/
|
||||
public fun check(
|
||||
mode: CheckMode,
|
||||
aci: ServiceId.Aci,
|
||||
aciIdentityKey: IdentityKey,
|
||||
e164: String?,
|
||||
unidentifiedAccessKey: ByteArray?,
|
||||
usernameHash: ByteArray?,
|
||||
store: Store,
|
||||
): CompletableFuture<RequestResult<Unit, KeyTransparencyException>> {
|
||||
val lastDistinguishedTreeHead =
|
||||
try {
|
||||
store.lastDistinguishedTreeHead
|
||||
} catch (t: Throwable) {
|
||||
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
|
||||
}
|
||||
|
||||
return try {
|
||||
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
|
||||
NativeHandleGuard(aciIdentityKey.publicKey).use { identityKeyGuard ->
|
||||
NativeHandleGuard(chatConnection).use { chatConnectionGuard ->
|
||||
Native
|
||||
.KeyTransparency_Check(
|
||||
tokioContextGuard.nativeHandle(),
|
||||
environment.value,
|
||||
chatConnectionGuard.nativeHandle(),
|
||||
aci.toServiceIdFixedWidthBinary(),
|
||||
identityKeyGuard.nativeHandle(),
|
||||
e164,
|
||||
unidentifiedAccessKey,
|
||||
usernameHash,
|
||||
// Technically this is a required parameter, but passing null
|
||||
// to generate the error on the Rust side.
|
||||
store.getAccountData(aci).orElse(null),
|
||||
lastDistinguishedTreeHead.orElse(null),
|
||||
mode.isSelf(),
|
||||
mode.isE164Discoverable() ?: true,
|
||||
).mapWithCancellation(
|
||||
onSuccess = { (updatedAccountData, distinguished) ->
|
||||
try {
|
||||
store.setAccountData(aci, updatedAccountData)
|
||||
if (distinguished.isNotEmpty()) {
|
||||
store.setLastDistinguishedTreeHead(distinguished)
|
||||
}
|
||||
RequestResult.Success(Unit)
|
||||
} catch (t: Throwable) {
|
||||
RequestResult.ApplicationError(t)
|
||||
}
|
||||
},
|
||||
onError = { err -> err.toRequestResult<KeyTransparencyException>() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,10 @@ import java.io.IOException
|
||||
*/
|
||||
public class MismatchedDeviceException :
|
||||
IOException,
|
||||
MultiRecipientSendFailure {
|
||||
MultiRecipientSendFailure,
|
||||
SealedSendFailure,
|
||||
SyncSendFailure,
|
||||
UnsealedSendFailure {
|
||||
public data class Entry(
|
||||
public val account: ServiceId,
|
||||
public val missingDevices: IntArray = intArrayOf(),
|
||||
@ -58,6 +61,7 @@ public class MismatchedDeviceException :
|
||||
|
||||
public val entries: Array<Entry>
|
||||
|
||||
@CalledFromNative
|
||||
public constructor(message: String, entries: Array<Entry>) : super(message) {
|
||||
this.entries = entries
|
||||
}
|
||||
|
||||
@ -318,8 +318,9 @@ public class Network {
|
||||
* to the chat service, and incoming events will be provided via the provided {@link
|
||||
* ChatConnectionListener} argument.
|
||||
*
|
||||
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
|
||||
* other exception type wrapped in a {@link ExecutionException}.
|
||||
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException},
|
||||
* {@link PossibleCaptiveNetworkException}, or other exception type wrapped in a {@link
|
||||
* ExecutionException}.
|
||||
*/
|
||||
public CompletableFuture<UnauthenticatedChatConnection> connectUnauthChat(
|
||||
final Locale locale, ChatConnectionListener listener) {
|
||||
@ -344,8 +345,9 @@ public class Network {
|
||||
* the chat service, and incoming events will be provided via the provided {@link
|
||||
* ChatConnectionListener} argument.
|
||||
*
|
||||
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
|
||||
* other exception type wrapped in a {@link ExecutionException}.
|
||||
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException},
|
||||
* {@link PossibleCaptiveNetworkException}, or other exception type wrapped in a {@link
|
||||
* ExecutionException}.
|
||||
*/
|
||||
public CompletableFuture<AuthenticatedChatConnection> connectAuthChat(
|
||||
final String username,
|
||||
@ -375,6 +377,17 @@ public class Network {
|
||||
: new String[] {locale.getLanguage() + "-" + locale.getCountry()};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link ProvisioningConnection}.
|
||||
*
|
||||
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
|
||||
* other exception type wrapped in a {@link ExecutionException}.
|
||||
*/
|
||||
public CompletableFuture<ProvisioningConnection> connectProvisioning(
|
||||
ProvisioningConnectionListener listener) {
|
||||
return ProvisioningConnection.connect(tokioAsyncContext, connectionManager, listener);
|
||||
}
|
||||
|
||||
static class ConnectionManager extends NativeHandleGuard.SimpleOwner
|
||||
implements ConnectChatBridge {
|
||||
private final Environment environment;
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
/**
|
||||
* Indicates that a server presented a TLS certificate that might have come from a captive network.
|
||||
*/
|
||||
public class PossibleCaptiveNetworkException extends NetworkException {
|
||||
public PossibleCaptiveNetworkException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import kotlin.Pair;
|
||||
import org.signal.libsignal.internal.CompletableFuture;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
import org.signal.libsignal.net.internal.BridgeProvisioningListener;
|
||||
|
||||
/**
|
||||
* A chat connection used specifically for provisioning linked devices.
|
||||
*
|
||||
* <p>Note that no messages are sent *from* the client for a provisioning connection; all the
|
||||
* interesting functionality is in the events delivered to the {@link
|
||||
* ProvisioningConnectionListener}.
|
||||
*/
|
||||
public class ProvisioningConnection extends NativeHandleGuard.SimpleOwner {
|
||||
private final TokioAsyncContext tokioAsyncContext;
|
||||
private final ProvisioningConnectionListener listener;
|
||||
|
||||
protected ProvisioningConnection(
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
final long nativeHandle,
|
||||
final ProvisioningConnectionListener listener) {
|
||||
super(nativeHandle);
|
||||
this.tokioAsyncContext = tokioAsyncContext;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
static CompletableFuture<ProvisioningConnection> connect(
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
final Network.ConnectionManager connectionManager,
|
||||
final ProvisioningConnectionListener listener) {
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle ->
|
||||
connectionManager.guardedMap(
|
||||
connectionManagerHandle ->
|
||||
Native.ProvisioningChatConnection_connect(
|
||||
asyncContextHandle, connectionManagerHandle)
|
||||
.makeCancelable(tokioAsyncContext)
|
||||
.thenApply(
|
||||
nativeHandle ->
|
||||
new ProvisioningConnection(
|
||||
tokioAsyncContext, nativeHandle, listener))));
|
||||
}
|
||||
|
||||
protected static class ListenerBridge implements BridgeProvisioningListener {
|
||||
// Stored as a weak reference because otherwise we'll have a reference cycle:
|
||||
// - After setting a listener, Rust has a GC GlobalRef to this ListenerBridge
|
||||
// - This field is a normal Java reference to the ProvisioningConnection
|
||||
// - ProvisioningConnection owns the Rust ProvisioningConnection object
|
||||
protected WeakReference<ProvisioningConnection> connection;
|
||||
|
||||
protected ListenerBridge(ProvisioningConnection connection) {
|
||||
this.connection = new WeakReference<>(connection);
|
||||
}
|
||||
|
||||
public void receivedAddress(String address, long sendAckHandle) {
|
||||
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
|
||||
ProvisioningConnection connection = this.connection.get();
|
||||
if (connection == null) return;
|
||||
if (connection.listener == null) return;
|
||||
|
||||
connection.listener.onReceivedAddress(connection, address, ack);
|
||||
}
|
||||
|
||||
public void receivedEnvelope(byte[] envelope, long sendAckHandle) {
|
||||
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
|
||||
ProvisioningConnection connection = this.connection.get();
|
||||
if (connection == null) return;
|
||||
if (connection.listener == null) return;
|
||||
|
||||
connection.listener.onReceivedEnvelope(connection, envelope, ack);
|
||||
}
|
||||
|
||||
public void connectionInterrupted(Throwable disconnectReason) {
|
||||
ProvisioningConnection connection = this.connection.get();
|
||||
if (connection == null) return;
|
||||
if (connection.listener == null) return;
|
||||
|
||||
ChatServiceException disconnectReasonChatServiceException =
|
||||
(disconnectReason == null)
|
||||
? null
|
||||
: (disconnectReason instanceof ChatServiceException)
|
||||
? (ChatServiceException) disconnectReason
|
||||
: new ChatServiceException("OtherDisconnectReason", disconnectReason);
|
||||
connection.listener.onConnectionInterrupted(connection, disconnectReasonChatServiceException);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final class SetChatLaterListenerBridge extends ListenerBridge {
|
||||
SetChatLaterListenerBridge() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
void setChat(ProvisioningConnection connection) {
|
||||
this.connection = new WeakReference<>(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only method to create a {@code ProvisioningConnection} connected to a fake remote.
|
||||
*
|
||||
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
|
||||
*/
|
||||
public static Pair<ProvisioningConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext, ProvisioningConnectionListener listener) {
|
||||
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle -> {
|
||||
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
|
||||
long fakeChatConnection =
|
||||
NativeTesting.TESTING_FakeChatConnection_CreateProvisioning(
|
||||
asyncContextHandle, bridgeListener);
|
||||
ProvisioningConnection chat =
|
||||
new ProvisioningConnection(
|
||||
tokioAsyncContext,
|
||||
NativeTesting.TESTING_FakeChatConnection_TakeProvisioningChat(fakeChatConnection),
|
||||
listener);
|
||||
bridgeListener.setChat(chat);
|
||||
FakeChatRemote fakeRemote =
|
||||
new FakeChatRemote(
|
||||
tokioAsyncContext,
|
||||
NativeTesting.TESTING_FakeChatConnection_TakeRemote(fakeChatConnection));
|
||||
NativeTesting.FakeChatConnection_Destroy(fakeChatConnection);
|
||||
return new Pair<>(chat, fakeRemote);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a created, but not yet active, provisioning connection.
|
||||
*
|
||||
* <p>This must be called on a new {@code ProvisioningConnection} before it can start receiving
|
||||
* incoming messages from the server. It is an error to call this method more than once on a
|
||||
* {@code ProvisioningConnection}.
|
||||
*/
|
||||
public void start() {
|
||||
ListenerBridge bridgedListener = new ListenerBridge(this);
|
||||
this.guardedRun(
|
||||
nativeChatConnectionHandle ->
|
||||
Native.ProvisioningChatConnection_init_listener(
|
||||
nativeChatConnectionHandle, bridgedListener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates termination of the underlying connection to the Chat Service. After the service is
|
||||
* disconnected, it cannot be reconnected.
|
||||
*
|
||||
* @return a future that completes when the underlying connection is terminated.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public CompletableFuture<Void> disconnect() {
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle ->
|
||||
guardedMap(
|
||||
chatConnectionHandle ->
|
||||
Native.ProvisioningChatConnection_disconnect(
|
||||
asyncContextHandle, chatConnectionHandle)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeChatConnectionHandle) {
|
||||
Native.ProvisioningChatConnection_Destroy(nativeChatConnectionHandle);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
public interface ProvisioningConnectionListener {
|
||||
/**
|
||||
* Called at the start of the provisioning process.
|
||||
*
|
||||
* <p>{@param address} should be considered an opaque token to pass to the primary device (usually
|
||||
* via QR code).
|
||||
*
|
||||
* <p>{@param ack}'s {@code send} method can be called immediately to indicate successful delivery
|
||||
* of the address.
|
||||
*/
|
||||
void onReceivedAddress(
|
||||
ProvisioningConnection chat, String address, ChatConnectionListener.ServerMessageAck ack);
|
||||
|
||||
/**
|
||||
* Called once when the primary sends an "envelope" via the server (using the address from {@link
|
||||
* #onReceivedAddress(String, ChatConnectionListener.ServerMessageAck)}).
|
||||
*
|
||||
* <p>Once the server receives the {@param ack} for this message, it will close this connection.
|
||||
*/
|
||||
void onReceivedEnvelope(
|
||||
ProvisioningConnection chat, byte[] envelope, ChatConnectionListener.ServerMessageAck ack);
|
||||
|
||||
/**
|
||||
* Called when the client gets disconnected from the server.
|
||||
*
|
||||
* <p>This includes both deliberate disconnects as well as unexpected socket closures. In the case
|
||||
* of the former, the {@param disconnectReason} will be null.
|
||||
*
|
||||
* <p>The default implementation of this method does nothing.
|
||||
*/
|
||||
default void onConnectionInterrupted(
|
||||
ProvisioningConnection chat, ChatServiceException disconnectReason) {}
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CalledFromNative
|
||||
import java.time.Duration
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
@ -14,13 +15,37 @@ import java.util.EnumSet
|
||||
* <p>When the websocket transport is in use, this corresponds to a {@code HTTP 428} response to
|
||||
* requests to a number of endpoints.
|
||||
*/
|
||||
public class RateLimitChallengeException : ChatServiceException {
|
||||
public class RateLimitChallengeException :
|
||||
ChatServiceException,
|
||||
BadRequestError,
|
||||
SyncSendFailure,
|
||||
UnsealedSendFailure {
|
||||
public val token: String
|
||||
public val options: Set<ChallengeOption>
|
||||
public val retryLater: Duration?
|
||||
|
||||
@CalledFromNative
|
||||
public constructor(message: String, token: String, options: Array<ChallengeOption>) : super(message) {
|
||||
public constructor(
|
||||
message: String,
|
||||
token: String,
|
||||
options: Array<ChallengeOption>,
|
||||
retryLater: Duration?,
|
||||
) : super(message) {
|
||||
this.token = token
|
||||
this.options = EnumSet.copyOf(options.asList())
|
||||
this.retryLater = retryLater
|
||||
}
|
||||
|
||||
@CalledFromNative
|
||||
internal constructor(
|
||||
message: String,
|
||||
token: String,
|
||||
options: Array<ChallengeOption>,
|
||||
retryLater: Long,
|
||||
) : this(
|
||||
message,
|
||||
token,
|
||||
options,
|
||||
if (retryLater < 0) null else Duration.ofSeconds(retryLater),
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
* <li>{@link RegistrationSessionNotFoundException} if the server rejects the session ID,
|
||||
* <li>{@link ChatServiceException} if a request times out after being sent to the server,
|
||||
* <li>{@link RetryLaterException} if the server responds with an HTTP 429,
|
||||
* <li>{@link PossibleCaptiveNetworkException} if the server's TLS response suggests a captive
|
||||
* network.
|
||||
* <li>{@link RegistrationSessionIdInvalidException} if the session ID is invalid,
|
||||
* <li>{@link RegistrationException} for other unexpected error responses
|
||||
* </ul>
|
||||
|
||||
@ -54,6 +54,8 @@ public sealed interface RequestResult<out T, out E : BadRequestError> {
|
||||
*
|
||||
* Possible types for [networkError] include but are not limited to:
|
||||
* - [TimeoutException]: occurs when the request takes too long to complete.
|
||||
* - [ChatServiceInactiveException]: occurs when the request is made on a closed chat
|
||||
* connection.
|
||||
* - [ConnectedElsewhereException]: occurs when a client connects elsewhere with
|
||||
* same credentials before the request could complete
|
||||
* - [ConnectionInvalidatedException]: occurs when the connection to the server is
|
||||
@ -110,6 +112,7 @@ internal inline fun <reified E : BadRequestError> Throwable.toRequestResult(): R
|
||||
internal fun Throwable.toRequestResult(): RequestResult<Nothing, Nothing> =
|
||||
when (this) {
|
||||
is TimeoutException -> RequestResult.RetryableNetworkError(this, null)
|
||||
is ChatServiceInactiveException -> RequestResult.RetryableNetworkError(this)
|
||||
is ConnectedElsewhereException -> RequestResult.RetryableNetworkError(this)
|
||||
// ConnectionInvalidated is mapped to a network error. Only one legacy API uses its
|
||||
// specific meaning; all other APIs treat it as a generic network error.
|
||||
@ -126,9 +129,9 @@ internal fun Throwable.toRequestResult(): RequestResult<Nothing, Nothing> =
|
||||
*
|
||||
* This extension function handles the case where the Future itself fails
|
||||
* (as opposed to the request returning an error result). Any exceptions
|
||||
* thrown while waiting for the Future are converted to [ApplicationError].
|
||||
* thrown while waiting for the Future are converted to [ApplicationError][RequestResult.ApplicationError].
|
||||
*
|
||||
* @return The [RequestResult] from the Future, or [ApplicationError] if the
|
||||
* @return The [RequestResult] from the Future, or [ApplicationError][RequestResult.ApplicationError] if the
|
||||
* Future failed to complete normally
|
||||
*/
|
||||
public fun <T, E : BadRequestError> CompletableFuture<RequestResult<T, E>>.getOrError(): RequestResult<T, E> =
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CalledFromNative
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
@ -15,6 +16,11 @@ import java.io.IOException
|
||||
*/
|
||||
public class RequestUnauthorizedException :
|
||||
IOException,
|
||||
MultiRecipientSendFailure {
|
||||
public constructor(message: String) : super(message) {}
|
||||
GetPreKeysError,
|
||||
GetUploadFormError,
|
||||
MultiRecipientSendFailure,
|
||||
SealedSendFailure {
|
||||
@CalledFromNative
|
||||
public constructor(message: String) : super(message) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CalledFromNative
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A request relating to a [org.signal.libsignal.protocol.ServiceId] could not be completed as the
|
||||
* ServiceId, or its devices, could not be found.
|
||||
*
|
||||
* See the specific request docs for more information.
|
||||
*/
|
||||
public class ServiceIdNotFoundException :
|
||||
IOException,
|
||||
GetPreKeysError,
|
||||
SealedSendFailure,
|
||||
UnsealedSendFailure {
|
||||
@CalledFromNative
|
||||
public constructor(message: String) : super(message) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.protocol.message.CiphertextMessage
|
||||
|
||||
/**
|
||||
* A message to send to a single device of a peer.
|
||||
*
|
||||
* Used by APIs like [UnauthMessagesService.sendMessage].
|
||||
*/
|
||||
public data class SingleOutboundMessage<T>(
|
||||
public val deviceId: Int,
|
||||
public val registrationId: Int,
|
||||
public val message: T,
|
||||
)
|
||||
|
||||
public typealias SingleOutboundSealedSenderMessage = SingleOutboundMessage<ByteArray>
|
||||
public typealias SingleOutboundUnsealedMessage = SingleOutboundMessage<CiphertextMessage>
|
||||
@ -73,7 +73,7 @@ import org.signal.libsignal.messagebackup.BackupKey
|
||||
* ```
|
||||
*
|
||||
* @see [BackupKey]
|
||||
* @see [MessageBackupKey](org.signal.libsignal.messagebackup.MessageBackupKey)
|
||||
* @see [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey]
|
||||
* @see [BackupForwardSecrecyToken]
|
||||
*/
|
||||
public class SvrB internal constructor(
|
||||
@ -183,9 +183,9 @@ public class SvrB internal constructor(
|
||||
* - [Result.failure] containing
|
||||
* [InvalidSvrBDataException](org.signal.libsignal.svr.InvalidSvrBDataException) if the backup
|
||||
* metadata is malformed. In this case the user's data is **not recoverable**.
|
||||
* - [Result.failure] containing [RestoreFailedException] if restoration fails (with remaining
|
||||
* - [Result.failure] containing [RestoreFailedException](org.signal.libsignal.svr.RestoreFailedException) if restoration fails (with remaining
|
||||
* tries count). This should never happen but if it does the user's data is **not recoverable**.
|
||||
* - [Result.failure] containing [DataMissingException] if the backup data is not found on the
|
||||
* - [Result.failure] containing [DataMissingException](org.signal.libsignal.svr.DataMissingException) if the backup data is not found on the
|
||||
* server, indicating an **incorrect backup key** (which may in turn imply the user's data is
|
||||
* not recoverable).
|
||||
* - [Result.failure] containing [RetryLaterException] if the server is rate limiting this client.
|
||||
@ -308,7 +308,7 @@ private class BackupRestoreResponse internal constructor(
|
||||
*/
|
||||
public data class SvrBStoreResponse(
|
||||
/**
|
||||
* The forward secrecy token used to derive [MessageBackupKey] instances.
|
||||
* The forward secrecy token used to derive [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey] instances.
|
||||
*
|
||||
* This token provides forward secrecy guarantees by ensuring that compromise of the backup key
|
||||
* alone is insufficient to decrypt backups. Each backup is protected by a value stored on
|
||||
@ -344,7 +344,7 @@ public data class SvrBStoreResponse(
|
||||
*/
|
||||
public data class SvrBRestoreResponse(
|
||||
/**
|
||||
* The forward secrecy token used to derive [MessageBackupKey] instances.
|
||||
* The forward secrecy token used to derive [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey] instances.
|
||||
*
|
||||
* This token provides forward secrecy guarantees by ensuring that compromise of the backup key
|
||||
* alone is insufficient to decrypt backups. Each backup is protected by a value stored on
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.libsignal.zkgroup.GenericServerPublicParams
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredential
|
||||
|
||||
/**
|
||||
* Either a [RequestUnauthorizedException] or [UploadTooLargeException]
|
||||
*/
|
||||
public sealed interface GetUploadFormError : BadRequestError
|
||||
|
||||
public data class BackupAuth(
|
||||
val credential: BackupAuthCredential,
|
||||
val serverKeys: GenericServerPublicParams,
|
||||
val signingKey: ECPrivateKey,
|
||||
)
|
||||
|
||||
public data class DeterministicRandomSeedUseOnlyForTesting(
|
||||
val seed: Long,
|
||||
)
|
||||
|
||||
public class UnauthBackupsService(
|
||||
private val connection: UnauthenticatedChatConnection,
|
||||
) {
|
||||
/**
|
||||
* Get a messages backup attachment upload form
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
|
||||
* too large. A [RequestUnauthorizedException] means that the authorization failed.
|
||||
*/
|
||||
public fun getUploadForm(
|
||||
auth: BackupAuth,
|
||||
uploadSize: Long,
|
||||
rngSeedForTesting: DeterministicRandomSeedUseOnlyForTesting? = null,
|
||||
): CompletableFuture<RequestResult<UploadForm, GetUploadFormError>> =
|
||||
try {
|
||||
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
auth.signingKey.guardedMap { signingKey ->
|
||||
Native
|
||||
.UnauthenticatedChatConnection_backup_get_upload_form(
|
||||
asyncCtx,
|
||||
conn,
|
||||
auth.credential.internalContentsForJNI,
|
||||
auth.serverKeys.internalContentsForJNI,
|
||||
signingKey,
|
||||
uploadSize,
|
||||
rngSeedForTesting?.seed ?: -1,
|
||||
).mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(it as UploadForm) },
|
||||
onError = { err -> err.toRequestResult<GetUploadFormError>() },
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment backup attachment upload form
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
|
||||
* too large. A [RequestUnauthorizedException] means that the authorization failed.
|
||||
*/
|
||||
public fun getMediaUploadForm(
|
||||
auth: BackupAuth,
|
||||
uploadSize: Long,
|
||||
rngSeedForTesting: DeterministicRandomSeedUseOnlyForTesting? = null,
|
||||
): CompletableFuture<RequestResult<UploadForm, GetUploadFormError>> =
|
||||
try {
|
||||
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
auth.signingKey.guardedMap { signingKey ->
|
||||
Native
|
||||
.UnauthenticatedChatConnection_backup_get_media_upload_form(
|
||||
asyncCtx,
|
||||
conn,
|
||||
auth.credential.internalContentsForJNI,
|
||||
auth.serverKeys.internalContentsForJNI,
|
||||
signingKey,
|
||||
uploadSize,
|
||||
rngSeedForTesting?.seed ?: -1,
|
||||
).mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(it as UploadForm) },
|
||||
onError = { err -> err.toRequestResult<GetUploadFormError>() },
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle
|
||||
|
||||
public sealed class DeviceSpecifier {
|
||||
public object AllDevices : DeviceSpecifier()
|
||||
|
||||
public data class SpecificDevice(
|
||||
val deviceId: Int,
|
||||
) : DeviceSpecifier()
|
||||
}
|
||||
|
||||
public sealed interface GetPreKeysError : BadRequestError
|
||||
|
||||
public class UnauthKeysService(
|
||||
private val connection: UnauthenticatedChatConnection,
|
||||
) {
|
||||
/**
|
||||
* Fetch the prekeys for a given target user
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [RequestUnauthorizedException] means `auth` is not valid
|
||||
* for the target. A [ServiceIdNotFoundException] means that the requested identity or device does
|
||||
* not exist or device has no available prekeys.
|
||||
*/
|
||||
public fun getPreKeys(
|
||||
target: ServiceId,
|
||||
device: DeviceSpecifier,
|
||||
auth: UserBasedAuthorization,
|
||||
): CompletableFuture<RequestResult<Pair<IdentityKey, List<PreKeyBundle>>, GetPreKeysError>> {
|
||||
val device =
|
||||
when (device) {
|
||||
is DeviceSpecifier.SpecificDevice -> {
|
||||
require(device.deviceId >= 0)
|
||||
device.deviceId
|
||||
}
|
||||
|
||||
is DeviceSpecifier.AllDevices -> -1
|
||||
}
|
||||
return try {
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
// Suppress the warnings about java.lang.Object being inferred as the type
|
||||
// parameter for mapWithCancellation
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
when (auth) {
|
||||
is UserBasedAuthorization.AccessKey -> {
|
||||
Native.UnauthenticatedChatConnection_get_pre_keys_access_key_auth(
|
||||
asyncCtx,
|
||||
conn,
|
||||
auth.bytes,
|
||||
target.toServiceIdFixedWidthBinary(),
|
||||
device,
|
||||
)
|
||||
}
|
||||
|
||||
is UserBasedAuthorization.GroupSend -> {
|
||||
Native.UnauthenticatedChatConnection_get_pre_keys_group_auth(
|
||||
asyncCtx,
|
||||
conn,
|
||||
auth.token.serialize(),
|
||||
target.toServiceIdFixedWidthBinary(),
|
||||
device,
|
||||
)
|
||||
}
|
||||
|
||||
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> {
|
||||
Native.UnauthenticatedChatConnection_get_pre_keys_unrestricted_auth(
|
||||
asyncCtx,
|
||||
conn,
|
||||
target.toServiceIdFixedWidthBinary(),
|
||||
device,
|
||||
)
|
||||
}
|
||||
}.mapWithCancellation(
|
||||
onSuccess = { out: Any ->
|
||||
val (publicKey, preKeyBundles) = out as Pair<*, *>
|
||||
@Suppress("UNCHECKED_CAST") // The cast _is_ checked because Arrays don't use type erasure
|
||||
RequestResult.Success(
|
||||
Pair(
|
||||
IdentityKey(publicKey as ECPublicKey),
|
||||
(preKeyBundles as Array<PreKeyBundle>).toList(),
|
||||
),
|
||||
)
|
||||
},
|
||||
onError = { err -> err.toRequestResult<GetPreKeysError>() },
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,61 @@ public class UnauthMessagesService(
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 1:1 message encrypted with Sealed Sender.
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError]. A [RequestUnauthorizedException] means `auth` is not valid
|
||||
* for `destination`; this cannot happen when `auth` is
|
||||
* [UserBasedSendAuthorization.Story]. A [MismatchedDeviceException] indicates the recipient
|
||||
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
|
||||
* result; the message has not been sent to anybody.) A [ServiceIdNotFoundException] indicates the
|
||||
* destination account has been unregistered.
|
||||
*
|
||||
* @see [SealedSessionCipher.encrypt]
|
||||
*/
|
||||
public fun sendMessage(
|
||||
destination: ServiceId,
|
||||
timestamp: Long,
|
||||
contents: List<SingleOutboundSealedSenderMessage>,
|
||||
auth: UserBasedSendAuthorization,
|
||||
onlineOnly: Boolean,
|
||||
urgent: Boolean,
|
||||
): CompletableFuture<RequestResult<Unit, SealedSendFailure>> =
|
||||
try {
|
||||
val deviceIds = IntArray(contents.size)
|
||||
val registrationIds = IntArray(contents.size)
|
||||
val messages = arrayOfNulls<ByteArray>(contents.size)
|
||||
|
||||
contents.forEachIndexed { i, next ->
|
||||
deviceIds[i] = next.deviceId
|
||||
registrationIds[i] = next.registrationId
|
||||
messages[i] = next.message
|
||||
}
|
||||
|
||||
connection
|
||||
.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native.UnauthenticatedChatConnection_send_message(
|
||||
asyncCtx,
|
||||
conn,
|
||||
destination.toServiceIdFixedWidthBinary(),
|
||||
timestamp,
|
||||
deviceIds,
|
||||
registrationIds,
|
||||
messages.requireNoNulls(),
|
||||
auth.rawKind(),
|
||||
auth.payloadBytesOrNull(),
|
||||
onlineOnly,
|
||||
urgent,
|
||||
)
|
||||
}.mapWithCancellation(
|
||||
onSuccess = { _ -> RequestResult.Success(Unit) },
|
||||
onError = { err -> err.toRequestResult<SealedSendFailure>() },
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
|
||||
public sealed interface MultiRecipientSendAuthorization {
|
||||
@ -93,3 +148,28 @@ public class MultiRecipientMessageResponse(
|
||||
rawUnregisteredIds: Array<ByteArray>,
|
||||
) : this(rawUnregisteredIds.map(ServiceId::parseFromFixedWidthBinary)) {}
|
||||
}
|
||||
|
||||
/** Either [UserBasedAuthorization], or `UserBasedSendAuthorization.Story`. */
|
||||
public sealed interface UserBasedSendAuthorization {
|
||||
public object Story : UserBasedSendAuthorization
|
||||
}
|
||||
|
||||
// Must be kept in sync with `UserBasedSendAuthorizationKind` in Rust.
|
||||
private fun UserBasedSendAuthorization.rawKind(): Int =
|
||||
when (this) {
|
||||
is UserBasedSendAuthorization.Story -> 0
|
||||
is UserBasedAuthorization.AccessKey -> 1
|
||||
is UserBasedAuthorization.GroupSend -> 2
|
||||
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> 3
|
||||
}
|
||||
|
||||
private fun UserBasedSendAuthorization.payloadBytesOrNull(): ByteArray? =
|
||||
when (this) {
|
||||
is UserBasedSendAuthorization.Story -> null
|
||||
is UserBasedAuthorization.AccessKey -> bytes
|
||||
is UserBasedAuthorization.GroupSend -> token.serialize()
|
||||
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> null
|
||||
}
|
||||
|
||||
/** Either [ServiceIdNotFoundException], [RequestUnauthorizedException] or [MismatchedDeviceException]. */
|
||||
public sealed interface SealedSendFailure : BadRequestError
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public class UnauthProfilesService(
|
||||
private val connection: UnauthenticatedChatConnection,
|
||||
) {
|
||||
/**
|
||||
* Does an account with the given ACI or PNI exist?
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError].
|
||||
*/
|
||||
public fun accountExists(account: ServiceId): CompletableFuture<RequestResult<Boolean, Nothing>> =
|
||||
try {
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native
|
||||
.UnauthenticatedChatConnection_account_exists(
|
||||
asyncCtx,
|
||||
conn,
|
||||
account.toServiceIdFixedWidthBinary(),
|
||||
).mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(it) },
|
||||
onError = { err -> err.toRequestResult() },
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ public class UnauthUsernamesService(
|
||||
if (pair == null) {
|
||||
RequestResult.Success(null)
|
||||
} else {
|
||||
RequestResult.Success(Username._withPrecomputedHash(pair.first(), pair.second()))
|
||||
RequestResult.Success(Username._withPrecomputedHash(pair.first, pair.second))
|
||||
}
|
||||
},
|
||||
onError = { err -> err.toRequestResult<LookUpUsernameLinkFailure>() },
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
package org.signal.libsignal.net;
|
||||
|
||||
import java.util.Locale;
|
||||
import kotlin.Pair;
|
||||
import org.signal.libsignal.internal.CompletableFuture;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.internal.TokioAsyncContext;
|
||||
import org.signal.libsignal.net.internal.BridgeChatListener;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
|
||||
/**
|
||||
* Represents an unauthenticated (i.e. hopefully anonymous) communication channel with the Chat
|
||||
@ -33,7 +33,7 @@ public class UnauthenticatedChatConnection extends ChatConnection {
|
||||
this.keyTransparencyClient = new KeyTransparencyClient(this, tokioAsyncContext, ktEnvironment);
|
||||
}
|
||||
|
||||
private KeyTransparencyClient keyTransparencyClient;
|
||||
private final KeyTransparencyClient keyTransparencyClient;
|
||||
|
||||
static CompletableFuture<UnauthenticatedChatConnection> connect(
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
@ -77,13 +77,26 @@ public class UnauthenticatedChatConnection extends ChatConnection {
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
Network.Environment ktEnvironment) {
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0], ktEnvironment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only method to create a {@code UnauthenticatedChatConnection} connected to a fake remote.
|
||||
*
|
||||
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
|
||||
*/
|
||||
public static Pair<UnauthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
String[] grpcOverrides,
|
||||
Network.Environment ktEnvironment) {
|
||||
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle -> {
|
||||
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
|
||||
long fakeChatConnection =
|
||||
NativeTesting.TESTING_FakeChatConnection_Create(
|
||||
asyncContextHandle, bridgeListener, "");
|
||||
asyncContextHandle, bridgeListener, String.join("\n", grpcOverrides), "");
|
||||
UnauthenticatedChatConnection chat =
|
||||
new UnauthenticatedChatConnection(
|
||||
tokioAsyncContext,
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CalledFromNative
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
public data class UploadForm(
|
||||
val cdn: Int,
|
||||
val key: String,
|
||||
val headers: Map<String, String>,
|
||||
val signedUploadUrl: URI,
|
||||
) {
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@CalledFromNative
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun fromNative(
|
||||
cdn: Int,
|
||||
key: String,
|
||||
headers: Array<*>,
|
||||
signedUploadUrl: String,
|
||||
): UploadForm =
|
||||
UploadForm(
|
||||
cdn = cdn,
|
||||
key = key,
|
||||
headers = (headers as Array<Pair<String, String>>).asList().toMap(),
|
||||
signedUploadUrl =
|
||||
try {
|
||||
URI(signedUploadUrl)
|
||||
} catch (_: URISyntaxException) {
|
||||
throw UnexpectedResponseException("Invalid URL for UploadForm's signedUploadUrl")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CalledFromNative
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* The request size was larger than the maximum supported upload size
|
||||
*
|
||||
* See the specific request docs for more information.
|
||||
*/
|
||||
public class UploadTooLargeException :
|
||||
IOException,
|
||||
BadRequestError,
|
||||
GetUploadFormError {
|
||||
@CalledFromNative
|
||||
public constructor(message: String) : super(message) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
|
||||
|
||||
public sealed class UserBasedAuthorization : UserBasedSendAuthorization {
|
||||
public data class AccessKey(
|
||||
val bytes: ByteArray,
|
||||
) : UserBasedAuthorization() {
|
||||
// Because the default equals+hashCode compare based on identity, not value
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as AccessKey
|
||||
|
||||
if (!bytes.contentEquals(other.bytes)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = bytes.contentHashCode()
|
||||
}
|
||||
|
||||
public data class GroupSend(
|
||||
val token: GroupSendFullToken,
|
||||
) : UserBasedAuthorization()
|
||||
|
||||
public object UnrestrictedUnauthenticatedAccess : UserBasedAuthorization()
|
||||
}
|
||||
@ -138,6 +138,45 @@ class AsyncTests {
|
||||
}.await()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapWithCancellationOnSuccessException() =
|
||||
runTest(timeout = 5.seconds) {
|
||||
val baseFuture = CompletableFuture<String>()
|
||||
val exception = RuntimeException("onSuccess error")
|
||||
val mappedFuture =
|
||||
baseFuture.mapWithCancellation(
|
||||
onSuccess = { throw exception },
|
||||
onError = { "error" },
|
||||
)
|
||||
|
||||
baseFuture.complete("value")
|
||||
|
||||
// If mapWithCancellation doesn't handle exceptions from onSuccess,
|
||||
// the outer future will never complete and this will time out.
|
||||
val thrown = assertFailsWith<RuntimeException> { mappedFuture.await() }
|
||||
assertEquals("onSuccess error", thrown.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapWithCancellationOnErrorException() =
|
||||
runTest(timeout = 5.seconds) {
|
||||
val baseFuture = CompletableFuture<String>()
|
||||
val exception = RuntimeException("onError error")
|
||||
val mappedFuture =
|
||||
baseFuture.mapWithCancellation(
|
||||
onSuccess = { "success" },
|
||||
onError = { throw exception },
|
||||
)
|
||||
|
||||
baseFuture.completeExceptionally(IllegalStateException("original"))
|
||||
|
||||
// Similar to above, if mapWithCancellation doesn't handle exceptions
|
||||
// from onError, the outer future will never complete and this will
|
||||
// time out.
|
||||
val thrown = assertFailsWith<RuntimeException> { mappedFuture.await() }
|
||||
assertEquals("onError error", thrown.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToResultFutureSuccessPath() =
|
||||
runTest {
|
||||
|
||||
@ -11,16 +11,16 @@ import org.signal.libsignal.protocol.ServiceId;
|
||||
public class TestStore implements Store {
|
||||
|
||||
public HashMap<ServiceId.Aci, Deque<byte[]>> storage = new HashMap<>();
|
||||
public byte[] lastDistinguishedTreeHead;
|
||||
public Deque<byte[]> distinguishedTreeHeads = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getLastDistinguishedTreeHead() {
|
||||
return Optional.ofNullable(lastDistinguishedTreeHead);
|
||||
return Optional.ofNullable(this.distinguishedTreeHeads.peekLast());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastDistinguishedTreeHead(byte[] lastDistinguishedTreeHead) {
|
||||
this.lastDistinguishedTreeHead = lastDistinguishedTreeHead;
|
||||
this.distinguishedTreeHeads.push(lastDistinguishedTreeHead);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -0,0 +1,206 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.messagebackup
|
||||
|
||||
import org.json.simple.JSONObject
|
||||
import org.json.simple.parser.JSONParser
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.chunkLengthDelimited
|
||||
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.insertLengthPrefix
|
||||
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.stripLengthPrefix
|
||||
import org.signal.libsignal.util.ResourceReader
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
class BackupJsonExporterTest {
|
||||
companion object {
|
||||
private fun concatFrames(frames: List<ByteArray>): ByteArray =
|
||||
frames.fold(ByteArrayOutputStream()) { out, frame -> out.also { it.write(frame) } }.toByteArray()
|
||||
|
||||
private val canonicalBackup: ByteArray by lazy {
|
||||
ResourceReader.readAll(
|
||||
BackupJsonExporterTest::class.java.getResourceAsStream("canonical-backup.binproto"),
|
||||
)
|
||||
}
|
||||
|
||||
// The canonical backup has 6 chunks: 1 BackupInfo + 5 frames.
|
||||
private val allChunks by lazy { chunkLengthDelimited(canonicalBackup) }
|
||||
private val backupInfo by lazy { stripLengthPrefix(allChunks.first()) }
|
||||
private val frameChunks by lazy { allChunks.drop(1) }
|
||||
|
||||
// Disappearing chat item frame. Regenerate with:
|
||||
// % protoc rust/message-backup/src/proto/backup.proto \
|
||||
// --encode signal.backup.Frame <<'PROTO' | base64
|
||||
// chatItem: { chatId: 1 authorId: 2 dateSent: 3 expiresInMs: 1 }
|
||||
// PROTO
|
||||
private val disappearingChatItemFrame: ByteArray =
|
||||
Base64.decode("IggIARACGAMoAQ==")
|
||||
|
||||
// View-once chat item frame with revisions. Regenerate with:
|
||||
// % protoc rust/message-backup/src/proto/backup.proto \
|
||||
// --encode signal.backup.Frame <<'PROTO' | base64
|
||||
// chatItem: {
|
||||
// chatId: 10 authorId: 11 dateSent: 12
|
||||
// viewOnceMessage: { attachment: { wasDownloaded: true } }
|
||||
// revisions: [{ chatId: 10 authorId: 11 dateSent: 9
|
||||
// viewOnceMessage: { attachment: { wasDownloaded: true } } }]
|
||||
// }
|
||||
// PROTO
|
||||
private val viewOnceChatItemFrame: ByteArray =
|
||||
Base64.decode("IhwIChALGAwyDQgKEAsYCZIBBAoCGAGSAQQKAhgB")
|
||||
}
|
||||
|
||||
// These tests verify basic streaming behavior and the Kotlin API surface.
|
||||
// More thorough JSON output validation is done in the Node.js tests.
|
||||
|
||||
@Test
|
||||
fun streamsJsonLinesForCanonicalBackup() {
|
||||
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo)
|
||||
exporter.use {
|
||||
val chunkGroups = listOf(frameChunks.take(2), frameChunks.drop(2))
|
||||
val exportedLines =
|
||||
chunkGroups.flatMap { exporter.exportFrames(concatFrames(it)) }.map {
|
||||
assertNotNull("canonical backup should produce a line", it.line)
|
||||
assertNull("canonical backup should validate cleanly", it.errorMessage)
|
||||
it.line!!
|
||||
}
|
||||
assertNull("canonical backup should validate cleanly", exporter.finishExport())
|
||||
|
||||
val allLines = listOf(initialChunk) + exportedLines
|
||||
assertEquals(frameChunks.size + 1, allLines.size)
|
||||
for (line in allLines) {
|
||||
assertFalse("each line should be a single line", line.contains('\n'))
|
||||
assertTrue("each line should be JSON", line.startsWith("{"))
|
||||
}
|
||||
|
||||
assertTrue(allLines[0].contains("\"version\""))
|
||||
assertTrue(allLines[1].contains("\"account\""))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsEmptyListWhenNoFramesProvided() {
|
||||
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo, validate = false)
|
||||
exporter.use {
|
||||
assertTrue(initialChunk.contains("\"version\""))
|
||||
assertEquals(emptyList<FrameExportResult>(), exporter.exportFrames(ByteArray(0)))
|
||||
assertNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filtersDisappearingMessages() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
|
||||
exporter.use {
|
||||
val results = exporter.exportFrames(insertLengthPrefix(disappearingChatItemFrame))
|
||||
assertEquals(1, results.size)
|
||||
assertNull(results[0].line)
|
||||
assertNull(results[0].errorMessage)
|
||||
assertNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filteredFramesHaveNoValidationErrorWhenValid() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = true)
|
||||
exporter.use {
|
||||
val results = exporter.exportFrames(insertLengthPrefix(disappearingChatItemFrame))
|
||||
assertEquals(1, results.size)
|
||||
assertNull(results[0].line)
|
||||
assertNull(results[0].errorMessage)
|
||||
// Finish should report an error because we never sent an AccountData frame.
|
||||
assertNotNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stripsAttachmentsFromViewOnceMessages() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
|
||||
exporter.use {
|
||||
val results = exporter.exportFrames(insertLengthPrefix(viewOnceChatItemFrame))
|
||||
assertEquals(1, results.size)
|
||||
assertNotNull(results[0].line)
|
||||
assertNull(results[0].errorMessage)
|
||||
|
||||
val json = JSONParser().parse(results[0].line!!) as JSONObject
|
||||
val expected =
|
||||
JSONParser().parse(
|
||||
"""
|
||||
{
|
||||
"chatItem": {
|
||||
"chatId": "10",
|
||||
"authorId": "11",
|
||||
"dateSent": "12",
|
||||
"viewOnceMessage": {},
|
||||
"revisions": [
|
||||
{
|
||||
"chatId": "10",
|
||||
"authorId": "11",
|
||||
"dateSent": "9",
|
||||
"viewOnceMessage": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
) as JSONObject
|
||||
assertEquals(expected, json)
|
||||
assertNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validationPassesWithNoErrorsPresent() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = true)
|
||||
exporter.use {
|
||||
for (group in listOf(frameChunks.take(1), frameChunks.drop(1))) {
|
||||
for (result in exporter.exportFrames(concatFrames(group))) {
|
||||
assertNotNull(result.line)
|
||||
assertNull(result.errorMessage)
|
||||
}
|
||||
}
|
||||
assertNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun finishReportsErrorWhenValidationFails() {
|
||||
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo, validate = true)
|
||||
exporter.use {
|
||||
assertTrue(initialChunk.startsWith("{"))
|
||||
// Skip the first frame (AccountData) to trigger a validation failure.
|
||||
exporter.exportFrames(concatFrames(frameChunks.drop(1)))
|
||||
val finishError = exporter.finishExport()
|
||||
assertNotNull(finishError)
|
||||
assertTrue(finishError!!.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canSkipValidation() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
|
||||
exporter.use {
|
||||
val results = exporter.exportFrames(concatFrames(frameChunks.drop(1)))
|
||||
for (result in results) {
|
||||
assertNull(result.errorMessage)
|
||||
}
|
||||
assertNull(exporter.finishExport())
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ValidationError::class)
|
||||
fun rejectsMalformedDataEvenWithoutValidation() {
|
||||
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
|
||||
exporter.use {
|
||||
exporter.exportFrames(byteArrayOf(0x02, 0x01))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,12 +19,12 @@ import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import kotlin.io.encoding.Base64;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
import org.signal.libsignal.util.Base64;
|
||||
import org.signal.libsignal.util.ResourceReader;
|
||||
|
||||
public class MessageBackupValidationTest {
|
||||
@ -95,23 +95,13 @@ public class MessageBackupValidationTest {
|
||||
public void onlineValidation() throws IOException, ValidationError {
|
||||
final InputStream input = ComparableBackupTest.getCanonicalBackupInputStream();
|
||||
|
||||
final int backupInfoLength = input.read();
|
||||
assertFalse("unexpected EOF", backupInfoLength == -1);
|
||||
assertTrue("single-byte varint", backupInfoLength < 0x80);
|
||||
final int backupInfoLength = VarintDelimitedTestUtil.readVarint(input);
|
||||
final byte[] backupInfo = new byte[backupInfoLength];
|
||||
assertEquals("unexpected EOF", backupInfoLength, input.read(backupInfo));
|
||||
final OnlineBackupValidator backup = new OnlineBackupValidator(backupInfo, BACKUP_PURPOSE);
|
||||
|
||||
int frameLength;
|
||||
while ((frameLength = input.read()) != -1) {
|
||||
// Tiny varint parser, only supports two bytes.
|
||||
if (frameLength >= 0x80) {
|
||||
final int secondByte = input.read();
|
||||
assertFalse("unexpected EOF", secondByte == -1);
|
||||
assertTrue("at most a two-byte varint", secondByte < 0x80);
|
||||
frameLength -= 0x80;
|
||||
frameLength |= secondByte << 7;
|
||||
}
|
||||
while ((frameLength = VarintDelimitedTestUtil.readVarint(input)) != -1) {
|
||||
final byte[] frame = new byte[frameLength];
|
||||
assertEquals("unexpected EOF", frameLength, input.read(frame));
|
||||
backup.addFrame(frame);
|
||||
@ -126,6 +116,10 @@ public class MessageBackupValidationTest {
|
||||
ValidationError.class, () -> new OnlineBackupValidator(new byte[0], BACKUP_PURPOSE));
|
||||
}
|
||||
|
||||
private static byte[] decodeBase64(String input) {
|
||||
return Base64.Default.decode(input, 0, input.length());
|
||||
}
|
||||
|
||||
// The following payload was generated via protoscope.
|
||||
// % protoscope -s | base64
|
||||
// The fields are described by Backup.proto.
|
||||
@ -134,7 +128,7 @@ public class MessageBackupValidationTest {
|
||||
// 2: 1731715200000
|
||||
// 3: {`00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`}
|
||||
private static byte[] VALID_BACKUP_INFO =
|
||||
Base64.decode("CAEQgOiTkrMyGiAAESIzRFVmd4iZqrvM3e7/ABEiM0RVZneImaq7zN3u/w==");
|
||||
decodeBase64("CAEQgOiTkrMyGiAAESIzRFVmd4iZqrvM3e7/ABEiM0RVZneImaq7zN3u/w==");
|
||||
|
||||
@Test
|
||||
public void onlineValidatorRejectsInvalidFrame() throws ValidationError {
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.messagebackup
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/** Helpers for working with varint-length-delimited protobuf streams in tests. */
|
||||
object VarintDelimitedTestUtil {
|
||||
/**
|
||||
* Reads a varint from [input], or returns -1 on EOF. Only supports up to two-byte varints.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun readVarint(input: InputStream): Int {
|
||||
val first = input.read()
|
||||
if (first == -1) return -1
|
||||
if (first < 0x80) return first
|
||||
val second = input.read()
|
||||
assertFalse("unexpected EOF in middle of varint", second == -1)
|
||||
assertTrue("at most a two-byte varint", second < 0x80)
|
||||
return (first - 0x80) + (second shl 7)
|
||||
}
|
||||
|
||||
// Tiny varint parser, only supports two bytes.
|
||||
@JvmStatic
|
||||
fun readVarint(buf: ByteBuffer): Int {
|
||||
val first = buf.get().toInt() and 0xFF
|
||||
if (first < 0x80) return first
|
||||
val second = buf.get().toInt() and 0xFF
|
||||
assertTrue("at most a two-byte varint", second < 0x80)
|
||||
return (first - 0x80) + (second shl 7)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun chunkLengthDelimited(data: ByteArray): List<ByteArray> {
|
||||
val buf = ByteBuffer.wrap(data)
|
||||
val chunks = mutableListOf<ByteArray>()
|
||||
while (buf.hasRemaining()) {
|
||||
val start = buf.position()
|
||||
val length = readVarint(buf)
|
||||
val end = buf.position() + length
|
||||
buf.position(end)
|
||||
chunks.add(data.copyOfRange(start, end))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun stripLengthPrefix(chunk: ByteArray): ByteArray {
|
||||
val buf = ByteBuffer.wrap(chunk)
|
||||
val length = readVarint(buf)
|
||||
return chunk.copyOfRange(buf.position(), buf.position() + length)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun insertLengthPrefix(data: ByteArray): ByteArray {
|
||||
assertTrue("test frame too large for single-byte varint", data.size < 0x80)
|
||||
return byteArrayOf(data.size.toByte()) + data
|
||||
}
|
||||
}
|
||||
@ -72,6 +72,7 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
throws UntrustedIdentityException,
|
||||
InvalidKeyException,
|
||||
InvalidCertificateException,
|
||||
NoSessionException,
|
||||
InvalidMetadataMessageException,
|
||||
ProtocolDuplicateMessageException,
|
||||
ProtocolUntrustedIdentityException,
|
||||
@ -86,8 +87,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
@ -141,8 +144,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
ECKeyPair falseTrustRoot = ECKeyPair.generate();
|
||||
@ -178,8 +183,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
@ -214,8 +221,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
ECKeyPair randomKeyPair = ECKeyPair.generate();
|
||||
@ -269,8 +278,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
@ -352,6 +363,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
@ -371,7 +384,7 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilder.process(bobBundle);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
@ -441,6 +454,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress carolAddress =
|
||||
new SignalProtocolAddress("38381c3b-2606-4ca7-9310-7cb927f2ab4a", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
@ -460,7 +475,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilderForBob = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilderForBob =
|
||||
new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilderForBob.process(bobBundle);
|
||||
|
||||
ECKeyPair carolPreKey = ECKeyPair.generate();
|
||||
@ -481,7 +497,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
carolKyberPreKey.getKeyPair().getPublicKey(),
|
||||
carolKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilderForCarol = new SessionBuilder(aliceStore, carolAddress);
|
||||
SessionBuilder aliceSessionBuilderForCarol =
|
||||
new SessionBuilder(aliceStore, carolAddress, aliceAddress);
|
||||
aliceSessionBuilderForCarol.process(carolBundle);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
@ -552,6 +569,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress carolAddress =
|
||||
new SignalProtocolAddress("38381c3b-2606-4ca7-9310-7cb927f2ab4a", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
@ -571,7 +590,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilderForBob = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilderForBob =
|
||||
new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilderForBob.process(bobBundle);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
@ -644,6 +664,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore carolStore = new TestInMemorySignalProtocolStore();
|
||||
final SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("7c32784d-37b5-4cb2-969d-b16e02d52fd7", 1);
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress carolAddress =
|
||||
@ -667,7 +689,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilderForBob = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilderForBob =
|
||||
new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilderForBob.process(bobBundle);
|
||||
|
||||
ECKeyPair carolPreKey = ECKeyPair.generate();
|
||||
@ -688,7 +711,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
carolKyberPreKey.getKeyPair().getPublicKey(),
|
||||
carolKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilderForCarol = new SessionBuilder(aliceStore, carolAddress);
|
||||
SessionBuilder aliceSessionBuilderForCarol =
|
||||
new SessionBuilder(aliceStore, carolAddress, aliceAddress);
|
||||
aliceSessionBuilderForCarol.process(carolBundle);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
@ -772,8 +796,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("469e88ac-af84-4908-bf16-8323cc1d579e", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
@ -851,8 +877,10 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
TestInMemorySignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
|
||||
TestInMemorySignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();
|
||||
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
|
||||
|
||||
initializeSessions(aliceStore, bobStore, bobAddress);
|
||||
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
CertificateValidator certificateValidator = new CertificateValidator(trustRoot.getPublicKey());
|
||||
@ -879,9 +907,9 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
bobCipher.decrypt(certificateValidator, ciphertext, 31335);
|
||||
|
||||
// Pretend Bob's reply fails to decrypt.
|
||||
SignalProtocolAddress aliceAddress =
|
||||
new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
|
||||
SessionCipher bobUnsealedCipher = new SessionCipher(bobStore, aliceAddress);
|
||||
SignalProtocolAddress bobAddressForReply =
|
||||
new SignalProtocolAddress("e80f7bbe-5b94-471e-bd8c-2173654ea3d1", 1);
|
||||
SessionCipher bobUnsealedCipher = new SessionCipher(bobStore, bobAddressForReply, aliceAddress);
|
||||
CiphertextMessage bobMessage = bobUnsealedCipher.encrypt("reply".getBytes());
|
||||
|
||||
DecryptionErrorMessage errorMessage =
|
||||
@ -930,7 +958,8 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
private void initializeSessions(
|
||||
TestInMemorySignalProtocolStore aliceStore,
|
||||
TestInMemorySignalProtocolStore bobStore,
|
||||
SignalProtocolAddress bobAddress)
|
||||
SignalProtocolAddress bobAddress,
|
||||
SignalProtocolAddress aliceAddress)
|
||||
throws InvalidKeyException, UntrustedIdentityException {
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
@ -950,7 +979,7 @@ public class SealedSessionCipherTest extends TestCase {
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress);
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilder.process(bobBundle);
|
||||
|
||||
bobStore.storeSignedPreKey(2, bobSignedPreKey);
|
||||
|
||||
@ -0,0 +1,336 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
import org.signal.libsignal.internal.TokioAsyncContext
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.protocol.message.PlaintextContent
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class AuthMessagesServiceTest {
|
||||
private val recipientUuid = UUID.fromString("4FCFE887-A600-40CD-9AB7-FD2A695E9981")
|
||||
|
||||
@Test
|
||||
fun testGetUploadForm() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val service = AuthMessagesService(chat)
|
||||
val responseFuture = service.getUploadForm(42)
|
||||
val (request, requestId) = fakeRemote.getNextIncomingRequest().get(1, TimeUnit.SECONDS)
|
||||
assertEquals("GET", request.method)
|
||||
assertEquals("/v4/attachments/form/upload?uploadLength=42", request.pathAndQuery)
|
||||
assertEquals(0, request.headers.size)
|
||||
assertEquals(0, request.body.size)
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
200,
|
||||
"OK",
|
||||
arrayOf("content-type: application/json"),
|
||||
"""
|
||||
{
|
||||
"cdn":123,
|
||||
"key":"abcde",
|
||||
"headers":{"one":"val1","two":"val2"},
|
||||
"signedUploadLocation":"http://example.org/upload"
|
||||
}
|
||||
""".encodeToByteArray(),
|
||||
)
|
||||
val result = responseFuture.get()
|
||||
val successResult = assertIs<RequestResult.Success<UploadForm>>(result)
|
||||
assertEquals(
|
||||
UploadForm(
|
||||
cdn = 123,
|
||||
key = "abcde",
|
||||
headers = mapOf("one" to "val1", "two" to "val2"),
|
||||
signedUploadUrl = URI("http://example.org/upload"),
|
||||
),
|
||||
successResult.result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetUploadFormTooLarge() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val service = AuthMessagesService(chat)
|
||||
val responseFuture = service.getUploadForm(42)
|
||||
val (request, requestId) = fakeRemote.getNextIncomingRequest().get(1, TimeUnit.SECONDS)
|
||||
assertEquals("GET", request.method)
|
||||
assertEquals("/v4/attachments/form/upload?uploadLength=42", request.pathAndQuery)
|
||||
assertEquals(0, request.headers.size)
|
||||
assertEquals(0, request.body.size)
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
413,
|
||||
"Content Too Large",
|
||||
arrayOf(),
|
||||
byteArrayOf(),
|
||||
)
|
||||
val error = assertIs<RequestResult.NonSuccess<UploadTooLargeException>>(responseFuture.get()).error
|
||||
assertIs<UploadTooLargeException>(error)
|
||||
}
|
||||
|
||||
private fun sendTestMessage(
|
||||
chat: AuthenticatedChatConnection,
|
||||
syncMessage: Boolean,
|
||||
fakeRemote: FakeChatRemote,
|
||||
): Pair<Future<out RequestResult<Unit, BadRequestError>>, Long> {
|
||||
val messagesService = AuthMessagesService(chat)
|
||||
|
||||
val timestamp = 1700000000000L
|
||||
val expectedBody =
|
||||
Json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"type": 8,
|
||||
"destinationDeviceId": 1,
|
||||
"destinationRegistrationId": 11,
|
||||
"content": "wAECA4A="
|
||||
},
|
||||
{
|
||||
"type": 8,
|
||||
"destinationDeviceId": 2,
|
||||
"destinationRegistrationId": 22,
|
||||
"content": "wAQFBoA="
|
||||
}
|
||||
],
|
||||
"online": false,
|
||||
"urgent": true,
|
||||
"timestamp": 1700000000000
|
||||
}
|
||||
""",
|
||||
)
|
||||
|
||||
val messages =
|
||||
listOf(
|
||||
SingleOutboundUnsealedMessage(1, 11, PlaintextContent(byteArrayOf(0xC0.toByte(), 1, 2, 3, 0x80.toByte()))),
|
||||
SingleOutboundUnsealedMessage(2, 22, PlaintextContent(byteArrayOf(0xC0.toByte(), 4, 5, 6, 0x80.toByte()))),
|
||||
)
|
||||
val responseFuture =
|
||||
if (syncMessage) {
|
||||
messagesService.sendSyncMessage(
|
||||
timestamp,
|
||||
messages,
|
||||
urgent = true,
|
||||
)
|
||||
} else {
|
||||
messagesService.sendMessage(
|
||||
Aci(recipientUuid),
|
||||
timestamp,
|
||||
messages,
|
||||
onlineOnly = false,
|
||||
urgent = true,
|
||||
)
|
||||
}
|
||||
|
||||
// Get the incoming request from the fake remote
|
||||
val (request, requestId) = fakeRemote.getNextIncomingRequest().get()
|
||||
|
||||
assertEquals("PUT", request.method)
|
||||
val expectedUuid = if (syncMessage) FakeChatRemote.FAKE_AUTH_CONNECT_SELF_UUID else recipientUuid
|
||||
assertEquals("/v1/messages/$expectedUuid", request.pathAndQuery)
|
||||
assertEquals(
|
||||
mapOf("content-type" to "application/json"),
|
||||
request.headers,
|
||||
)
|
||||
assertEquals(expectedBody, Json.parseToJsonElement(request.body.toString(Charsets.UTF_8)))
|
||||
|
||||
return Pair(responseFuture, requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessageSuccess() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
listOf(
|
||||
false,
|
||||
true,
|
||||
).forEach { syncMessage ->
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage, fakeRemote)
|
||||
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
200,
|
||||
"OK",
|
||||
arrayOf("content-type: application/json"),
|
||||
"{}".toByteArray(),
|
||||
)
|
||||
|
||||
// Verify the result
|
||||
val result = responseFuture.get()
|
||||
assertIs<RequestResult.Success<Unit>>(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessageNotFound() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
404,
|
||||
"Not Found",
|
||||
arrayOf(),
|
||||
byteArrayOf(),
|
||||
)
|
||||
|
||||
// Verify the result
|
||||
val result = responseFuture.get()
|
||||
val nonSuccessResult = assertIs<RequestResult.NonSuccess<UnsealedSendFailure>>(result)
|
||||
assertIs<ServiceIdNotFoundException>(nonSuccessResult.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessageMismatchedDevices() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
|
||||
val jsonResponse =
|
||||
"""
|
||||
{
|
||||
"missingDevices": [4, 5],
|
||||
"extraDevices": [40, 50]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
409,
|
||||
"Conflict",
|
||||
arrayOf("content-type: application/json"),
|
||||
jsonResponse.toByteArray(StandardCharsets.UTF_8),
|
||||
)
|
||||
|
||||
// Verify the result
|
||||
val result = responseFuture.get()
|
||||
val nonSuccessResult = assertIs<RequestResult.NonSuccess<UnsealedSendFailure>>(result)
|
||||
val mismatchedDevices = assertIs<MismatchedDeviceException>(nonSuccessResult.error)
|
||||
assertContentEquals(
|
||||
arrayOf(
|
||||
MismatchedDeviceException.Entry(
|
||||
Aci(recipientUuid),
|
||||
missingDevices = intArrayOf(4, 5),
|
||||
extraDevices = intArrayOf(40, 50),
|
||||
),
|
||||
),
|
||||
mismatchedDevices.entries,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessageStaleDevices() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
|
||||
val jsonResponse =
|
||||
"""
|
||||
{
|
||||
"staleDevices": [4, 5]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
410,
|
||||
"Gone",
|
||||
arrayOf("content-type: application/json"),
|
||||
jsonResponse.toByteArray(StandardCharsets.UTF_8),
|
||||
)
|
||||
|
||||
// Verify the result
|
||||
val result = responseFuture.get()
|
||||
val nonSuccessResult = assertIs<RequestResult.NonSuccess<UnsealedSendFailure>>(result)
|
||||
val mismatchedDevices = assertIs<MismatchedDeviceException>(nonSuccessResult.error)
|
||||
assertContentEquals(
|
||||
arrayOf(
|
||||
MismatchedDeviceException.Entry(
|
||||
Aci(recipientUuid),
|
||||
staleDevices = intArrayOf(4, 5),
|
||||
),
|
||||
),
|
||||
mismatchedDevices.entries,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessageCaptcha() {
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
|
||||
val jsonResponse =
|
||||
"""
|
||||
{
|
||||
"token": "zzz",
|
||||
"options": ["captcha"]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
428,
|
||||
"Precondition Required",
|
||||
arrayOf("content-type: application/json"),
|
||||
jsonResponse.toByteArray(StandardCharsets.UTF_8),
|
||||
)
|
||||
|
||||
// Verify the result
|
||||
val result = responseFuture.get()
|
||||
val nonSuccessResult = assertIs<RequestResult.NonSuccess<UnsealedSendFailure>>(result)
|
||||
val challengeException = assertIs<RateLimitChallengeException>(nonSuccessResult.error)
|
||||
assertEquals("zzz", challengeException.token)
|
||||
assertEquals(setOf(ChallengeOption.CAPTCHA), challengeException.options)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user