Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e465b77cb6 | ||
|
|
4388fac6a7 |
@ -1,6 +0,0 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained, used by libcrux
|
||||
"RUSTSEC-2024-0436", # paste is unmaintained, used by libsignal-bridge
|
||||
"RUSTSEC-2025-0141", # bincode is unmaintained, used by zkgroup
|
||||
]
|
||||
@ -1 +0,0 @@
|
||||
0.29.2
|
||||
10
.clippy.toml
10
.clippy.toml
@ -1,11 +1 @@
|
||||
too-many-arguments-threshold = 8
|
||||
disallowed-methods = [
|
||||
{ path = "jni::JNIEnv::find_class", reason = "use lookup helper instead" },
|
||||
{ path = "jni::JNIEnv::call_method", reason = "use helper method instead" },
|
||||
{ path = "jni::JNIEnv::call_method_unchecked", reason = "use helper method instead" },
|
||||
{ path = "jni::JNIEnv::call_static_method", reason = "use helper method instead" },
|
||||
{ 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" },
|
||||
]
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
@ -37,7 +37,3 @@ indent_size = 4
|
||||
|
||||
[*.swift]
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
10
.gitattributes
vendored
10
.gitattributes
vendored
@ -3,11 +3,5 @@ acknowledgments/acknowledgments.* -merge -text
|
||||
acknowledgments/acknowledgments.*.hbs merge text=auto
|
||||
|
||||
# Treat encrypted and unencrypted message backup files as binary
|
||||
**/*.binproto binary
|
||||
**/*.binproto.encrypted binary
|
||||
|
||||
# Avoid Windows line-endings for files compared literally.
|
||||
**/*.expected.json text eol=lf
|
||||
|
||||
**/*.kt.in linguist-language=Kotlin
|
||||
**/*.ts.in linguist-language=TypeScript
|
||||
*.binproto binary
|
||||
*.binproto.encrypted binary
|
||||
|
||||
24
.github/actionlint.yaml
vendored
24
.github/actionlint.yaml
vendored
@ -1,24 +0,0 @@
|
||||
self-hosted-runner:
|
||||
# Labels of self-hosted runner in array of strings.
|
||||
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.
|
||||
# Empty array means no configuration variable is allowed.
|
||||
config-variables: null
|
||||
|
||||
# Configuration for file paths. The keys are glob patterns to match to file
|
||||
# paths relative to the repository root. The values are the configurations for
|
||||
# the file paths. Note that the path separator is always '/'.
|
||||
# The following configurations are available.
|
||||
#
|
||||
# "ignore" is an array of regular expression patterns. Matched error messages
|
||||
# are ignored. This is similar to the "-ignore" command line option.
|
||||
paths:
|
||||
# .github/workflows/**/*.yml:
|
||||
# ignore: []
|
||||
70
.github/actions/restore-cargo-cache/action.yml
vendored
70
.github/actions/restore-cargo-cache/action.yml
vendored
@ -1,70 +0,0 @@
|
||||
name: 'Restore Cargo Cache'
|
||||
description: 'Restore cargo and build cache with appropriate keys'
|
||||
inputs:
|
||||
job-name:
|
||||
description: 'Name for the cache (e.g., rust-nightly, node, java)'
|
||||
required: true
|
||||
toolchain:
|
||||
description: 'Optional rustup toolchain spec to override the repo default'
|
||||
required: false
|
||||
default: 'workspace'
|
||||
outputs:
|
||||
rustc-version:
|
||||
description: 'Full rustc --version string for the resolved toolchain'
|
||||
value: ${{ steps.calculate.outputs.rustc-version }}
|
||||
cache-key-merge-base:
|
||||
description: 'The merge base commit with origin/main'
|
||||
value: ${{ steps.calculate.outputs['cache-key-merge-base'] }}
|
||||
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:
|
||||
- name: Calculate cache key inputs
|
||||
id: calculate
|
||||
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
|
||||
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
|
||||
# tools are not long-path aware.
|
||||
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) || '' }}
|
||||
# Cache key strategy:
|
||||
# - The GitHub Actions cache API treats an exact key match as authoritative and skips re-uploading.
|
||||
# - 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.
|
||||
# See: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-key-matching
|
||||
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 }}-
|
||||
@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Calculate cache keys for GitHub Actions workflow."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional, Sequence, Tuple
|
||||
|
||||
|
||||
def run_command(cmd: Sequence[str], check: bool = True) -> Tuple[int, str, str]:
|
||||
"""Run a command and return its exit code, stdout, and stderr."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
)
|
||||
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
stdout = exc.stdout.strip() if exc.stdout else ''
|
||||
stderr = exc.stderr.strip() if exc.stderr else ''
|
||||
return exc.returncode, stdout, stderr
|
||||
|
||||
|
||||
def get_merge_base() -> str:
|
||||
"""Get the merge base commit with origin/main."""
|
||||
# Deepen all currently tracked tags to help find the merge base
|
||||
run_command(['git', 'fetch', '--deepen=100', 'origin'], check=False)
|
||||
|
||||
code, output, _ = run_command(
|
||||
['git', 'merge-base', 'HEAD', 'origin/main'],
|
||||
check=False,
|
||||
)
|
||||
|
||||
if code != 0 or not output:
|
||||
print('Warning: could not determine merge base', file=sys.stderr)
|
||||
return 'none'
|
||||
|
||||
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(
|
||||
['git', 'rev-parse', 'HEAD^{tree}'],
|
||||
check=False,
|
||||
)
|
||||
|
||||
if returncode != 0 or not working_tree_hash:
|
||||
raise RuntimeError('Could not determine working tree hash')
|
||||
|
||||
return working_tree_hash
|
||||
|
||||
|
||||
def read_rust_toolchain_file(path: pathlib.Path) -> Optional[str]:
|
||||
"""Read the rust-toolchain file if present."""
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
content = path.read_text(encoding='utf-8').strip()
|
||||
return content or None
|
||||
|
||||
|
||||
def resolve_toolchain(override: Optional[str]) -> str:
|
||||
"""Determine which toolchain spec to use."""
|
||||
if override:
|
||||
return override
|
||||
|
||||
workspace_root = pathlib.Path.cwd()
|
||||
toolchain_from_file = read_rust_toolchain_file(workspace_root / 'rust-toolchain')
|
||||
if toolchain_from_file:
|
||||
return toolchain_from_file
|
||||
|
||||
raise RuntimeError('Could not determine toolchain')
|
||||
|
||||
|
||||
def get_rustc_version(toolchain: str) -> str:
|
||||
"""Resolve the full rustc --version string for the given toolchain."""
|
||||
if not toolchain:
|
||||
return 'unknown'
|
||||
|
||||
returncode, stdout, stderr = run_command(['rustup', 'run', toolchain, 'rustc', '--version'], check=False)
|
||||
if returncode == 0 and stdout:
|
||||
return stdout
|
||||
|
||||
if stderr:
|
||||
print(f'Warning: command rustup run {toolchain} rustc --version failed: {stderr}', file=sys.stderr)
|
||||
|
||||
raise RuntimeError(f'Could not determine rustc version for toolchain {toolchain}')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Calculate cache keys')
|
||||
parser.add_argument(
|
||||
'--toolchain',
|
||||
default='workspace',
|
||||
help='Rust toolchain spec to use, or "workspace" to use the repository configuration.',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
requested = args.toolchain.strip()
|
||||
if not requested or requested == 'workspace':
|
||||
requested = None
|
||||
toolchain = resolve_toolchain(requested)
|
||||
rustc_version = get_rustc_version(toolchain)
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
23
.github/actions/save-cargo-cache/action.yml
vendored
23
.github/actions/save-cargo-cache/action.yml
vendored
@ -1,23 +0,0 @@
|
||||
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 }}
|
||||
63
.github/stale.yml
vendored
Normal file
63
.github/stale.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright 2022 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- acknowledged
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: false
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: true
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been closed due to inactivity.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 5
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
||||
|
||||
18
.github/workflows/README.md
vendored
18
.github/workflows/README.md
vendored
@ -1,18 +0,0 @@
|
||||
# Notes on GitHub Actions
|
||||
|
||||
## Why not use `actions/cache` in the Rust jobs?
|
||||
|
||||
In Sep 2024, the slowest part of `build_and_test.yml` was the main Rust job, which runs several Rust-related checks---some using our pinned nightly, others using our MSRV, and still others with both toolchains. The slowest *parts* of the job are just building things, and that's at least partly because each step requires slightly different configurations, making the rebuilds less incremental than they might otherwise be. The second slowest job is the Java one, which builds the main library in several slices.
|
||||
|
||||
It might be reasonable to try to cache some of this work, either using [`actions/cache`][] directly or another action built on top of it like [`Swatinem/rust-cache`][]. However, it's not clear how much of a benefit we'll actually get:
|
||||
|
||||
- Turning off `CARGO_INCREMENTAL` (as suggested by `rust-cache`) would save some space in our target directories, but we actually do build our local crates in a few different configurations, so we might make builds longer if we do that.
|
||||
|
||||
- Fetching dependencies takes about 1m out of our total time, not enough to be worth targeting specifically.
|
||||
|
||||
- We build with two different Rust toolchains, so any caching we do is doubled. The Java build only uses one toolchain, but it builds release instead of debug, and does multiple slices. If we fill up our entire cache quota (10GB) by accident, we lose most of the benefits as each job's cache evicts one of the other ones.
|
||||
|
||||
- Building with a lower debug info setting might save on the space of build intermediates, but is then testing something different than what people usually use at their desk.
|
||||
|
||||
[`actions/cache`]: https://github.com/actions/cache
|
||||
[`Swatinem/rust-cache`]: https://github.com/Swatinem/rust-cache
|
||||
72
.github/workflows/android_integration.yml
vendored
72
.github/workflows/android_integration.yml
vendored
@ -1,72 +0,0 @@
|
||||
name: "Integration - Android"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
signal_android_branch:
|
||||
description: 'Signal-Android branch to test against'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NDK_VERSION: 28.0.13004108
|
||||
|
||||
jobs:
|
||||
android-integration:
|
||||
name: Android Client Integration Test
|
||||
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout libsignal
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: libsignal
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Signal-Android
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: signalapp/Signal-Android
|
||||
ref: ${{ inputs.signal_android_branch }}
|
||||
path: Signal-Android
|
||||
|
||||
- run: 'echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"'
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- run: |
|
||||
cd libsignal
|
||||
rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
|
||||
|
||||
- name: Install protoc
|
||||
run: |
|
||||
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip
|
||||
sudo unzip -o protoc-32.0-linux-x86_64.zip -d /usr/local bin/protoc
|
||||
sudo unzip -o protoc-32.0-linux-x86_64.zip -d /usr/local 'include/*'
|
||||
rm protoc-32.0-linux-x86_64.zip
|
||||
|
||||
- name: Install Android NDK
|
||||
run: |
|
||||
sudo "${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${NDK_VERSION}"
|
||||
echo "ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run Android's QA checks with local libsignal
|
||||
run: ./gradlew qa --no-daemon --stacktrace -PlibsignalClientPath=../libsignal -F OFF
|
||||
working-directory: Signal-Android
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
Signal-Android/**/build/reports/
|
||||
Signal-Android/**/build/test-results/
|
||||
retention-days: 7
|
||||
679
.github/workflows/build_and_test.yml
vendored
679
.github/workflows/build_and_test.yml
vendored
@ -1,34 +1,18 @@
|
||||
name: "[CI] Build and Test"
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request: # all target branches
|
||||
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
|
||||
# every run; as a result, all actions on pushes will run to completion.
|
||||
#
|
||||
# Reference: https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NDK_VERSION: 28.0.13004108
|
||||
NDK_VERSION: 25.2.9519653
|
||||
RUST_BACKTRACE: 1
|
||||
LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION: 1.72
|
||||
# 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
|
||||
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:
|
||||
@ -49,18 +33,14 @@ jobs:
|
||||
rust_ios: ${{ steps.filter.outputs.rust_ios }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
all: &all
|
||||
- '.github/workflows/build_and_test.yml'
|
||||
- '.github/actions/**'
|
||||
- 'bin/**'
|
||||
- 'rust/*'
|
||||
- 'rust/!(bridge|protocol)/**'
|
||||
@ -70,7 +50,6 @@ jobs:
|
||||
- 'rust-toolchain'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.cargo/**' # overly conservative, but it's fine
|
||||
rust:
|
||||
- *all
|
||||
- '.clippy.toml'
|
||||
@ -105,20 +84,15 @@ jobs:
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- '.editorconfig'
|
||||
- '.tool-versions'
|
||||
- 'justfile'
|
||||
- 'doc/**'
|
||||
|
||||
- name: Check pattern completeness
|
||||
run: echo "::error file=.github/workflows/build_and_test.yml::File not included in any filter" && false
|
||||
# `actionlint` does not like it when you write this like: `!contains(steps.filter.outputs.*, 'true')`
|
||||
# It also does not include a way to inline ignore a single instance of a warning. C'est la vie.
|
||||
if: ${{ !contains(toJSON(steps.filter.outputs), '"true"') }}
|
||||
if: ${{ !contains(steps.filter.outputs.*, 'true') }}
|
||||
|
||||
rust:
|
||||
name: Rust
|
||||
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
@ -132,270 +106,134 @@ jobs:
|
||||
- version: nightly
|
||||
toolchain: "$(cat rust-toolchain)"
|
||||
- version: stable
|
||||
# Extract 'rust-version' value from Cargo.toml.
|
||||
toolchain: "$(yq '.workspace.package.rust-version' $(git rev-parse --show-toplevel)/Cargo.toml)"
|
||||
|
||||
timeout-minutes: 45
|
||||
toolchain: "${LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION}"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install "${{ matrix.toolchain }}" --profile minimal --component rustfmt,clippy
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust-${{ matrix.version }}
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
||||
- name: Build
|
||||
run: cargo +${{ matrix.toolchain }} build --workspace --features libsignal-ffi/signal-media --verbose --keep-going
|
||||
|
||||
- name: Run tests
|
||||
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --no-fail-fast -- --include-ignored
|
||||
|
||||
- name: Test run benches
|
||||
# Run with a match-all regex to select all the benchmarks, which (confusingly) causes other tests to be skipped.
|
||||
run: cargo +${{ matrix.toolchain }} test --workspace --benches --all-features --verbose --no-fail-fast '.*'
|
||||
|
||||
- name: Build bins and examples
|
||||
run: cargo +${{ matrix.toolchain }} build --workspace --bins --examples --all-features --verbose --keep-going
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace --all-targets --all-features --keep-going -- -D warnings
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Rust docs
|
||||
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)
|
||||
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [nightly, stable]
|
||||
include:
|
||||
- version: nightly
|
||||
toolchain: "$(cat rust-toolchain)"
|
||||
- version: stable
|
||||
# Extract 'rust-version' value from Cargo.toml.
|
||||
toolchain: "$(yq '.workspace.package.rust-version' $(git rev-parse --show-toplevel)/Cargo.toml)"
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: sudo apt-get install -U gcc-multilib g++-multilib
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: rustup toolchain install "${{ matrix.toolchain }}" --profile minimal --target i686-unknown-linux-gnu
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust32-${{ matrix.version }}
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
||||
- 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)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Resolve latest stable MSRV toolchain
|
||||
id: rust-fuzz-build-toolchain
|
||||
run: echo "latest-stable-msrv-toolchain=$(yq '.workspace.package.rust-version' Cargo.toml)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
run: rustup toolchain install "${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }}" --profile minimal
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: rust-fuzz-build
|
||||
toolchain: ${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }}
|
||||
|
||||
# We check the fuzz targets on stable because they don't have lockfiles,
|
||||
# and crates don't generally support arbitrary nightly versions.
|
||||
# See https://github.com/dtolnay/proc-macro2/issues/307 for an example.
|
||||
- name: Check that the protocol fuzz target still builds
|
||||
run: cargo +${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }} check --all-targets --keep-going
|
||||
working-directory: rust/protocol/fuzz
|
||||
env:
|
||||
RUSTFLAGS: --cfg fuzzing
|
||||
|
||||
- name: Check that the attest fuzz target still builds
|
||||
run: cargo +${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }} check --all-targets --keep-going
|
||||
working-directory: rust/attest/fuzz
|
||||
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)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Resolve pinned nightly toolchain
|
||||
id: rust-fmt-toolchain
|
||||
run: echo "pinned-nightly-toolchain=$(cat rust-toolchain)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install pinned nightly toolchain
|
||||
run: rustup toolchain install "${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }}" --profile minimal --component rustfmt
|
||||
- run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal --component rustfmt,clippy
|
||||
|
||||
- name: Cache locally-built tools
|
||||
uses: runs-on/cache@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
|
||||
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
|
||||
with:
|
||||
path: local-tools
|
||||
key: local-tools-${{ runner.os }}-infra-${{ hashFiles('acknowledgments/cargo-about-version', '.taplo-cli-version') }}
|
||||
key: ${{ runner.os }}-local-tools-${{ matrix.version }}-${{ hashFiles('acknowledgments/cargo-about-version') }}
|
||||
|
||||
- name: Build cargo-about if needed
|
||||
run: cargo +stable install --version "$(cat acknowledgments/cargo-about-version)" --locked cargo-about --root local-tools
|
||||
|
||||
- name: Build taplo-cli if needed
|
||||
run: cargo +stable install --version "$(cat .taplo-cli-version)" --locked taplo-cli --root local-tools
|
||||
run: cargo +stable install --version $(cat acknowledgments/cargo-about-version) --locked cargo-about --root local-tools
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
# This should be done before anything else
|
||||
# because it also checks that the lockfile is up to date.
|
||||
- name: Check for duplicate dependencies
|
||||
run: ./bin/verify_duplicate_crates
|
||||
|
||||
- name: Cargo.toml formatting check
|
||||
run: PATH="$PATH:$PWD/local-tools/bin" taplo format -c .taplo.toml --check
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Rustfmt check
|
||||
run: cargo +${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }} fmt --all -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Rustfmt check for cross-version-testing
|
||||
run: cargo +${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }} fmt --all -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: rust/protocol/cross-version-testing
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Check bridge versioning
|
||||
run: ./bin/update_versions.py
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: Check acknowledgments
|
||||
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh --check
|
||||
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh && git diff --name-status --exit-code acknowledgments
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
java_android:
|
||||
name: Java Android
|
||||
- name: Build
|
||||
run: cargo +${{ matrix.toolchain }} build --workspace --features libsignal-ffi/signal-media --verbose
|
||||
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
- name: Run tests
|
||||
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose -- --include-ignored
|
||||
|
||||
- name: Test run benches
|
||||
run: cargo +${{ matrix.toolchain }} test --workspace --benches --all-features --verbose
|
||||
|
||||
- name: Build bins and examples
|
||||
run: cargo +${{ matrix.toolchain }} build --workspace --bins --examples --all-features --verbose
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
if: matrix.version == 'nightly'
|
||||
|
||||
- name: cargo clean (reclaim disk space)
|
||||
# Clean the contents of the target directory to avoid running out of disk space during the
|
||||
# doc build.
|
||||
run: cargo clean
|
||||
if: matrix.version == 'stable'
|
||||
|
||||
- name: Rust docs
|
||||
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features
|
||||
if: matrix.version == 'stable'
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
# We check the fuzz targets on stable because they don't have lockfiles,
|
||||
# and crates don't generally support arbitrary nightly versions.
|
||||
# See https://github.com/dtolnay/proc-macro2/issues/307 for an example.
|
||||
|
||||
- name: cargo clean (reclaim disk space)
|
||||
# Clean the contents of the target directory to avoid running out of disk space during the
|
||||
# following steps.
|
||||
run: cargo clean
|
||||
if: matrix.version == 'stable'
|
||||
|
||||
- name: Check that the protocol fuzz target still builds
|
||||
run: cargo +${{ matrix.toolchain }} check --all-targets
|
||||
working-directory: rust/protocol/fuzz
|
||||
env:
|
||||
RUSTFLAGS: --cfg fuzzing
|
||||
if: matrix.version == 'stable'
|
||||
|
||||
- name: Check that the attest fuzz target still builds
|
||||
run: cargo +${{ matrix.toolchain }} check --all-targets
|
||||
working-directory: rust/attest/fuzz
|
||||
env:
|
||||
RUSTFLAGS: --cfg fuzzing
|
||||
if: matrix.version == 'stable'
|
||||
|
||||
rust32:
|
||||
name: Rust (32-bit testing)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [nightly, stable]
|
||||
include:
|
||||
- version: nightly
|
||||
toolchain: "$(cat rust-toolchain)"
|
||||
- version: stable
|
||||
toolchain: "${LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION}"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal --target i686-unknown-linux-gnu
|
||||
|
||||
- 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 -- --include-ignored
|
||||
|
||||
java:
|
||||
name: Java
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
@ -406,44 +244,25 @@ jobs:
|
||||
|
||||
if: ${{ needs.changes.outputs.java == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
submodules: recursive
|
||||
# Download all commits so we can search for the merge base with origin/main.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
"${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${NDK_VERSION}"
|
||||
run: sudo ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${NDK_VERSION}"
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
|
||||
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-linux-android,armv7-linux-androideabi
|
||||
- name: Verify that the JNI bindings are up to date
|
||||
run: rust/bridge/jni/bin/gen_java_decl.py --verify
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: java-android
|
||||
|
||||
- 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
|
||||
- run: ./gradlew build assembleDebugAndroidTest android:lintDebug -PandroidArchs=arm,arm64 -PandroidTestingArchs=x86_64 | tee ./gradle-output.txt
|
||||
working-directory: java
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
@ -451,86 +270,10 @@ jobs:
|
||||
- run: "! grep WARNING ./gradle-output.txt"
|
||||
working-directory: java
|
||||
|
||||
- run: java/check_code_size.py | tee ./check_code_size-output.txt
|
||||
- run: java/check_code_size.py
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- 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
|
||||
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
|
||||
needs: changes
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
if: ${{ needs.changes.outputs.java == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: cargo +stable install cbindgen
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: java-jvm
|
||||
|
||||
- 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 --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
|
||||
|
||||
@ -538,33 +281,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest-4-cores, windows-latest-4-cores, macos-15-xlarge]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.node == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: node
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
|
||||
|
||||
# install nasm compiler for boring
|
||||
- name: Install nasm
|
||||
@ -572,58 +298,40 @@ jobs:
|
||||
run: choco install nasm
|
||||
shell: cmd
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- run: choco install protoc
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- run: npm ci
|
||||
- run: yarn install --frozen-lockfile
|
||||
working-directory: node
|
||||
|
||||
- run: npm run build
|
||||
- run: yarn tsc
|
||||
working-directory: node
|
||||
|
||||
- run: npm run tsc
|
||||
- run: yarn lint
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: node
|
||||
|
||||
- run: npm run lint
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
- run: yarn format -c
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: node
|
||||
|
||||
- run: npm run format-check
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
- run: yarn test
|
||||
working-directory: node
|
||||
|
||||
- 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
|
||||
|
||||
@ -633,36 +341,12 @@ jobs:
|
||||
|
||||
if: ${{ needs.changes.outputs.swift == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
with:
|
||||
job-name: swift-package
|
||||
|
||||
- name: Install protoc
|
||||
run: ./bin/install_protoc_linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
|
||||
|
||||
- run: swift/verify_error_codes.sh
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
|
||||
- name: Build libsignal-ffi
|
||||
run: swift/build_ffi.sh -d -v --verify-ffi
|
||||
@ -671,73 +355,45 @@ jobs:
|
||||
run: swift test -v
|
||||
working-directory: swift
|
||||
|
||||
- name: Build and run Swift benchmarks (in debug mode)
|
||||
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
|
||||
# run: swift package plugin generate-documentation --analyze --warnings-as-errors
|
||||
# working-directory: swift
|
||||
- name: Build Swift package documentation
|
||||
run: swift package plugin generate-documentation --analyze --warnings-as-errors
|
||||
working-directory: swift
|
||||
|
||||
swift_cocoapod:
|
||||
name: Swift CocoaPod
|
||||
|
||||
runs-on: macos-15-xlarge
|
||||
runs-on: macOS-latest
|
||||
|
||||
needs: changes
|
||||
|
||||
if: ${{ needs.changes.outputs.swift == 'true' }}
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: brew install protobuf swiftlint
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Check formatting
|
||||
run: swift format --in-place --parallel --recursive . && git diff --exit-code .
|
||||
run: swiftformat --swiftversion 5 --reporter github-actions-log --lint .
|
||||
working-directory: swift
|
||||
|
||||
- name: Run lint
|
||||
run: swiftlint lint --strict --reporter github-actions-logging
|
||||
working-directory: swift
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-apple-ios-sim
|
||||
|
||||
- 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: ${{ secrets.R2_BUCKET_NAME }}
|
||||
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
- name: Check out SignalCoreKit
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
job-name: swift-cocoapod
|
||||
repository: signalapp/SignalCoreKit
|
||||
path: SignalCoreKit
|
||||
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios-sim
|
||||
|
||||
- run: brew install protobuf
|
||||
|
||||
# Build only the targets that `pod lib lint` will test building.
|
||||
- name: Build for x86_64-apple-ios
|
||||
run: swift/build_ffi.sh --release
|
||||
env:
|
||||
CARGO_BUILD_TARGET: x86_64-apple-ios
|
||||
|
||||
- name: Build for aarch64-apple-ios-sim
|
||||
run: swift/build_ffi.sh --release
|
||||
@ -745,18 +401,5 @@ jobs:
|
||||
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
|
||||
|
||||
- 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'] }}
|
||||
# No import validation because it tries to build unsupported platforms (like 32-bit iOS).
|
||||
run: pod lib lint --verbose --platforms=ios --include-podspecs=SignalCoreKit/SignalCoreKit.podspec --skip-import-validation
|
||||
|
||||
30
.github/workflows/check_versions.yml
vendored
30
.github/workflows/check_versions.yml
vendored
@ -1,30 +0,0 @@
|
||||
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.
|
||||
#
|
||||
# Thus, this job explicitly does not depend on the "Classify Changes" job, like all the other
|
||||
# jobs in Build and Test do. The lint job also just runs on a subset of changes. So, this ends
|
||||
# up being a completely independent job.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request: # all target branches
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
check_versions:
|
||||
name: Check version number consistency
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# The update_versions.py script checks that the version numbers in the source code
|
||||
# are consistent with themselves and the version number in RELEASE_NOTES.md.
|
||||
# It exits with a non-zero exit code if they are not consistent.
|
||||
- run: ./bin/update_versions.py
|
||||
31
.github/workflows/docs.yml
vendored
31
.github/workflows/docs.yml
vendored
@ -1,31 +0,0 @@
|
||||
name: "[CI] Docs"
|
||||
|
||||
env:
|
||||
MDBOOK_VERSION: "0.4.43"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths: ['doc/**', '.github/workflows/docs.yml']
|
||||
pull_request:
|
||||
paths: ['doc/**', '.github/workflows/docs.yml']
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: Check docs
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download mdbook ${{ env.MDBOOK_VERSION }}
|
||||
run: mkdir ~/bin && curl -sSL https://github.com/rust-lang/mdBook/releases/download/v${{ env.MDBOOK_VERSION }}/mdbook-v${{ env.MDBOOK_VERSION }}-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory ~/bin
|
||||
|
||||
- run: ~/bin/mdbook build
|
||||
working-directory: doc
|
||||
|
||||
- run: ~/bin/mdbook test
|
||||
working-directory: doc
|
||||
32
.github/workflows/ios_artifacts.yml
vendored
32
.github/workflows/ios_artifacts.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Release - iOS"
|
||||
name: Build iOS Artifacts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -22,35 +22,31 @@ jobs:
|
||||
# Needed for google-github-actions/auth.
|
||||
id-token: 'write'
|
||||
|
||||
runs-on: macos-15-xlarge
|
||||
|
||||
timeout-minutes: 45
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
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");
|
||||
}
|
||||
|
||||
- id: archive-name
|
||||
run: echo "name=libsignal-client-ios-build-v$(sed -En "s/${VERSION_REGEX}/\1/p" LibSignalClient.podspec).tar.gz" >> "$GITHUB_OUTPUT"
|
||||
run: echo name=libsignal-client-ios-build-v$(sed -En "s/${VERSION_REGEX}/\1/p" LibSignalClient.podspec).tar.gz >> $GITHUB_OUTPUT
|
||||
env:
|
||||
VERSION_REGEX: "^.*[.]version += '(.+)'$"
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
|
||||
|
||||
- run: brew install protobuf
|
||||
|
||||
@ -69,24 +65,24 @@ jobs:
|
||||
env:
|
||||
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
|
||||
|
||||
- run: tar -c --auto-compress --no-mac-metadata -f '${{ steps.archive-name.outputs.name }}' target/*/release/libsignal_ffi.a
|
||||
- run: tar -c --auto-compress --no-mac-metadata -f ${{ steps.archive-name.outputs.name }} target/*/release/libsignal_ffi.a
|
||||
|
||||
- run: shasum -a 256 '${{ steps.archive-name.outputs.name }}' | tee -a "$GITHUB_STEP_SUMMARY" '${{ steps.archive-name.outputs.name }}.sha256'
|
||||
- run: 'shasum -a 256 ${{ steps.archive-name.outputs.name }} | tee -a $GITHUB_STEP_SUMMARY ${{ steps.archive-name.outputs.name }}.sha256'
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
- name: Attach artifact to the run
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
path: ${{ steps.archive-name.outputs.name }}
|
||||
name: libsignal-client-ios
|
||||
|
||||
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
- uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
|
||||
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@6397bd7208e18d13ba2619ee21b9873edc94427a # v3.0.0
|
||||
- uses: google-github-actions/upload-cloud-storage@22121cd842b0d185e042e28d969925b538c33d77 # v2.1.0
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
path: ${{ steps.archive-name.outputs.name }}
|
||||
@ -94,7 +90,7 @@ jobs:
|
||||
|
||||
# This step is expected to fail if not run on a tag.
|
||||
- name: Upload checksum to release
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
|
||||
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
allowUpdates: true
|
||||
|
||||
131
.github/workflows/jni_artifacts.yml
vendored
131
.github/workflows/jni_artifacts.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Release - Java"
|
||||
name: Upload Java libraries to Sonatype
|
||||
run-name: ${{ github.workflow }} (${{ github.ref_name }})
|
||||
|
||||
on:
|
||||
@ -21,37 +21,35 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest-8-cores, macos-15-xlarge]
|
||||
os: [windows-latest, macos-latest]
|
||||
include:
|
||||
- os: windows-latest-8-cores
|
||||
- os: macos-15-xlarge
|
||||
additional-rust-target: x86_64-apple-darwin
|
||||
- os: windows-latest
|
||||
library: signal_jni.dll
|
||||
- os: macos-latest
|
||||
library: libsignal_jni.dylib
|
||||
additional-rust-target: aarch64-apple-darwin
|
||||
# Ubuntu binaries are built using Docker, below
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
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");
|
||||
}
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
|
||||
|
||||
- run: rustup target add "${{ matrix.additional-rust-target }}"
|
||||
- run: rustup target add ${{ matrix.additional-rust-target }}
|
||||
if: ${{ matrix.additional-rust-target != '' }}
|
||||
|
||||
# install nasm compiler for boring
|
||||
@ -61,63 +59,43 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- run: choco install protoc
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- name: Build client for host
|
||||
- name: Build for host (should be x86_64)
|
||||
run: java/build_jni.sh desktop
|
||||
shell: bash
|
||||
|
||||
- name: Build server for host
|
||||
run: java/build_jni.sh server
|
||||
shell: bash
|
||||
|
||||
- name: Build client for alternate target
|
||||
- name: Build for alternate target (arm64)
|
||||
run: java/build_jni.sh desktop
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
if: matrix.os == 'macos-latest'
|
||||
env:
|
||||
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
|
||||
|
||||
- name: Build server for alternate target
|
||||
run: java/build_jni.sh server
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
env:
|
||||
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
|
||||
- name: Merge library slices (for macOS)
|
||||
# Using target/release/ for both the input and output wouldn't normally be ideal
|
||||
# from a build system perspective, but we're going to immediately upload the merged library.
|
||||
run: lipo -create target/release/${{ matrix.library }} target/${{ matrix.additional-rust-target }}/release/${{ matrix.library }} -output target/release/${{ matrix.library }}
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- name: Upload client libraries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
- name: Upload library
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: libsignal-client libraries (${{matrix.os}})
|
||||
path: |
|
||||
java/client/src/main/resources/*.dll
|
||||
java/client/src/main/resources/*.dylib
|
||||
|
||||
- name: Upload server libraries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: libsignal-server libraries (${{matrix.os}})
|
||||
path: |
|
||||
java/server/src/main/resources/*.dll
|
||||
java/server/src/main/resources/*.dylib
|
||||
name: libsignal_jni (${{matrix.os}})
|
||||
path: target/release/${{ matrix.library }}
|
||||
|
||||
verify-rust:
|
||||
name: Verify JNI bindings
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
|
||||
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
|
||||
- name: Verify that the JNI bindings are up to date
|
||||
run: rust/bridge/jni/bin/gen_java_decl.py --verify
|
||||
@ -125,35 +103,21 @@ jobs:
|
||||
publish:
|
||||
name: Build for production and publish
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed for google-github-actions/auth.
|
||||
id-token: write
|
||||
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: [build, verify-rust]
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Download built client libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- name: Download built libraries
|
||||
id: download
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
path: java/client/src/main/resources
|
||||
pattern: libsignal-client*
|
||||
merge-multiple: true
|
||||
path: artifacts
|
||||
|
||||
- name: Download built server libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: java/server/src/main/resources
|
||||
pattern: libsignal-server*
|
||||
merge-multiple: true
|
||||
- name: Copy libraries
|
||||
run: mv ${{ steps.download.outputs.download-path }}/*/* java/shared/resources && find java/shared/resources
|
||||
|
||||
- run: make
|
||||
if: ${{ inputs.dry_run }}
|
||||
@ -161,38 +125,31 @@ jobs:
|
||||
|
||||
- name: Upload libsignal-android
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: libsignal-client
|
||||
path: java/client/build/libs/libsignal-client-*.jar
|
||||
|
||||
- name: Upload libsignal-server
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
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:
|
||||
CLOUDSDK_AUTH_ACCESS_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
|
||||
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
|
||||
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||
# ASCII-armored PGP secret key
|
||||
|
||||
29
.github/workflows/lints.yml
vendored
29
.github/workflows/lints.yml
vendored
@ -1,13 +1,13 @@
|
||||
name: "[CI] Lints"
|
||||
name: Lints
|
||||
# This is in a separate job because we have shell scripts scattered across all our targets,
|
||||
# *and* some of them have common dependencies.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
|
||||
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
|
||||
pull_request:
|
||||
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
|
||||
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@ -16,22 +16,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- run: pip3 install flake8 mypy
|
||||
- run: shellcheck **/*.sh bin/verify_duplicate_crates bin/adb-run-test
|
||||
- run: python3 -m flake8 .
|
||||
- run: python3 -m mypy . --python-version 3.9 --strict
|
||||
env:
|
||||
# Some scripts modify sys.path to fetch from ./bin
|
||||
MYPYPATH: ./bin
|
||||
- name: Install actionlint
|
||||
run: |
|
||||
mkdir -p "$HOME/bin"
|
||||
curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/e7d448ef7507c20fc4c88a95d0c448b848cd6127/scripts/download-actionlint.bash \
|
||||
| bash -s -- 1.7.8 "$HOME/bin"
|
||||
echo "$HOME/bin" >> "$GITHUB_PATH"
|
||||
- run: actionlint
|
||||
# Only include typed Python scripts here.
|
||||
- run: python3 -m mypy bin/fetch_archive.py --python-version 3.8 --strict
|
||||
|
||||
153
.github/workflows/npm.yml
vendored
153
.github/workflows/npm.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Release - NPM"
|
||||
name: Publish to NPM
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -24,36 +24,34 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest-8-cores, macos-15-xlarge]
|
||||
os: [windows-latest, macos-11]
|
||||
include:
|
||||
- os: macos-15-xlarge
|
||||
rust-cross-target: x86_64-apple-darwin
|
||||
- os: windows-latest-8-cores
|
||||
rust-cross-target: aarch64-pc-windows-msvc
|
||||
- os: macos-11
|
||||
arm64-rust-target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
arm64-rust-target: aarch64-pc-windows-msvc
|
||||
# This can be removed when we update to a Node version that officially supports win-arm64.
|
||||
custom-arm64-dist-url: https://unofficial-builds.nodejs.org/download/release
|
||||
# Ubuntu binaries are built using Docker, below
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Checking run eligibility
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
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");
|
||||
}
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target "${{ matrix.rust-cross-target }}"
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target ${{ matrix.arm64-rust-target }}
|
||||
|
||||
# install nasm compiler for boring
|
||||
- name: (Windows) Install nasm
|
||||
@ -67,162 +65,107 @@ jobs:
|
||||
- run: brew install protobuf
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
|
||||
- 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"
|
||||
run: echo "node-version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.24
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: npx yarn install --ignore-scripts --frozen-lockfile
|
||||
working-directory: node
|
||||
|
||||
- name: Build for arm64
|
||||
run: npm run build -- --arch arm64 --copy-to-prebuilds
|
||||
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }} --arch arm64
|
||||
working-directory: node
|
||||
env:
|
||||
npm_config_dist_url: ${{ matrix.custom-arm64-dist-url }}
|
||||
|
||||
- name: Save arm64 debug info
|
||||
run: mv build/Release/*-debuginfo.* .
|
||||
- name: Build for the host (should be x64)
|
||||
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }}
|
||||
working-directory: node
|
||||
shell: bash
|
||||
|
||||
- name: Build for x64
|
||||
run: npm run build -- --arch x64 --copy-to-prebuilds
|
||||
working-directory: node
|
||||
|
||||
- name: Save x64 debug info
|
||||
run: mv build/Release/*-debuginfo.* .
|
||||
working-directory: node
|
||||
shell: bash
|
||||
|
||||
- name: Upload library
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: libsignal_client (${{matrix.os}})
|
||||
path: node/prebuilds/*
|
||||
|
||||
- name: Upload debug info
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: Debug info (${{matrix.os}})
|
||||
path: |
|
||||
node/*-debuginfo.*
|
||||
!node/*.sha256
|
||||
|
||||
build-docker:
|
||||
name: Build (Ubuntu via Docker)
|
||||
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: node/docker-prebuildify.sh
|
||||
|
||||
- name: Upload library
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: libsignal_client (ubuntu-docker)
|
||||
path: node/prebuilds/*
|
||||
|
||||
- name: Upload debug info
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: Debug info (ubuntu-docker)
|
||||
path: |
|
||||
node/*-debuginfo.*
|
||||
!node/*.sha256
|
||||
|
||||
verify-rust:
|
||||
name: Verify Node bindings
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
|
||||
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
|
||||
permissions:
|
||||
# Required for OIDC
|
||||
id-token: write
|
||||
# Needed for ncipollo/release-action.
|
||||
contents: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: [build, build-docker, verify-rust]
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Download built libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
id: download
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
pattern: libsignal_client*
|
||||
path: node/prebuilds
|
||||
merge-multiple: true
|
||||
path: artifacts
|
||||
|
||||
- name: Download debug info
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Debug info*
|
||||
path: debuginfo
|
||||
merge-multiple: true
|
||||
- name: Copy libraries
|
||||
run: mkdir node/prebuilds && mv ${{ steps.download.outputs.download-path }}/*/* node/prebuilds && find node/prebuilds
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- run: npm ci
|
||||
- run: yarn install --frozen-lockfile
|
||||
working-directory: node
|
||||
|
||||
- run: npm run tsc
|
||||
- run: yarn tsc
|
||||
working-directory: node
|
||||
|
||||
- run: npm run lint
|
||||
- run: yarn lint
|
||||
working-directory: node
|
||||
|
||||
- run: npm run format -c
|
||||
- run: yarn format -c
|
||||
working-directory: node
|
||||
|
||||
- run: npm run test
|
||||
- run: yarn test
|
||||
working-directory: node
|
||||
env:
|
||||
PREBUILDS_ONLY: 1
|
||||
|
||||
- run: npm publish --tag '${{ github.event.inputs.npm_tag }}' --access public ${{ inputs.dry_run && '--dry-run' || ''}}
|
||||
- if: ${{ !inputs.dry_run }}
|
||||
run: npm publish --tag ${{ github.event.inputs.npm_tag }} --access public
|
||||
working-directory: node
|
||||
|
||||
# This step is expected to fail if not run on a tag.
|
||||
- name: Upload debug info to release
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
|
||||
if: ${{ !inputs.dry_run }}
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: debuginfo/*-debuginfo.*
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
33
.github/workflows/release_notes.yml
vendored
33
.github/workflows/release_notes.yml
vendored
@ -1,33 +0,0 @@
|
||||
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.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, labeled, unlabeled, converted_to_draft, ready_for_review]
|
||||
# all target branches
|
||||
|
||||
env:
|
||||
LABEL_NAME: no release notes
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check for release notes
|
||||
# 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
|
||||
|
||||
permissions:
|
||||
# Needed to read the list of files modified by the pull request.
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Check for release notes change
|
||||
uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
skip-label: ${{ env.LABEL_NAME }}
|
||||
file-pattern: |
|
||||
RELEASE_NOTES.md
|
||||
failure-message: "RELEASE_NOTES.md is unchanged. If that's intentional, set the '${{ env.LABEL_NAME }}' tag"
|
||||
517
.github/workflows/slow_tests.yml
vendored
517
.github/workflows/slow_tests.yml
vendored
@ -1,11 +1,4 @@
|
||||
name: "Integration - Slow Tests"
|
||||
|
||||
env:
|
||||
ANDROID_NDK_VERSION: 28.0.13004108
|
||||
LIBSIGNAL_TESTING_CDSI_ENCLAVE_SECRET: ${{secrets.CDSI_ENCLAVE_SECRET}}
|
||||
LIBSIGNAL_TESTING_PROXY_SERVER: ${{secrets.LIBSIGNAL_TESTING_PROXY_SERVER}}
|
||||
LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS: true
|
||||
LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET: ${{secrets.LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET}}
|
||||
name: Slow Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@ -15,166 +8,51 @@ on:
|
||||
# We pick 8:25 UTC, aiming for "later than PST/UTC-8 night work" and
|
||||
# "earlier than ADT/UTC-3 morning work".
|
||||
- cron: '25 8 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ios_runner:
|
||||
description: 'Runner for iOS tests'
|
||||
required: true
|
||||
# This is redundant with specifying it at the use site, but makes it appear in the website UI.
|
||||
# See https://github.com/actions/runner-images/blob/main/README.md#available-images
|
||||
default: 'macos-15-xlarge'
|
||||
ignore_kt_tests:
|
||||
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
|
||||
workflow_dispatch: {} # no parameters
|
||||
|
||||
jobs:
|
||||
check-up-to-date:
|
||||
name: Already up to date?
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'schedule' && github.repository_owner == 'signalapp' && endsWith(github.repository, '-private') }}
|
||||
outputs:
|
||||
has-changes: ${{ steps.check.outputs.has-changes }}
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- run: git log --after '24 hours ago' --exit-code || echo 'has-changes=true' >> $GITHUB_OUTPUT
|
||||
id: check
|
||||
|
||||
java-docker:
|
||||
name: Java (Docker)
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-up-to-date]
|
||||
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
|
||||
|
||||
steps:
|
||||
- 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"
|
||||
- name: Print KT env toggle
|
||||
run: |
|
||||
echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=${LIBSIGNAL_TESTING_IGNORE_KT_TESTS:-<unset>}"
|
||||
- run: make -C java
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- run: make -C java java_test
|
||||
- name: Upload JNI libraries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: jniLibs
|
||||
path: java/android/src/main/jniLibs/*
|
||||
path: java/android/src/androidTest/jniLibs/*
|
||||
retention-days: 2
|
||||
- name: Upload full JARs
|
||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: aars
|
||||
path: java/android/build/outputs/aar/*
|
||||
retention-days: 2
|
||||
|
||||
java-docker-secondary:
|
||||
name: Java (Secondary Docker)
|
||||
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@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: make -C java
|
||||
- name: Upload full JARs
|
||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: aars-secondary
|
||||
path: java/android/build/outputs/aar/*
|
||||
retention-days: 2
|
||||
|
||||
java-reproducibility:
|
||||
name: Verify Java Reproducible Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: [java-docker, java-docker-secondary]
|
||||
if: ${{ needs.java-docker.result == 'success' && needs.java-docker-secondary.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Download jars
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: jars
|
||||
path: a/jars/
|
||||
- name: Download jars (secondary)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: jars-secondary
|
||||
path: b/jars/
|
||||
- name: Download aars
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: aars
|
||||
path: a/aars/
|
||||
- name: Download aars (secondary)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: aars-secondary
|
||||
path: b/aars/
|
||||
- run: diff -qr a/ b/
|
||||
|
||||
java-extra-bridging-checks:
|
||||
name: Java with runtime bridging checks
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- 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 -U protobuf-compiler
|
||||
|
||||
- run: ./gradlew :client:test :server:test -PskipAndroid -PjniTypeTagging -PjniCheckAnnotations
|
||||
working-directory: java
|
||||
|
||||
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: ${{ 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 || '' }}
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
needs: [java-docker]
|
||||
if: ${{ needs.java-docker.result == 'success' }}
|
||||
timeout-minutes: 45
|
||||
if: ${{ always() && needs.java-docker.result == 'success' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 23 is our minimal API level
|
||||
# 33 is our target API level
|
||||
include:
|
||||
- api_level: 23
|
||||
arch: x86
|
||||
- api_level: 23
|
||||
arch: x86_64
|
||||
- api_level: 33
|
||||
arch: x86_64
|
||||
arch: [x86, x86_64]
|
||||
|
||||
steps:
|
||||
- run: 'echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"'
|
||||
|
||||
- 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"
|
||||
|
||||
# For hardware acceleration; see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
@ -182,168 +60,68 @@ jobs:
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Download JNI libraries
|
||||
id: download
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
name: jniLibs
|
||||
path: java/android/src/main/jniLibs/
|
||||
path: java/android/src/androidTest/jniLibs/
|
||||
|
||||
# From reactivecircus/android-emulator-runner
|
||||
- name: AVD cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-${{ matrix.arch }}-${{ matrix.api_level }}-linux
|
||||
key: avd-${{ matrix.arch }}-21-linux
|
||||
|
||||
- name: Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api_level }}
|
||||
ndk: ${{ env.ANDROID_NDK_VERSION }}
|
||||
api-level: 21
|
||||
force-avd-creation: false
|
||||
disk-size: 4096M
|
||||
emulator-options: -no-window -noaudio -no-boot-anim -no-metrics
|
||||
emulator-options: -no-window -noaudio -no-boot-anim
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Run tests
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
api-level: ${{ matrix.api_level }}
|
||||
ndk: ${{ env.ANDROID_NDK_VERSION }}
|
||||
disk-size: 4096M
|
||||
api-level: 21
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim -no-metrics
|
||||
script: |
|
||||
adb logcat -c
|
||||
adb logcat > logcat.log &
|
||||
LOGCAT_PID="$!"
|
||||
echo "LOGCAT_PID=$LOGCAT_PID" >> "$GITHUB_ENV"
|
||||
./gradlew android:connectedCheck android:packaging-test:connectedCheck -x makeJniLibrariesDesktop -x android:makeJniLibraries
|
||||
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim
|
||||
script: ./gradlew android:connectedCheck -x makeJniLibrariesDesktop -x android:makeJniLibraries -x android:makeTestJniLibraries
|
||||
working-directory: java
|
||||
|
||||
- name: Stop logcat
|
||||
if: always()
|
||||
run: kill ${{ env.LOGCAT_PID}} || true
|
||||
|
||||
- name: Upload logcat logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: logcat-logs-api${{ matrix.api_level }}-${{ matrix.arch }}
|
||||
path: java/logcat.log
|
||||
retention-days: 2
|
||||
|
||||
aarch64:
|
||||
name: AArch64 Linux Tests
|
||||
runs-on: ubuntu-24.04-arm64-4-cores
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- 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 -U protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
# Skip building for Android; that's handled by the previous tests.
|
||||
- run: ./gradlew build -PskipAndroid | tee ./gradle-output.txt
|
||||
working-directory: java
|
||||
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
|
||||
|
||||
# Check for -Xcheck:jni warnings manually; Gradle doesn't capture them for some reason.
|
||||
- run: "! grep WARNING ./gradle-output.txt"
|
||||
working-directory: java
|
||||
|
||||
node-docker:
|
||||
name: Node (Ubuntu via Docker)
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-up-to-date]
|
||||
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
|
||||
|
||||
steps:
|
||||
- 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"
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- run: node/docker-prebuildify.sh
|
||||
- run: npm ci && npm run tsc && npm run test
|
||||
- run: yarn tsc && yarn test
|
||||
working-directory: node
|
||||
env:
|
||||
PREBUILDS_ONLY: 1
|
||||
- name: Upload prebuilds
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: node-prebuilds
|
||||
path: node/prebuilds
|
||||
retention-days: 2
|
||||
|
||||
node-docker-secondary:
|
||||
name: Node (Secondary Ubuntu via Docker)
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: node/docker-prebuildify.sh
|
||||
- name: Upload prebuilds
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: node-prebuilds-secondary
|
||||
path: node/prebuilds
|
||||
retention-days: 2
|
||||
|
||||
node-reproducibility:
|
||||
name: Verify Desktop Linux Reproducible Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: [node-docker, node-docker-secondary]
|
||||
if: ${{ needs.node-docker.result == 'success' && needs.node-docker-secondary.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Download prebuilds
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: node-prebuilds
|
||||
path: a/prebuilds/
|
||||
- name: Download prebuilds (secondary)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: node-prebuilds-secondary
|
||||
path: b/prebuilds/
|
||||
- run: diff -qr a/ b/
|
||||
|
||||
node-windows-arm64:
|
||||
name: Node (Windows ARM64 cross-compile)
|
||||
runs-on: windows-latest
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
needs: [check-up-to-date]
|
||||
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-pc-windows-msvc
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target aarch64-pc-windows-msvc
|
||||
# install nasm compiler for boring
|
||||
- name: Install nasm
|
||||
run: choco install nasm
|
||||
@ -354,216 +132,102 @@ jobs:
|
||||
- name: Get Node version from .nvmrc
|
||||
id: get-nvm-version
|
||||
shell: bash
|
||||
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
run: echo "node-version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Build for arm64
|
||||
run: npm run build -- --arch arm64 --copy-to-prebuilds
|
||||
- run: npx yarn install --ignore-scripts --frozen-lockfile
|
||||
working-directory: node
|
||||
- name: Build for arm64
|
||||
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }} --arch arm64
|
||||
working-directory: node
|
||||
env:
|
||||
npm_config_dist_url: https://unofficial-builds.nodejs.org/download/release
|
||||
|
||||
swift-cocoapod:
|
||||
name: Swift CocoaPod (all architectures)
|
||||
runs-on: ${{ inputs.ios_runner || 'macos-15-xlarge' }}
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
|
||||
# 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:
|
||||
# For Swift 6.2. Can be commented out again when the default runner moves to macos-26.
|
||||
DEVELOPER_DIR: /Applications/Xcode_26.3.app
|
||||
runs-on: macOS-latest
|
||||
needs: [check-up-to-date]
|
||||
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
|
||||
|
||||
steps:
|
||||
- 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"
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
|
||||
- name: Check out SignalCoreKit
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
repository: signalapp/SignalCoreKit
|
||||
path: SignalCoreKit
|
||||
|
||||
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
|
||||
|
||||
- run: brew install protobuf
|
||||
|
||||
- name: Build for x86_64-apple-ios
|
||||
run: swift/build_ffi.sh --release
|
||||
env:
|
||||
CARGO_BUILD_TARGET: x86_64-apple-ios
|
||||
|
||||
- name: Build for aarch64-apple-ios
|
||||
run: swift/build_ffi.sh --release
|
||||
env:
|
||||
CARGO_BUILD_TARGET: aarch64-apple-ios
|
||||
|
||||
# Build the simulator architectures for `pod lib lint` below.
|
||||
- name: Build for x86_64-apple-ios
|
||||
run: swift/build_ffi.sh --release
|
||||
env:
|
||||
CARGO_BUILD_TARGET: x86_64-apple-ios
|
||||
|
||||
- name: Build for aarch64-apple-ios-sim
|
||||
run: swift/build_ffi.sh --release
|
||||
env:
|
||||
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
|
||||
|
||||
# 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
|
||||
run: pod install
|
||||
working-directory: swift/cocoapods-testing
|
||||
|
||||
- name: Manually build for device
|
||||
run: xcodebuild -scheme LibSignalClient -sdk iphoneos build-for-testing
|
||||
working-directory: swift/cocoapods-testing
|
||||
|
||||
- name: Build in Release for device as well
|
||||
run: xcodebuild -scheme LibSignalClient -sdk iphoneos -configuration Release
|
||||
working-directory: swift/cocoapods-testing
|
||||
# No import validation because it tries to build unsupported platforms (like 32-bit iOS).
|
||||
run: pod lib lint --verbose --platforms=ios --include-podspecs=SignalCoreKit/SignalCoreKit.podspec --skip-import-validation
|
||||
|
||||
rust-stable-testing:
|
||||
name: Rust tests (using latest stable)
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-up-to-date]
|
||||
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
|
||||
|
||||
steps:
|
||||
- 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"
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- run: sudo apt-get install -U gcc-multilib g++-multilib protobuf-compiler
|
||||
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
|
||||
|
||||
- run: rustup +stable target add i686-unknown-linux-gnu
|
||||
|
||||
- run: cargo +stable install cargo-audit
|
||||
|
||||
- run: cargo +stable audit -D warnings
|
||||
|
||||
- name: Run tests
|
||||
run: cargo +stable test --workspace --all-features --verbose --no-fail-fast -- --include-ignored
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
|
||||
- name: Run hermetic tests without network access
|
||||
run: |
|
||||
TEST_CMD='unset LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS LIBSIGNAL_TESTING_CDSI_ENCLAVE_SECRET LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET && \
|
||||
cargo +stable test --workspace --all-features --verbose --no-fail-fast -- --include-ignored'
|
||||
./bin/run_with_network_isolation.sh "${TEST_CMD}"
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
run: cargo +stable test --workspace --all-features --verbose -- --include-ignored
|
||||
|
||||
- name: Test run benches
|
||||
# Run with a match-all regex to select all the benchmarks, which (confusingly) causes other tests to be skipped.
|
||||
run: cargo +stable test --workspace --benches --all-features --no-fail-fast --verbose '.*'
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
run: cargo +stable test --workspace --benches --all-features --verbose
|
||||
|
||||
- name: Build bins and examples
|
||||
run: cargo +stable build --workspace --bins --examples --all-features --verbose --keep-going
|
||||
|
||||
- name: Run libsignal-net smoke tests
|
||||
run: cargo +stable run --example chat_smoke_test -p libsignal-net --features="test-util" -- --try-all-routes staging
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
run: cargo +stable build --workspace --bins --examples --all-features --verbose
|
||||
|
||||
- name: Run tests (32-bit)
|
||||
# Exclude signal-neon-futures because those tests run Node
|
||||
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
|
||||
run: cargo +stable test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures -- --include-ignored
|
||||
|
||||
- name: cargo clean (reclaim disk space)
|
||||
# Clean the contents of the target directory to avoid running out of disk space during the
|
||||
# following steps.
|
||||
run: cargo +stable clean
|
||||
|
||||
- name: Run libsignal-protocol cross-version tests
|
||||
run: cargo +stable test --no-fail-fast
|
||||
run: cargo +stable test
|
||||
working-directory: rust/protocol/cross-version-testing
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
|
||||
- name: Run libsignal-protocol cross-version tests (32-bit)
|
||||
run: cargo +stable test --target i686-unknown-linux-gnu --no-fail-fast
|
||||
run: cargo +stable test --target i686-unknown-linux-gnu
|
||||
working-directory: rust/protocol/cross-version-testing
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
|
||||
# We don't run Clippy because GitHub silently updates `stable` and that can introduce new lints,
|
||||
# and we don't have a guarantee that any particular pinned nightly can build older libsignals.
|
||||
|
||||
rust-fuzzing:
|
||||
name: Rust fuzzing
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
|
||||
timeout-minutes: 45
|
||||
|
||||
env:
|
||||
CARGO_FUZZ_VERSION: 0.12.0
|
||||
FUZZ_TIME_SECONDS: 60
|
||||
FUZZ_JOBS: 4 # because this is a "4-cores" runner
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
|
||||
|
||||
- name: Cache cargo-fuzz
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: local-tools
|
||||
key: ${{ runner.os }}-fuzzing-local-tools-${{ env.CARGO_FUZZ_VERSION }}
|
||||
|
||||
- name: Install cargo-fuzz if needed
|
||||
run: cargo +stable install --version ${{ env.CARGO_FUZZ_VERSION }} --locked cargo-fuzz --root local-tools
|
||||
|
||||
- run: echo "$PWD/local-tools/bin" >> "$GITHUB_PATH"
|
||||
|
||||
# Note that these invocations will use libsignal's pinned toolchain,
|
||||
# but it's always possible cargo-fuzz will want an older/newer nightly.
|
||||
- run: cargo fuzz build interaction && cargo fuzz run interaction -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
|
||||
working-directory: rust/protocol
|
||||
|
||||
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: fuzzing-artifacts-${{ github.sha }}
|
||||
path: rust/*/fuzz/artifacts
|
||||
|
||||
# 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 && 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
|
||||
- java-extra-bridging-checks
|
||||
- android-emulator-tests
|
||||
- aarch64
|
||||
- node-docker
|
||||
- node-reproducibility
|
||||
- node-windows-arm64
|
||||
- swift-cocoapod
|
||||
- rust-stable-testing
|
||||
- rust-fuzzing
|
||||
needs: [java-docker, android-emulator-tests, node-docker, node-windows-arm64, swift-cocoapod, rust-stable-testing]
|
||||
if: ${{ failure() && github.event_name == 'schedule' }}
|
||||
|
||||
permissions:
|
||||
@ -572,13 +236,12 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha,
|
||||
body: 'Failed Slow Tests: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\n\n' +
|
||||
'Note that a [later run](${{ github.server_url }}/${{ github.repository }}/actions/workflows/slow_tests.yml) may have succeeded.'
|
||||
body: 'Failed Slow Tests: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>'
|
||||
})
|
||||
|
||||
37
.github/workflows/stale.yml
vendored
37
.github/workflows/stale.yml
vendored
@ -1,37 +0,0 @@
|
||||
name: "[auto] Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 12 * * *' # 7:15 EST, early in a workday
|
||||
workflow_dispatch: {} # no parameters
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
close-issue-message: >
|
||||
This issue has been closed due to inactivity.
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
close-pr-message: >
|
||||
This PR has been closed due to inactivity.
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
# These are comma-separated lists, if we ever want more than one.
|
||||
exempt-issue-labels: "acknowledged"
|
||||
exempt-pr-labels: "acknowledged"
|
||||
exempt-all-assignees: true
|
||||
operations-per-run: 30
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@ -1,8 +1,7 @@
|
||||
.idea
|
||||
/.idea
|
||||
*.iml
|
||||
/target
|
||||
/swift/**/.build
|
||||
/swift/**/.swiftpm
|
||||
/swift/.build
|
||||
/node/dist
|
||||
/node/node_modules
|
||||
/node/build
|
||||
@ -21,17 +20,8 @@ java/.gradle
|
||||
java/local.properties
|
||||
java/android/src/*/jniLibs
|
||||
java/android/src/main/assets/acknowledgments
|
||||
java/backup-tool/bin/
|
||||
java/client/bin/
|
||||
java/client/src/main/resources/*.dylib
|
||||
java/client/src/main/resources/*.so
|
||||
java/client/src/main/resources/*.dll
|
||||
java/server/bin/
|
||||
java/server/src/main/resources/*.dylib
|
||||
java/server/src/main/resources/*.so
|
||||
java/server/src/main/resources/*.dll
|
||||
java/shared/resources/*.dylib
|
||||
java/shared/resources/*.so
|
||||
java/shared/resources/*.dll
|
||||
|
||||
.DS_Store
|
||||
Brewfile.lock.json
|
||||
|
||||
__pycache__
|
||||
|
||||
@ -1,3 +1 @@
|
||||
imports_granularity = "Module"
|
||||
group_imports = "StdExternalCrate"
|
||||
style_edition = "2024"
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"lineLength": 120,
|
||||
"indentation": { "spaces": 4 },
|
||||
"indentConditionalCompilationBlocks": false,
|
||||
"lineBreakBeforeEachArgument": true,
|
||||
"lineBreakBetweenDeclarationAttributes": true,
|
||||
"prioritizeKeepingFunctionOutputTogether": true
|
||||
}
|
||||
25
.swiftformat
Normal file
25
.swiftformat
Normal file
@ -0,0 +1,25 @@
|
||||
#--header "\nCopyright {created.year} Signal Messenger, LLC.\nSPDX-License-Identifier: AGPL-3.0-only\n"
|
||||
--disable hoistPatternLet
|
||||
# Explicit self is better than implicit self.
|
||||
--self insert
|
||||
# Some arguments that it considers unused are used in doc comments, and replacing them with '_' is an error.
|
||||
--stripunusedargs unnamed-only
|
||||
--wraparguments before-first
|
||||
--wrapcollections before-first
|
||||
# Libsignal is a collection of many languages, remembering specific of each one is hard. Make it explicit.
|
||||
--disable redundantinternal
|
||||
# Ranges look better without spaces
|
||||
--ranges no-space
|
||||
# Pragmas should start at the begining of line.
|
||||
--ifdef outdent
|
||||
--indent 4
|
||||
# Patters are not redundant, they show the shape of thing, they show the shape of things.
|
||||
--disable redundantPattern
|
||||
# Leave try in the innermost position.
|
||||
--disable hoistTry
|
||||
# Explicit ACL even in extensions.
|
||||
--extensionacl "on-declarations"
|
||||
# Explicit is better than implicit.
|
||||
--disable redundantNilInit
|
||||
# Indentation for multi-line string literals.
|
||||
--indentstrings true
|
||||
@ -1 +0,0 @@
|
||||
0.9.0
|
||||
14
.taplo.toml
14
.taplo.toml
@ -1,14 +0,0 @@
|
||||
include = ["Cargo.toml", "rust/**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
align_comments = false
|
||||
indent_string = ' '
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "workspace.dependencies", "dev-dependencies", "build-dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
inline_table_expand = false
|
||||
reorder_keys = true
|
||||
@ -1 +0,0 @@
|
||||
java openjdk-17.0.2
|
||||
@ -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 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!
|
||||
- **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!
|
||||
|
||||
(Not that you should be sloppy in the bridging layer. Maintainability is still a priority!)
|
||||
|
||||
@ -21,21 +21,12 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
- **Every change should have tests** or be covered by existing tests. There are sometimes exceptions to this, but a lot of times the act of justifying the exception can suggest how to write the tests instead.
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
- **Logs should not contain user data**, including the default stringification for errors (Rust's Display, Java and TypeScript's `toString()`, Swift's `description`). "Debug" and "verbose" log levels are an exception to this, since they are turned off at compile time in our client library release builds. Note that this isn't "any information that can uniquely distinguish one user from another" (an ephemeral public key can do that, and there are legitimate reasons to log those; use your best judgment), but it is "any information that includes user input" (such as unencrypted usernames), "any information that can be linked back to a Signal account" (such as identity keys), and of course "any passwords or private keys".
|
||||
|
||||
One place where this is particularly subtle is when working with types that come from dependencies, especially errors. If the dependency has access to any such potentially-sensitive information, it's best to assume it could make it into arbitrary output, including error messages. The libsignal-net crate is particularly sensitive to this and constrains its errors with a custom LogSafeDisplay trait, but this isn't perfect.
|
||||
|
||||
Low-level objects like ServiceId and ProtocolAddress do not follow this rule; instead, they stringify in fixed formats that are easy to filter from higher-level logs en masse.
|
||||
|
||||
- **Logs should be kept minimal on success paths**. It's harder to find significant information in a sea of "operation succeeded!", and in the worst case we'd hit the log size limit sooner. (Clients only keep a few days of logs, and they'll keep even less if the recent logs are taking up too much space.) Even on failure paths, consider how much will end up in client logs, and if it'll be redundant with a higher-level log. Especially when logging in a loop. But don't go too far: it's important to know when certain events happen in relation to earlier or later failures.
|
||||
|
||||
As with the previous rule, this does not apply to the "debug" and "verbose" log levels, which are turned off at compile time in our client library release builds.
|
||||
|
||||
- **Only use "error"-level logs for bugs**. The apps and our log-processing tools may highlight "error"-level logs specially (e.g. asking the user to submit a debug log), so something bad that can happen for benign reasons like "a network connection dropped" should only be a "warning", not an "error". This doesn't have to be perfect, e.g. an incoming message might not be decryptable because the local user has restored their desktop OS from a snapshot. Instead, take it as "in the absence of other information, would we investigate this event alone as a possible bug?"
|
||||
|
||||
|
||||
# Rust
|
||||
|
||||
@ -45,30 +36,16 @@ 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.
|
||||
|
||||
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.
|
||||
(Yes, there's a Clippy lint for this, but we also have a lot of code that predates this guideline.)
|
||||
|
||||
- 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.
|
||||
|
||||
- We build with a pinned nightly toolchain, but **we also support stable**. The specific minimum supported version of stable is listed in our top-level Cargo.toml and checked in CI. We permit ourselves to bump this as needed, but try not to do so capriciously because we know external people might be in non-rustup scenarios where getting a new stable is tricky; in practice we often end up following tokio's "six months back" policy. If you need to bump the minimum supported version of stable, make sure the next release has a "breaking" version number.
|
||||
- We build with a pinned nightly toolchain, but **we also support stable**. The specific minimum supported version of stable is checked in CI (specifically, at the top of [build_and_test.yml](.github/workflows/build_and_test.yml)). We permit ourselves to bump this as needed, but try not to do so capriciously because we know external people might be in non-rustup scenarios where getting a new stable is tricky. If you need to bump the minimum supported version of stable, make sure the next release has a "breaking" version number.
|
||||
|
||||
- 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). 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.
|
||||
- **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 consistent guidelines for how to do errors in Rust, and the different crates do them differently. :-(
|
||||
|
||||
- When profiling on an aarch64 device, you need to **explicitly enable hardware AES support** in the `aes` crate:
|
||||
|
||||
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}"
|
||||
|
||||
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
|
||||
|
||||
@ -78,14 +55,10 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
More background here: "[Why doesn't tokio::select! require FusedFuture?](https://users.rust-lang.org/t/why-doesnt-tokio-select-require-fusedfuture/46975)"
|
||||
|
||||
- When bridging async APIs that use `#[bridge_io]`, **remember that the arguments and results will cross thread/queue/actor boundaries**, even in Node where there's only one JavaScript thread. Most of the time Rust's own Send/Sync checking will prevent this from being a problem, but whatever types are passed across the bridge layer will be unchecked, and you, the author of the code, will have to think about whether it's a problem (on both sides of the bridge). Usually it won't be! Value types like C structs and immutable Java objects are fine, it's only mutable objects and raw pointers where you have to be careful.
|
||||
|
||||
Async APIs that do not use `#[bridge_io]` are always run on the calling thread: for Java and Swift, they are run to completion immediately, and for Node they are run by being scheduled on the JavaScript microtask queue. However, in theory any calls back into app code could still lead to reentrant use, and any borrowed Rust objects might be accessed from other threads while the operation is ongoing.
|
||||
|
||||
|
||||
# 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 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.)
|
||||
- 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.)
|
||||
|
||||
- **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.
|
||||
|
||||
@ -93,28 +66,16 @@ These should usually be prioritized in that order, but adjust the trade-off as n
|
||||
|
||||
- **Write javadocs** unless an API is trivial (or not app-team-facing). Even for internal methods, though, if you do write a comment, make it a doc comment (like for Rust code), because it shows up in IDEs.
|
||||
|
||||
- Our Java code gets minified with [Android's R8] tool, which scans for usages of all items (classes, methods, fields) and prunes those that are never used. It can't see usages from Rust code via JNI, so additional annotations are required. **Annotate classes, methods, and fields that are accessed via JNI with `@CalledFromNative`**, which is recognized by the directives in [`libsignal.pro`], to ensure they are kept.
|
||||
|
||||
[Android's R8]: https://developer.android.com/build/shrink-code
|
||||
[`libsignal.pro`]: ./java/shared/resources/META-INF/proguard/libsignal.pro
|
||||
|
||||
|
||||
# Swift
|
||||
|
||||
- We support back to **iOS 15** (at the time of this writing), so newer APIs may not be available. This will be checked on build, so you can't get it wrong.
|
||||
- We support back to **iOS 13** (at the time of this writing), so newer APIs may not be available. This will be checked on build, so you can't get it wrong.
|
||||
|
||||
- **Write API docs** using [DocC syntax][] (a Markdown dialect), unless an API is trivial (or not app-team-facing). Even for internal methods, though, if you do write a comment, make it a doc comment (like for Rust code), because it shows up in IDEs.
|
||||
|
||||
- To make sure that error messages get into logs, we use the `failOnError` helper instead of `try!` for forcing an unwrap on the result of an operation that can throw an error.
|
||||
|
||||
- [`Sendable`][] is a part of Swift's concurrency-checking model similar to Rust's `Send` and `Sync`. In general, **any `public` struct or enum should be marked `Sendable`** unless it wraps something that isn't Sendable (or if it's an enum just used for namespacing). You don't have to do this for non-public structs and enums; Swift will infer whether they are Sendable within the library automatically.
|
||||
|
||||
Classes are trickier: a class that will forever be immutable is safe to mark `Sendable`, as is a class whose methods are designed to be called from multiple threads (often shortened to "this class is thread-safe"). However, unless you can make the class `final` *and* it doesn't have a superclass that's non-`Sendable`, the compiler won't be able to check it for you, and you'll have to write `@unchecked Sendable` instead. Be careful that this really is safe, and that we won't ever want to introduce mutating operations! It's easier to add Sendable later than to remove it, so err on the side of not including it.
|
||||
|
||||
As an approximation, `Sendable` in Swift is *roughly* equivalent to Rust's `Send` for value types (e.g. structs) and `Sync` for reference types (e.g. classes), because every reference in Swift has an implicit `Arc` around it.
|
||||
|
||||
[DocC syntax]: https://www.swift.org/documentation/docc/writing-symbol-documentation-in-your-source-files
|
||||
[`Sendable`]: https://developer.apple.com/documentation/swift/sendable
|
||||
|
||||
|
||||
# TypeScript
|
||||
|
||||
4621
Cargo.lock
generated
4621
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
228
Cargo.toml
228
Cargo.toml
@ -3,33 +3,27 @@
|
||||
members = [
|
||||
"rust/attest",
|
||||
"rust/crypto",
|
||||
"rust/debug",
|
||||
"rust/device-transfer",
|
||||
"rust/keytrans",
|
||||
"rust/media",
|
||||
"rust/message-backup",
|
||||
"rust/net",
|
||||
"rust/net/chat",
|
||||
"rust/net/infra",
|
||||
"rust/account-keys",
|
||||
"rust/pin",
|
||||
"rust/poksho",
|
||||
"rust/protocol",
|
||||
"rust/svr3",
|
||||
"rust/usernames",
|
||||
"rust/zkcredential",
|
||||
"rust/zkgroup",
|
||||
"rust/bridge/ffi",
|
||||
"rust/bridge/jni",
|
||||
"rust/bridge/jni/impl",
|
||||
"rust/bridge/jni/testing",
|
||||
"rust/bridge/node",
|
||||
"rust/bridge/node/native_ts",
|
||||
]
|
||||
default-members = [
|
||||
"rust/crypto",
|
||||
"rust/device-transfer",
|
||||
"rust/media",
|
||||
"rust/message-backup",
|
||||
"rust/account-keys",
|
||||
"rust/pin",
|
||||
"rust/poksho",
|
||||
"rust/protocol",
|
||||
"rust/usernames",
|
||||
@ -38,218 +32,10 @@ default-members = [
|
||||
]
|
||||
resolver = "2" # so that our dev-dependency features don't leak into products
|
||||
|
||||
[workspace.package]
|
||||
version = "0.94.1"
|
||||
authors = ["Signal Messenger LLC"]
|
||||
license = "AGPL-3.0-only"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Prefer TryFrom between integers unless truncation is desired.
|
||||
# For converting between floats and integers, there may not be an alternative.
|
||||
cast_possible_truncation = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(fuzzing)',
|
||||
'cfg(tokio_unstable)',
|
||||
] }
|
||||
|
||||
[workspace.dependencies]
|
||||
# Our own crates, so that we don't have to depend on them by inter-crate paths
|
||||
attest = { path = "rust/attest" }
|
||||
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" }
|
||||
signal-crypto = { path = "rust/crypto" }
|
||||
signal-media = { path = "rust/media" }
|
||||
usernames = { path = "rust/usernames" }
|
||||
zkcredential = { path = "rust/zkcredential" }
|
||||
zkgroup = { path = "rust/zkgroup" }
|
||||
|
||||
libsignal-bridge = { path = "rust/bridge/shared" }
|
||||
libsignal-bridge-macros = { path = "rust/bridge/shared/macros" }
|
||||
libsignal-bridge-testing = { path = "rust/bridge/shared/testing" }
|
||||
libsignal-bridge-types = { path = "rust/bridge/shared/types" }
|
||||
libsignal-jni-impl = { path = "rust/bridge/jni/impl" }
|
||||
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-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.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.23.0"
|
||||
assert_cmd = "2.0.13"
|
||||
assert_matches = "1.5"
|
||||
async-compression = "0.4.5"
|
||||
async-trait = "0.1.79"
|
||||
atomic-take = "1.1.0"
|
||||
auto_enums = "0.8.7"
|
||||
base64 = "0.22.1"
|
||||
bincode = "1.3.2"
|
||||
bitflags = "2.9"
|
||||
bitstream-io = "1.10.0"
|
||||
blake2 = "0.10.6"
|
||||
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.42"
|
||||
clap = "4.4.11"
|
||||
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.6.1"
|
||||
derive_more = "2.0.0"
|
||||
dir-test = "0.4.1"
|
||||
displaydoc = "0.2.5"
|
||||
ed25519-dalek = "2.1.0"
|
||||
either = "1.13.0"
|
||||
env_logger = "0.11.7"
|
||||
flate2 = { version = "1.1.1", default-features = false }
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
ghash = "0.5.0"
|
||||
heck = "0.5"
|
||||
hex = "0.4.3"
|
||||
hickory-proto = "0.26.1"
|
||||
hkdf = "0.12"
|
||||
hmac = "0.12.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.7.0"
|
||||
intmap = "3.1.2"
|
||||
itertools = "0.14.0"
|
||||
jni = "0.21"
|
||||
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"
|
||||
macro_rules_attribute = "0.2.0"
|
||||
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.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.14"
|
||||
prost-build = "0.14"
|
||||
prost-types = "0.14"
|
||||
protobuf = "3.7.2"
|
||||
protobuf-codegen = "3.7.2"
|
||||
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.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"
|
||||
snow = { version = "0.10", default-features = false }
|
||||
socks5-server = "0.10.1"
|
||||
static_assertions = "1.1"
|
||||
strum = "0.27.0"
|
||||
subtle = "2.6"
|
||||
syn = "2.0.98"
|
||||
syn-mid = "0.6"
|
||||
test-case = "3.3"
|
||||
test-log = "0.2.16"
|
||||
testing_logger = "0.1.1"
|
||||
thiserror = "2.0.11"
|
||||
tls-parser = "0.12.2"
|
||||
tokio = "1.52.2"
|
||||
tokio-socks = "0.5.2"
|
||||
tokio-stream = "0.1.16"
|
||||
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.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.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-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' }
|
||||
# Use our fork of curve25519-dalek for zkgroup support.
|
||||
curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.1' }
|
||||
boring = { git = 'https://github.com/signalapp/boring', branch = 'libsignal' }
|
||||
|
||||
[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
|
||||
opt-level = 2 # signal-signal-pin unit tests are too slow with an unoptimized argon2
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'LibSignalClient'
|
||||
s.version = '0.94.1'
|
||||
s.version = '0.41.2'
|
||||
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
|
||||
|
||||
s.homepage = 'https://github.com/signalapp/libsignal'
|
||||
@ -14,16 +14,18 @@ Pod::Spec.new do |s|
|
||||
s.source = { :git => 'https://github.com/signalapp/libsignal.git', :tag => "v#{s.version}" }
|
||||
|
||||
s.swift_version = '5'
|
||||
s.platform = :ios, '15.0'
|
||||
s.platform = :ios, '13.0'
|
||||
|
||||
s.dependency 'SignalCoreKit'
|
||||
|
||||
s.source_files = ['swift/Sources/**/*.swift', 'swift/Sources/**/*.m']
|
||||
s.preserve_paths = [
|
||||
'swift/Sources/SignalFfi',
|
||||
'bin/fetch_archive.py',
|
||||
'acknowledgments/acknowledgments-ios.plist',
|
||||
'acknowledgments/acknowledgments.plist',
|
||||
]
|
||||
|
||||
pod_target_xcconfig = {
|
||||
s.pod_target_xcconfig = {
|
||||
'HEADER_SEARCH_PATHS' => '$(PODS_TARGET_SRCROOT)/swift/Sources/SignalFfi',
|
||||
# Duplicate this here to make sure the search path is passed on to Swift dependencies.
|
||||
'SWIFT_INCLUDE_PATHS' => '$(HEADER_SEARCH_PATHS)',
|
||||
@ -42,7 +44,6 @@ Pod::Spec.new do |s|
|
||||
|
||||
'CARGO_BUILD_TARGET[sdk=iphonesimulator*][arch=arm64]' => 'aarch64-apple-ios-sim',
|
||||
'CARGO_BUILD_TARGET[sdk=iphonesimulator*][arch=*]' => 'x86_64-apple-ios',
|
||||
'CARGO_BUILD_TARGET[sdk=iphoneos*][arch=arm64e]' => 'arm64e-apple-ios',
|
||||
'CARGO_BUILD_TARGET[sdk=iphoneos*]' => 'aarch64-apple-ios',
|
||||
# Presently, there's no special SDK or arch for maccatalyst,
|
||||
# so we need to hackily use the "IS_MACCATALYST" build flag
|
||||
@ -57,29 +58,10 @@ 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'
|
||||
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',
|
||||
{ name: 'Download and cache libsignal-ffi',
|
||||
execution_position: :before_compile,
|
||||
# It's not *ideal* to check the cache every build, but it's usually just a shasum.
|
||||
# It might be possible to rely on the relative mtimes of the podspec and the fetched archive,
|
||||
# but I wouldn't want to risk a mismatched archive giving us cryptic errors at link or run
|
||||
# time later. This Is Fine.
|
||||
always_out_of_date: '1',
|
||||
script: %q(
|
||||
set -euo pipefail
|
||||
if [ -e "${PODS_TARGET_SRCROOT}/swift/build_ffi.sh" ]; then
|
||||
@ -98,7 +80,7 @@ Pod::Spec.new do |s|
|
||||
rm -rf "${LIBSIGNAL_FFI_TEMP_DIR}"
|
||||
if [ -e "${PODS_TARGET_SRCROOT}/swift/build_ffi.sh" ]; then
|
||||
# Local development
|
||||
ln -fns "${PODS_TARGET_SRCROOT}" "${LIBSIGNAL_FFI_TEMP_DIR}"
|
||||
ln -fhs "${PODS_TARGET_SRCROOT}" "${LIBSIGNAL_FFI_TEMP_DIR}"
|
||||
elif [ -e "${SCRIPT_INPUT_FILE_0}" ]; then
|
||||
mkdir -p "${LIBSIGNAL_FFI_TEMP_DIR}"
|
||||
cd "${LIBSIGNAL_FFI_TEMP_DIR}"
|
||||
@ -116,16 +98,9 @@ Pod::Spec.new do |s|
|
||||
test_spec.preserve_paths = [
|
||||
'swift/Tests/*/Resources',
|
||||
]
|
||||
test_pod_target_xcconfig = {
|
||||
test_spec.pod_target_xcconfig = {
|
||||
# Don't also link into the test target.
|
||||
'LIBSIGNAL_FFI_LIB_TO_LINK' => '',
|
||||
}
|
||||
test_spec.pod_target_xcconfig = test_pod_target_xcconfig
|
||||
|
||||
# Ideally we'd do this at run time, not configuration time, but CocoaPods doesn't make that easy.
|
||||
# This is good enough.
|
||||
test_spec.scheme = {
|
||||
environment_variables: ENV.select { |name, value| name.start_with?('LIBSIGNAL_TESTING_') }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
147
README.md
147
README.md
@ -12,7 +12,7 @@ as a Java, Swift, or TypeScript library. The underlying implementations are writ
|
||||
- zkgroup: Functionality for [zero-knowledge groups][] and related features available in Signal.
|
||||
- zkcredential: An abstraction for the sort of zero-knowledge credentials used by zkgroup, based on the paper "[The Signal Private Group System][]" by Chase, Perrin, and Zaverucha.
|
||||
- poksho: Utilities for implementing zero-knowledge proofs (such as those used by zkgroup); stands for "proof-of-knowledge, stateful-hash-object".
|
||||
- account-keys: Functionality for consistently using [PINs][] as passwords in Signal's Secure Value Recovery system, as well as other account-wide key operations.
|
||||
- pin: Functionality for consistently using [PINs][] as passwords in Signal's Secure Value Recovery system.
|
||||
- usernames: Functionality for username generation, hashing, and proofs.
|
||||
- media: Utilities for manipulating media.
|
||||
|
||||
@ -41,31 +41,14 @@ increases to the minimum supported tools versions.
|
||||
|
||||
# Building
|
||||
|
||||
### Toolchain Installation
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
On a Debian-like system, you can get these extra dependencies through `apt`:
|
||||
|
||||
```shell
|
||||
$ apt-get install clang libclang-dev cmake make protobuf-compiler libprotobuf-dev python3 git
|
||||
$ apt-get install clang libclang-dev cmake make protobuf-compiler git
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
On macOS, we have a best-effort maintained script to set up the Rust toolchain you can run by:
|
||||
|
||||
```shell
|
||||
$ bin/mac_setup.sh
|
||||
```
|
||||
|
||||
## Rust
|
||||
|
||||
### First Build and Test
|
||||
|
||||
The build currently uses a specific version of the Rust nightly compiler, which
|
||||
will be downloaded automatically by cargo. To build and test the basic protocol
|
||||
libraries:
|
||||
@ -77,53 +60,13 @@ $ cargo test
|
||||
...
|
||||
```
|
||||
|
||||
### Additional Rust Tools
|
||||
|
||||
The basic tools above should get you set up for most libsignal Rust development.
|
||||
|
||||
Eventually, you may find that you need some additional Rust tools like `cbindgen` to modify the bridges to the
|
||||
client libraries or `taplo` for code formatting.
|
||||
|
||||
You should always install any Rust tools you need that may affect the build from cargo rather than from your system
|
||||
package manager (e.g. `apt` or `brew`). Package managers sometimes contain outdated versions of these tools that can break
|
||||
the build with incompatibility issues (especially cbindgen).
|
||||
|
||||
To install the main Rust extra dependencies matching the versions we use, you can run the following commands:
|
||||
|
||||
```shell
|
||||
$ cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
|
||||
$ cargo +stable install --version "$(cat acknowledgments/cargo-about-version)" --locked cargo-about
|
||||
$ cargo +stable install --version "$(cat .taplo-cli-version)" --locked taplo-cli
|
||||
$ cargo +stable install cargo-fuzz
|
||||
```
|
||||
|
||||
## Java/Android
|
||||
|
||||
### Toolchain Setup / Configuration
|
||||
|
||||
To build for Android you must install several additional packages including a JDK,
|
||||
the Android NDK/SDK, and add the Android targets to the Rust compiler, using
|
||||
|
||||
```rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android```
|
||||
|
||||
Our officially supported JDK version for Android builds is JDK 17, so be sure to install e.g. OpenJDK 17, and then point JAVA_HOME to it.
|
||||
|
||||
You can easily do this on macOS via:
|
||||
|
||||
```shell
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
|
||||
```
|
||||
|
||||
On Linux, the way you do this varies by distribution. For Debian based distributions like Ubuntu, you can use:
|
||||
|
||||
```shell
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
We also check-in a `.tools_version` file for use with runtime version managers.
|
||||
|
||||
### Building and Testing
|
||||
|
||||
To build the Java/Android ``jar`` and ``aar``, and run the tests:
|
||||
|
||||
```shell
|
||||
@ -132,64 +75,41 @@ $ ./gradlew test
|
||||
$ ./gradlew build # if you need AAR outputs
|
||||
```
|
||||
|
||||
You can pass `-P debugLevelLogs` to Gradle to build without filtering out debug- and verbose-level
|
||||
logs from Rust, and `-P jniTypeTagging` to enable additional checks in the Rust JNI bridging code.
|
||||
|
||||
Alternately, a build system using Docker is available:
|
||||
|
||||
```shell
|
||||
$ cd java
|
||||
$ make
|
||||
$ make java_test
|
||||
```
|
||||
|
||||
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.
|
||||
addition to rebuilding.
|
||||
|
||||
### Use as a library
|
||||
### Maven Central
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 `packaging`:
|
||||
explicitly exclude them using `packagingOptions`:
|
||||
|
||||
```
|
||||
android {
|
||||
// ...
|
||||
packaging {
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += setOf("libsignal_jni*.dylib", "signal_jni*.dll")
|
||||
exclude "libsignal_jni.dylib"
|
||||
exclude "signal_jni.dll"
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@ -201,20 +121,19 @@ To learn about the Swift build process see [``swift/README.md``](swift/)
|
||||
You'll need Node installed to build. If you have [nvm][], you can run `nvm use` to select an
|
||||
appropriate version automatically.
|
||||
|
||||
We use `npm` as our package manager, and a Python script to control building the Rust library, accessible as `npm run build`.
|
||||
We use [`yarn`](https://classic.yarnpkg.com/) as our package manager. The Rust library will automatically be built when you run `yarn install`.
|
||||
|
||||
```shell
|
||||
$ cd node
|
||||
$ nvm use
|
||||
$ npm install
|
||||
$ npm run build
|
||||
$ npm run tsc
|
||||
$ npm run test
|
||||
$ yarn install
|
||||
$ yarn tsc
|
||||
$ yarn 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 testing changes locally, you can use `yarn build` to do an incremental rebuild of the Rust library.
|
||||
|
||||
When exposing new APIs to Node, you will need to run `just generate-node` in
|
||||
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
|
||||
addition to rebuilding.
|
||||
|
||||
[nvm]: https://github.com/nvm-sh/nvm
|
||||
@ -226,10 +145,6 @@ 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
|
||||
|
||||
@ -244,22 +159,6 @@ the project.
|
||||
|
||||
Signing a [CLA (Contributor License Agreement)](https://signal.org/cla/) is required for all contributions.
|
||||
|
||||
## Code Formatting and Acknowledgments
|
||||
|
||||
You can run the styler on the entire project by running:
|
||||
|
||||
```shell
|
||||
just format-all
|
||||
```
|
||||
|
||||
You can run more extensive tests as well as linters and clippy by running:
|
||||
|
||||
```shell
|
||||
just check-pre-commit
|
||||
```
|
||||
|
||||
When making a PR that adjusts dependencies, you'll need to regenerate our acknowledgments files. See [``acknowledgments/README.md``](acknowledgments/).
|
||||
|
||||
# Legal things
|
||||
## Cryptography Notice
|
||||
|
||||
@ -276,6 +175,6 @@ Administration Regulations, Section 740.13) for both object code and source code
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2020-2026 Signal Messenger, LLC
|
||||
Copyright 2020-2024 Signal Messenger, LLC
|
||||
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
143
RELEASE.md
143
RELEASE.md
@ -1,91 +1,76 @@
|
||||
# Making a libsignal release
|
||||
|
||||
## 1. Run bin/prepare_release.py
|
||||
## 0. Make sure all CI tests are passing on the latest commit
|
||||
|
||||
We maintain a helper script, prepare_release.py, to automate most of the rote work involved in cutting a release.
|
||||
Check GitHub to see if the latest commit has all tests passing, including the nightly "Slow Tests". If not, fix the tests before releasing! (You can run the Slow Tests manually under the repository Actions tab on GitHub.)
|
||||
|
||||
This script:
|
||||
|
||||
1. Automatically checks to ensure the Continuous Integration tests pass.
|
||||
2. Tags the release commit with the appropriate annotated tag, with the version number as the name and the release notes as the comment.
|
||||
3. Prepares the repository for the next version, by:
|
||||
1. Recording the code size of the just-released version in the repository,
|
||||
2. Clearing RELEASE_NOTES.md and preparing it for new release notes for the presumed next version,
|
||||
3. Updating the version number references throughout the repository to match the presumed next version, and finally
|
||||
4. commiting all these changes in a single commit.
|
||||
|
||||
All these steps can be done manually if desired/needed, but the script makes it easier, incentivizing more frequent releases.
|
||||
|
||||
## 2. Push the release commit to signalapp/libsignal on GitHub
|
||||
|
||||
Once you have tagged a release commit using the script, you should push it to GitHub as discussed below. After you have pushed the tag, you can then kick off the submission of that version to the package repositories.
|
||||
|
||||
#### Pushing to Multiple Remotes
|
||||
|
||||
If you need to push the multiple remotes, you must take care, as it is a little tricky to ensure each remote ends up in the desired end state.
|
||||
|
||||
#### Pushing Only the Release to a Remote
|
||||
|
||||
If you want to push just the newly cut release to a remote, you need to push the following items:
|
||||
|
||||
1. All commits up to and including the tagged commit that marks the release. (This commit should be `HEAD~1` after running the `./bin/prepare_release.py` script.)
|
||||
2. You should fast forward the main branch ref on that remote to point to that same commit.
|
||||
3. You should also push the tag marking the release you just cut.
|
||||
|
||||
Pushing all these items generally looks something like this:
|
||||
|
||||
```
|
||||
git push <remote> HEAD~1:main <release tag, e.g. v0.x.y>
|
||||
```
|
||||
|
||||
#### Pushing the Release and the Preparation Commit to a Remote
|
||||
|
||||
If you want to push both the release and the preparation commit that resets the repository state in anticipation of the next commit to a remote, so that e.g. you can continue working on the next release, you need to push the following items:
|
||||
|
||||
1. All commits up to and including the preparation commit, which should be `HEAD` on after running `./bin/prepare_release.py`.
|
||||
2. You should fast-forward the main branch ref to point to that preparation commit.
|
||||
3. You should also push the tag marking the release you just cut.
|
||||
|
||||
Pushing all these items should generally look like:
|
||||
|
||||
```
|
||||
git push <remote> HEAD:main <release tag, e.g. v0.x.y>
|
||||
```
|
||||
|
||||
## 3. Submit to package repositories as needed
|
||||
|
||||
### Android and Server: Maven
|
||||
|
||||
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 "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 "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
|
||||
|
||||
### Versioning Methodology
|
||||
## 1. Update the library version
|
||||
|
||||
The first version component should always be 0, to indicate that Signal does not promise stability between releases of the library.
|
||||
|
||||
A change is "breaking" if it will require updates in any of the Signal client apps or server components, or in external Rust clients of libsignal-protocol, zkgroup, poksho, attest, device-transfer, or signal-crypto. If there are any breaking changes, increase the second version component and reset the third to 0. Otherwise, increase the third version component.
|
||||
|
||||
### Release Notes Formatting
|
||||
|
||||
As we work, we keep running release notes in RELEASE_NOTES.md.
|
||||
|
||||
The format of these release notes should generally look something like:
|
||||
|
||||
```
|
||||
v0.x.y
|
||||
|
||||
- Bar: Added a fancy new feature
|
||||
- Fixed a bug in the foo crate
|
||||
- Android: Exposed baz to Java clients
|
||||
bin/update_versions.py 0.x.y
|
||||
cargo check --workspace --all-features # make sure Cargo.lock is updated
|
||||
bin/regenerate_acknowledgments.sh # include the new version number in the acknowledgments
|
||||
```
|
||||
|
||||
v0.x.y is the version of the release. The changes are then listed in arbitrary order. It's important that the tag comment also includes the version number as the first line, because GitHub formats it as a title.
|
||||
## 2. Record the code size for the Java library
|
||||
|
||||
On GitHub, under the Java tests for the most recent commit, copy the code size computed in the "java/check_code_size.py" step into a new entry in java/code_size.json.
|
||||
|
||||
## 3. Commit the version change and tag with release notes
|
||||
|
||||
```
|
||||
git commit -am 'Bump to version v0.x.y'
|
||||
git tag -a v0.x.y
|
||||
```
|
||||
|
||||
Take a look at a past release for examples of the format:
|
||||
|
||||
```
|
||||
v0.8.3
|
||||
|
||||
- Fixed several issues running signal-crypto operations on 32-bit
|
||||
platforms.
|
||||
- Removed custom implementation of AES-GCM-SIV, AES, AES-CTR, and
|
||||
GHash in favor of the implementations from RustCrypto. The interface
|
||||
presented to Java, Swift, and TypeScript clients has not changed.
|
||||
- Updated several Rust dependencies.
|
||||
- Java: Exposed the tag size for Aes256GcmDecryption.
|
||||
```
|
||||
|
||||
(You might think repeating the version number in the summary field is redundant, but GitHub shows it as a title.)
|
||||
|
||||
## 4. Push the version bump and tag to GitHub
|
||||
|
||||
Note that both the tag *and* the branch need to be pushed.
|
||||
|
||||
## 5. Tag signalapp/boring if needed
|
||||
|
||||
If the depended-on version of `boring` has changed (check Cargo.lock), tag the commit in the public [signalapp/boring][] repository.
|
||||
|
||||
```
|
||||
# In the checkout for signalapp/boring
|
||||
git tag -a libsignal-v0.x.y -m 'libsignal v0.x.y' BORING_COMMIT_HASH
|
||||
git push origin libsignal-v0.x.y
|
||||
```
|
||||
|
||||
[signalapp/boring]: https://github.com/signalapp/boring
|
||||
|
||||
## 6. Submit to package repositories as needed
|
||||
|
||||
### Android and Server: Sonatype
|
||||
|
||||
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
|
||||
|
||||
### 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".
|
||||
|
||||
### 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.
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
v0.94.1
|
||||
|
||||
- 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.
|
||||
13
TESTING.md
13
TESTING.md
@ -8,7 +8,7 @@ For the most part, libsignal is tested using each language's usual testing infra
|
||||
% ./gradlew client:test server:test android:connectedAndroidTest
|
||||
|
||||
# Node
|
||||
% npm run build && npm run tsc && npm run test
|
||||
% yarn build && yarn tsc && yarn test
|
||||
|
||||
# Swift
|
||||
% ./build_ffi.sh --generate-ffi && swift test
|
||||
@ -17,15 +17,6 @@ For the most part, libsignal is tested using each language's usual testing infra
|
||||
However, sometimes there are some more interesting test configurations; those are documented here.
|
||||
|
||||
|
||||
# Rust Benchmarks
|
||||
|
||||
- If you are testing on an ARM64 device (including Desktop), you should compile with `RUSTFLAGS="--cfg aes_armv8"` to enable hardware support in the `aes` crate.
|
||||
|
||||
- Similarly, although most tests are not very sensitive to the speed of SHA-2, you should also compile with `--features sha2/asm`. (`libsignal-message-backup` turns this on by default as a dev-dependency.) This will go away when we get to update to sha2 0.11.
|
||||
|
||||
All of these configuration options are normally set either at the bridge crate level or in the build scripts for each bridged platform, but they may not be set when running with plain `cargo bench`.
|
||||
|
||||
|
||||
# Running cross-compiling Rust tests with custom runners
|
||||
|
||||
Rust allows running tests with cross-compiled targets, but normally that only works if your system supports executing the cross-compiled binary (like Intel targets on ARM64 macOS or Windows, or 32-bit targets on 64-bit Linux or Windows). However, by overriding the "runner" setting for a particular target, we can run cross-compiled tests as well.
|
||||
@ -42,7 +33,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-android23-clang
|
||||
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_RUNNER=bin/adb-run-test # in the repo root
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,10 @@ Then:
|
||||
Apart from the projects in this very repo, there are a few other crates that unavoidably have "synthesized" licenses based on their Cargo manifests:
|
||||
|
||||
- cesu8: Very old crate whose repository contains a license file for the Rust project itself, rather than the crate.
|
||||
- hpke-rs-\*: Uploaded without a license file, though a license is listed in the Cargo.toml for each crate.
|
||||
- curve25519-dalek-derive: Uploaded without a license file, though a license is listed in the Cargo.toml. Not the same as the license of curve25519-dalek.
|
||||
- half: Not actually synthesized! Their license file just matches the synthesized text perfectly. A bug in cargo-about, presumably.
|
||||
- pqcrypto-\*: Uploaded without a license file, though a license is listed in the Cargo.toml for each crate. The Kyber implementations we use are released as [Public Domain][kyber], so no acknowledgment is necessary.
|
||||
|
||||
[cargo-about]: https://embarkstudios.github.io/cargo-about/
|
||||
[clarify]: https://embarkstudios.github.io/cargo-about/cli/generate/config.html#the-clarify-field-optional
|
||||
[kyber]: https://github.com/PQClean/PQClean/blob/round3/crypto_kem/kyber1024/clean/LICENSE
|
||||
|
||||
@ -6,9 +6,8 @@ accepted = [
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MPL-2.0",
|
||||
"AGPL-3.0-only",
|
||||
"Unicode-3.0",
|
||||
"AGPL-3.0",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
|
||||
@ -24,35 +23,26 @@ no-clearly-defined = true
|
||||
# in a fraction of the time.
|
||||
max-depth = 1
|
||||
|
||||
workarounds = [
|
||||
"chrono",
|
||||
"prost",
|
||||
"ring",
|
||||
"tonic",
|
||||
# List every target we ship, just in case some dependencies are platform-gated.
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
|
||||
"aarch64-apple-ios",
|
||||
|
||||
"aarch64-linux-android",
|
||||
"armv7-linux-androideabi",
|
||||
"i686-linux-android",
|
||||
"x86_64-linux-android",
|
||||
]
|
||||
|
||||
|
||||
# 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"
|
||||
workarounds = [
|
||||
"chrono"
|
||||
]
|
||||
|
||||
|
||||
# Boring's main license isn't at the root of the repo
|
||||
@ -69,7 +59,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 Apache-2.0"
|
||||
license = "MIT AND ISC AND OpenSSL"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The MIT license of the Rust code
|
||||
@ -78,10 +68,37 @@ license = "MIT"
|
||||
checksum = "ad2e7bdef7c00b92eaf4f657a472c7d3f8b36aac3cdc270e65bb0c287eec0d4e"
|
||||
|
||||
[[boring-sys.clarify.files]]
|
||||
# The Apache 2.0 license of BoringSSL
|
||||
# The original OpenSSL license
|
||||
path = "deps/boringssl/LICENSE"
|
||||
license = "Apache-2.0"
|
||||
checksum = "827c8d8fc207c2392794eef9e00fe246f9f61fdcc132556c275be3dd8c3cd97f"
|
||||
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"
|
||||
|
||||
|
||||
# const-str is embedded in a larger repo
|
||||
[const-str.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[const-str.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "565aacda8f5ea53f937f867ed49a0ac7e6828b60b4568803185cd0a13297d4e4"
|
||||
|
||||
|
||||
# Newer versions of convert_case have a LICENSE file, we'll use that one
|
||||
@ -133,113 +150,44 @@ path = "LICENSE"
|
||||
checksum = "d2c376e2d8ee747383aaf0d5b52997bd6aa04ab73720b6797edeb64fe90c05a3"
|
||||
|
||||
|
||||
# The hax crates are embedded in a larger repo.
|
||||
[hax-lib.clarify]
|
||||
license = "Apache-2.0"
|
||||
# half includes both its licenses as separate files
|
||||
[half.clarify]
|
||||
license = "MIT" # OR Apache-2.0, but we're using MIT
|
||||
|
||||
[[hax-lib.clarify.git]]
|
||||
[[half.clarify.files]]
|
||||
path = "LICENSES/MIT.txt"
|
||||
checksum = "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0"
|
||||
|
||||
|
||||
# http-body is embedded in a larger repo
|
||||
[http-body.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[http-body.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "9a50bad5a51e0ad726ea3a7f4b7b758e1b4d1784e6abefe1367f5bf01e972725"
|
||||
checksum = "0345e2b98685e3807fd802a2478085dcae35023e3da59b5a00f712504314d83a"
|
||||
|
||||
[hax-lib-macros.clarify]
|
||||
license = "Apache-2.0"
|
||||
[http-body-util.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[hax-lib-macros.clarify.git]]
|
||||
[[http-body-util.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "9a50bad5a51e0ad726ea3a7f4b7b758e1b4d1784e6abefe1367f5bf01e972725"
|
||||
checksum = "0345e2b98685e3807fd802a2478085dcae35023e3da59b5a00f712504314d83a"
|
||||
|
||||
|
||||
# The libcrux crates are embedded in a larger repo.
|
||||
[libcrux-hacl-rs.clarify]
|
||||
license = "Apache-2.0"
|
||||
# linkme-impl is embedded in a larger repo.
|
||||
[linkme-impl.clarify]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[[libcrux-hacl-rs.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-hkdf.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-hkdf.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-hmac.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-hmac.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-intrinsics.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-intrinsics.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-macros.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-macros.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-ml-kem.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-ml-kem.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-platform.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-platform.clarify.git]]
|
||||
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"
|
||||
|
||||
[[libcrux-sha2.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-sha3.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-sha3.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
|
||||
[libcrux-traits.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[libcrux-traits.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
|
||||
[[linkme-impl.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"
|
||||
|
||||
|
||||
# miniz_oxide's LICENSE and LICENSE-MIT.md don't get consistently chosen between. Force the choice here.
|
||||
[miniz_oxide.clarify]
|
||||
license = "MIT OR Zlib OR Apache-2.0"
|
||||
|
||||
[[miniz_oxide.clarify.files]]
|
||||
path = "LICENSE"
|
||||
checksum = "4108245a1f2df9d4e94df8abed5b4ba0759bb2f9b40a6b939f1be141077ae50b"
|
||||
|
||||
|
||||
# mp4san is embedded in a larger repo
|
||||
# mp4san is embedded in a larger repo, and has a tag that doesn't match the revision in Cargo
|
||||
[mediasan-common.clarify]
|
||||
license = "MIT"
|
||||
override-git-commit = "0.5.1"
|
||||
|
||||
[[mediasan-common.clarify.git]]
|
||||
path = "LICENSE"
|
||||
@ -247,6 +195,7 @@ checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
|
||||
|
||||
[mp4san.clarify]
|
||||
license = "MIT"
|
||||
override-git-commit = "0.5.1"
|
||||
|
||||
[[mp4san.clarify.git]]
|
||||
path = "LICENSE"
|
||||
@ -254,20 +203,13 @@ checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
|
||||
|
||||
[mp4san-derive.clarify]
|
||||
license = "MIT"
|
||||
override-git-commit = "0.5.1"
|
||||
|
||||
[[mp4san-derive.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
|
||||
|
||||
|
||||
[neon.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[neon.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "e47f19ffc3ed618c75d166781681b27c30f841f9b5b10fc488150b9128b19cac"
|
||||
|
||||
|
||||
# partial-default-derive is embedded in a larger repo
|
||||
[partial-default-derive.clarify]
|
||||
license = "AGPL-3.0-only"
|
||||
@ -277,62 +219,78 @@ path = "LICENSE"
|
||||
checksum = "0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"
|
||||
|
||||
|
||||
# protobuf-parse has an addendum for the standard Google protobufs
|
||||
# procfs-core is embedded in a larger repo
|
||||
[procfs-core.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[procfs-core.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "c5bbf39118b0639bf8bd391ae0d7d81f25c1cb4066e0fdae6a405b20fb7ca170"
|
||||
|
||||
|
||||
# The prost-* crates are embedded in a larger repo.
|
||||
[prost-build.clarify]
|
||||
license = "Apache-2.0"
|
||||
override-git-commit = "v0.9.0"
|
||||
|
||||
[[prost-build.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
|
||||
|
||||
[prost-derive.clarify]
|
||||
license = "Apache-2.0"
|
||||
override-git-commit = "v0.9.0"
|
||||
|
||||
[[prost-derive.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
|
||||
|
||||
[prost-types.clarify]
|
||||
license = "Apache-2.0"
|
||||
override-git-commit = "v0.9.0"
|
||||
|
||||
[[prost-types.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
|
||||
|
||||
|
||||
# The protobuf crates are embedded in a larger repo
|
||||
[protobuf-parse.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[protobuf-parse.clarify.files]]
|
||||
[[protobuf-parse.clarify.git]]
|
||||
path = "LICENSE.txt"
|
||||
checksum = "ea240b0b1a772a073d2f8941f2145dd8f0b5b2d83c700107a84a1f7eb8ac7af1"
|
||||
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
|
||||
|
||||
|
||||
# rustls-platform-verifier-android is embedded in a larger repo
|
||||
[rustls-platform-verifier-android.clarify]
|
||||
license = "MIT OR Apache-2.0"
|
||||
override-git-commit = "v/0.3.2"
|
||||
|
||||
[[rustls-platform-verifier-android.clarify.git]]
|
||||
path = "LICENSE-MIT"
|
||||
checksum = "1c7cf76689c837a68ed8d704994e52a0f2940c087958f860d17f3186afbdcc0c"
|
||||
|
||||
|
||||
# ryu has an unusual choice of licenses
|
||||
[ryu.clarify]
|
||||
license = "Apache-2.0 OR BSL-1.0"
|
||||
|
||||
[[ryu.clarify.files]]
|
||||
path = "LICENSE-APACHE"
|
||||
checksum = "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"
|
||||
|
||||
|
||||
# sync_wrapper's license isn't recognized for some reason
|
||||
[sync_wrapper.clarify]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[sync_wrapper.clarify.files]]
|
||||
path = "LICENSE"
|
||||
checksum = "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
|
||||
|
||||
|
||||
# The tonic-prost crates are embedded in a larger repo
|
||||
[tonic-prost.clarify]
|
||||
[protobuf-support.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[tonic-prost.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
|
||||
[[protobuf-support.clarify.git]]
|
||||
path = "LICENSE.txt"
|
||||
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
|
||||
|
||||
[tonic-prost-build.clarify]
|
||||
[protobuf-json-mapping.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[tonic-prost-build.clarify.git]]
|
||||
[[protobuf-json-mapping.clarify.git]]
|
||||
path = "LICENSE.txt"
|
||||
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
|
||||
|
||||
|
||||
|
||||
# tokio-macros is embedded in a larger repo
|
||||
[tokio-macros.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[tokio-macros.clarify.git]]
|
||||
path = "LICENSE"
|
||||
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
|
||||
checksum = "1a594f153f129c2de7b15f3262394bdca3dcc2da40058e3ea435c8473eb1f3a0"
|
||||
|
||||
|
||||
# webpsan is embedded in a larger repo
|
||||
# webpsan is embedded in a larger repo, and has a tag that doesn't match the revision in Cargo
|
||||
[webpsan.clarify]
|
||||
license = "MIT"
|
||||
override-git-commit = "0.5.1"
|
||||
|
||||
[[webpsan.clarify.git]]
|
||||
path = "LICENSE"
|
||||
@ -347,41 +305,6 @@ license = "MIT"
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-implement.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[windows-implement.clarify.files]]
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-interface.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[windows-interface.clarify.files]]
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-link.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[windows-link.clarify.files]]
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-result.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[windows-result.clarify.files]]
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-strings.clarify]
|
||||
license = "MIT"
|
||||
|
||||
[[windows-strings.clarify.files]]
|
||||
path = "license-mit"
|
||||
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
|
||||
|
||||
[windows-sys.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
@ -59,7 +59,7 @@
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
{{#each used_by}}
|
||||
<li><a href="{{#if crate.repository~}} {{crate.repository}} {{~else~}} https://crates.io/crates/{{crate.name}} {{~/if}}">{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}</a></li>
|
||||
<li><a href="{{#if crate.repository~}} {{crate.repository}} {{~else~}} https://crates.io/crates/{{crate.name}} {{~/if}}">{{crate.name}} {{crate.version}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<pre class="license-text">{{text}}</pre>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
libsignal makes use of the following open source projects.
|
||||
|
||||
{{#each licenses}}
|
||||
## {{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}{{/each}}
|
||||
## {{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}} {{crate.version}}{{/each}}
|
||||
|
||||
```
|
||||
{{{text}}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,7 @@
|
||||
<key>License</key>
|
||||
<string>{{name}}</string>
|
||||
<key>Title</key>
|
||||
<string>{{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}{{/each}}</string>
|
||||
<string>{{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}} {{crate.version}}{{/each}}</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
|
||||
@ -1 +1 @@
|
||||
0.8.2
|
||||
0.6.0
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
#!/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[@]}"
|
||||
@ -1,12 +0,0 @@
|
||||
#!/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
|
||||
@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# Copyright 2025 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Iterator
|
||||
|
||||
|
||||
def rust_paths_to_remap() -> Iterator[str]:
|
||||
# Repo root
|
||||
yield os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
rust_sysroot = subprocess.check_output(['rustc', '--print', 'sysroot'], text=True).strip()
|
||||
yield rust_sysroot
|
||||
# Rust stdlib internals (must go after sysroot)
|
||||
yield os.path.join(rust_sysroot, 'lib', 'rustlib', 'src', 'rust')
|
||||
# There's a library/ folder inside rustlib/src/rust as well that's also redundant,
|
||||
# but (a) there are precompiled strings with library/ as the root in the stdlib,
|
||||
# and (b) both the stdlib and libsignal have a core/ subdirectory.
|
||||
|
||||
cargo_home = os.environ.get('CARGO_HOME', os.path.join(os.path.expanduser('~'), '.cargo'))
|
||||
# Git dependencies
|
||||
yield os.path.join(cargo_home, 'git', 'checkouts')
|
||||
# Iterate over all crates.io dependency directories:
|
||||
for index_dir in os.scandir(os.path.join(cargo_home, 'registry', 'src')):
|
||||
if not index_dir.name.startswith('index.'):
|
||||
continue
|
||||
yield index_dir.path
|
||||
|
||||
|
||||
def _main() -> int:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
# For invoking as `build_helpers.py print-rust-paths-to-remap`.
|
||||
print_remap_parser = subparsers.add_parser('print-rust-paths-to-remap')
|
||||
print_remap_parser.set_defaults(action='print-rust-paths-to-remap')
|
||||
|
||||
args = parser.parse_args(sys.argv[1:])
|
||||
if 'action' not in args:
|
||||
parser.print_usage(file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# This should be replaced with a `match` when we drop Python 3.9.
|
||||
if args.action == 'print-rust-paths-to-remap':
|
||||
for path in rust_paths_to_remap():
|
||||
print(path)
|
||||
return 0
|
||||
else:
|
||||
raise NotImplementedError(args.action)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(_main())
|
||||
@ -6,8 +6,6 @@
|
||||
# shellcheck shell=bash
|
||||
|
||||
check_rust() {
|
||||
build_std="$1"
|
||||
|
||||
if ! command -v rustup > /dev/null && [[ -d ~/.cargo/bin ]]; then
|
||||
# Try to find rustup in its default per-user install location.
|
||||
# This will be important when running from inside Xcode,
|
||||
@ -28,7 +26,7 @@ check_rust() {
|
||||
fi
|
||||
|
||||
if [[ -n "${CARGO_BUILD_TARGET:-}" ]] && ! (rustup target list --installed | grep -q "${CARGO_BUILD_TARGET:-}"); then
|
||||
if [[ -n "${build_std:-}" ]]; then
|
||||
if [[ -n "${BUILD_STD:-}" ]]; then
|
||||
echo "warning: Building using -Zbuild-std to support tier 3 target ${CARGO_BUILD_TARGET}." >&2
|
||||
else
|
||||
echo "error: Rust target ${CARGO_BUILD_TARGET} not installed" >&2
|
||||
@ -39,38 +37,21 @@ check_rust() {
|
||||
fi
|
||||
}
|
||||
|
||||
# usage: copy_built_library target/release signal_jni out_dir/ signal_jni_amd64
|
||||
# usage: copy_built_library target/release signal_node out_dir/libsignal_node.node
|
||||
# copy_built_library target/release signal_jni out_dir/
|
||||
copy_built_library() {
|
||||
for pattern in "libX.dylib" "libX.so" "X.dll"; do
|
||||
possible_library_name="${pattern%X*}${2}${pattern#*X}"
|
||||
possible_augmented_name="${pattern%X*}${4}${pattern#*X}"
|
||||
for possible_library_name in "lib$2.dylib" "lib$2.so" "$2.dll"; do
|
||||
possible_library_path="$1/${possible_library_name}"
|
||||
if [ -e "${possible_library_path}" ]; then
|
||||
out_dir=$(dirname "$3"x) # trailing x to distinguish directories from files
|
||||
echo_then_run mkdir -p "${out_dir}"
|
||||
echo_then_run cp "${possible_library_path}" "$3/${possible_augmented_name}"
|
||||
echo_then_run cp "${possible_library_path}" "$3"
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo_then_run() {
|
||||
for x in "$@"; do
|
||||
# Put single quotes around any argument with spaces in it.
|
||||
if [[ "$x" == *" "* ]]; then
|
||||
echo -n "'$x' "
|
||||
else
|
||||
echo -n "$x "
|
||||
fi
|
||||
done
|
||||
echo
|
||||
echo "$@"
|
||||
"$@"
|
||||
}
|
||||
|
||||
rust_remap_path_options() {
|
||||
python3 "$(dirname "${BASH_SOURCE[0]}")"/build_helpers.py print-rust-paths-to-remap |
|
||||
while read -r prefix; do
|
||||
# Echo everything on a single line, since it's going into an environment variable.
|
||||
echo -n "--remap-path-prefix ${prefix}= "
|
||||
done
|
||||
}
|
||||
|
||||
@ -1,12 +1,4 @@
|
||||
#!/usr/bin/env -S bloaty -d crates -s vm -c
|
||||
#
|
||||
# Uses bloaty from https://github.com/google/bloaty. Run as
|
||||
#
|
||||
# ./crates_code_size.bloaty target/aarch64-linux-android/release/libsignal_jni.so -- baseline.so
|
||||
#
|
||||
# where baseline.so is the same file (here, libsignal_jni.so) built at the
|
||||
# version to compare against.
|
||||
#
|
||||
#!/usr/bin/env bloaty -d crates -s vm -c
|
||||
# We use VM size because otherwise the debug sections are included.
|
||||
|
||||
custom_data_source: {
|
||||
@ -17,10 +9,6 @@ custom_data_source: {
|
||||
pattern: "^(/rustc/|library/)"
|
||||
replacement: "stdlib"
|
||||
}
|
||||
rewrite: {
|
||||
pattern: "/boring-sys-[^/]+/out/boringssl/"
|
||||
replacement: "BoringSSL"
|
||||
}
|
||||
rewrite: {
|
||||
pattern: "/\\.?cargo/registry/src/(github.com|index.crates.io)-[^/]+/([^/]+)-\\d[^/]*/"
|
||||
replacement: "\\2"
|
||||
@ -30,7 +18,7 @@ custom_data_source: {
|
||||
replacement: "\\1"
|
||||
}
|
||||
rewrite: {
|
||||
pattern: "^(/?([^/]+/)+)src/"
|
||||
pattern: "^(/?([^/]+/)*)src/"
|
||||
replacement: "\\1"
|
||||
}
|
||||
rewrite: {
|
||||
|
||||
@ -11,12 +11,12 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
from typing import BinaryIO
|
||||
|
||||
UNVERIFIED_DOWNLOAD_NAME = 'unverified.tmp'
|
||||
UNVERIFIED_DOWNLOAD_NAME = "unverified.tmp"
|
||||
|
||||
|
||||
def build_argument_parser() -> argparse.ArgumentParser:
|
||||
@ -36,40 +36,33 @@ def build_argument_parser() -> argparse.ArgumentParser:
|
||||
|
||||
def download_if_needed(archive_file: str, url: str, checksum: str) -> BinaryIO:
|
||||
try:
|
||||
fr = open(archive_file, 'rb')
|
||||
f = open(archive_file, 'rb')
|
||||
digest = hashlib.sha256()
|
||||
chunk = fr.read1()
|
||||
chunk = f.read1()
|
||||
while chunk:
|
||||
digest.update(chunk)
|
||||
chunk = fr.read1()
|
||||
chunk = f.read1()
|
||||
if digest.hexdigest() == checksum.lower():
|
||||
return fr
|
||||
return f
|
||||
print("existing file '{}' has non-matching checksum {}; re-downloading...".format(archive_file, digest.hexdigest()), file=sys.stderr)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
print('downloading {}...'.format(archive_file), file=sys.stderr)
|
||||
print("downloading {}...".format(archive_file), file=sys.stderr)
|
||||
try:
|
||||
with urllib.request.urlopen(url) as response:
|
||||
digest = hashlib.sha256()
|
||||
fw = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
|
||||
f = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
|
||||
chunk = response.read1()
|
||||
while chunk:
|
||||
digest.update(chunk)
|
||||
fw.write(chunk)
|
||||
f.write(chunk)
|
||||
chunk = response.read1()
|
||||
assert digest.hexdigest() == checksum.lower(), 'expected {}, actual {}'.format(checksum.lower(), digest.hexdigest())
|
||||
assert digest.hexdigest() == checksum.lower(), "expected {}, actual {}".format(checksum.lower(), digest.hexdigest())
|
||||
os.replace(UNVERIFIED_DOWNLOAD_NAME, archive_file)
|
||||
return fw
|
||||
except (urllib.error.HTTPError, urllib.error.URLError) as e:
|
||||
if isinstance(e.reason, ssl.SSLCertVerificationError):
|
||||
# See:
|
||||
#
|
||||
# - https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error
|
||||
# - https://stackoverflow.com/a/77491061
|
||||
print('Failed to verify SSL certificate. Do you need to `pip install pip-system-certs`?', file=sys.stderr)
|
||||
else:
|
||||
print(e, e.filename, file=sys.stderr)
|
||||
return f
|
||||
except urllib.error.HTTPError as e:
|
||||
print(e, e.filename, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install the latest Linux x86_64 protoc release using the GitHub CLI.
|
||||
|
||||
archive=$(mktemp)
|
||||
trap 'rm -f "$archive"' EXIT
|
||||
|
||||
gh release download \
|
||||
-R protocolbuffers/protobuf \
|
||||
--pattern 'protoc-*-linux-x86_64.zip' \
|
||||
--output "$archive" \
|
||||
--clobber
|
||||
|
||||
# This extracts just bin/protoc and anything in the include directory
|
||||
# to usr/local. We don't need anything else.
|
||||
sudo unzip -q -o "$archive" -d /usr/local bin/protoc 'include/*'
|
||||
|
||||
echo "Installed protoc to /usr/local/bin/protoc"
|
||||
@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#set -ex
|
||||
|
||||
brew bundle install --file=- << EOF
|
||||
brew "awscli"
|
||||
brew "cmake"
|
||||
brew "cocoapods"
|
||||
brew "coreutils"
|
||||
brew "fnm"
|
||||
brew "gh"
|
||||
brew "git"
|
||||
brew "jq"
|
||||
brew "just"
|
||||
brew "pipx"
|
||||
brew "protobuf"
|
||||
brew "python"
|
||||
brew "rocksdb"
|
||||
brew "ruby"
|
||||
brew "rustup"
|
||||
brew "shellcheck"
|
||||
brew "swiftlint"
|
||||
brew "taplo"
|
||||
brew "yamllint"
|
||||
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<2.0"
|
||||
"$(brew --prefix pipx)/bin/pipx" install flake8
|
||||
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
|
||||
flake8-comprehensions \
|
||||
flake8-deprecated \
|
||||
flake8-import-order \
|
||||
flake8-quotes
|
||||
@ -1,506 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
This script automates the rote work to prepare for a release, as specified in RELEASE.md:
|
||||
|
||||
1) Checks that "Slow Tests" and "Build and Test" (CI) have succeeded on the current commit.
|
||||
2) Creates a new annotated tag for the current commit, based on the release version found in RELEASE_NOTES.md.
|
||||
3) Attempts to parse the Java code size from the GitHub Actions logs and appends that value (along with the version)
|
||||
to java/code_size.json.
|
||||
4) Resets RELEASE_NOTES.md to the next presumed version (e.g., incrementing PATCH).
|
||||
5) Updates the version throughout the repository to that new version.
|
||||
6) Commits these changes in a single "Reset for version X" commit.
|
||||
|
||||
Usage:
|
||||
1) Ensure you are on the commit you wish to mark as a release
|
||||
and that both Build and Test and Slow Tests have passed on that commit.
|
||||
2) Run this script: ./prepare_release.py
|
||||
3) Push the tag, the tag's commit, and the version reset/update commit to the proper remotes.
|
||||
|
||||
Optional arguments:
|
||||
--skip-main-branch-check Skip the check that ensures we are on 'main' branch.
|
||||
--skip-ci-tests-pass-check Skip the check that continous integration tests have passed on this commit.
|
||||
--skip-worktree-clean-check Skip the check that the working tree is clean before running this script. Not recommended.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
|
||||
|
||||
class ReleaseFailedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Before we make any changes to the working tree/repository state, we add the command to rollback that change
|
||||
# to this list. If we encounter an error, we execute these commands in order to return the repository to its
|
||||
# original state.
|
||||
# Each of these commands should be independent of each other, as if one of them fails, we will still try to
|
||||
# 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(
|
||||
description='Automates the release preparation workflow.'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--skip-main-branch-check',
|
||||
action='store_true',
|
||||
help="Skip the check to ensure the current branch is 'main'."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-ci-tests-pass-check',
|
||||
action='store_true',
|
||||
help='Skip the check that continous integration tests have passed on this commit.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-worktree-clean-check',
|
||||
action='store_true',
|
||||
help='Skip the check that the working tree is clean before running this script. Not recommended.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--dry-run',
|
||||
action='store_true',
|
||||
help='Skip any steps that would actually mutate the repository, for testing purposes.'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
prepare_release(skip_main_check=args.skip_main_branch_check, skip_tests_pass_check=args.skip_ci_tests_pass_check, skip_worktree_clean_check=args.skip_worktree_clean_check, dry_run=args.dry_run)
|
||||
exit_code = 0
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f'Error: command {e.cmd} exited with status {e.returncode}.')
|
||||
exit_code = e.returncode
|
||||
except ReleaseFailedException:
|
||||
# We printed out the user friendly error before we threw the exception.
|
||||
exit_code = 1
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted execution! Aborting...')
|
||||
exit_code = 1
|
||||
except Exception as ex:
|
||||
traceback.print_exception(None, value=ex, tb=ex.__traceback__)
|
||||
exit_code = 1
|
||||
|
||||
if exit_code != 0:
|
||||
for rollback_command in on_failure_rollback_commands:
|
||||
try:
|
||||
run_command(rollback_command)
|
||||
except subprocess.CalledProcessError:
|
||||
rollback_command_str = ' '.join(rollback_command)
|
||||
print(f'Unable to execute `{rollback_command_str}` after failure, working tree or repository may still be in dirty state.')
|
||||
|
||||
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]
|
||||
print(f'Searching for GitHub Actions runs for commit {short_sha}...')
|
||||
|
||||
# Release Step 1: Ensure that CI tests pass!
|
||||
# - Check GitHub to see if the latest commit has all tests passing, including the nightly "Slow Tests".
|
||||
# - If not, fix the tests before releasing!
|
||||
# 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, 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}')
|
||||
print(f'Slow Tests: https://github.com/signalapp/{REPO_NAME}/actions/runs/{slow_test_run_id}')
|
||||
else:
|
||||
print('Skipping checking that tests pass!')
|
||||
print('Be sure to manually check for passing test runs at:')
|
||||
print(f' https://github.com/signalapp/{REPO_NAME}/actions/workflows/build_and_test.yml')
|
||||
print(f' https://github.com/signalapp/{REPO_NAME}/actions/workflows/slow_tests.yml')
|
||||
|
||||
# Release Step 2: Tag the release commit.
|
||||
# - Look up the next version number vX.Y.Z according to our semantic versioning scheme, which
|
||||
# is manually adjusted as needed in RELEASE_NOTES.md
|
||||
# - Tag the release commit with an annotated tag titled with that version number and a message
|
||||
# containing the release notes summarizing the notable changes since the last release from
|
||||
# RELEASE_NOTES.md
|
||||
# - Prompt the user to give the Release Notes a final human review. The expected format of the
|
||||
# release notes is specified in RELEASE.md
|
||||
head_release_version = tag_new_release(RELEASE_NOTES_FILE_PATH, dry_run=dry_run)
|
||||
|
||||
# Release Step 3: Prepare the repository for the next version
|
||||
#
|
||||
# Step 3, Stage 1: Update the version number throughout the repository to match the next presumed version
|
||||
#
|
||||
# We already have a script that does most of this, update_versions.py. We run it and pass the presumed next version
|
||||
# number as an argument.
|
||||
#
|
||||
# We also run cargo check to make sure the version number in Cargo.lock is updated.
|
||||
|
||||
# We always start a release by presuming the next release will not be a breaking one. So, if the last release was v0.x.y, the next release
|
||||
# is always presumed to be v0.x.(y+1) until a breaking change is merged.
|
||||
major, minor, patch = parse_version(head_release_version)
|
||||
next_patch = patch + 1
|
||||
presumptive_next_version = f'v{major}.{minor}.{next_patch}'
|
||||
|
||||
if not skip_worktree_clean_check:
|
||||
# Check again that the worktree is clean, just to be doubly sure we don't lose data.
|
||||
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
|
||||
on_failure_rollback_commands.append(['git', 'reset', '--hard'])
|
||||
|
||||
if not dry_run:
|
||||
run_command(['./bin/update_versions.py', presumptive_next_version])
|
||||
# Use subprocess.run() directly here to pass through `cargo check` output, because it may take a while.
|
||||
subprocess.run(['cargo', 'check', '--workspace', '--all-features'], check=True)
|
||||
|
||||
# Step 3, Stage 2: Record the code size of the just cut release in code_size.json
|
||||
# Get the cannonical computed code size for the Java library on the commit for the tagged release from GitHub
|
||||
# Actions, and then add it to a new entry in java/code_size.json.
|
||||
#
|
||||
# The version for the new entry is the same as the version for the release that was just tagged, i.e. v0.x.y, not v0.x.(y+1).
|
||||
|
||||
# The "Build and Test" log contains the output of the 'java/check_code_size.py', which records the code size.
|
||||
# So, we try to find the "Build and Test" log for this commit, but one may not exist.
|
||||
# If it doesn't exist, we prompt the user to look it up manually.
|
||||
if not skip_tests_pass_check:
|
||||
print(f'Extracting Java library size from GitHub Actions run (ID: {build_and_test_run_id})...')
|
||||
build_and_test_log = run_command([
|
||||
'gh', 'run', 'view', str(build_and_test_run_id),
|
||||
'--repo', f'signalapp/{REPO_NAME}',
|
||||
'--log'
|
||||
])
|
||||
else:
|
||||
build_and_test_log = ''
|
||||
|
||||
pattern = r'update code_size\.json with (\d+)' # Matches output of print_size_for_release in check_code_size.py
|
||||
match = re.search(pattern, build_and_test_log)
|
||||
if match:
|
||||
java_code_size_int = int(match.group(1))
|
||||
else:
|
||||
print('Could not get logs to find Java code size automatically.')
|
||||
print('This might be due to a known gh cli bug: https://github.com/cli/cli/issues/5011')
|
||||
print(f"You'll have to find it manually in the list of runs: https://github.com/signalapp/{REPO_NAME}/actions/workflows/build_and_test.yml")
|
||||
input_str = input('Please lookup the code size manually and enter it: ')
|
||||
java_code_size_int = int(input_str)
|
||||
|
||||
code_size_file = Path('java/code_size.json')
|
||||
append_code_size(code_size_file, head_release_version, java_code_size_int, dry_run=dry_run)
|
||||
|
||||
# Step 3, Stage 3: Clear RELEASE_NOTES.md, and update it with the presumptive next version number
|
||||
#
|
||||
# As we work, we keep updated running release notes for *just* the next release in RELEASE_NOTES.md. Because we just made a release that
|
||||
# included all the changes previously in RELEASE_NOTES.md, it's now time to reset RELEASE_NOTES.md
|
||||
#
|
||||
# Thus, we edit RELEASE_NOTES.md so that it just contains the next version number on its own line, followed by one newline.
|
||||
if not dry_run:
|
||||
with RELEASE_NOTES_FILE_PATH.open('w', encoding='utf-8') as f:
|
||||
f.write(presumptive_next_version + '\n\n')
|
||||
|
||||
# Step 3, Stage 4: Commit all changes in a single commit!
|
||||
if not dry_run:
|
||||
new_release_version = get_first_line_of_file(RELEASE_NOTES_FILE_PATH)
|
||||
run_command([
|
||||
'git', 'commit', '-am', f'Reset for version {new_release_version}'
|
||||
])
|
||||
|
||||
upstream = os.environ.get('LIBSIGNAL_UPSTREAM_REMOTE') or '<remote>'
|
||||
origin = os.environ.get('LIBSIGNAL_ORIGIN_REMOTE') or '<working-remote>'
|
||||
|
||||
print('\nRelease process complete!')
|
||||
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'\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('\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:
|
||||
"""
|
||||
Checks release environment pre-conditions.
|
||||
Throws on failure.
|
||||
"""
|
||||
# We change into the repo root dir so we can use root-relative paths throughout
|
||||
# the script. This matches the convention in other scripts, like update_versions.py.
|
||||
repo_dir_path = run_command(['git', 'rev-parse', '--show-toplevel'])
|
||||
os.chdir(repo_dir_path)
|
||||
|
||||
# We need to be authenticated with GitHub to fetch Actions run results from
|
||||
# the API. We use these results to check that tests are passing, and to fetch
|
||||
# the Java library code size from the Java test run logs.
|
||||
# We opt to check this up front now and fail early, to try to minimize failures
|
||||
# part way through the script that may leave the repository in a weird state.
|
||||
check_gh_installed_and_authed()
|
||||
|
||||
# Optionally, we check to make sure we are on main as a convenience.
|
||||
# Some people prefer instead to make this commit on a different branch, and
|
||||
# then to 'git push <origin> HEAD:main', so we accomodate that with an opt-out.
|
||||
if not skip_main_check:
|
||||
current_branch = run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
|
||||
if current_branch != 'main':
|
||||
print(f"Error: You are on branch '{current_branch}'.")
|
||||
print("Please switch to 'main' or add the '--skip-main-branch-check' flag and then try again.")
|
||||
raise ReleaseFailedException
|
||||
|
||||
if not skip_worktree_clean_check:
|
||||
try:
|
||||
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
|
||||
except subprocess.CalledProcessError:
|
||||
print('Error: Git working tree is not clean! This can cause unexpected behavior, as this script commits to Git.')
|
||||
print('Please stash or commit your changes.')
|
||||
print('You can also pass `--skip-worktree-clean-check` and try again to bypass this check, but this will result in')
|
||||
print('any changes in your worktree being comitted to Git as part of the release, and is thus not recommended.')
|
||||
raise ReleaseFailedException
|
||||
|
||||
if not skip_worktree_clean_check:
|
||||
try:
|
||||
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
|
||||
except subprocess.CalledProcessError:
|
||||
print('Error: Git working tree is not clean! This can cause unexpected behavior, as this script commits to Git.')
|
||||
print('Please stash or commit your changes.')
|
||||
print('You can also pass `--skip-worktree-clean-check` and try again to bypass this check, but this will result in')
|
||||
print('any changes in your worktree being comitted to Git as part of the release, and is thus not recommended.')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def tag_new_release(release_notes_file_path: Path, *, dry_run: bool) -> str:
|
||||
if not release_notes_file_path.is_file():
|
||||
print(f'Error: {release_notes_file_path} not found. Cannot proceed with release.')
|
||||
raise ReleaseFailedException
|
||||
|
||||
# Read the top line of RELEASE_NOTES.md for the release version
|
||||
head_release_version = get_first_line_of_file(release_notes_file_path)
|
||||
|
||||
if dry_run:
|
||||
print(f'The release version is: {head_release_version}. [Normally the tag step would happen here.]')
|
||||
return head_release_version
|
||||
|
||||
print('Opening an editor to create an annotated tag for this release.')
|
||||
print('Please review and edit the release notes as needed.')
|
||||
print('Once they look good, save and exit the editor to finalize the tag.\n')
|
||||
time.sleep(5)
|
||||
|
||||
# Tag the release (and open an editor for the user)
|
||||
# NB: We call subprocess.run() directly rather than run_command so we don't redirect stdin/stdout.
|
||||
subprocess.run(
|
||||
['git', 'tag', '--annotate', '--force', '--edit', head_release_version, '-F', str(release_notes_file_path)],
|
||||
check=True
|
||||
)
|
||||
on_failure_rollback_commands.append(['git', 'tag', '-d', head_release_version])
|
||||
print(f'Tagged new release: {head_release_version}')
|
||||
return head_release_version
|
||||
|
||||
|
||||
def get_repo_name() -> str:
|
||||
# Some devs store the repo as "origin" remote, others store it as "private"
|
||||
for remote in ('private', 'origin'):
|
||||
try:
|
||||
remote_url = run_command(['git', 'remote', 'get-url', remote], print_error=False).strip()
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('Could not find a valid remote (origin or private).')
|
||||
|
||||
repo = remote_url.rsplit('/', 1)[-1]
|
||||
if repo.endswith('.git'):
|
||||
repo = repo[:-4]
|
||||
return repo
|
||||
|
||||
|
||||
def check_gh_installed_and_authed() -> None:
|
||||
"""
|
||||
Checks that the GitHub CLI ('gh') is installed and the user is authenticated.
|
||||
Throws ReleaseFailedException if gh is not installed or authenticated.
|
||||
"""
|
||||
if which('gh') is None:
|
||||
print('Error: GitHub CLI (gh) is not installed. Please install it and re-run.')
|
||||
raise ReleaseFailedException
|
||||
|
||||
auth_status = subprocess.run(
|
||||
['gh', 'auth', 'status'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
if auth_status.returncode != 0:
|
||||
print("You are not logged into GitHub CLI. Please run 'gh auth login' and re-run this script.")
|
||||
raise ReleaseFailedException
|
||||
|
||||
|
||||
def run_command(cmd: list[str], print_error: bool = True) -> str:
|
||||
"""
|
||||
Runs a shell command and returns its stdout as a string.
|
||||
If check=True, raises a CalledProcessError for non-zero exit codes.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
if print_error:
|
||||
print(f'Error while running command: {cmd}')
|
||||
if e.stdout:
|
||||
print('STDOUT:', e.stdout)
|
||||
if e.stderr:
|
||||
print('STDERR:', e.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) -> int:
|
||||
"""
|
||||
Checks if a GitHub Actions workflow (workflow_name) has a run on HEAD (head_sha)
|
||||
that completed successfully. Returns the run ID if found and successful;
|
||||
otherwise prints an error and throws an exception.
|
||||
"""
|
||||
run_search_limit = '100'
|
||||
list_cmd = [
|
||||
'gh', 'run', 'list',
|
||||
'--repo', f'signalapp/{repo_name}',
|
||||
'--workflow', workflow_name,
|
||||
'--limit', run_search_limit,
|
||||
'--json', 'databaseId,headSha,status,conclusion'
|
||||
]
|
||||
|
||||
raw_json = run_command(list_cmd)
|
||||
runs_data = json.loads(raw_json)
|
||||
|
||||
matching_runs = [rd for rd in runs_data if rd['headSha'] == head_sha]
|
||||
if not matching_runs:
|
||||
print(f"Error: Could not find a matching '{workflow_name}' run for commit {head_sha}.")
|
||||
print('Make sure CI has run successfully on the current commit before releasing.')
|
||||
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
|
||||
|
||||
# Sort by run ID and pick the lowest
|
||||
# NB: I opted to pick the lowest one, because as the first, it is less likely to be a re-run.
|
||||
matching_runs.sort(key=lambda x: x['databaseId'])
|
||||
selected_run_id = int(matching_runs[0]['databaseId'])
|
||||
|
||||
run_view_cmd = [
|
||||
'gh', 'run', 'view', str(selected_run_id),
|
||||
'--repo', f'signalapp/{repo_name}',
|
||||
'--json', 'status,conclusion'
|
||||
]
|
||||
run_view_json = run_command(run_view_cmd)
|
||||
try:
|
||||
view_data = json.loads(run_view_json)
|
||||
except json.JSONDecodeError:
|
||||
print(f'Error: Could not parse JSON for run {selected_run_id}.')
|
||||
raise ReleaseFailedException
|
||||
|
||||
status = view_data.get('status')
|
||||
conclusion = view_data.get('conclusion')
|
||||
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
|
||||
|
||||
|
||||
def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
Given a string in the form 'vMAJOR.MINOR.PATCH',
|
||||
returns (MAJOR, MINOR, PATCH) as integers.
|
||||
"""
|
||||
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', version_str.strip())
|
||||
if not match:
|
||||
print(f"Error: version string '{version_str}' is not in 'vMAJOR.MINOR.PATCH' format.")
|
||||
raise ValueError
|
||||
major, minor, patch = match.groups()
|
||||
assert int(major) == 0, 'Major version should always be zero, because we never promise stability to external users'
|
||||
return int(major), int(minor), int(patch)
|
||||
|
||||
|
||||
def get_first_line_of_file(filepath: Path) -> str:
|
||||
"""
|
||||
Returns the first line of the given file (stripped).
|
||||
Throws on failure
|
||||
"""
|
||||
if not filepath.is_file():
|
||||
print(f'Error: {filepath} not found.')
|
||||
raise FileNotFoundError
|
||||
with filepath.open('r', encoding='utf-8') as f:
|
||||
return f.readline().strip()
|
||||
|
||||
|
||||
def append_code_size(code_size_file: Path, version: str, code_size: int, *, dry_run: bool) -> None:
|
||||
"""
|
||||
Appends an object of the form { 'version': <version>, 'size': <code_size> }
|
||||
to an existing JSON array in code_size_file.
|
||||
Throws an exception if file not found or unable to load JSON.
|
||||
"""
|
||||
with code_size_file.open('r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
new_entry = {'version': version, 'size': code_size}
|
||||
data.append(new_entry)
|
||||
|
||||
if dry_run:
|
||||
print(json.dumps(new_entry))
|
||||
return
|
||||
|
||||
with code_size_file.open('w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -11,24 +11,6 @@ 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"
|
||||
@ -39,55 +21,6 @@ if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
false
|
||||
fi
|
||||
|
||||
generate() {
|
||||
template="$1"
|
||||
output="$2"
|
||||
shift 2
|
||||
echo_then_run cargo about generate \
|
||||
--config acknowledgments/about.toml \
|
||||
--all-features --fail \
|
||||
"$template" --output-file "$output" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
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
|
||||
armv7-linux-androideabi
|
||||
i686-linux-android
|
||||
x86_64-linux-android
|
||||
)
|
||||
DESKTOP_TARGETS=(
|
||||
aarch64-apple-darwin
|
||||
aarch64-pc-windows-msvc
|
||||
aarch64-unknown-linux-gnu
|
||||
x86_64-apple-darwin
|
||||
x86_64-pc-windows-msvc
|
||||
x86_64-unknown-linux-gnu
|
||||
)
|
||||
IOS_TARGETS=(aarch64-apple-ios)
|
||||
|
||||
# shellcheck disable=SC2068 # We want "--target" to end up as a separate argument.
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
|
||||
# shellcheck disable=SC2068
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
|
||||
# shellcheck disable=SC2068
|
||||
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_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
|
||||
# shellcheck disable=SC2068
|
||||
generate_and_maybe_check acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml
|
||||
for template in acknowledgments/*.hbs; do
|
||||
echo_then_run cargo about generate --config acknowledgments/about.toml --all-features --fail "$template" --output-file "${template%.hbs}"
|
||||
done
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Script to run commands in a network-isolated namespace
|
||||
# Usage:
|
||||
# ./run_with_network_isolation.sh [command...]
|
||||
# ./run_with_network_isolation.sh bash # interactive shell
|
||||
#
|
||||
# If no command is provided, defaults to bash
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
echo "Error: This script uses network namespaces, and so it only works on Linux." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_UID=$(id -u)
|
||||
RUN_GID=$(id -g)
|
||||
ORIG_PATH="$PATH"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
# No arguments, default to bash interactive shell
|
||||
CMD="bash"
|
||||
else
|
||||
# Multiple arguments, join as command string
|
||||
CMD="$*"
|
||||
fi
|
||||
|
||||
DEESCALATE_AND_RUN_CMD="setpriv --reuid=${RUN_UID} --regid=${RUN_GID} --clear-groups -- bash -c \"${CMD}\""
|
||||
SETUP_NETWORKING="ip link set lo up"
|
||||
|
||||
# Enter a network-isolated namespace as root, set up loopback, then run the command as the original user
|
||||
# We have to pass PATH separetely to the de-escalated environment because it is stripped by sudo for safety.
|
||||
sudo -E env PATH="$ORIG_PATH" unshare --net -- bash -c "${SETUP_NETWORKING} && ${DEESCALATE_AND_RUN_CMD}"
|
||||
@ -7,80 +7,69 @@
|
||||
|
||||
# Keep crate versions and lib package versions in accord
|
||||
|
||||
import collections
|
||||
import fileinput
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def read_version(file: str, pattern: re.Pattern[str]) -> str:
|
||||
def read_version(file, pattern):
|
||||
with open(file) as f:
|
||||
for line in f:
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
return match.group(2)
|
||||
raise Exception(f'Could not determine version from {file}')
|
||||
raise Exception(f"Could not determine version from {file}")
|
||||
|
||||
|
||||
def update_version(file: str, pattern: re.Pattern[str], new_version: str) -> None:
|
||||
def update_version(file, pattern, new_version):
|
||||
with fileinput.input(files=(file,), inplace=True) as f:
|
||||
for line in f:
|
||||
print(pattern.sub(f'\\g<1>{new_version}\\g<3>', line, count=1), end='')
|
||||
print(pattern.sub(f"\\g<1>{new_version}\\g<3>", line, count=1), end='')
|
||||
|
||||
|
||||
# Note that all of these capture three groups; update_version() relies on that.
|
||||
PODSPEC_PATTERN = re.compile(r"^(.*\.version\s+=\s+')(.*)(')")
|
||||
GRADLE_PATTERN = re.compile(r'^(\s+version\s+=\s+")(.*)(")')
|
||||
NODE_PATTERN = re.compile(r'^(\s+"version": ")(.*)(")')
|
||||
CARGO_PATTERN = re.compile(r'^(version = ")(.*)(")')
|
||||
RUST_PATTERN = re.compile(r'^(pub const VERSION: &str = ")(.*)(")')
|
||||
RELEASE_NOTES_PATTERN = re.compile(r'^(v)(.*)()$')
|
||||
|
||||
|
||||
def bridge_path(*bridge: str) -> str:
|
||||
return os.path.join('rust', 'bridge', *bridge, 'Cargo.toml')
|
||||
def bridge_path(bridge):
|
||||
return os.path.join('rust', 'bridge', bridge, 'Cargo.toml')
|
||||
|
||||
|
||||
VERSION_FILES = [
|
||||
('RELEASE_NOTES.md', RELEASE_NOTES_PATTERN),
|
||||
('LibSignalClient.podspec', PODSPEC_PATTERN),
|
||||
(os.path.join('java', 'build.gradle'), GRADLE_PATTERN),
|
||||
(os.path.join('node', 'package.json'), NODE_PATTERN),
|
||||
(os.path.join('rust', 'core', 'src', 'version.rs'), RUST_PATTERN),
|
||||
('Cargo.toml', CARGO_PATTERN),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def main():
|
||||
os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
new_version = sys.argv[1]
|
||||
if new_version[0] == 'v':
|
||||
new_version = new_version[1:]
|
||||
for (path, pattern) in VERSION_FILES:
|
||||
update_version(path, pattern, new_version)
|
||||
# It's hard to update the package-lock.json file in a straightforward way with regexes, so use the appropriate
|
||||
# tool.
|
||||
subprocess.run(['npm', 'install', '--package-lock-only'], cwd='node')
|
||||
|
||||
update_version('LibSignalClient.podspec', PODSPEC_PATTERN, new_version)
|
||||
update_version(os.path.join('java', 'build.gradle'), GRADLE_PATTERN, new_version)
|
||||
update_version(os.path.join('node', 'package.json'), NODE_PATTERN, new_version)
|
||||
update_version(bridge_path('ffi'), CARGO_PATTERN, new_version)
|
||||
update_version(bridge_path('jni'), CARGO_PATTERN, new_version)
|
||||
update_version(bridge_path('node'), CARGO_PATTERN, new_version)
|
||||
return 0
|
||||
|
||||
found_versions = collections.defaultdict(list)
|
||||
for (path, pattern) in VERSION_FILES:
|
||||
version = read_version(path, pattern)
|
||||
found_versions[version].append(path)
|
||||
package_versions = {
|
||||
'swift': read_version('LibSignalClient.podspec', PODSPEC_PATTERN),
|
||||
'java': read_version(os.path.join('java', 'build.gradle'), GRADLE_PATTERN),
|
||||
'node': read_version(os.path.join('node', 'package.json'), NODE_PATTERN)
|
||||
}
|
||||
|
||||
if len(found_versions) != 1:
|
||||
print('ERROR: found inconsistent versions:')
|
||||
for (version, files) in sorted(found_versions.items()):
|
||||
print(f'{version}:')
|
||||
for file in files:
|
||||
print(f' {file}')
|
||||
bridge_versions = {
|
||||
'swift': read_version(bridge_path('ffi'), CARGO_PATTERN),
|
||||
'java': read_version(bridge_path('jni'), CARGO_PATTERN),
|
||||
'node': read_version(bridge_path('node'), CARGO_PATTERN),
|
||||
}
|
||||
|
||||
return 1
|
||||
for bridge in package_versions:
|
||||
if bridge_versions[bridge] != package_versions[bridge]:
|
||||
print("ERROR: Bridge %s has package version %s but crate version is %s" % (
|
||||
bridge, package_versions[bridge], bridge_versions[bridge]))
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ -11,38 +11,22 @@
|
||||
# You can use the `cargo tree` command below to see where they come from,
|
||||
# and then document them here.
|
||||
#
|
||||
# thiserror: minimal and highly inlinable, most of the code is synthesized at the use site
|
||||
# rand_core, getrandom: waiting to update all the RustCrypto crates together
|
||||
# pqcrypto-kyber: v0.7 is what we shipped PQXDH on, v0.8 contains the NIST standard version
|
||||
EXPECTED="
|
||||
getrandom v0.2.16
|
||||
getrandom v0.3.4
|
||||
rand_core v0.6.4
|
||||
rand_core v0.9.3
|
||||
thiserror v1.0.69
|
||||
thiserror v2.0.17"
|
||||
pqcrypto-kyber v0.7.9
|
||||
pqcrypto-kyber v0.8.0"
|
||||
|
||||
check_cargo_tree() {
|
||||
# Only check the mobile targets, where we care most about code size.
|
||||
cargo tree \
|
||||
-p libsignal-node -p libsignal-jni -p libsignal-ffi \
|
||||
--quiet --duplicates --edges normal,no-proc-macro \
|
||||
--all-features --locked \
|
||||
cargo tree --quiet --duplicates --edges normal,no-proc-macro \
|
||||
--workspace --all-features --locked \
|
||||
--target aarch64-apple-ios \
|
||||
--target armv7-linux-androideabi \
|
||||
--target aarch64-linux-android \
|
||||
"$@"
|
||||
}
|
||||
|
||||
ACTUAL="$(check_cargo_tree --depth 0 | sort -u -V)"
|
||||
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
|
||||
cat <<EOF
|
||||
----- EXPECTED -----
|
||||
${EXPECTED}
|
||||
|
||||
------ ACTUAL ------
|
||||
${ACTUAL}
|
||||
|
||||
EOF
|
||||
if [[ "$(check_cargo_tree --depth 0 | sort -u -V)" != "${EXPECTED}" ]]; then
|
||||
check_cargo_tree
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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
|
||||
command "$@"
|
||||
1
doc/.gitignore
vendored
1
doc/.gitignore
vendored
@ -1 +0,0 @@
|
||||
book
|
||||
@ -1,4 +0,0 @@
|
||||
[book]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
title = "libsignal"
|
||||
@ -1,24 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
libsignal contains platform-agnostic APIs used by the official Signal clients and servers, exposed as a Java, Swift, or TypeScript library. The underlying implementations are written in Rust.
|
||||
|
||||
This documentation is meant primarily for developers working at Signal who will be calling the libsignal APIs, and secondarily for those who maintain libsignal itself. It's meant to be a high-level guide to what's available, and generally won't contain implementation details or detailed API-by-API descriptions. It's *not* meant to be any sort of promise or commitment across versions of the library.
|
||||
|
||||
That is, if you're outside of Signal, please don't read too much into these.
|
||||
|
||||
|
||||
## Viewing the book
|
||||
|
||||
First, [install mdBook](https://rust-lang.github.io/mdBook/guide/installation.html). Then, from the `doc` directory:
|
||||
|
||||
```console
|
||||
% mdbook serve
|
||||
2025-01-21 18:05:27 [INFO] (mdbook::book): Book building has started
|
||||
2025-01-21 18:05:27 [INFO] (mdbook::book): Running the html backend
|
||||
2025-01-21 18:05:27 [INFO] (mdbook::cmd::serve): Serving on: http://localhost:3000
|
||||
2025-01-21 18:05:27 [INFO] (mdbook::cmd::watch::poller): Watching for changes...
|
||||
2025-01-21 18:05:27 [INFO] (warp::server): Server::run; addr=[::1]:3000
|
||||
2025-01-21 18:05:27 [INFO] (warp::server): listening on http://[::1]:3000
|
||||
```
|
||||
|
||||
Now you can open the URL listed (probably <http://localhost:3000>) and view the rendered book. This is also a "watch" mode, which is convenient when editing the book---just save and watch the page reload.
|
||||
@ -1,8 +0,0 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](README.md)
|
||||
|
||||
- [Backups](backups/README.md)
|
||||
- [Networking](net/README.md)
|
||||
- [CDS]()
|
||||
- [Chat](net/chat.md)
|
||||
@ -1,73 +0,0 @@
|
||||
# Backups
|
||||
|
||||
libsignal has a handful of APIs related to backups:
|
||||
|
||||
- [Account keys](#account-keys)
|
||||
- [Backup validation](#backup-validation)
|
||||
- [BackupAuthCredential](#backupauthcredential)
|
||||
|
||||
## Account keys
|
||||
|
||||
Going forward, a number of account keys, including backup keys, will be derived from an *account entropy pool,* a 64-character alphanumeric ASCII string. While the AEP is represented as a plain String in libsignal APIs, methods to work with it can be found on `AccountEntropyPool`, including generation, validation, and derivation of other keys.
|
||||
|
||||
Derived from the account entropy pool is the `BackupKey`, a strongly-typed 32-byte blob that is used for all aspects of backups. There are many keys and identifiers that are derivable from a BackupKey.
|
||||
|
||||
Finally, the key specifically used to encrypt backup files is the `MessageBackupKey`, another strongly-typed object that consists of an HMAC key and an AES key for signing and encrypting the backup, respectively.
|
||||
|
||||

|
||||
|
||||
|
||||
## Backup validation
|
||||
|
||||
There are a few different APIs that work with backup files:
|
||||
|
||||
### MessageBackup: Bulk validation of an encrypted backup file stream
|
||||
|
||||
Takes in an encrypted input stream and produces validation results. Hard errors are thrown as exceptions, soft errors (unknown fields) are returned for manual checking or logging.
|
||||
|
||||
Validation makes two passes over the stream to verify its contained MAC both before and after parsing, so the relevant APIs take a callback to *produce* streams rather than a single stream object. There is no guarantee that the first stream has been fully consumed before the second is produced, so do not reuse the same stream object.
|
||||
|
||||
Provided by:
|
||||
|
||||
- `MessageBackup` in Java
|
||||
- `validateMessageBackup` in Swift
|
||||
- `validate` in the `MessageBackup` module in TypeScript
|
||||
|
||||
|
||||
### OnlineBackupValidator: Validation of a backup as it's being made
|
||||
|
||||
Feed frames into the validator to check them one by one, but don't forget to finalize the backup (`close` in Java, `finalize` in Swift and TypeScript) to run the end-of-file checks!
|
||||
|
||||
This is usually going to be faster than the bulk validation because it skips the decryption and decompression steps, but of course this also means the encryption and compression of the backup being created aren't tested.
|
||||
|
||||
Only logs soft errors rather than returning them in a manipulatable form.
|
||||
|
||||
|
||||
### ComparableBackup: "Canonicalization" of a backup for testing purposes
|
||||
|
||||
Takes an **unencrypted** backup as input and produces a JSON string as output, which can be formatted and diffed as JSON or just line-by-line. Don't forget to check for any unknown fields that get reported as well; they won't be included in the JSON and could invalidate any comparison.
|
||||
|
||||
Note that this format should not be considered stable (i.e. don't persist it or try to parse it). It's only intended for comparing two backups to each other; the usual way this is used is for an app to import Backup A and then immediately export its data as Backup B, then verify that the results are identical.
|
||||
|
||||
|
||||
## BackupAuthCredential
|
||||
|
||||
The BackupAuthCredential types follow the usual zkgroup construction from the client's perspective, similar to the other AuthCredential variants:
|
||||
|
||||
```pseudocode
|
||||
let requestContext = BackupAuthCredentialRequestContext.create(backupKey, aci)
|
||||
let httpResponse = goRequestBackupAuthCredentialsFromServer(requestContext.getRequest())
|
||||
for each response in httpResponse {
|
||||
let expectedRedemptionTime = start of each day
|
||||
let credential = requestContext.receive(response, expectedRedemptionTime, serverParams)
|
||||
log(credential.backupLevel, credential.type)
|
||||
goSaveCredential(credential, expectedRedemptionTime)
|
||||
}
|
||||
```
|
||||
|
||||
```pseudocode
|
||||
let presentation = credential.present(serverParams)
|
||||
goDoSomeBackupOperation(presentation)
|
||||
```
|
||||
|
||||
More information on these credentials in the client/server docs.
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 234 KiB |
@ -1,21 +0,0 @@
|
||||
# Networking
|
||||
|
||||
libsignal has a number of networking-related APIs, collectively referred to as "libsignal-net". These currently provide connections to the [chat server][chat], contact discovery service, and SVR-B, with the possibility of eventually handling every connection to a server run by Signal.
|
||||
|
||||
|
||||
## The Net class
|
||||
|
||||
**Net** (or **Network** on Android) is the top-level manager for connections made from libsignal. It records the environment (production or staging), the user agent to use for all connections (appending its own version string), and any configurable options that apply to all connections, such as whether IPv6 networking is enabled. Internally, it also owns a Rust-managed thread pool for dispatching I/O operations and processing responses. Some operations (e.g. CDS) are provided directly on Net; others use a separate connection object (e.g. chat) where the Net instance is merely used to connect.
|
||||
|
||||
|
||||
## Implementation Organization
|
||||
|
||||
In the Rust layer, libsignal-net is broken up into three separate crates:
|
||||
|
||||
- `libsignal-net-infra`: Server- and connection-agnostic implementations of networking protocols
|
||||
- `libsignal-net`: Connections specifically to Signal services, rather than generic reusable work
|
||||
- `libsignal-net-chat`: Presents the high-level request APIs of the Signal chat server in a protocol-agnostic way (see the [Chat][] page for more info)
|
||||
|
||||
(These boundaries are approximate, because ultimately it's all going to be exposed to the apps anyway; these are *not* some of the crates designed to be generally reusable outside Signal.)
|
||||
|
||||
[chat]: ./chat.md
|
||||
@ -1,55 +0,0 @@
|
||||
# Chat
|
||||
|
||||
Signal's chat server was historically been built on plain HTTP REST requests; to improve responsiveness for online clients, this was switched over to a pair of persistent WebSocket connections---one authenticated, one unauthenticated. To ease migration, these connections use an HTTP-like protobuf interface to provide the same API that REST used to, along with a dedicated "reverse request" mode for pushing incoming messages and notifying clients when the queue is empty. libsignal has two modes for working with chat connections: plain RPC, and "typed APIs".
|
||||
|
||||
|
||||
## WebSocket RPC
|
||||
|
||||
To directly use this WebSocket RPC from libsignal, use the `connectAuth(enticated)Chat` or `connectUnauth(enticated)Chat` methods on a Net(work) instance. This produces an AuthenticatedChatConnection or an UnauthenticatedChatConnection, respectively, each of which has a `send()` method that can send a REST-like request. Note that for Android or iOS, the connection must be `start()`ed with a listener before it can be used.
|
||||
|
||||
The listener callbacks only cover disconnection and the two message-queue events, though these events are guaranteed to be delivered in order. They do not provide general-purpose server->client communication, even though the underlying protobuf interface would allow it.
|
||||
|
||||
### Preconnecting
|
||||
|
||||
If the time spent establishing a TLS connection becomes significant, Net also has a `preconnectChat()` call, which does the "early" part of connection establishment and then pauses, waiting for a later call to `connectAuthenticatedChat()`. This allows parallelizing the connection attempt with, say, loading the username and password used for the auth socket. This is considered an optimization; if `connectAuthenticatedChat()` isn't called soon after the initial `preconnectChat()` call, or if the connection parameters change in between, the preconnected socket will be silently discarded. If `connectAuthenticatedChat()` isn't called at *all,* the preconnected socket may not ever be cleaned up (but the server will eventually hang up on it).
|
||||
|
||||
|
||||
## High-level Request APIs (a.k.a "Typed APIs")
|
||||
|
||||
To improve on the limitations of the current endpoints and the WebSocket RPC system, the chat server will support a new gRPC-based API that can replace the WebSocket RPC. Rather than have all clients bring up their own gRPC clients, libsignal will provide high-level equivalents for all the APIs currently using `send()` calls, and then transparently switch them to gRPC calls later. Using these high-level APIs differs slightly on each platform.
|
||||
|
||||
### Android
|
||||
|
||||
The typed APIs are provided as "services" that wrap the corresponding ChatConnection. For example:
|
||||
|
||||
```kotlin
|
||||
val usernamesService = UnauthUsernamesService(chatConnection)
|
||||
val response = usernamesService.lookUpUsernameHash(hash).get() // or await()
|
||||
```
|
||||
|
||||
Unlike most libsignal APIs, which throw exceptions, the service APIs produce `RequestResult`s, a sealed interface of `Success` (what you wanted), `NonSuccess` (a request-specific error), and `Failure` (a standard transport error of some kind). This design was based on what Signal-Android was using elsewhere!
|
||||
|
||||
### iOS
|
||||
|
||||
The typed APIs are implemented directly on the corresponding ChatConnection, but also grouped into "service" protocols. Several hoops were jumped through to make it possible to access these in a generic way via the helper `UnauthServiceSelector` type (see there for more details).
|
||||
|
||||
```swift
|
||||
// Assuming a helper accessService(_:as:) method added in the app.
|
||||
try await accessService(chatConnection, as: .usernames) { usernamesService in
|
||||
let response = try await usernamesService.lookUpUsernameHash(hash)
|
||||
}
|
||||
```
|
||||
|
||||
Each request can throw request-specific errors as well as standard transport errors.
|
||||
|
||||
### Desktop
|
||||
|
||||
The typed APIs are implemented directly on the corresponding ChatConnection, but also grouped into "service" interfaces. The libsignal tests contain an example of how to limit access in a generic way (see ServiceTestUtils.ts).
|
||||
|
||||
```typescript
|
||||
// Assuming a helper accessUnauthService() method added in the app.
|
||||
const service = connectionManager.accessUnauthService<UnauthUsernamesService>();
|
||||
const response = await service.lookUpUsernameHash(hash);
|
||||
```
|
||||
|
||||
Each request can throw (reject the promise) with request-specific errors as well as standard transport errors.
|
||||
8
java/.gitignore
vendored
8
java/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
# IntelliJ specific files/directories
|
||||
out
|
||||
.shelf
|
||||
.idea
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.kotlin
|
||||
@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
FROM ubuntu:jammy-20260109@sha256:c7eb020043d8fc2ae0793fb35a37bff1cf33f156d4d4b12ccc7f3ef8706c38b1
|
||||
FROM ubuntu:jammy-20230624@sha256:b060fffe8e1561c9c3e6dea6db487b900100fc26830b9ea2ec966c151ab4c020
|
||||
|
||||
COPY java/docker/ docker/
|
||||
COPY java/docker/apt.conf java/docker/sources.list /etc/apt/
|
||||
@ -19,7 +19,7 @@ RUN apt-get update
|
||||
|
||||
# Install only what's needed to set up Rust and Android
|
||||
# We'll install additional tools at the end to take advantage of Docker's caching of earlier steps.
|
||||
RUN apt-get install -y openjdk-17-jdk-headless unzip
|
||||
RUN apt-get install -y curl openjdk-17-jdk-headless unzip
|
||||
|
||||
ARG UID
|
||||
ARG GID
|
||||
@ -29,10 +29,9 @@ RUN groupadd -o -g "${GID}" libsignal \
|
||||
&& useradd -m -o -u "${UID}" -g "${GID}" -s /bin/bash libsignal
|
||||
|
||||
USER libsignal
|
||||
ENV HOME=/home/libsignal
|
||||
ENV USER=libsignal
|
||||
ENV SHELL=/bin/bash
|
||||
ENV LANG=C.UTF-8
|
||||
ENV HOME /home/libsignal
|
||||
ENV USER libsignal
|
||||
ENV SHELL /bin/bash
|
||||
|
||||
WORKDIR /home/libsignal
|
||||
|
||||
@ -41,43 +40,35 @@ ARG ANDROID_SDK_FILENAME=commandlinetools-linux-7583922_latest.zip
|
||||
ARG ANDROID_SDK_SHA=124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e2fcfa7faf
|
||||
ARG ANDROID_API_LEVELS=android-34
|
||||
ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
|
||||
ARG NDK_VERSION=28.0.13004108
|
||||
ENV ANDROID_HOME=/home/libsignal/android-sdk
|
||||
ENV PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/cmdline-tools/bin
|
||||
ARG NDK_VERSION=25.2.9519653
|
||||
ENV ANDROID_HOME /home/libsignal/android-sdk
|
||||
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/cmdline-tools/bin
|
||||
|
||||
ADD --chown=libsignal --checksum=sha256:${ANDROID_SDK_SHA} \
|
||||
https://dl.google.com/android/repository/${ANDROID_SDK_FILENAME} ${ANDROID_SDK_FILENAME}
|
||||
|
||||
RUN unzip -q ${ANDROID_SDK_FILENAME} -d android-sdk \
|
||||
RUN curl -O https://dl.google.com/android/repository/${ANDROID_SDK_FILENAME} \
|
||||
&& echo "${ANDROID_SDK_SHA} ${ANDROID_SDK_FILENAME}" | sha256sum -c - \
|
||||
&& unzip -q ${ANDROID_SDK_FILENAME} -d android-sdk \
|
||||
&& rm -rf ${ANDROID_SDK_FILENAME}
|
||||
|
||||
RUN yes | sdkmanager --sdk_root=${ANDROID_HOME} "platforms;${ANDROID_API_LEVELS}" "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" "platform-tools" "ndk;${NDK_VERSION}"
|
||||
|
||||
# Pre-download Gradle.
|
||||
COPY java/gradle/wrapper gradle/wrapper
|
||||
COPY java/gradle gradle
|
||||
COPY java/gradlew gradlew
|
||||
RUN ./gradlew --version
|
||||
|
||||
# Rust setup...
|
||||
|
||||
COPY rust-toolchain rust-toolchain
|
||||
ARG RUSTUP_SHA=20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c
|
||||
ARG RUSTUP_SHA=ad1f8b5199b3b9e231472ed7aa08d2e5d1d539198a15c5b1e53c746aad81d27b
|
||||
ENV PATH="/home/libsignal/.cargo/bin:${PATH}"
|
||||
|
||||
ADD --chown=libsignal --chmod=755 --checksum=sha256:${RUSTUP_SHA} \
|
||||
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)" \
|
||||
RUN curl -f https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-unknown-linux-gnu/rustup-init -o /tmp/rustup-init \
|
||||
&& echo "${RUSTUP_SHA} /tmp/rustup-init" | sha256sum -c - \
|
||||
&& chmod a+x /tmp/rustup-init \
|
||||
&& /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}"
|
||||
RUN rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||
|
||||
# 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.
|
||||
@ -87,12 +78,11 @@ USER root
|
||||
RUN apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
crossbuild-essential-arm64 \
|
||||
git \
|
||||
gpg-agent \
|
||||
libclang-dev \
|
||||
make \
|
||||
python3
|
||||
protobuf-compiler
|
||||
USER libsignal
|
||||
|
||||
# Convert ssh to https for git dependency access without a key.
|
||||
|
||||
@ -5,38 +5,38 @@
|
||||
|
||||
DOCKER ?= docker
|
||||
|
||||
.PHONY: docker java_build publish_java
|
||||
.PHONY: docker java_build java_test publish_java
|
||||
|
||||
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 --platform=$(DOCKER_PLATFORM) --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
|
||||
cd .. && $(DOCKER) build --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
|
||||
$(DOCKER) run $(DOCKER_TTY_FLAG) --init --rm --user $$(id -u):$$(id -g) \
|
||||
--env LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS \
|
||||
--env LIBSIGNAL_TESTING_IGNORE_KT_TESTS \
|
||||
--env LIBSIGNAL_TESTING_PROXY_SERVER \
|
||||
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
|
||||
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) build $(CROSS_COMPILE_SERVER)"
|
||||
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
|
||||
sh -c "cd src/java; ./gradlew build"
|
||||
|
||||
java_test: java_build
|
||||
$(DOCKER) run $(DOCKER_TTY_FLAG) --init --rm --user $$(id -u):$$(id -g) \
|
||||
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
|
||||
sh -c "cd src/java; ./gradlew test"
|
||||
|
||||
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 $(CROSS_COMPILE_SERVER)"
|
||||
sh -c "cd src/java; ./gradlew publish closeAndReleaseSonatypeStagingRepository"
|
||||
|
||||
# We could run these through Docker, but they would have the same result anyway.
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'com.android.library' version '8.3.0'
|
||||
id 'androidx.benchmark' version '1.1.1'
|
||||
}
|
||||
|
||||
@ -13,11 +13,9 @@ android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
|
||||
multiDexEnabled true
|
||||
|
||||
// Uncomment this to build 32-bit-only benchmarks.
|
||||
// (Gradle will still build a 64-bit libsignal,
|
||||
@ -27,25 +25,21 @@ android {
|
||||
// }
|
||||
}
|
||||
|
||||
testBuildType = "release"
|
||||
testBuildType "release"
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled = true
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
namespace = "org.signal.libsignal.benchmarks"
|
||||
|
||||
packagingOptions {
|
||||
doNotStrip '**/*.so'
|
||||
}
|
||||
namespace "org.signal.libsignal.benchmarks"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3'
|
||||
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.1.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
androidTestImplementation project(':android')
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:debuggable="false">
|
||||
<profileable android:shell="true"/>
|
||||
</application>
|
||||
<application
|
||||
android:debuggable="false"/>
|
||||
</manifest>
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import androidx.benchmark.BenchmarkState;
|
||||
import androidx.benchmark.junit4.BenchmarkRule;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.ServerPublicParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
|
||||
public class ClientZkOperations {
|
||||
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
|
||||
|
||||
private final Instant now = Instant.now();
|
||||
|
||||
private final ServerSecretParams serverParams = ServerSecretParams.generate();
|
||||
private final ServerPublicParams serverPublicParams = serverParams.getPublicParams();
|
||||
private final GroupSecretParams groupParams = GroupSecretParams.generate();
|
||||
private final ServerZkAuthOperations serverZkAuthOperations =
|
||||
new ServerZkAuthOperations(serverParams);
|
||||
private final org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations clientZkOperations =
|
||||
new org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations(serverPublicParams);
|
||||
|
||||
private final ServiceId.Aci aci = new ServiceId.Aci(UUID.randomUUID());
|
||||
private final ServiceId.Pni pni = new ServiceId.Pni(UUID.randomUUID());
|
||||
|
||||
@Test
|
||||
public void receiveAuthCredentialWithPni() throws VerificationFailedException {
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
state.pauseTiming();
|
||||
final AuthCredentialWithPniResponse authCredentialWithPniResponse =
|
||||
serverZkAuthOperations.issueAuthCredentialWithPniZkc(aci, pni, now);
|
||||
state.resumeTiming();
|
||||
|
||||
while (state.keepRunning()) {
|
||||
clientZkOperations.receiveAuthCredentialWithPniAsServiceId(
|
||||
aci, pni, now.getEpochSecond(), authCredentialWithPniResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAuthCredentialPresentation() throws VerificationFailedException {
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
state.pauseTiming();
|
||||
final AuthCredentialWithPniResponse authCredentialWithPniResponse =
|
||||
serverZkAuthOperations.issueAuthCredentialWithPniZkc(aci, pni, now);
|
||||
final AuthCredentialWithPni authCredentialWithPni =
|
||||
clientZkOperations.receiveAuthCredentialWithPniAsServiceId(
|
||||
aci, pni, now.getEpochSecond(), authCredentialWithPniResponse);
|
||||
state.resumeTiming();
|
||||
|
||||
while (state.keepRunning()) {
|
||||
clientZkOperations.createAuthCredentialPresentation(groupParams, authCredentialWithPni);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,15 +5,18 @@
|
||||
|
||||
import androidx.benchmark.BenchmarkState;
|
||||
import androidx.benchmark.junit4.BenchmarkRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.signal.libsignal.protocol.ecc.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ECCBenchmark {
|
||||
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
|
||||
|
||||
private final ECKeyPair alicePair = ECKeyPair.generate();
|
||||
private final ECKeyPair bobPair = ECKeyPair.generate();
|
||||
private final ECKeyPair alicePair = Curve.generateKeyPair();
|
||||
private final ECKeyPair bobPair = Curve.generateKeyPair();
|
||||
private final byte[] arbitraryData = new byte[] {0x53, 0x69, 0x67, 0x6E, 0x61, 0x6C};
|
||||
|
||||
@Test
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import androidx.benchmark.BenchmarkState;
|
||||
import androidx.benchmark.junit4.BenchmarkRule;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.ServerPublicParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class GroupSendEndorsements {
|
||||
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
|
||||
|
||||
@Parameters(name = "groupSize={0}")
|
||||
public static Object[] data() {
|
||||
return new Integer[] {10, 100, 1000};
|
||||
}
|
||||
|
||||
private final ServerSecretParams serverParams = ServerSecretParams.generate();
|
||||
private final ServerPublicParams serverPublicParams = serverParams.getPublicParams();
|
||||
private final GroupSecretParams groupParams = GroupSecretParams.generate();
|
||||
|
||||
private final Instant expiration =
|
||||
Instant.now().truncatedTo(ChronoUnit.DAYS).plus(2, ChronoUnit.DAYS);
|
||||
|
||||
private final ServiceId.Aci[] members;
|
||||
private final UuidCiphertext[] encryptedMembers;
|
||||
private final GroupSendEndorsementsResponse response;
|
||||
|
||||
public GroupSendEndorsements(int groupSize) {
|
||||
members = new ServiceId.Aci[groupSize];
|
||||
for (int i = 0; i < groupSize; ++i) {
|
||||
members[i] = new ServiceId.Aci(UUID.randomUUID());
|
||||
}
|
||||
|
||||
encryptedMembers = new UuidCiphertext[groupSize];
|
||||
final ClientZkGroupCipher cipher = new ClientZkGroupCipher(groupParams);
|
||||
for (int i = 0; i < groupSize; ++i) {
|
||||
encryptedMembers[i] = cipher.encrypt(members[i]);
|
||||
}
|
||||
|
||||
GroupSendDerivedKeyPair keyPair =
|
||||
GroupSendDerivedKeyPair.forExpiration(expiration, serverParams);
|
||||
response = GroupSendEndorsementsResponse.issue(Arrays.asList(encryptedMembers), keyPair);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void benchmarkReceiveWithServiceIds() throws VerificationFailedException {
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
|
||||
while (state.keepRunning()) {
|
||||
response.receive(Arrays.asList(members), members[0], groupParams, serverPublicParams);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void benchmarkReceiveWithCiphertexts() throws VerificationFailedException {
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
|
||||
while (state.keepRunning()) {
|
||||
response.receive(Arrays.asList(encryptedMembers), encryptedMembers[0], serverPublicParams);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void benchmarkToToken() throws VerificationFailedException {
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
final List<GroupSendEndorsement> endorsements =
|
||||
response
|
||||
.receive(Arrays.asList(encryptedMembers), encryptedMembers[0], serverPublicParams)
|
||||
.endorsements();
|
||||
|
||||
while (state.keepRunning()) {
|
||||
for (GroupSendEndorsement next : endorsements) {
|
||||
next.toToken(groupParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,217 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import androidx.benchmark.BenchmarkState;
|
||||
import androidx.benchmark.junit4.BenchmarkRule;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.signal.libsignal.metadata.SealedSessionCipher;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.metadata.certificate.ServerCertificate;
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.SessionBuilder;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.UntrustedIdentityException;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.groups.GroupCipher;
|
||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType;
|
||||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
||||
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle;
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.impl.InMemorySignalProtocolStore;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class SealedSender {
|
||||
public static class V1 {
|
||||
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
|
||||
|
||||
@Test
|
||||
public void sealedSenderV1Encrypt() throws Exception {
|
||||
InMemorySignalProtocolStore aliceStore =
|
||||
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xAA);
|
||||
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, aliceAddress);
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
createCertificateFor(
|
||||
trustRoot,
|
||||
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
|
||||
"+14151111111",
|
||||
1,
|
||||
aliceStore.getIdentityKeyPair().getPublicKey().getPublicKey(),
|
||||
31337);
|
||||
SealedSessionCipher aliceCipher =
|
||||
new SealedSessionCipher(
|
||||
aliceStore, UUID.fromString(aliceAddress.getName()), "+14151111111", 1);
|
||||
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
while (state.keepRunning()) {
|
||||
aliceCipher.encrypt(bobAddress, senderCertificate, "smert za smert".getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class V2 {
|
||||
@Parameterized.Parameters(name = "recipients={0}")
|
||||
public static Object[] multiRecipientSizes() {
|
||||
return new Integer[] {10, 100, 1000};
|
||||
}
|
||||
|
||||
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
|
||||
|
||||
final InMemorySignalProtocolStore aliceStore =
|
||||
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xAA);
|
||||
final SignalProtocolAddress aliceAddress = new SignalProtocolAddress("+14151111111", 1);
|
||||
final List<SignalProtocolAddress> recipients;
|
||||
|
||||
public V2(int recipientCount) {
|
||||
recipients = new ArrayList<>(recipientCount);
|
||||
for (int i = 0; i < recipientCount; ++i) {
|
||||
InMemorySignalProtocolStore bobStore =
|
||||
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), i);
|
||||
SignalProtocolAddress bobAddress =
|
||||
new SignalProtocolAddress(UUID.randomUUID().toString(), i % 127 + 1);
|
||||
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress));
|
||||
recipients.add(bobAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sealedSenderV2Encrypt() throws Exception {
|
||||
GroupCipher aliceGroupCipher = new GroupCipher(aliceStore, aliceAddress);
|
||||
UUID distributionId = UUID.randomUUID();
|
||||
|
||||
SenderKeyDistributionMessage sentAliceDistributionMessage =
|
||||
new GroupSessionBuilder(aliceStore).create(aliceAddress, distributionId);
|
||||
|
||||
CiphertextMessage ciphertextFromAlice =
|
||||
aliceGroupCipher.encrypt(distributionId, "smert ze smert".getBytes());
|
||||
|
||||
ECKeyPair trustRoot = ECKeyPair.generate();
|
||||
SenderCertificate senderCertificate =
|
||||
createCertificateFor(
|
||||
trustRoot,
|
||||
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
|
||||
"+14151111111",
|
||||
1,
|
||||
aliceStore.getIdentityKeyPair().getPublicKey().getPublicKey(),
|
||||
31337);
|
||||
UnidentifiedSenderMessageContent content =
|
||||
new UnidentifiedSenderMessageContent(
|
||||
ciphertextFromAlice,
|
||||
senderCertificate,
|
||||
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
|
||||
Optional.empty());
|
||||
SealedSessionCipher aliceCipher =
|
||||
new SealedSessionCipher(
|
||||
aliceStore,
|
||||
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
|
||||
"+14151111111",
|
||||
1);
|
||||
|
||||
final BenchmarkState state = benchmarkRule.getState();
|
||||
while (state.keepRunning()) {
|
||||
aliceCipher.multiRecipientEncrypt(recipients, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from SealedSessionCipherTest.java
|
||||
|
||||
private static SignedPreKeyRecord generateSignedPreKey(
|
||||
IdentityKeyPair identityKeyPair, int signedPreKeyId) throws InvalidKeyException {
|
||||
ECKeyPair keyPair = ECKeyPair.generate();
|
||||
byte[] signature =
|
||||
identityKeyPair.getPrivateKey().calculateSignature(keyPair.getPublicKey().serialize());
|
||||
|
||||
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
}
|
||||
|
||||
private static KyberPreKeyRecord generateKyberPreKey(
|
||||
IdentityKeyPair identityKeyPair, int kyberPreKeyId) throws InvalidKeyException {
|
||||
KEMKeyPair keyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024);
|
||||
byte[] signature =
|
||||
identityKeyPair.getPrivateKey().calculateSignature(keyPair.getPublicKey().serialize());
|
||||
|
||||
return new KyberPreKeyRecord(kyberPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
}
|
||||
|
||||
private static SenderCertificate createCertificateFor(
|
||||
ECKeyPair trustRoot,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
int deviceId,
|
||||
ECPublicKey identityKey,
|
||||
long expires)
|
||||
throws InvalidKeyException, InvalidCertificateException {
|
||||
ECKeyPair serverKey = ECKeyPair.generate();
|
||||
ServerCertificate serverCertificate =
|
||||
new ServerCertificate(trustRoot.getPrivateKey(), 1, serverKey.getPublicKey());
|
||||
return serverCertificate.issue(
|
||||
serverKey.getPrivateKey(),
|
||||
uuid.toString(),
|
||||
Optional.ofNullable(e164),
|
||||
deviceId,
|
||||
identityKey,
|
||||
expires);
|
||||
}
|
||||
|
||||
private static void initializeSessions(
|
||||
InMemorySignalProtocolStore aliceStore,
|
||||
InMemorySignalProtocolStore bobStore,
|
||||
SignalProtocolAddress bobAddress,
|
||||
SignalProtocolAddress aliceAddress)
|
||||
throws InvalidKeyException, UntrustedIdentityException {
|
||||
ECKeyPair bobPreKey = ECKeyPair.generate();
|
||||
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
|
||||
SignedPreKeyRecord bobSignedPreKey = generateSignedPreKey(bobIdentityKey, 2);
|
||||
KyberPreKeyRecord bobKyberPreKey = generateKyberPreKey(bobIdentityKey, 12);
|
||||
|
||||
PreKeyBundle bobBundle =
|
||||
new PreKeyBundle(
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
bobPreKey.getPublicKey(),
|
||||
2,
|
||||
bobSignedPreKey.getKeyPair().getPublicKey(),
|
||||
bobSignedPreKey.getSignature(),
|
||||
bobIdentityKey.getPublicKey(),
|
||||
12,
|
||||
bobKyberPreKey.getKeyPair().getPublicKey(),
|
||||
bobKyberPreKey.getSignature());
|
||||
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
|
||||
aliceSessionBuilder.process(bobBundle);
|
||||
|
||||
bobStore.storeSignedPreKey(2, bobSignedPreKey);
|
||||
bobStore.storeKyberPreKey(12, bobKyberPreKey);
|
||||
bobStore.storePreKey(1, new PreKeyRecord(1, bobPreKey));
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,10 @@
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'com.android.library' version '8.3.0'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "libsignal-android"
|
||||
}
|
||||
archivesBaseName = "libsignal-android"
|
||||
|
||||
repositories {
|
||||
google()
|
||||
@ -18,23 +13,20 @@ repositories {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = 'org.signal.libsignal'
|
||||
namespace 'org.signal.libsignal'
|
||||
|
||||
compileSdk 34
|
||||
ndkVersion = '28.0.13004108'
|
||||
ndkVersion '25.2.9519653'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
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.
|
||||
testInstrumentationRunnerArguments["org.signal.libsignal.test.environment"] = collectTestEnvironment()
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled = true
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
@ -46,10 +38,6 @@ android {
|
||||
srcDir '../client/src/test/java'
|
||||
srcDir '../shared/test/java'
|
||||
}
|
||||
kotlin {
|
||||
srcDir '../client/src/test/java'
|
||||
srcDir '../shared/test/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../client/src/test/resources'
|
||||
}
|
||||
@ -57,8 +45,9 @@ android {
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// Defer stripping to the Android app project.
|
||||
doNotStrip '**/*.so'
|
||||
jniLibs {
|
||||
pickFirst 'lib/*/libsignal_jni.so'
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
@ -66,71 +55,23 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
explicitApi()
|
||||
}
|
||||
|
||||
task dokkaHtmlJar(type: Jar) {
|
||||
dependsOn(dokkaGeneratePublicationHtml)
|
||||
from(dokkaGeneratePublicationHtml)
|
||||
archiveClassifier.set("dokka")
|
||||
}
|
||||
|
||||
task dokkaJavadocJar(type: Jar) {
|
||||
dependsOn(dokkaGeneratePublicationJavadoc)
|
||||
from(dokkaGeneratePublicationJavadoc)
|
||||
archiveClassifier.set("javadoc")
|
||||
}
|
||||
|
||||
String collectTestEnvironment() {
|
||||
def result = []
|
||||
System.getenv().each { k, v ->
|
||||
if (k.startsWith("LIBSIGNAL_TESTING_")) {
|
||||
// Limit what characters we accept in values.
|
||||
// This is going to get mashed down to a single command-line argument.
|
||||
// (This pattern is only meant to head off likely problems and was not specifically
|
||||
// tested; if you need to use one of these characters, you can remove the check and see
|
||||
// if things Just Work, or tweak our AndroidJUnitRunner to handle different delimiters
|
||||
// or escaping.)
|
||||
if (v.matches(".*[, \t\r\n].*")) {
|
||||
logger.warn("warning: ignoring ${k} for running tests; it contains invalid characters")
|
||||
return
|
||||
}
|
||||
result << "${k}=${v}"
|
||||
}
|
||||
}
|
||||
result.join(",")
|
||||
}
|
||||
|
||||
// We include the classes and data for rustls-platform-verifier ourselves,
|
||||
// but we want to make sure it's in sync with the Rust side.
|
||||
// So we check that there hasn't been a new release of the Android package since we made our fork.
|
||||
void checkRustlsPlatformVerifierVersion() {
|
||||
def dependencyText = providers.exec {
|
||||
it.workingDir = project.rootDir.parentFile
|
||||
commandLine("bash", "java/find_cargo.sh", "metadata", "--format-version", "1")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
def dependencyJson = new JsonSlurper().parseText(dependencyText)
|
||||
def dependencyVersion = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }.version
|
||||
assert dependencyVersion == "0.1.1"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
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')
|
||||
}
|
||||
|
||||
tasks.register('libsWithDebugSymbols', Zip) {
|
||||
from 'src/main/jniLibs'
|
||||
archiveClassifier = 'debug-symbols'
|
||||
dependsOn 'makeJniLibraries'
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn 'collectAssets'
|
||||
dependsOn 'makeJniLibraries'
|
||||
dependsOn 'makeTestJniLibraries'
|
||||
}
|
||||
|
||||
String[] archsFromProperty(String prop) {
|
||||
@ -138,42 +79,43 @@ 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', *libsignalDebugFlag, *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
|
||||
commandLine 'bash', '../build_jni.sh', *archs
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
}
|
||||
|
||||
task makeTestJniLibraries(type:Exec) {
|
||||
group 'Rust'
|
||||
description 'Build JNI libraries for Android for testing'
|
||||
|
||||
def archs = archsFromProperty('androidTestingArchs') ?: archsFromProperty('androidArchs') ?: ['android']
|
||||
// Explicitly specify 'bash' for Windows compatibility.
|
||||
commandLine 'bash', '../build_jni.sh', '--testing', *archs
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
}
|
||||
|
||||
task collectAssets(type:Copy) {
|
||||
from('../../acknowledgments') {
|
||||
include 'acknowledgments-android*.md'
|
||||
rename 'acknowledgments-android(.*)[.]md', 'libsignal$1.md'
|
||||
from('../../acknowledgments/acknowledgments.md') {
|
||||
rename 'acknowledgments.md', 'libsignal.md'
|
||||
}
|
||||
into 'src/main/assets/acknowledgments'
|
||||
}
|
||||
|
||||
// MARK: Publication
|
||||
afterEvaluate {
|
||||
checkRustlsPlatformVerifierVersion()
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = base.archivesName.get()
|
||||
artifactId = archivesBaseName
|
||||
from components.release
|
||||
artifact dokkaHtmlJar
|
||||
artifact dokkaJavadocJar
|
||||
artifact libsWithDebugSymbols
|
||||
|
||||
pom {
|
||||
name = base.archivesName.get()
|
||||
name = archivesBaseName
|
||||
packaging = 'aar'
|
||||
description = 'Signal Protocol cryptography library for Android'
|
||||
url = 'https://github.com/signalapp/libsignal'
|
||||
@ -203,7 +145,7 @@ afterEvaluate {
|
||||
|
||||
setUpSigningKey(signing)
|
||||
signing {
|
||||
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
|
||||
required { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs.excludes.add("**/libsignal_jni_testing.so")
|
||||
}
|
||||
|
||||
namespace = "org.signal.libsignal.packagingtest"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
androidTestImplementation project(':android')
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal;
|
||||
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
|
||||
/**
|
||||
* Tests that check that libsignal is loadable and available.
|
||||
*
|
||||
* <p>This test is expected to run in the production configuration, where only {@code
|
||||
* libsignal_jni.so} is available (as opposed to the test/debug configuration, which also includes
|
||||
* {@code libsignal_jni_testing.so}). The difference is important, since when both are available,
|
||||
* {@code libsignal_jni_testing.so} is loaded, with <code>libsignal_jni.so</code> as a fallback.
|
||||
* {@code libsignal_jni_testing.so} exposes a superset of the <code>
|
||||
* libsignal_jni.so</code> API, including some test only functions, but the actual production
|
||||
* configuration only loads {@code libsignal_jni.so}. These tests check that the custom loading code
|
||||
* works correctly in production configurations in addition to the test/debug configuration.
|
||||
*/
|
||||
public class SmokeTest {
|
||||
@Test
|
||||
public void testCanCallNativeMethod() {
|
||||
Native.keepAlive(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCantCallNativeTestingMethod() {
|
||||
assertThrows(UnsatisfiedLinkError.class, () -> NativeTesting.test_only_fn_returns_123());
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@ -1,36 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
import org.signal.libsignal.protocol.logging.AndroidSignalProtocolLogger;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLogger;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
/** Custom setup for our JUnit tests, when run as instrumentation tests. */
|
||||
public class AndroidJUnitRunner extends androidx.test.runner.AndroidJUnitRunner {
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
// Make sure libsignal logs get caught correctly.
|
||||
SignalProtocolLoggerProvider.setProvider(
|
||||
new TestLoggerDecorator(new AndroidSignalProtocolLogger()));
|
||||
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.VERBOSE);
|
||||
|
||||
// Propagate any "environment variables" the test might need into System properties.
|
||||
String testEnvironment = bundle.getString(TestEnvironment.PROPERTY_NAMESPACE);
|
||||
if (testEnvironment != null) {
|
||||
for (String joinedProp : testEnvironment.split(",")) {
|
||||
String[] splitProp = joinedProp.split("=", 2);
|
||||
if (splitProp.length != 2) {
|
||||
continue;
|
||||
}
|
||||
System.setProperty(TestEnvironment.PROPERTY_NAMESPACE + "." + splitProp[0], splitProp[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,503 +0,0 @@
|
||||
// Forked from https://github.com/rustls/rustls-platform-verifier/blob/v/0.5.1/android/rustls-platform-verifier/src/main/java/org/rustls/platformverifier/CertificateVerifier.kt.
|
||||
// under the MIT License:
|
||||
//
|
||||
// Copyright (c) 2022 1Password
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Additional modifications Copyright 2025 Signal Messenger, LLC.
|
||||
|
||||
// We use the same package and class name to avoid having to change the Rust side of the bridge.
|
||||
package org.rustls.platformverifier
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.http.X509TrustManagerExtensions
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.MessageDigest
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateExpiredException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.CertificateNotYetValidException
|
||||
import java.security.cert.CertificateParsingException
|
||||
import java.security.cert.PKIXBuilderParameters
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
import java.util.EnumSet
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
// If this is updated, update the Rust definition too.
|
||||
// Marked private as this is not meant to be used in Android code.
|
||||
private enum class StatusCode(val value: Int) {
|
||||
Ok(0),
|
||||
Unavailable(1),
|
||||
Expired(2),
|
||||
UnknownCert(3),
|
||||
Revoked(4),
|
||||
InvalidEncoding(5),
|
||||
InvalidExtension(6),
|
||||
}
|
||||
|
||||
// Marked private as this is not meant to be used in Android code.
|
||||
private class VerificationResult(
|
||||
status: StatusCode,
|
||||
@Suppress("unused") val message: String? = null
|
||||
) {
|
||||
@Suppress("unused")
|
||||
private val code: Int = status.value
|
||||
}
|
||||
|
||||
// ADDED BY SIGNAL: Takes the place of an Android library BuildConfig.
|
||||
private object BuildConfig {
|
||||
const val TEST: Boolean = false
|
||||
}
|
||||
|
||||
// NOTE: All TrustManager and certificate validation methods are not thread safe. These
|
||||
// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior.
|
||||
|
||||
// Only JNI and test code calls this, so unused code warnings are suppressed.
|
||||
// Internal for test code - no other Kotlin code should use this object directly.
|
||||
// MODIFIED FOR SIGNAL: exposed as public so we can set `shouldCheckRevocation`
|
||||
@Suppress("unused")
|
||||
// We want to show a difference between Kotlin-side logs and those in Rust code
|
||||
@SuppressLint("LongLogTag")
|
||||
public object CertificateVerifier {
|
||||
private const val TAG = "rustls-platform-verifier-android"
|
||||
|
||||
// ADDED BY SIGNAL
|
||||
@JvmStatic
|
||||
public var shouldCheckRevocation: Boolean = false
|
||||
|
||||
private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? {
|
||||
// This can never throw since the default algorithm is used.
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
factory.init(keystore)
|
||||
|
||||
val availableTrustManagers = try {
|
||||
factory.trustManagers
|
||||
} catch (e: RuntimeException) {
|
||||
Log.w(TAG, "exception thrown creating a TrustManager: $e")
|
||||
return null
|
||||
}
|
||||
|
||||
for (manager in availableTrustManagers) {
|
||||
if (manager is X509TrustManager) {
|
||||
// Kotlin ensures this can't throw at runtime since it knows that
|
||||
// it must be the correct type by now.
|
||||
return X509TrustManagerExtensions(manager)
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "failed to find a usable trust manager")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun makeLazyTrustManager(keystore: KeyStore?): Lazy<X509TrustManagerExtensions?> {
|
||||
// Ensure the keystore is loaded. Since all of the trust managers are initialized in a
|
||||
// `Lazy`, this will only run once.
|
||||
keystore?.load(null)
|
||||
|
||||
return lazy { createTrustManager(keystore) }
|
||||
}
|
||||
|
||||
// -- Test only --
|
||||
// Ideally, all of this will be optimized out at compile time due to not being accessed
|
||||
// in release builds.
|
||||
|
||||
@get:Synchronized
|
||||
private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
|
||||
@get:Synchronized
|
||||
private var mockTrustManager: Lazy<X509TrustManagerExtensions?> =
|
||||
makeLazyTrustManager(mockKeystore)
|
||||
|
||||
@JvmStatic
|
||||
private fun addMockRoot(root: ByteArray) {
|
||||
if (!BuildConfig.TEST) {
|
||||
throw Exception("attempted to add a mock root outside a test!")
|
||||
}
|
||||
|
||||
val alias = "root_${mockKeystore.size()}"
|
||||
// Throwing here is fine since test roots should always be well-formed
|
||||
val cert = certFactory.generateCertificate(ByteArrayInputStream(root))
|
||||
mockKeystore.setCertificateEntry(alias, cert)
|
||||
|
||||
reloadMockData()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun clearMockRoots() {
|
||||
// Reload to get a completely fresh internal state
|
||||
mockKeystore.load(null)
|
||||
reloadMockData()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun reloadMockData() {
|
||||
if (mockTrustManager.isInitialized()) {
|
||||
mockTrustManager = makeLazyTrustManager(mockKeystore)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of the system's root CAs.
|
||||
// Function is public for testing only.
|
||||
@JvmStatic
|
||||
public fun getSystemRootCAs(): List<X509Certificate> {
|
||||
val rootCAs = mutableListOf<X509Certificate>()
|
||||
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(systemKeystore)
|
||||
|
||||
val availableTrustManagers = try {
|
||||
factory.trustManagers
|
||||
} catch (e: RuntimeException) {
|
||||
Log.w(TAG, "exception thrown creating a TrustManager: $e")
|
||||
return rootCAs
|
||||
}
|
||||
|
||||
availableTrustManagers.forEach { trustManager ->
|
||||
if (trustManager is X509TrustManager) {
|
||||
rootCAs.addAll(trustManager.acceptedIssuers)
|
||||
}
|
||||
}
|
||||
|
||||
return rootCAs
|
||||
}
|
||||
|
||||
// -- End testing requirements --
|
||||
|
||||
private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
|
||||
|
||||
private var systemTrustAnchorCache = hashSetOf<Pair<X500Principal, PublicKey>>()
|
||||
|
||||
@get:Synchronized
|
||||
private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath ->
|
||||
File("$rootPath/etc/security/cacerts")
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
private val systemKeystore: KeyStore? = try {
|
||||
KeyStore.getInstance("AndroidCAStore")
|
||||
} catch (_: KeyStoreException) {
|
||||
null
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
private val systemTrustManager: Lazy<X509TrustManagerExtensions?> =
|
||||
makeLazyTrustManager(systemKeystore)
|
||||
|
||||
@JvmStatic
|
||||
private fun verifyCertificateChain(
|
||||
@Suppress("UNUSED_PARAMETER") context: Context,
|
||||
serverName: String,
|
||||
authMethod: String,
|
||||
allowedEkus: Array<String>,
|
||||
ocspResponse: ByteArray?,
|
||||
time: Long,
|
||||
certChain: Array<ByteArray>
|
||||
): VerificationResult {
|
||||
// Convert the array of (supposedly) DER bytes into certificates.
|
||||
val certificateChain = mutableListOf<X509Certificate>()
|
||||
certChain.forEach { certBytes ->
|
||||
val certificate = try {
|
||||
certFactory.generateCertificate(ByteArrayInputStream(certBytes))
|
||||
} catch (e: CertificateException) {
|
||||
return VerificationResult(StatusCode.InvalidEncoding)
|
||||
}
|
||||
certificateChain.add(certificate as X509Certificate)
|
||||
}
|
||||
|
||||
// Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait
|
||||
// has a mandatory `end_entity` parameter in `verify_server_cert`.
|
||||
val endEntity = certificateChain[0]
|
||||
|
||||
// Check that the certificate is valid at the point of time provided by `rustls`.
|
||||
try {
|
||||
endEntity.checkValidity(Date(time))
|
||||
} catch (e: CertificateExpiredException) {
|
||||
return VerificationResult(StatusCode.Expired)
|
||||
} catch (e: CertificateNotYetValidException) {
|
||||
return VerificationResult(StatusCode.Expired)
|
||||
}
|
||||
|
||||
// Check that this certificate can be used in a TLS server.
|
||||
if (!verifyCertUsage(endEntity, allowedEkus)) {
|
||||
return VerificationResult(StatusCode.InvalidExtension)
|
||||
}
|
||||
|
||||
// Select the trust manager to use.
|
||||
//
|
||||
// We select them as follows:
|
||||
// - If built for release, only use the system trust manager. This should let all test-related
|
||||
// code be optimized out.
|
||||
// - If built for tests:
|
||||
// - If the mock CA store has any values, use the mock trust manager.
|
||||
// - Otherwise, use the system trust manager.
|
||||
val (trustManager, keystore) = if (!BuildConfig.TEST) {
|
||||
val trustManager =
|
||||
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
|
||||
Pair(trustManager, systemKeystore)
|
||||
} else {
|
||||
if (mockKeystore.size() != 0) {
|
||||
val trustManager = mockTrustManager.value!!
|
||||
Pair(trustManager, mockKeystore)
|
||||
} else {
|
||||
val trustManager =
|
||||
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
|
||||
Pair(trustManager, systemKeystore)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the certificate chain is valid and correct, and nothing more.
|
||||
//
|
||||
// NOTE: This does not validate `serverName` is valid for the end-entity certificate.
|
||||
// That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant
|
||||
// hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24.
|
||||
//
|
||||
// `serverName` is only used for pinning/CT requirements.
|
||||
//
|
||||
// Returns the "the properly ordered chain used for verification as a list of X509Certificates.",
|
||||
// meaning a list from end-entity certificate to trust-anchor.
|
||||
val validChain = try {
|
||||
trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName)
|
||||
} catch (e: CertificateException) {
|
||||
// In test configurations we may see `checkServerTrusted` fail once vendored test
|
||||
// certificates pass their expiry date. We try to avoid that by using a fixed
|
||||
// verification time when calling `endEntity.checkValidity` above, however we can't
|
||||
// fix the time for the `checkServerTrusted` call.
|
||||
//
|
||||
// To make diagnosing CI test failures easier we try to find the root cause of
|
||||
// checkServerTrusted failing, returning a different `StatusCode` as appropriate.
|
||||
if (BuildConfig.TEST) {
|
||||
var rootCause: Throwable? = e
|
||||
while (rootCause?.cause != null && rootCause.cause != rootCause) {
|
||||
rootCause = rootCause.cause
|
||||
}
|
||||
return when (rootCause) {
|
||||
is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult(
|
||||
StatusCode.Expired,
|
||||
rootCause.toString()
|
||||
)
|
||||
|
||||
else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString())
|
||||
}
|
||||
}
|
||||
// In non-test configurations we should have caught expiry errors earlier and
|
||||
// can simply return an unknown cert error without digging through the exception
|
||||
// cause chain.
|
||||
return VerificationResult(StatusCode.UnknownCert, e.toString())
|
||||
}
|
||||
|
||||
// TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled,
|
||||
// because Android requires certificates to an specify OCSP responder for network fetch in this case.
|
||||
// If in testing w/o OCSP stapled, short-circuit here - only prior checks apply.
|
||||
if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) {
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
// Try to check the revocation status of the cert, if it is supported.
|
||||
//
|
||||
// This is supported at >= API 24, but we're supporting 22 (Android 5) for the best
|
||||
// compatibility.
|
||||
//
|
||||
// MODIFIED BY SIGNAL: only if shouldCheckRevocation is set.
|
||||
if (shouldCheckRevocation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Note:
|
||||
//
|
||||
// 1. Android does not provide any way only to attempt to validate revocation from cached
|
||||
// data like the other platforms do. This means it will always use the network for
|
||||
// certificates which had no stapled response.
|
||||
//
|
||||
// 2: Likely because of 1, Android requires all issued certificates to have some form of
|
||||
// revocation included in their authority information. This doesn't work universally as
|
||||
// issuing certificates in use may omit authority access information (for example the
|
||||
// Let's Encrypt R3 Intermediate Certificate).
|
||||
//
|
||||
// Given these constraints, the best option is to only check revocation information
|
||||
// at the end-entity depth. We will prefer OCSP (to use stapled information if possible).
|
||||
// If there is no stapled OCSP response, Android may use the network to attempt to fetch
|
||||
// one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft"
|
||||
// failures, for example transient network errors.
|
||||
//
|
||||
// In the case of a non-public root, such as an internal CA or self-signed certificate,
|
||||
// we opt to skip revocation checks entirely. The only exception is if the server
|
||||
// provided stapled OCSP data, which is an explicit signal and won't introduce non-ideal
|
||||
// platform behavior when attempting validation.
|
||||
//
|
||||
// This is because these are cases where a user or administrator has explicitly opted to
|
||||
// trust a certificate they (at least believe) have control over. These certificates rarely
|
||||
// contain revocation information as well, so these cases don't lose much.
|
||||
// See https://github.com/rustls/rustls-platform-verifier/issues/69 as well.
|
||||
if (ocspResponse == null && !isKnownRoot(validChain.last())) {
|
||||
// Chain validation must have succeeded by this point.
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
val parameters = PKIXBuilderParameters(keystore, null)
|
||||
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
val revocationChecker = validator.revocationChecker as PKIXRevocationChecker
|
||||
|
||||
revocationChecker.options = EnumSet.of(
|
||||
PKIXRevocationChecker.Option.SOFT_FAIL,
|
||||
PKIXRevocationChecker.Option.ONLY_END_ENTITY
|
||||
)
|
||||
|
||||
// Use the OCSP data `rustls` provided, if present.
|
||||
// Its expected that the server only sends revocation data for its own leaf certificate.
|
||||
//
|
||||
// If this field is set, then Android will use it and skip any networking to
|
||||
// attempt a fetch for that certificate. Otherwise, it will attempt to fetch it from the network.
|
||||
// Ref: https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/provider/certpath/RevocationChecker.java;l=694
|
||||
ocspResponse?.let { providedResponse ->
|
||||
revocationChecker.ocspResponses = mapOf(endEntity to providedResponse)
|
||||
}
|
||||
|
||||
// Use the custom revocation definition.
|
||||
// "Note that when a `PKIXRevocationChecker` is added to `PKIXParameters`, it clones the `PKIXRevocationChecker`;
|
||||
// thus any subsequent modifications to the `PKIXRevocationChecker` have no effect."
|
||||
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
|
||||
parameters.certPathCheckers = listOf(revocationChecker)
|
||||
// "When supplying a revocation checker in this manner, it will be used to check revocation
|
||||
// irrespective of the setting of the `RevocationEnabled` flag."
|
||||
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
|
||||
parameters.isRevocationEnabled = false
|
||||
|
||||
// Validate the revocation status of the end entity certificate.
|
||||
try {
|
||||
validator.validate(certFactory.generateCertPath(validChain), parameters)
|
||||
} catch (e: CertPathValidatorException) {
|
||||
return VerificationResult(StatusCode.Revoked, e.toString())
|
||||
}
|
||||
|
||||
// MODIFIED BY SIGNAL: The warning log used to be unconditional.
|
||||
} else if (shouldCheckRevocation) {
|
||||
// This is allowed to be skipped since revocation checking is best-effort.
|
||||
Log.w(TAG, "did not attempt to validate OCSP due to Android version")
|
||||
} else {
|
||||
Log.v(TAG, "note: revocation checking disabled")
|
||||
}
|
||||
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array<String>): Boolean {
|
||||
val ekus = try {
|
||||
certificate.extendedKeyUsage
|
||||
}
|
||||
// This should be unreachable, but could happen.
|
||||
catch (_: CertificateParsingException) {
|
||||
return false
|
||||
} catch (_: NullPointerException) {
|
||||
// According to Chromium's implementation, this can crash when the EKU data is malformed.
|
||||
Log.w(TAG, "exception handling certificate EKU")
|
||||
return false
|
||||
} ?: return true // If the list is empty, we have nothing to do.
|
||||
|
||||
return ekus.any { allowedEkus.contains(it) }
|
||||
}
|
||||
|
||||
// Android hashes a principal using the first four bytes of its MD5 digest, encoded in
|
||||
// lowercase hex and reversed.
|
||||
//
|
||||
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339
|
||||
private fun hashPrincipal(principal: X500Principal): String {
|
||||
val hexDigits = "0123456789abcdef".toCharArray()
|
||||
val digest = MessageDigest.getInstance("MD5").digest(principal.encoded)
|
||||
val hexChars = CharArray(8)
|
||||
|
||||
for (i in 0..3) {
|
||||
// Kotlin doesn't support bitwise operators for bytes, only Int and Long.
|
||||
val digestByte = digest[3 - i].toInt()
|
||||
hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf]
|
||||
hexChars[2 * i + 1] = hexDigits[digestByte and 0xf]
|
||||
}
|
||||
|
||||
return String(hexChars)
|
||||
}
|
||||
|
||||
// Check if CA root is known or not.
|
||||
// Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user.
|
||||
//
|
||||
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351
|
||||
public fun isKnownRoot(root: X509Certificate): Boolean {
|
||||
// System keystore and cert directory must be non-null to perform checking
|
||||
systemKeystore?.let { loadedSystemKeystore ->
|
||||
systemCertificateDirectory?.let { loadedSystemCertificateDirectory ->
|
||||
|
||||
// Check the in-memory cache first
|
||||
val key = Pair(root.subjectX500Principal, root.publicKey)
|
||||
if (systemTrustAnchorCache.contains(key)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// System trust anchors are stored under a hash of the principal.
|
||||
// In case of collisions, append number.
|
||||
val hash = hashPrincipal(root.subjectX500Principal)
|
||||
var i = 0
|
||||
while (true) {
|
||||
val alias = "$hash.$i"
|
||||
|
||||
if (!File(loadedSystemCertificateDirectory, alias).exists()) {
|
||||
break
|
||||
}
|
||||
|
||||
val anchor = loadedSystemKeystore.getCertificate("system:$alias")
|
||||
|
||||
// It's possible for `anchor` to be `null` if the user deleted a trust anchor.
|
||||
// Continue iterating as there may be further collisions after the deleted anchor.
|
||||
if (anchor == null) {
|
||||
continue
|
||||
// This should never happen
|
||||
} else if (anchor !is X509Certificate) {
|
||||
// SAFETY: This logs a unique identifier (hash value) only in cases where a file within the
|
||||
// system's root trust store is not a valid X509 certificate (extremely unlikely error).
|
||||
// The hash doesn't tell us any sensitive information about the invalid cert or reveal any of
|
||||
// its contents - it just lets us ID the bad file if a user is having TLS failure issues.
|
||||
Log.e(TAG, "anchor is not a certificate, alias: $alias")
|
||||
continue
|
||||
// If subject and public key match, it's a system root.
|
||||
} else {
|
||||
if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) {
|
||||
systemTrustAnchorCache.add(key)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in cache or store: non-public
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = "BackupTool"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':client')
|
||||
implementation 'info.picocli:picocli:4.7.6'
|
||||
annotationProcessor 'info.picocli:picocli-codegen:4.7.6'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] // recommended by picocli
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.concurrent.Callable;
|
||||
import org.signal.libsignal.messagebackup.MessageBackup;
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLogger;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
class BackupTool implements Callable<Integer> {
|
||||
@Option(names = "--hmac-key")
|
||||
String hmacKey;
|
||||
|
||||
@Option(names = "--aes-key")
|
||||
String aesKey;
|
||||
|
||||
@Parameters File input;
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = new CommandLine(new BackupTool()).execute(args);
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.INFO);
|
||||
SignalProtocolLoggerProvider.setProvider(
|
||||
new SignalProtocolLogger() {
|
||||
public void log(int priority, String tag, String message) {
|
||||
System.err.println(priority + " " + message);
|
||||
}
|
||||
});
|
||||
|
||||
byte[] hmacKey = Hex.fromStringCondensed(this.hmacKey);
|
||||
byte[] aesKey = Hex.fromStringCondensed(this.aesKey);
|
||||
var backupKey = MessageBackupKey.fromParts(hmacKey, aesKey);
|
||||
|
||||
MessageBackup.ValidationResult result =
|
||||
MessageBackup.validate(
|
||||
backupKey,
|
||||
MessageBackup.Purpose.REMOTE_BACKUP,
|
||||
() -> {
|
||||
try {
|
||||
return new FileInputStream(input);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
},
|
||||
input.length());
|
||||
return result.unknownFieldMessages.length == 0 ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@ -1,92 +1,66 @@
|
||||
import org.gradle.api.publish.PublishingExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id "base"
|
||||
id "signing"
|
||||
id "com.diffplug.spotless" version "7.2.1"
|
||||
|
||||
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.13.2" apply false
|
||||
id 'org.jetbrains.kotlin.android' version "2.2.20" apply false
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
mavenLocal()
|
||||
id "com.diffplug.spotless" version "6.20.0"
|
||||
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.94.1"
|
||||
version = "0.41.2"
|
||||
group = "org.signal"
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ["-Xlint:deprecation", "-Xlint:fallthrough", "-Xlint:unchecked"]
|
||||
}
|
||||
tasks.withType(Javadoc) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
compilerOptions.jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
subprojects {
|
||||
if (JavaVersion.current().isJava8Compatible()) {
|
||||
allprojects {
|
||||
tasks.withType(Javadoc) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "org.jetbrains.dokka"
|
||||
apply plugin: "org.jetbrains.dokka-javadoc"
|
||||
allprojects {
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ["-Xlint:deprecation", "-Xlint:fallthrough", "-Xlint:unchecked"]
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.diffplug.spotless"
|
||||
spotless {
|
||||
java {
|
||||
target('**/*.java')
|
||||
targetExclude('**/Native.java')
|
||||
importOrder()
|
||||
removeUnusedImports()
|
||||
|
||||
googleJavaFormat()
|
||||
formatAnnotations()
|
||||
licenseHeaderFile rootProject.file('license_header.txt')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'] : []
|
||||
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
|
||||
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
|
||||
// Explicitly specify 'bash' for Windows compatibility.
|
||||
commandLine 'bash', './build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, 'desktop'
|
||||
}
|
||||
|
||||
task makeJniLibrariesServer(type:Exec) {
|
||||
group = 'Rust'
|
||||
description = 'Build the JNI libraries'
|
||||
|
||||
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 target = project.hasProperty('crossCompileServer') ? 'server-all' : 'server'
|
||||
// Explicitly specify 'bash' for Windows compatibility.
|
||||
commandLine 'bash', './build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, target
|
||||
commandLine 'bash', './build_jni.sh', 'desktop'
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
delete fileTree('./client/src/main/resources') {
|
||||
include '**/*.so'
|
||||
include '**/*.dylib'
|
||||
include '**/*.dll'
|
||||
}
|
||||
delete fileTree('./server/src/main/resources') {
|
||||
delete fileTree('./shared/resources') {
|
||||
include '**/*.so'
|
||||
include '**/*.dylib'
|
||||
include '**/*.dll'
|
||||
@ -97,7 +71,7 @@ clean.dependsOn([cargoClean, cleanJni])
|
||||
|
||||
// PUBLISHING
|
||||
|
||||
ext.setUpSigningKey = { signingExt ->
|
||||
ext.setUpSigningKey = { signingExt ->
|
||||
def signingKeyId = findProperty("signingKeyId")
|
||||
def signingKey = findProperty("signingKey")
|
||||
def signingPassword = findProperty("signingPassword")
|
||||
@ -106,25 +80,11 @@ ext.setUpSigningKey = { signingExt ->
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
nexusPublishing {
|
||||
repositories {
|
||||
sonatype {
|
||||
username = project.findProperty('sonatypeUsername') ?: ""
|
||||
password = project.findProperty('sonatypePassword') ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,32 +92,3 @@ subprojects { subproject ->
|
||||
def isReleaseBuild() {
|
||||
return version.contains("SNAPSHOT") == false
|
||||
}
|
||||
|
||||
// Late evaluation after this point.
|
||||
|
||||
evaluationDependsOnChildren()
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
target allprojects.collectMany {
|
||||
return it.tasks.withType(KotlinCompile)
|
||||
}.inject(files()) { collected, next ->
|
||||
collected + next.sources
|
||||
}
|
||||
targetExclude('**/Native.kt', '**/NativeTesting.kt', '**/org/rustls/**')
|
||||
ktlint()
|
||||
}
|
||||
java {
|
||||
target allprojects.collectMany {
|
||||
return it.tasks.withType(JavaCompile)
|
||||
}.inject(files()) { collected, next ->
|
||||
collected + next.source
|
||||
}
|
||||
importOrder()
|
||||
removeUnusedImports()
|
||||
|
||||
googleJavaFormat()
|
||||
formatAnnotations()
|
||||
licenseHeaderFile rootProject.file('license_header.txt')
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,162 +13,41 @@ cd "${SCRIPT_DIR}"/..
|
||||
|
||||
# These paths are relative to the root directory
|
||||
ANDROID_LIB_DIR=java/android/src/main/jniLibs
|
||||
DESKTOP_LIB_DIR=java/client/src/main/resources
|
||||
SERVER_LIB_DIR=java/server/src/main/resources
|
||||
DESKTOP_LIB_DIR=java/shared/resources
|
||||
|
||||
# Fetch dependencies first, so we can use them in computing later options.
|
||||
# But allow this to fail in case we're offline.
|
||||
cargo fetch || true
|
||||
export CARGO_PROFILE_RELEASE_DEBUG=1 # enable line tables
|
||||
export CARGO_PROFILE_RELEASE_OPT_LEVEL=s # optimize for size over speed
|
||||
|
||||
export CARGO_PROFILE_RELEASE_DEBUG=1 # Enable line tables
|
||||
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
|
||||
RUSTFLAGS="--cfg tokio_unstable ${RUSTFLAGS:-}" # Access tokio's unstable metrics
|
||||
RUSTFLAGS="$(rust_remap_path_options) ${RUSTFLAGS:-}" # Strip absolute paths
|
||||
export RUSTFLAGS
|
||||
|
||||
DEBUG_LEVEL_LOGS=
|
||||
JNI_TYPE_TAGGING=
|
||||
RUST_RELEASE="release"
|
||||
while [ "${1:-}" != "" ]; do
|
||||
case "${1:-}" in
|
||||
--debug-level-logs )
|
||||
DEBUG_LEVEL_LOGS=1
|
||||
shift
|
||||
;;
|
||||
--libsignal-debug )
|
||||
LIBSIGNAL_DEBUG=1
|
||||
shift
|
||||
;;
|
||||
--jni-type-tagging )
|
||||
JNI_TYPE_TAGGING=1
|
||||
shift
|
||||
;;
|
||||
--jni-check-annotations )
|
||||
JNI_CHECK_ANNOTATIONS=1
|
||||
shift
|
||||
;;
|
||||
--debug )
|
||||
RUST_RELEASE=
|
||||
shift
|
||||
;;
|
||||
-* )
|
||||
echo "Unrecognized flag $1; expected --debug-level-logs, --jni-type-tagging, or --debug" >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${DEBUG_LEVEL_LOGS:-}" ]]; then
|
||||
FEATURES+=("log/release_max_level_info")
|
||||
fi
|
||||
if [[ -n "${JNI_TYPE_TAGGING:-}" ]]; then
|
||||
FEATURES+=("libsignal-bridge-types/jni-type-tagging")
|
||||
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 () {
|
||||
if [[ "${RUST_RELEASE}" == "" ]]; then
|
||||
# Unused strings are only stripped in release builds, not debug builds,
|
||||
# so the check below won't tell us anything.
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "${DEBUG_LEVEL_LOGS:-}" ]]; then
|
||||
# See libsignal-jni's logging.rs for the strings matched by this pattern.
|
||||
# Searching *every* file in the lib directory is probably overkill,
|
||||
# but it's easier than figuring out prefixes and suffixes like copy_built_library does.
|
||||
if grep -q -- '-LEVEL LOGS ENABLED' "$1"/*; then
|
||||
echo 'error: debug-level logs found in build that should not have them!' >&2
|
||||
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
|
||||
build_desktop_for_arch () {
|
||||
local CC
|
||||
local CXX
|
||||
local CPATH
|
||||
|
||||
local lib_dir="${3}/"
|
||||
local cpuarch="${1%%-*}"
|
||||
case "$cpuarch" in
|
||||
x86_64)
|
||||
suffix=amd64
|
||||
;;
|
||||
aarch64)
|
||||
suffix=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "building for unknown CPU architecture ${cpuarch}; update build_jni.sh"
|
||||
exit 2
|
||||
esac
|
||||
if [[ "$1" != "$2" ]]; then
|
||||
# Set up cross-compiling flags
|
||||
if [[ "$1" == *-linux-* && "$2" == *-linux-* && -z "${CC:-}" ]]; then
|
||||
# When cross-compiling *from* Linux *to* Linux,
|
||||
# set up standard cross-compiling environment if not already set
|
||||
echo 'setting Linux cross-compilation options...'
|
||||
export "CARGO_TARGET_$(echo "$cpuarch" | tr "[:lower:]" "[:upper:]")_UNKNOWN_LINUX_GNU_LINKER"="${cpuarch}-linux-gnu-gcc"
|
||||
export CC="${cpuarch}-linux-gnu-gcc"
|
||||
export CXX="${cpuarch}-linux-gnu-g++"
|
||||
export CPATH="/usr/${cpuarch}-linux-gnu/include"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo_then_run cargo build -p libsignal-jni -p libsignal-jni-testing ${RUST_RELEASE:+--release} ${FEATURES:+--features "${FEATURES[*]}"} --target "$1"
|
||||
copy_built_library "target/${1}/${RUST_RELEASE:-debug}" signal_jni "$lib_dir" "signal_jni_${suffix}"
|
||||
copy_built_library "target/${1}/${RUST_RELEASE:-debug}" signal_jni_testing "$lib_dir" "signal_jni_testing_${suffix}"
|
||||
check_for_debug_level_logs_if_needed "$lib_dir"
|
||||
}
|
||||
BUILD_FOR_TEST=
|
||||
case "${1:-}" in
|
||||
--testing )
|
||||
BUILD_FOR_TEST=1
|
||||
shift
|
||||
;;
|
||||
-* )
|
||||
echo "Unrecognized flag $1; use --testing to compile with test functions" >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
# Do nothing
|
||||
;;
|
||||
esac
|
||||
|
||||
android_abis=()
|
||||
|
||||
while [ "${1:-}" != "" ]; do
|
||||
case "${1:-}" in
|
||||
desktop | server | server-all )
|
||||
if [[ "$1" == desktop ]]; then
|
||||
lib_dir=$DESKTOP_LIB_DIR
|
||||
else
|
||||
lib_dir=$SERVER_LIB_DIR
|
||||
fi
|
||||
desktop )
|
||||
# On Linux, cdylibs don't include public symbols from their dependencies,
|
||||
# even if those symbols have been re-exported in the Rust source.
|
||||
# Using LTO works around this at the cost of a slightly slower build.
|
||||
# https://github.com/rust-lang/rfcs/issues/2771
|
||||
export CARGO_PROFILE_RELEASE_LTO=thin
|
||||
host_triple=$(rustc -vV | sed -n 's|host: ||p')
|
||||
if [[ "$1" == "server-all" ]]; then
|
||||
build_desktop_for_arch x86_64-unknown-linux-gnu "$host_triple" $lib_dir
|
||||
# Enable ARMv8.2 extensions for a production aarch64 server build
|
||||
RUSTFLAGS="-C target-feature=+v8.2a ${RUSTFLAGS:-}" \
|
||||
build_desktop_for_arch aarch64-unknown-linux-gnu "$host_triple" $lib_dir
|
||||
else
|
||||
build_desktop_for_arch "${CARGO_BUILD_TARGET:-$host_triple}" "$host_triple" $lib_dir
|
||||
echo_then_run cargo build -p libsignal-jni --release --features testing-fns
|
||||
if [[ -z "${CARGO_BUILD_TARGET:-}" ]]; then
|
||||
copy_built_library target/release signal_jni "${DESKTOP_LIB_DIR}/"
|
||||
fi
|
||||
exit
|
||||
;;
|
||||
|
||||
android )
|
||||
android_abis+=(arm64-v8a armeabi-v7a x86_64 x86)
|
||||
;;
|
||||
@ -192,38 +71,19 @@ while [ "${1:-}" != "" ]; do
|
||||
shift
|
||||
done
|
||||
|
||||
if (( ${#android_abis[@]} == 0 )); then
|
||||
echo "Missing target (use 'desktop', 'android', or 'android-\$ARCH')" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Everything from here down is Android-only.
|
||||
export CARGO_PROFILE_RELEASE_OPT_LEVEL=s # optimize for size over speed
|
||||
|
||||
# Use full LTO and small BoringSSL curve tables to reduce binary size.
|
||||
export CFLAGS="-DOPENSSL_SMALL -flto=full ${CFLAGS:-}"
|
||||
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/)
|
||||
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 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"
|
||||
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${CC_aarch64_linux_android}"
|
||||
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${CC_armv7_linux_androideabi}"
|
||||
@ -231,11 +91,17 @@ export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="${CC_x86_64_linux_android}"
|
||||
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="${CC_i686_linux_android}"
|
||||
|
||||
export TARGET_AR="${ANDROID_TOOLCHAIN_DIR}/llvm-ar"
|
||||
export RUSTFLAGS="--cfg aes_armv8 --cfg polyval_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
|
||||
|
||||
# The 64-bit curve25519-dalek backend is faster than the 32-bit one on at least some armv7a phones.
|
||||
# Comment out the following to allow the 32-bit backend on 32-bit targets.
|
||||
export RUSTFLAGS="--cfg curve25519_dalek_bits=\"64\" ${RUSTFLAGS:-}"
|
||||
|
||||
if [ $BUILD_FOR_TEST ]; then
|
||||
FEATURES="testing-fns"
|
||||
ANDROID_LIB_DIR="${ANDROID_LIB_DIR}/../../androidTest/jniLibs"
|
||||
fi
|
||||
|
||||
target_for_abi() {
|
||||
case "$1" in
|
||||
arm64-v8a)
|
||||
@ -257,9 +123,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
|
||||
check_for_debug_level_logs_if_needed "${ANDROID_LIB_DIR}/$abi"
|
||||
echo_then_run cargo build -p libsignal-jni --release ${FEATURES:+--features $FEATURES} -Z unstable-options --target "$rust_target" --out-dir "${ANDROID_LIB_DIR}/$abi"
|
||||
done
|
||||
|
||||
@ -5,130 +5,95 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Callable, Iterable, List, Mapping, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def warn(message: str) -> None:
|
||||
def warn(message):
|
||||
if 'GITHUB_ACTIONS' in os.environ:
|
||||
print('::warning ::' + message)
|
||||
print("::warning ::" + message)
|
||||
else:
|
||||
print('warning: ' + message, file=sys.stderr)
|
||||
print("warning: " + message, file=sys.stderr)
|
||||
|
||||
|
||||
def measure_stripped_library_size(lib_path: str) -> int:
|
||||
ndk_home = os.environ.get('ANDROID_NDK_HOME')
|
||||
if not ndk_home:
|
||||
raise Exception('must set ANDROID_NDK_HOME to an Android NDK to run this script')
|
||||
|
||||
strip_glob = os.path.join(ndk_home, 'toolchains', 'llvm', 'prebuilt', '*', 'bin', 'llvm-strip')
|
||||
strip = next(glob.iglob(strip_glob), None)
|
||||
if not strip:
|
||||
raise Exception('NDK does not contain llvm-strip (tried {})'.format(strip_glob))
|
||||
|
||||
return len(subprocess.check_output([strip, '-o', '-', lib_path]))
|
||||
|
||||
|
||||
def print_size_diff(lib_size: int, old_entry: Mapping[str, Any], *, warn_on_jump: bool = True) -> None:
|
||||
def print_size_diff(lib_size, old_entry):
|
||||
delta = lib_size - old_entry['size']
|
||||
delta_fraction = (float(delta) / old_entry['size'])
|
||||
message = f"current build is {delta} bytes ({int(delta_fraction * 100)}%) larger than {old_entry['version']}"
|
||||
if warn_on_jump and delta > 100_000:
|
||||
message = "current build is {0}% larger than {1} (current: {2} bytes, {1}: {3} bytes)".format(
|
||||
int(delta_fraction * 100),
|
||||
old_entry['version'],
|
||||
lib_size,
|
||||
old_entry['size']
|
||||
)
|
||||
if delta_fraction > 0.10:
|
||||
warn(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
|
||||
def print_size_for_release(lib_size: int) -> None:
|
||||
message = f'if this this commit marks a release, update code_size.json with {lib_size}'
|
||||
print(message)
|
||||
|
||||
|
||||
def current_origin_main_entry() -> Optional[Mapping[str, Any]]:
|
||||
def current_origin_main_entry():
|
||||
try:
|
||||
if os.environ.get('GITHUB_EVENT_NAME') == 'push':
|
||||
base_ref = os.environ.get('GITHUB_REF_NAME', 'HEAD^')
|
||||
most_recent_commit = subprocess.run(['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True).stdout.decode().strip()
|
||||
else:
|
||||
base_ref = os.environ.get('GITHUB_BASE_REF', 'main')
|
||||
remote_name = os.environ.get('CHECK_CODE_SIZE_REMOTE', 'origin')
|
||||
most_recent_commit = subprocess.run(['git', 'merge-base', 'HEAD', f'{remote_name}/{base_ref}'], capture_output=True, check=True).stdout.decode().strip()
|
||||
most_recent_main = subprocess.run(["git", "merge-base", "HEAD", "origin/main"], capture_output=True, check=True).stdout.decode().strip()
|
||||
|
||||
repo_path = os.environ.get('GITHUB_REPOSITORY')
|
||||
if repo_path is None:
|
||||
repo_path = subprocess.run(['gh', 'repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'], capture_output=True, check=True).stdout.decode().strip()
|
||||
repo_path = subprocess.run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], capture_output=True, check=True).stdout.decode().strip()
|
||||
|
||||
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_info = subprocess.run(["gh", "api", "--method=GET", f"repos/{repo_path}/actions/runs", "-f", f"head_sha={most_recent_main}"], capture_output=True, check=True).stdout
|
||||
runs_json = json.loads(runs_info)
|
||||
|
||||
run_id = [run['id'] for run in runs_json['workflow_runs'] if 'Build and Test' in run['name']][0]
|
||||
run_id = [run['id'] for run in runs_json['workflow_runs'] if run['name'] == 'Build and Test'][0]
|
||||
|
||||
run_jobs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, f'{run_id}', '--json', 'jobs'], capture_output=True, check=True).stdout
|
||||
run_jobs = subprocess.run(["gh", "run", "view", 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 Android'][0]
|
||||
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == "Java"][0]
|
||||
|
||||
job_logs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, '--job', f'{job_id}', '--log'], capture_output=True, check=True).stdout.decode()
|
||||
job_logs = subprocess.run(["gh", "run", "view", "--job", f"{job_id}", "--log"], capture_output=True, check=True).stdout.decode()
|
||||
|
||||
for line in job_logs.splitlines():
|
||||
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})'}
|
||||
if "check_code_size.py" in line and "current build" in line:
|
||||
(_, after) = line.split("(current: ", maxsplit=1)
|
||||
(bytes_count, _) = after.split(" ", maxsplit=1)
|
||||
return {'size': int(bytes_count), 'version': most_recent_main[:6] + ' (main)'}
|
||||
|
||||
print(f'skipping checking current {base_ref} (most recent run did not include check_code_size.py)', file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
print(f'skipping checking current {base_ref}: {e}', file=sys.stderr)
|
||||
if isinstance(e, subprocess.CalledProcessError):
|
||||
print('stdout:', e.stdout.decode(), file=sys.stderr)
|
||||
print('stderr:', e.stderr.decode(), file=sys.stderr)
|
||||
|
||||
return None
|
||||
return None
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("not checking current origin/main:", e, file=sys.stderr)
|
||||
print("stdout:", e.stdout.decode(), file=sys.stderr)
|
||||
print("stderr:", e.stderr.decode(), file=sys.stderr)
|
||||
|
||||
|
||||
our_abs_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
lib_size = measure_stripped_library_size(os.path.join(
|
||||
our_abs_dir, 'android', 'src', 'main', 'jniLibs', 'arm64-v8a', 'libsignal_jni.so'))
|
||||
lib_size = os.path.getsize(os.path.join(
|
||||
our_abs_dir, 'android', 'build', 'intermediates', 'stripped_native_libs', 'release', 'stripReleaseDebugSymbols',
|
||||
'out', 'lib', 'arm64-v8a', 'libsignal_jni.so'))
|
||||
|
||||
with open(os.path.join(our_abs_dir, 'code_size.json')) as old_sizes_file:
|
||||
old_sizes = json.load(old_sizes_file)
|
||||
|
||||
most_recent_tag_entry = old_sizes[-1]
|
||||
print_size_diff(lib_size, most_recent_tag_entry)
|
||||
|
||||
origin_main_entry = current_origin_main_entry()
|
||||
if origin_main_entry is not None:
|
||||
print_size_diff(lib_size, most_recent_tag_entry, warn_on_jump=False)
|
||||
print_size_diff(lib_size, origin_main_entry)
|
||||
else:
|
||||
print_size_diff(lib_size, most_recent_tag_entry)
|
||||
print_size_for_release(lib_size)
|
||||
|
||||
|
||||
# Typing this properly requires a bunch of helpers in Python 3.9,
|
||||
# and we don't have a strict type at the use site anyway.
|
||||
def max_map(items: Iterable[T], transform: Callable[[T], Any]) -> Any:
|
||||
return transform(max(items, key=transform))
|
||||
def print_plot(sizes):
|
||||
highest_size = max(recent_sizes, key=lambda x: x['size'])['size']
|
||||
|
||||
|
||||
def print_plot(sizes: List[Mapping[str, Any]]) -> None:
|
||||
highest_size = max_map(recent_sizes, lambda x: x['size'])
|
||||
version_width = max_map(recent_sizes, lambda x: len(x['version']))
|
||||
|
||||
scale = 1.0 * 1024 * 1024
|
||||
scale = 1 * 1024 * 1024
|
||||
while scale < highest_size:
|
||||
scale *= 2
|
||||
scale /= 20
|
||||
plot_width = int(highest_size / scale) + 1
|
||||
|
||||
for entry in sizes:
|
||||
bucket = int(entry['size'] / scale) + 1
|
||||
print('{:>{}}: {:<{}} ({} bytes)'.format(entry['version'], version_width, '*' * bucket, plot_width, entry['size']))
|
||||
print('{:>14}: {} ({} bytes)'.format(entry['version'], '*' * bucket, entry['size']))
|
||||
|
||||
|
||||
recent_sizes = old_sizes[-10:]
|
||||
|
||||
@ -1,22 +1,11 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
// This isn't compatible with the `plugins` lookup method, so it has to
|
||||
// be declared in a `buildscript` block. See
|
||||
// https://github.com/gradle/gradle/issues/1541 for info.
|
||||
classpath 'com.guardsquare:proguard-gradle:7.4.2'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "libsignal-client"
|
||||
}
|
||||
sourceCompatibility = 17
|
||||
archivesBaseName = "libsignal-client"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -29,11 +18,6 @@ sourceSets {
|
||||
// Include libsignal sources shared between the client and server
|
||||
srcDir '../shared/java'
|
||||
}
|
||||
kotlin {
|
||||
srcDir 'src/main/java'
|
||||
// Include libsignal sources shared between the client and server
|
||||
srcDir '../shared/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../shared/resources'
|
||||
}
|
||||
@ -42,60 +26,28 @@ sourceSets {
|
||||
java {
|
||||
srcDir '../shared/test/java'
|
||||
}
|
||||
kotlin {
|
||||
srcDir 'src/test/java'
|
||||
srcDir '../shared/test/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '-Xcheck:jni'
|
||||
testLogging {
|
||||
events 'passed','skipped','failed'
|
||||
events 'passed'
|
||||
showStandardStreams = true
|
||||
showExceptions = true
|
||||
exceptionFormat = 'full'
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
showExceptions true
|
||||
exceptionFormat 'full'
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
explicitApi()
|
||||
}
|
||||
|
||||
sourcesJar {
|
||||
// Cut down on artifact size by leaving these out of the sources jar.
|
||||
exclude '*.dll'
|
||||
exclude '*.dylib'
|
||||
exclude '*.so'
|
||||
}
|
||||
|
||||
task dokkaHtmlJar(type: Jar) {
|
||||
dependsOn(dokkaGeneratePublicationHtml)
|
||||
from(dokkaGeneratePublicationHtml)
|
||||
archiveClassifier.set("dokka")
|
||||
}
|
||||
|
||||
task dokkaJavadocJar(type: Jar) {
|
||||
dependsOn(dokkaGeneratePublicationJavadoc)
|
||||
from(dokkaGeneratePublicationJavadoc)
|
||||
archiveClassifier.set("javadoc")
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
@ -105,6 +57,7 @@ tasks.named('jar') {
|
||||
}
|
||||
|
||||
processResources {
|
||||
// TODO: Build a different variant of the JNI library for server.
|
||||
dependsOn ':makeJniLibrariesDesktop'
|
||||
}
|
||||
|
||||
@ -113,13 +66,11 @@ processResources {
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = base.archivesName.get()
|
||||
artifactId = archivesBaseName
|
||||
from components.java
|
||||
artifact dokkaHtmlJar
|
||||
artifact dokkaJavadocJar
|
||||
|
||||
pom {
|
||||
name = base.archivesName.get()
|
||||
name = archivesBaseName
|
||||
description = 'Signal Protocol cryptography library for Java'
|
||||
url = 'https://github.com/signalapp/libsignal'
|
||||
|
||||
@ -148,6 +99,6 @@ publishing {
|
||||
|
||||
setUpSigningKey(signing)
|
||||
signing {
|
||||
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
|
||||
required { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
@ -9,25 +9,23 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.signal.libsignal.attest.AttestationDataException;
|
||||
import org.signal.libsignal.attest.AttestationFailedException;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.sgxsession.SgxClient;
|
||||
|
||||
/**
|
||||
* Cds2Client provides bindings to interact with Signal's v2 Contact Discovery Service.
|
||||
*
|
||||
* <p>See the {@link SgxClient} docs for more information.
|
||||
* <p>{@inheritDoc}
|
||||
*
|
||||
* <p>A future update to Cds2Client will implement additional parts of the contact discovery
|
||||
* protocol.
|
||||
*/
|
||||
public class Cds2Client extends SgxClient {
|
||||
public Cds2Client(byte[] mrenclave, byte[] attestationMsg, Instant currentInstant)
|
||||
throws AttestationDataException, AttestationFailedException {
|
||||
throws AttestationDataException {
|
||||
super(
|
||||
filterExceptions(
|
||||
AttestationDataException.class,
|
||||
AttestationFailedException.class,
|
||||
() ->
|
||||
Native.Cds2ClientState_New(
|
||||
mrenclave, attestationMsg, currentInstant.toEpochMilli())));
|
||||
|
||||
@ -11,40 +11,34 @@ 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 class Aes256Ctr32 implements NativeHandleGuard.Owner {
|
||||
private final long unsafeHandle;
|
||||
|
||||
public Aes256Ctr32(byte[] key, byte[] nonce, int initialCtr) throws InvalidKeyException {
|
||||
super(
|
||||
this.unsafeHandle =
|
||||
filterExceptions(
|
||||
InvalidKeyException.class, () -> Native.Aes256Ctr32_New(key, nonce, initialCtr)));
|
||||
InvalidKeyException.class, () -> Native.Aes256Ctr32_New(key, nonce, initialCtr));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.Aes256Ctr32_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.Aes256Ctr32_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256Ctr32_Process(guard.nativeHandle(), 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));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256Ctr32_Process(guard.nativeHandle(), data, offset, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,72 +11,48 @@ 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 class Aes256GcmDecryption implements NativeHandleGuard.Owner {
|
||||
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.
|
||||
*/
|
||||
private long unsafeHandle;
|
||||
|
||||
public Aes256GcmDecryption(byte[] key, byte[] nonce, byte[] associatedData)
|
||||
throws InvalidKeyException {
|
||||
super(
|
||||
this.unsafeHandle =
|
||||
filterExceptions(
|
||||
InvalidKeyException.class,
|
||||
() -> Native.Aes256GcmDecryption_New(key, nonce, associatedData)));
|
||||
() -> Native.Aes256GcmDecryption_New(key, nonce, associatedData));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.Aes256GcmDecryption_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.Aes256GcmDecryption_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, ciphertext, 0, ciphertext.length));
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, ciphertext, offset, length));
|
||||
public void decrypt(byte[] plaintext) {
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256GcmDecryption_Update(guard.nativeHandle(), plaintext, 0, plaintext.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void decrypt(byte[] plaintext, int offset, int length) {
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256GcmDecryption_Update(guard.nativeHandle(), plaintext, 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) ->
|
||||
filterExceptions(() -> Native.Aes256GcmDecryption_VerifyTag(nativeHandle, tag)));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
boolean tagOk =
|
||||
filterExceptions(() -> Native.Aes256GcmDecryption_VerifyTag(guard.nativeHandle(), tag));
|
||||
Native.Aes256GcmDecryption_Destroy(guard.nativeHandle());
|
||||
this.unsafeHandle = 0;
|
||||
return tagOk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,68 +11,45 @@ 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 class Aes256GcmEncryption implements NativeHandleGuard.Owner {
|
||||
private long unsafeHandle;
|
||||
|
||||
public Aes256GcmEncryption(byte[] key, byte[] nonce, byte[] associatedData)
|
||||
throws InvalidKeyException {
|
||||
super(
|
||||
this.unsafeHandle =
|
||||
filterExceptions(
|
||||
InvalidKeyException.class,
|
||||
() -> Native.Aes256GcmEncryption_New(key, nonce, associatedData)));
|
||||
() -> Native.Aes256GcmEncryption_New(key, nonce, associatedData));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.Aes256GcmEncryption_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.Aes256GcmEncryption_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256GcmEncryption_Update(guard.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));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.Aes256GcmEncryption_Update(guard.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);
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
byte[] tag = Native.Aes256GcmEncryption_ComputeTag(guard.nativeHandle());
|
||||
Native.Aes256GcmEncryption_Destroy(guard.nativeHandle());
|
||||
this.unsafeHandle = 0;
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,57 +12,40 @@ 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 {
|
||||
class Aes256GcmSiv implements NativeHandleGuard.Owner {
|
||||
private final long unsafeHandle;
|
||||
|
||||
public Aes256GcmSiv(byte[] key) throws InvalidKeyException {
|
||||
super(filterExceptions(InvalidKeyException.class, () -> Native.Aes256GcmSiv_New(key)));
|
||||
this.unsafeHandle =
|
||||
filterExceptions(InvalidKeyException.class, () -> Native.Aes256GcmSiv_New(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.Aes256GcmSiv_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.Aes256GcmSiv_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
() ->
|
||||
guardedMapChecked(
|
||||
nativeHandle ->
|
||||
Native.Aes256GcmSiv_Encrypt(nativeHandle, plaintext, nonce, associated_data)));
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return filterExceptions(
|
||||
() ->
|
||||
Native.Aes256GcmSiv_Encrypt(guard.nativeHandle(), plaintext, nonce, associated_data));
|
||||
}
|
||||
}
|
||||
|
||||
byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
|
||||
throws InvalidMessageException {
|
||||
return filterExceptions(
|
||||
InvalidMessageException.class,
|
||||
() ->
|
||||
guardedMapChecked(
|
||||
(nativeHandle) ->
|
||||
Native.Aes256GcmSiv_Decrypt(nativeHandle, ciphertext, nonce, associated_data)));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return filterExceptions(
|
||||
InvalidMessageException.class,
|
||||
() ->
|
||||
Native.Aes256GcmSiv_Decrypt(
|
||||
guard.nativeHandle(), ciphertext, nonce, associated_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,27 +10,38 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
|
||||
public class CryptographicHash extends NativeHandleGuard.SimpleOwner {
|
||||
public class CryptographicHash implements NativeHandleGuard.Owner {
|
||||
private final long unsafeHandle;
|
||||
|
||||
public CryptographicHash(String algo) {
|
||||
super(filterExceptions(() -> Native.CryptographicHash_New(algo)));
|
||||
this.unsafeHandle = filterExceptions(() -> Native.CryptographicHash_New(algo));
|
||||
}
|
||||
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return unsafeHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.CryptographicHash_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.CryptographicHash_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
public void update(byte[] input, int offset, int len) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.CryptographicHash_UpdateWithOffset(nativeHandle, input, offset, len));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.CryptographicHash_UpdateWithOffset(guard.nativeHandle(), input, offset, len);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(byte[] input) {
|
||||
guardedRun((nativeHandle) -> Native.CryptographicHash_Update(nativeHandle, input));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.CryptographicHash_Update(guard.nativeHandle(), input);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] finish() {
|
||||
return guardedMap(Native::CryptographicHash_Finalize);
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return Native.CryptographicHash_Finalize(guard.nativeHandle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,27 +10,38 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
|
||||
public class CryptographicMac extends NativeHandleGuard.SimpleOwner {
|
||||
public class CryptographicMac implements NativeHandleGuard.Owner {
|
||||
private final long unsafeHandle;
|
||||
|
||||
public CryptographicMac(String algo, byte[] key) {
|
||||
super(filterExceptions(() -> Native.CryptographicMac_New(algo, key)));
|
||||
this.unsafeHandle = filterExceptions(() -> Native.CryptographicMac_New(algo, key));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.CryptographicMac_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.CryptographicMac_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
public void update(byte[] input, int offset, int len) {
|
||||
guardedRun(
|
||||
(nativeHandle) ->
|
||||
Native.CryptographicMac_UpdateWithOffset(nativeHandle, input, offset, len));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.CryptographicMac_UpdateWithOffset(guard.nativeHandle(), input, offset, len);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(byte[] input) {
|
||||
guardedRun((nativeHandle) -> Native.CryptographicMac_Update(nativeHandle, input));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
Native.CryptographicMac_Update(guard.nativeHandle(), input);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] finish() {
|
||||
return guardedMap(Native::CryptographicMac_Finalize);
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return Native.CryptographicMac_Finalize(guard.nativeHandle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,13 +30,10 @@ import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
* HsmEnclaveClient.establishedRecv(), which decrypts and verifies it, passing the plaintext back to
|
||||
* the client for processing.
|
||||
*/
|
||||
public class HsmEnclaveClient extends NativeHandleGuard.SimpleOwner {
|
||||
public class HsmEnclaveClient implements NativeHandleGuard.Owner {
|
||||
private final long unsafeHandle;
|
||||
|
||||
public HsmEnclaveClient(byte[] public_key, List<byte[]> code_hashes) {
|
||||
super(HsmEnclaveClient.createNativeFrom(public_key, code_hashes));
|
||||
}
|
||||
|
||||
private static long createNativeFrom(byte[] public_key, List<byte[]> code_hashes) {
|
||||
ByteArrayOutputStream concatHashes = new ByteArrayOutputStream();
|
||||
for (byte[] hash : code_hashes) {
|
||||
if (hash.length != 32) {
|
||||
@ -48,52 +45,55 @@ public class HsmEnclaveClient extends NativeHandleGuard.SimpleOwner {
|
||||
throw new AssertionError("writing to ByteArrayOutputStream failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
return filterExceptions(
|
||||
() -> Native.HsmEnclaveClient_New(public_key, concatHashes.toByteArray()));
|
||||
this.unsafeHandle =
|
||||
filterExceptions(() -> Native.HsmEnclaveClient_New(public_key, concatHashes.toByteArray()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release(long nativeHandle) {
|
||||
Native.HsmEnclaveClient_Destroy(nativeHandle);
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
Native.HsmEnclaveClient_Destroy(this.unsafeHandle);
|
||||
}
|
||||
|
||||
public long unsafeNativeHandleWithoutGuard() {
|
||||
return this.unsafeHandle;
|
||||
}
|
||||
|
||||
/** Initial request to send to HSM enclave, to begin handshake. */
|
||||
public byte[] initialRequest() {
|
||||
return filterExceptions(() -> guardedMapChecked(Native::HsmEnclaveClient_InitialRequest));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return filterExceptions(() -> Native.HsmEnclaveClient_InitialRequest(guard.nativeHandle()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by client upon receipt of first message from HSM enclave, to complete handshake. */
|
||||
public void completeHandshake(byte[] handshakeResponse)
|
||||
throws EnclaveCommunicationFailureException, TrustedCodeMismatchException {
|
||||
filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
TrustedCodeMismatchException.class,
|
||||
() ->
|
||||
guardedRunChecked(
|
||||
(nativeHandle) ->
|
||||
Native.HsmEnclaveClient_CompleteHandshake(nativeHandle, handshakeResponse)));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
TrustedCodeMismatchException.class,
|
||||
() -> Native.HsmEnclaveClient_CompleteHandshake(guard.nativeHandle(), handshakeResponse));
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by client after completeHandshake has succeeded, to encrypt a message to send. */
|
||||
public byte[] establishedSend(byte[] plaintextToSend)
|
||||
throws EnclaveCommunicationFailureException {
|
||||
return filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
() ->
|
||||
guardedMapChecked(
|
||||
(nativeHandle) ->
|
||||
Native.HsmEnclaveClient_EstablishedSend(nativeHandle, plaintextToSend)));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
() -> Native.HsmEnclaveClient_EstablishedSend(guard.nativeHandle(), plaintextToSend));
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by client after completeHandshake has succeeded, to decrypt a received message. */
|
||||
public byte[] establishedRecv(byte[] receivedCiphertext)
|
||||
throws EnclaveCommunicationFailureException {
|
||||
return filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
() ->
|
||||
guardedMapChecked(
|
||||
(nativeHandle) ->
|
||||
Native.HsmEnclaveClient_EstablishedRecv(nativeHandle, receivedCiphertext)));
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||
return filterExceptions(
|
||||
EnclaveCommunicationFailureException.class,
|
||||
() -> Native.HsmEnclaveClient_EstablishedRecv(guard.nativeHandle(), receivedCiphertext));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user