Import IKAR messenger client and server
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: make test
|
||||
- run: make aggregate
|
||||
- run: make asan
|
||||
- run: make vectors && git diff --exit-code tests/vectors/v2.json
|
||||
|
||||
cmake:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: cmake -S . -B build/cmake -DCMAKE_BUILD_TYPE=Release
|
||||
- run: cmake --build build/cmake --config Release
|
||||
- run: ctest --test-dir build/cmake -C Release --output-on-failure
|
||||
|
||||
fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: sudo apt-get update && sudo apt-get install -y clang
|
||||
- run: make fuzz FUZZ_SECONDS=120
|
||||
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: android-actions/setup-android@v3
|
||||
- run: sdkmanager "ndk;28.2.13676358"
|
||||
- run: tools/build-android.sh
|
||||
env:
|
||||
ANDROID_SDK_ROOT: ${{ env.ANDROID_HOME }}
|
||||
|
||||
flutter:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.44.3
|
||||
channel: stable
|
||||
- run: make shared
|
||||
- run: flutter pub get
|
||||
working-directory: flutter/komet_crypto
|
||||
- run: flutter analyze
|
||||
working-directory: flutter/komet_crypto
|
||||
- run: flutter test
|
||||
working-directory: flutter/komet_crypto
|
||||
@@ -0,0 +1,15 @@
|
||||
build/
|
||||
.DS_Store
|
||||
flutter/komet_crypto/.dart_tool/
|
||||
flutter/komet_crypto/build/
|
||||
flutter/komet_crypto/pubspec.lock
|
||||
flutter/komet_crypto/example/
|
||||
|
||||
# артефакты Gradle/NDK от сборки плагина
|
||||
flutter/komet_crypto/android/.cxx/
|
||||
flutter/komet_crypto/android/build/
|
||||
flutter/komet_crypto/android/.gradle/
|
||||
flutter/komet_crypto/.flutter-plugins*
|
||||
flutter/komet_crypto/ios/Pods/
|
||||
flutter/komet_crypto/macos/Pods/
|
||||
flutter/komet_crypto/*/Flutter/ephemeral/
|
||||
@@ -0,0 +1,90 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
project(komet_crypto VERSION 0.2.0 LANGUAGES C)
|
||||
|
||||
set(KC_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
file(GLOB KC_SOURCES CONFIGURE_DEPENDS ${KC_ROOT}/src/*.c)
|
||||
list(FILTER KC_SOURCES EXCLUDE REGEX "kc_all\\.c$")
|
||||
|
||||
if(MSVC)
|
||||
set(KC_WARNINGS /W4)
|
||||
set(KC_MONOCYPHER_WARNINGS /W3)
|
||||
else()
|
||||
set(KC_WARNINGS -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion -Wstrict-prototypes)
|
||||
set(KC_MONOCYPHER_WARNINGS -Wall -Wextra -pedantic)
|
||||
endif()
|
||||
|
||||
add_library(kc_monocypher OBJECT ${KC_ROOT}/third_party/monocypher/monocypher.c)
|
||||
target_include_directories(kc_monocypher PUBLIC ${KC_ROOT}/third_party/monocypher)
|
||||
target_compile_options(kc_monocypher PRIVATE ${KC_MONOCYPHER_WARNINGS})
|
||||
set_target_properties(kc_monocypher PROPERTIES
|
||||
C_STANDARD 99
|
||||
C_STANDARD_REQUIRED ON
|
||||
C_EXTENSIONS OFF
|
||||
C_VISIBILITY_PRESET hidden
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
function(kc_configure_target target)
|
||||
target_include_directories(${target}
|
||||
PUBLIC ${KC_ROOT}/include
|
||||
PRIVATE ${KC_ROOT}/src ${KC_ROOT}/third_party/monocypher)
|
||||
target_compile_definitions(${target} PRIVATE KC_BUILDING)
|
||||
target_compile_options(${target} PRIVATE ${KC_WARNINGS})
|
||||
set_target_properties(${target} PROPERTIES
|
||||
C_STANDARD 99
|
||||
C_STANDARD_REQUIRED ON
|
||||
C_EXTENSIONS OFF
|
||||
C_VISIBILITY_PRESET hidden
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
PUBLIC_HEADER ${KC_ROOT}/include/komet_crypto.h)
|
||||
if(NOT MSVC)
|
||||
target_compile_options(${target} PRIVATE -ffunction-sections -fdata-sections)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
option(KC_BUILD_TESTS "Build the komet_crypto test binary" ON)
|
||||
else()
|
||||
option(KC_BUILD_TESTS "Build the komet_crypto test binary" OFF)
|
||||
endif()
|
||||
|
||||
option(KC_BUILD_SHARED "Build the komet_crypto shared library" ON)
|
||||
option(KC_BUILD_STATIC "Build the komet_crypto_static library" ON)
|
||||
|
||||
if(KC_BUILD_SHARED)
|
||||
add_library(komet_crypto SHARED ${KC_SOURCES} $<TARGET_OBJECTS:kc_monocypher>)
|
||||
kc_configure_target(komet_crypto)
|
||||
set_target_properties(komet_crypto PROPERTIES OUTPUT_NAME komet_crypto)
|
||||
if(ANDROID)
|
||||
target_link_options(komet_crypto PRIVATE -Wl,--gc-sections -Wl,-z,max-page-size=16384)
|
||||
elseif(APPLE)
|
||||
target_link_options(komet_crypto PRIVATE -Wl,-dead_strip)
|
||||
elseif(NOT MSVC)
|
||||
target_link_options(komet_crypto PRIVATE -Wl,--gc-sections)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(KC_BUILD_STATIC OR KC_BUILD_TESTS)
|
||||
add_library(komet_crypto_static STATIC ${KC_SOURCES} $<TARGET_OBJECTS:kc_monocypher>)
|
||||
kc_configure_target(komet_crypto_static)
|
||||
target_compile_definitions(komet_crypto_static PUBLIC KC_STATIC)
|
||||
endif()
|
||||
|
||||
|
||||
if(KC_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(kc_test ${KC_ROOT}/tests/test_core.c ${KC_ROOT}/tests/test_v2.c)
|
||||
target_include_directories(kc_test PRIVATE ${KC_ROOT}/src ${KC_ROOT}/third_party/monocypher)
|
||||
target_link_libraries(kc_test PRIVATE komet_crypto_static)
|
||||
set_target_properties(kc_test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
|
||||
if(NOT MSVC)
|
||||
target_compile_options(kc_test PRIVATE ${KC_WARNINGS})
|
||||
endif()
|
||||
add_test(NAME kc_test COMMAND kc_test)
|
||||
|
||||
add_executable(kc_gen_vectors ${KC_ROOT}/tools/gen_vectors.c)
|
||||
target_include_directories(kc_gen_vectors PRIVATE ${KC_ROOT}/third_party/monocypher)
|
||||
target_link_libraries(kc_gen_vectors PRIVATE komet_crypto_static)
|
||||
set_target_properties(kc_gen_vectors PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
|
||||
endif()
|
||||
@@ -0,0 +1,26 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Komet Team
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
third_party/monocypher is Monocypher 4.0.2 by Loup Vaillant and contributors,
|
||||
dual-licensed under CC0-1.0 and BSD-2-Clause. See third_party/monocypher/LICENCE.md.
|
||||
@@ -0,0 +1,78 @@
|
||||
CC ?= cc
|
||||
BUILD = build
|
||||
CPPFLAGS = -Iinclude -Isrc -Ithird_party/monocypher -DKC_BUILDING
|
||||
CFLAGS = -std=c99 -O2 -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion -Wstrict-prototypes
|
||||
CORE_SOURCES = $(filter-out src/kc_all.c,$(wildcard src/*.c))
|
||||
CORE_HEADERS = $(wildcard src/*.h) include/komet_crypto.h
|
||||
MONOCYPHER_SOURCE = third_party/monocypher/monocypher.c
|
||||
MONOCYPHER_OBJECT = $(BUILD)/monocypher.o
|
||||
TEST_SOURCES = tests/test_core.c tests/test_v2.c
|
||||
TEST = $(BUILD)/test_core
|
||||
FUZZ = $(BUILD)/fuzz_decode
|
||||
VECTORS = $(BUILD)/gen_vectors
|
||||
UNAME_S = $(shell uname -s)
|
||||
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SHARED = $(BUILD)/libkomet_crypto.dylib
|
||||
SHARED_FLAGS = -dynamiclib -Wl,-dead_strip -Wl,-install_name,@rpath/libkomet_crypto.dylib
|
||||
else
|
||||
SHARED = $(BUILD)/libkomet_crypto.so
|
||||
SHARED_FLAGS = -shared -Wl,--gc-sections
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := test
|
||||
|
||||
.PHONY: test asan shared fuzz vectors aggregate android clean
|
||||
|
||||
$(BUILD):
|
||||
mkdir -p $(BUILD)
|
||||
|
||||
$(MONOCYPHER_OBJECT): $(MONOCYPHER_SOURCE) third_party/monocypher/monocypher.h | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(filter-out -Wconversion,$(CFLAGS)) -fPIC -fvisibility=hidden -ffunction-sections -fdata-sections -c $(MONOCYPHER_SOURCE) -o $(MONOCYPHER_OBJECT)
|
||||
|
||||
$(TEST): $(CORE_SOURCES) $(MONOCYPHER_OBJECT) $(TEST_SOURCES) $(CORE_HEADERS) | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SOURCES) $(MONOCYPHER_OBJECT) $(TEST_SOURCES) -o $(TEST)
|
||||
|
||||
test: $(TEST) aggregate
|
||||
$(TEST)
|
||||
|
||||
aggregate: | $(BUILD)
|
||||
@for f in $(notdir $(CORE_SOURCES)); do \
|
||||
grep -q "\"$$f\"" src/kc_all.c || { echo "kc_all.c не включает $$f"; exit 1; }; \
|
||||
done
|
||||
@grep -c '#include "' src/kc_all.c | grep -qx "$(words $(CORE_SOURCES))" || \
|
||||
{ echo "kc_all.c включает файлы, которых нет в src/"; exit 1; }
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -fsyntax-only src/kc_all.c $(TEST_SOURCES) \
|
||||
tests/fuzz_decode.c tools/gen_vectors.c
|
||||
|
||||
$(SHARED): $(CORE_SOURCES) $(MONOCYPHER_OBJECT) $(CORE_HEADERS) | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -fvisibility=hidden -ffunction-sections -fdata-sections $(SHARED_FLAGS) $(CORE_SOURCES) $(MONOCYPHER_OBJECT) -o $(SHARED)
|
||||
|
||||
shared: $(SHARED)
|
||||
|
||||
$(VECTORS): $(CORE_SOURCES) $(MONOCYPHER_OBJECT) tools/gen_vectors.c $(CORE_HEADERS) | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SOURCES) $(MONOCYPHER_OBJECT) tools/gen_vectors.c -o $(VECTORS)
|
||||
|
||||
vectors: $(VECTORS)
|
||||
$(VECTORS) > tests/vectors/v2.json
|
||||
|
||||
ASAN_CFLAGS = -std=c99 -O1 -g -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion -Wstrict-prototypes -fsanitize=address,undefined -fno-omit-frame-pointer
|
||||
FUZZ_CFLAGS = -std=c99 -O1 -g -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion -Wstrict-prototypes -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer
|
||||
|
||||
asan:
|
||||
$(MAKE) BUILD=build/asan CFLAGS="$(ASAN_CFLAGS)" test
|
||||
|
||||
$(FUZZ): $(CORE_SOURCES) $(MONOCYPHER_OBJECT) tests/fuzz_decode.c $(CORE_HEADERS) | $(BUILD)
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SOURCES) $(MONOCYPHER_OBJECT) tests/fuzz_decode.c -o $(FUZZ)
|
||||
|
||||
fuzz: $(VECTORS)
|
||||
$(MAKE) BUILD=build/fuzz CC=clang CFLAGS="$(FUZZ_CFLAGS)" build/fuzz/fuzz_decode
|
||||
mkdir -p build/fuzz/corpus
|
||||
$(VECTORS) --seed-corpus build/fuzz/corpus
|
||||
build/fuzz/fuzz_decode -dict=tests/fuzz.dict -max_total_time=$(or $(FUZZ_SECONDS),60) -max_len=4096 build/fuzz/corpus
|
||||
|
||||
android:
|
||||
tools/build-android.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Origin
|
||||
|
||||
Vendored from https://github.com/KometTeam/crypto-core at
|
||||
`6e5dc4f97902b506b242a2433ee847060541dac4` for the Komet FullStack
|
||||
`539f7800d5f326fd52f53c6142a503b9739b9197` integration.
|
||||
|
||||
The upstream LICENSE and Monocypher attribution are retained.
|
||||
Qlyra adds a `QLYRA_E2EE_LIBRARY` environment override to the Dart FFI loader
|
||||
for native integration tests. The v2 wire protocol is unchanged.
|
||||
The ratchet refuses a forced extra DH step when an imported initiator has
|
||||
never sent its first chain: the peer cannot reconstruct that skipped root.
|
||||
The user can reset and re-establish this unused session. This is covered by
|
||||
`test/e2ee_integration_test.dart` in Qlyra.
|
||||
Legacy Qlyra password derivation remains in `native/qlyra_crypto`: its salt
|
||||
context is `qlyra-enc-v1`, whereas upstream Komet uses `komet-enc-v1`.
|
||||
@@ -0,0 +1,54 @@
|
||||
# crypto-core
|
||||
|
||||
Portable C99 message-encryption core for Komet clients (Flutter and Android),
|
||||
built on the vendored [monocypher 4.0.2](third_party/monocypher).
|
||||
|
||||
* **v2** — per-installation identity keys, X25519 handshake carried as ordinary
|
||||
chat messages, Double Ratchet (forward secrecy, post-compromise security),
|
||||
envelopes without constant prefixes, chunked file encryption, sealed local
|
||||
storage, passphrase-protected device transfer. Formats: [docs/PROTOCOL.md](docs/PROTOCOL.md).
|
||||
* **legacy (v1)** — shared-passphrase scheme, kept so old history and group chats
|
||||
stay readable.
|
||||
|
||||
The API is `include/komet_crypto.h`: stateless functions over caller-provided
|
||||
buffers, `*_bound()` helpers for sizing, `kc_status` results. The core has no
|
||||
RNG — every function that needs randomness takes it as an argument
|
||||
(`KC_*_RANDOM_SIZE`). Session state is an opaque blob the caller stores.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
make test # build and run the suite
|
||||
make asan # AddressSanitizer + UBSan
|
||||
make vectors # regenerate tests/vectors/v2.json
|
||||
make fuzz # libFuzzer (needs clang with libFuzzer; Linux in CI)
|
||||
tools/build-android.sh
|
||||
cmake -S . -B build/cmake && cmake --build build/cmake && ctest --test-dir build/cmake
|
||||
```
|
||||
|
||||
Consumers use CMake `add_subdirectory` (`komet_crypto` shared or
|
||||
`komet_crypto_static`) or the Flutter plugin in `flutter/komet_crypto`.
|
||||
|
||||
## Using it from Komet
|
||||
|
||||
Komet consumes this repository as a git submodule at `native/crypto-core` and
|
||||
depends on the Flutter plugin by path. After cloning Komet:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
Komet-Android points at a checkout instead, through `komet.crypto.dir` in its
|
||||
`gradle.properties`, and builds the C sources with `add_subdirectory`.
|
||||
|
||||
## Threat model in one paragraph
|
||||
|
||||
The MAX server is the adversary: it stores every message, can forge, drop,
|
||||
delay or replay them, and knows all metadata. This core protects message and
|
||||
file contents between two Komet installations that completed a handshake, and
|
||||
limits the damage of a later key compromise to the current ratchet window. It
|
||||
does not hide that encryption is used, does not hide who talks to whom, cannot
|
||||
prevent the server from suppressing delivery, and cannot detect an active
|
||||
substitution of the very first handshake — only an out-of-band fingerprint
|
||||
comparison can.
|
||||
@@ -0,0 +1,19 @@
|
||||
# komet_crypto (Flutter)
|
||||
|
||||
`dart:ffi` bindings for the C core in the repository root. The native library is
|
||||
compiled from `../../CMakeLists.txt` (Android, Linux, Windows) or from the
|
||||
forwarder files in `ios/Classes` and `macos/Classes` (CocoaPods). On the web
|
||||
every call throws `KometCryptoException(CryptoStatus.unavailable)`.
|
||||
|
||||
Add to an app:
|
||||
|
||||
```yaml
|
||||
komet_crypto:
|
||||
git:
|
||||
url: https://github.com/KometTeam/komet-crypto-core.git
|
||||
path: flutter/komet_crypto
|
||||
ref: <commit>
|
||||
```
|
||||
|
||||
Tests run against the host build of the core: `make shared` in the repository
|
||||
root, then `flutter test` here.
|
||||
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
@@ -0,0 +1,48 @@
|
||||
group = "pw.komet.komet_crypto"
|
||||
version = "0.2.0"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
|
||||
android {
|
||||
namespace = "pw.komet.komet_crypto"
|
||||
compileSdk = 35
|
||||
ndkVersion = android.ndkVersion
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = "../../../CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DKC_BUILD_STATIC=OFF", "-DKC_BUILD_TESTS=OFF", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../../src/kc_all.c"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../../third_party/monocypher/monocypher.c"
|
||||
@@ -0,0 +1,27 @@
|
||||
core_root = File.expand_path('../../..', File.realpath(__dir__))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'komet_crypto'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'Komet message-encryption core.'
|
||||
s.description = <<-DESC
|
||||
C core over monocypher: legacy passphrase scheme and v2 end-to-end encryption.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/KometTeam/komet-crypto-core'
|
||||
s.license = { :type => 'MIT' }
|
||||
s.author = { 'Komet' => 'dev@komet.pw' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '13.0'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||
'HEADER_SEARCH_PATHS' => "\"#{core_root}/include\" \"#{core_root}/src\" \"#{core_root}/third_party/monocypher\"",
|
||||
'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) KC_BUILDING=1',
|
||||
'GCC_SYMBOLS_PRIVATE_EXTERN' => 'YES',
|
||||
}
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'src/types.dart';
|
||||
export 'src/komet_crypto_stub.dart'
|
||||
if (dart.library.ffi) 'src/komet_crypto_ffi.dart';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'types.dart';
|
||||
|
||||
Never _unavailable() =>
|
||||
throw const KometCryptoException(CryptoStatus.unavailable);
|
||||
|
||||
class FileSealer {
|
||||
Uint8List chunk(Uint8List plaintext, {required bool last}) => _unavailable();
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
class FileOpener {
|
||||
Uint8List chunk(Uint8List ciphertext, {required bool last}) =>
|
||||
_unavailable();
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
abstract final class KometCrypto {
|
||||
static String? libraryPath;
|
||||
|
||||
static bool get isAvailable => false;
|
||||
|
||||
static Uint8List randomBytes(int length) => _unavailable();
|
||||
|
||||
static void wipe(Uint8List bytes) => bytes.fillRange(0, bytes.length, 0);
|
||||
|
||||
static Uint8List deriveKey(String password) => _unavailable();
|
||||
|
||||
static String encryptMessage(
|
||||
String plaintext,
|
||||
Uint8List key, {
|
||||
Uint8List? nonce,
|
||||
}) => _unavailable();
|
||||
|
||||
static String decryptMessage(String text, Uint8List key) => _unavailable();
|
||||
|
||||
static bool looksEncryptedMessage(String text) => false;
|
||||
|
||||
static Uint8List encryptImageBlob(
|
||||
Uint8List plaintext,
|
||||
Uint8List key, {
|
||||
Uint8List? nonce,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List decryptImageBlob(Uint8List blob, Uint8List key) =>
|
||||
_unavailable();
|
||||
|
||||
static bool looksEncryptedImageBlob(Uint8List blob) => false;
|
||||
|
||||
static Uint8List identityCreate({Uint8List? seed}) => _unavailable();
|
||||
|
||||
static Uint8List identityPublicKey(Uint8List identity) => _unavailable();
|
||||
|
||||
static String fingerprint({
|
||||
required int myId,
|
||||
required Uint8List myPublic,
|
||||
required int peerId,
|
||||
required Uint8List peerPublic,
|
||||
}) => _unavailable();
|
||||
|
||||
static TextClass classifyText(String text) => TextClass.none;
|
||||
|
||||
static HandshakePeek handshakePeek(String text) => _unavailable();
|
||||
|
||||
static SessionOffer sessionOffer({
|
||||
required Uint8List identity,
|
||||
required int chatId,
|
||||
required int myId,
|
||||
required int peerId,
|
||||
Uint8List? random,
|
||||
}) => _unavailable();
|
||||
|
||||
static SessionAnswer sessionAnswer({
|
||||
required Uint8List identity,
|
||||
required int chatId,
|
||||
required int myId,
|
||||
required int peerId,
|
||||
required String offerText,
|
||||
Uint8List? oldState,
|
||||
Uint8List? random,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List sessionAccept({
|
||||
required Uint8List identity,
|
||||
required Uint8List pending,
|
||||
required String answerText,
|
||||
Uint8List? expectedPeer,
|
||||
Uint8List? random,
|
||||
}) => _unavailable();
|
||||
|
||||
static SessionEncrypted sessionEncrypt({
|
||||
required Uint8List state,
|
||||
required ContentType contentType,
|
||||
required Uint8List plaintext,
|
||||
Uint8List? random,
|
||||
}) => _unavailable();
|
||||
|
||||
static SessionDecrypted sessionDecrypt({
|
||||
required Uint8List state,
|
||||
required String text,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List sessionPeerPublicKey(Uint8List state) => _unavailable();
|
||||
|
||||
static FileSealer fileSealer(Uint8List key, Uint8List nonce) =>
|
||||
_unavailable();
|
||||
|
||||
static FileOpener fileOpener(Uint8List key, Uint8List nonce) =>
|
||||
_unavailable();
|
||||
|
||||
static Uint8List localSeal({
|
||||
required Uint8List key,
|
||||
required Uint8List plaintext,
|
||||
Uint8List? nonce,
|
||||
Uint8List? aad,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List localOpen({
|
||||
required Uint8List key,
|
||||
required Uint8List blob,
|
||||
Uint8List? aad,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List exportSeal({
|
||||
required Uint8List password,
|
||||
required int memoryKib,
|
||||
required int passes,
|
||||
required Uint8List container,
|
||||
Uint8List? random,
|
||||
}) => _unavailable();
|
||||
|
||||
static Uint8List exportOpen({
|
||||
required Uint8List password,
|
||||
required Uint8List blob,
|
||||
}) => _unavailable();
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
enum CryptoStatus {
|
||||
ok(0, 'ok'),
|
||||
emptyPassword(-1, 'empty_password'),
|
||||
badKeyLength(-2, 'bad_key_length'),
|
||||
notEncrypted(-3, 'not_encrypted'),
|
||||
malformed(-4, 'malformed'),
|
||||
wrongKey(-5, 'wrong_key'),
|
||||
internal(-6, 'internal'),
|
||||
bufferTooSmall(-7, 'buffer_too_small'),
|
||||
invalidArgument(-8, 'invalid_argument'),
|
||||
outOfMemory(-9, 'out_of_memory'),
|
||||
badSignature(-10, 'bad_signature'),
|
||||
badState(-11, 'bad_state'),
|
||||
tooManySkipped(-12, 'too_many_skipped'),
|
||||
replay(-13, 'replay'),
|
||||
badPeer(-14, 'bad_peer'),
|
||||
unsupportedVersion(-15, 'unsupported_version'),
|
||||
handshakeMessage(-16, 'handshake_message'),
|
||||
tooLong(-17, 'too_long'),
|
||||
awaitingPeer(-18, 'awaiting_peer'),
|
||||
unavailable(-100, 'unavailable');
|
||||
|
||||
const CryptoStatus(this.code, this.name_);
|
||||
|
||||
final int code;
|
||||
final String name_;
|
||||
|
||||
static CryptoStatus fromCode(int code) => CryptoStatus.values.firstWhere(
|
||||
(status) => status.code == code,
|
||||
orElse: () => CryptoStatus.internal,
|
||||
);
|
||||
}
|
||||
|
||||
class KometCryptoException implements Exception {
|
||||
const KometCryptoException(this.status);
|
||||
|
||||
final CryptoStatus status;
|
||||
|
||||
@override
|
||||
String toString() => 'KometCryptoException(${status.name_})';
|
||||
}
|
||||
|
||||
enum TextClass { none, legacy, offer, answer, session }
|
||||
|
||||
enum ContentType {
|
||||
text(1),
|
||||
file(2),
|
||||
control(3);
|
||||
|
||||
const ContentType(this.code);
|
||||
|
||||
final int code;
|
||||
|
||||
static ContentType fromCode(int code) =>
|
||||
ContentType.values.firstWhere((type) => type.code == code);
|
||||
}
|
||||
|
||||
enum HandshakeType { offer, answer }
|
||||
|
||||
class HandshakePeek {
|
||||
const HandshakePeek({
|
||||
required this.type,
|
||||
required this.offerId,
|
||||
required this.publicKey,
|
||||
});
|
||||
|
||||
final HandshakeType type;
|
||||
final Uint8List offerId;
|
||||
final Uint8List publicKey;
|
||||
}
|
||||
|
||||
class SessionOffer {
|
||||
const SessionOffer({required this.text, required this.pending});
|
||||
|
||||
final String text;
|
||||
final Uint8List pending;
|
||||
}
|
||||
|
||||
class SessionAnswer {
|
||||
const SessionAnswer({required this.text, required this.state});
|
||||
|
||||
final String text;
|
||||
final Uint8List state;
|
||||
}
|
||||
|
||||
class SessionEncrypted {
|
||||
const SessionEncrypted({required this.text, required this.state});
|
||||
|
||||
final String text;
|
||||
final Uint8List state;
|
||||
}
|
||||
|
||||
class SessionDecrypted {
|
||||
const SessionDecrypted({
|
||||
required this.contentType,
|
||||
required this.plaintext,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
final ContentType contentType;
|
||||
final Uint8List plaintext;
|
||||
final Uint8List state;
|
||||
}
|
||||
|
||||
abstract final class KometCryptoSizes {
|
||||
static const int key = 32;
|
||||
static const int nonce = 12;
|
||||
static const int tag = 16;
|
||||
static const int seed = 32;
|
||||
static const int publicKey = 32;
|
||||
static const int identity = 64;
|
||||
static const int fingerprintDigits = 60;
|
||||
static const int offerId = 8;
|
||||
static const int handshakeTextBound = 497;
|
||||
static const int pendingState = 203;
|
||||
static const int sessionStateMax = 17712;
|
||||
static const int sessionPlaintextMax = 414;
|
||||
static const int textTransportMax = 1000;
|
||||
static const int identityRandom = 32;
|
||||
static const int offerRandom = 40;
|
||||
static const int answerRandom = 32;
|
||||
static const int acceptRandom = 32;
|
||||
static const int encryptRandom = 32;
|
||||
static const int exportRandom = 40;
|
||||
static const int fileKey = 32;
|
||||
static const int fileNonce = 12;
|
||||
static const int fileChunk = 65536;
|
||||
static const int fileContext = 96;
|
||||
static const int localKey = 32;
|
||||
static const int localNonce = 24;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(PROJECT_NAME "komet_crypto")
|
||||
project(${PROJECT_NAME} LANGUAGES C CXX)
|
||||
|
||||
set(KC_BUILD_STATIC OFF CACHE BOOL "" FORCE)
|
||||
set(KC_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
get_filename_component(KC_PLUGIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH)
|
||||
get_filename_component(KC_CORE_DIR "${KC_PLUGIN_DIR}/../../.." ABSOLUTE)
|
||||
add_subdirectory("${KC_CORE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/komet_crypto_core")
|
||||
|
||||
set(komet_crypto_bundled_libraries
|
||||
$<TARGET_FILE:komet_crypto>
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../../src/kc_all.c"
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../../../third_party/monocypher/monocypher.c"
|
||||
@@ -0,0 +1,26 @@
|
||||
core_root = File.expand_path('../../..', File.realpath(__dir__))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'komet_crypto'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'Komet message-encryption core.'
|
||||
s.description = <<-DESC
|
||||
C core over monocypher: legacy passphrase scheme and v2 end-to-end encryption.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/KometTeam/komet-crypto-core'
|
||||
s.license = { :type => 'MIT' }
|
||||
s.author = { 'Komet' => 'dev@komet.pw' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.platform = :osx, '10.14'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'HEADER_SEARCH_PATHS' => "\"#{core_root}/include\" \"#{core_root}/src\" \"#{core_root}/third_party/monocypher\"",
|
||||
'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) KC_BUILDING=1',
|
||||
'GCC_SYMBOLS_PRIVATE_EXTERN' => 'YES',
|
||||
}
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
name: komet_crypto
|
||||
description: Komet message-encryption core (C over monocypher) — legacy passphrase scheme and v2 end-to-end encryption.
|
||||
version: 0.2.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
ffi: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
ffiPlugin: true
|
||||
ios:
|
||||
ffiPlugin: true
|
||||
linux:
|
||||
ffiPlugin: true
|
||||
macos:
|
||||
ffiPlugin: true
|
||||
windows:
|
||||
ffiPlugin: true
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:komet_crypto/komet_crypto.dart';
|
||||
|
||||
Uint8List hex(String text) {
|
||||
final out = Uint8List(text.length ~/ 2);
|
||||
for (var index = 0; index < out.length; index++) {
|
||||
out[index] = int.parse(text.substring(index * 2, index * 2 + 2), radix: 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
String toHex(Uint8List bytes) =>
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
Uint8List utf8Bytes(String text) => Uint8List.fromList(utf8.encode(text));
|
||||
|
||||
Uint8List pattern(int length) {
|
||||
final out = Uint8List(length);
|
||||
for (var index = 0; index < length; index++) {
|
||||
out[index] = (index * 7 + 3) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Map<String, dynamic> vectors;
|
||||
|
||||
setUpAll(() {
|
||||
final name = Platform.isMacOS
|
||||
? 'libkomet_crypto.dylib'
|
||||
: Platform.isWindows
|
||||
? 'komet_crypto.dll'
|
||||
: 'libkomet_crypto.so';
|
||||
KometCrypto.libraryPath = File('../../build/$name').absolute.path;
|
||||
vectors =
|
||||
jsonDecode(File('../../tests/vectors/v2.json').readAsStringSync())
|
||||
as Map<String, dynamic>;
|
||||
});
|
||||
|
||||
test('native library loads', () {
|
||||
expect(KometCrypto.isAvailable, isTrue);
|
||||
});
|
||||
|
||||
test('legacy key derivation vector', () {
|
||||
expect(
|
||||
toHex(KometCrypto.deriveKey('fixture-password-01')),
|
||||
'2acd8a97ecc4690d5d90455991241200bb42e40b5c5efa334709a0eb17ad2e3d',
|
||||
);
|
||||
});
|
||||
|
||||
test('legacy message written by the Rust build', () {
|
||||
final key = Uint8List.fromList(List.generate(32, (i) => (i * 7 + 3) & 0xff));
|
||||
const text =
|
||||
'ймау чзйыцяэ укяфмеои йцаныэшт жхсрж ярпцр щукз уугумчеб лвсхш '
|
||||
'щрше ьдзфзчк хтхыс щпыи учфххфгх жшхон с';
|
||||
expect(KometCrypto.decryptMessage(text, key), 'fixture-message-from-rust');
|
||||
expect(KometCrypto.looksEncryptedMessage(text), isTrue);
|
||||
expect(KometCrypto.classifyText(text), TextClass.legacy);
|
||||
final other = Uint8List.fromList(key.map((b) => b ^ 0x5a).toList());
|
||||
expect(
|
||||
() => KometCrypto.decryptMessage(text, other),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.wrongKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('legacy round trip', () {
|
||||
final key = KometCrypto.randomBytes(32);
|
||||
final text = KometCrypto.encryptMessage('Привет, мир! 🙂', key);
|
||||
expect(KometCrypto.decryptMessage(text, key), 'Привет, мир! 🙂');
|
||||
final image = KometCrypto.encryptImageBlob(pattern(73), key);
|
||||
expect(KometCrypto.looksEncryptedImageBlob(image), isTrue);
|
||||
expect(KometCrypto.decryptImageBlob(image, key), pattern(73));
|
||||
});
|
||||
|
||||
test('identity vectors', () {
|
||||
final a = vectors['identity_a'] as Map<String, dynamic>;
|
||||
final identity = KometCrypto.identityCreate(seed: hex(a['seed'] as String));
|
||||
expect(toHex(identity), a['identity']);
|
||||
expect(toHex(KometCrypto.identityPublicKey(identity)), a['public']);
|
||||
final b = vectors['identity_b'] as Map<String, dynamic>;
|
||||
expect(
|
||||
KometCrypto.fingerprint(
|
||||
myId: 1001,
|
||||
myPublic: hex(a['public'] as String),
|
||||
peerId: 2002,
|
||||
peerPublic: hex(b['public'] as String),
|
||||
),
|
||||
vectors['fingerprint'],
|
||||
);
|
||||
expect(
|
||||
KometCrypto.fingerprint(
|
||||
myId: 2002,
|
||||
myPublic: hex(b['public'] as String),
|
||||
peerId: 1001,
|
||||
peerPublic: hex(a['public'] as String),
|
||||
),
|
||||
vectors['fingerprint'],
|
||||
);
|
||||
});
|
||||
|
||||
test('handshake and message vectors', () {
|
||||
final a = hex((vectors['identity_a'] as Map)['identity'] as String);
|
||||
final b = hex((vectors['identity_b'] as Map)['identity'] as String);
|
||||
final hs = vectors['handshake'] as Map<String, dynamic>;
|
||||
final offer = KometCrypto.sessionOffer(
|
||||
identity: a,
|
||||
chatId: hs['chat_id'] as int,
|
||||
myId: hs['a_id'] as int,
|
||||
peerId: hs['b_id'] as int,
|
||||
random: hex(hs['offer_random'] as String),
|
||||
);
|
||||
expect(offer.text, hs['offer_text']);
|
||||
expect(toHex(offer.pending), hs['pending']);
|
||||
expect(KometCrypto.classifyText(offer.text), TextClass.offer);
|
||||
final peek = KometCrypto.handshakePeek('🔐 Komet\n${offer.text}');
|
||||
expect(peek.type, HandshakeType.offer);
|
||||
expect(toHex(peek.publicKey), (vectors['identity_a'] as Map)['public']);
|
||||
|
||||
final answer = KometCrypto.sessionAnswer(
|
||||
identity: b,
|
||||
chatId: hs['chat_id'] as int,
|
||||
myId: hs['b_id'] as int,
|
||||
peerId: hs['a_id'] as int,
|
||||
offerText: offer.text,
|
||||
random: hex(hs['answer_random'] as String),
|
||||
);
|
||||
expect(answer.text, hs['answer_text']);
|
||||
expect(toHex(answer.state), hs['b_state_after_answer']);
|
||||
var stateA = KometCrypto.sessionAccept(
|
||||
identity: a,
|
||||
pending: offer.pending,
|
||||
answerText: answer.text,
|
||||
expectedPeer: hex((vectors['identity_b'] as Map)['public'] as String),
|
||||
random: hex(hs['accept_random'] as String),
|
||||
);
|
||||
expect(
|
||||
() => KometCrypto.sessionAccept(
|
||||
identity: a,
|
||||
pending: offer.pending,
|
||||
answerText: answer.text,
|
||||
expectedPeer: hex((vectors['identity_a'] as Map)['public'] as String),
|
||||
random: hex(hs['accept_random'] as String),
|
||||
),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.badPeer,
|
||||
),
|
||||
),
|
||||
reason: 'a pinned peer must reject a substituted responder',
|
||||
);
|
||||
expect(toHex(stateA), hs['a_state_after_accept']);
|
||||
var stateB = answer.state;
|
||||
expect(
|
||||
toHex(KometCrypto.sessionPeerPublicKey(stateA)),
|
||||
(vectors['identity_b'] as Map)['public'],
|
||||
);
|
||||
|
||||
String? delayed;
|
||||
for (final entry in vectors['messages'] as List) {
|
||||
final message = entry as Map<String, dynamic>;
|
||||
if (message.containsKey('replay_of')) {
|
||||
final decrypted = KometCrypto.sessionDecrypt(
|
||||
state: stateB,
|
||||
text: delayed!,
|
||||
);
|
||||
expect(utf8.decode(decrypted.plaintext), 'пропущенное');
|
||||
stateB = decrypted.state;
|
||||
expect(toHex(stateB), message['receiver_state_after']);
|
||||
continue;
|
||||
}
|
||||
final fromA = message['from'] == 'a';
|
||||
final encrypted = KometCrypto.sessionEncrypt(
|
||||
state: fromA ? stateA : stateB,
|
||||
contentType: ContentType.text,
|
||||
plaintext: utf8Bytes(message['plaintext'] as String),
|
||||
random: hex(message['random'] as String),
|
||||
);
|
||||
expect(encrypted.text, message['text']);
|
||||
expect(toHex(encrypted.state), message['sender_state_after']);
|
||||
expect(KometCrypto.classifyText(encrypted.text), TextClass.session);
|
||||
if (fromA) {
|
||||
stateA = encrypted.state;
|
||||
} else {
|
||||
stateB = encrypted.state;
|
||||
}
|
||||
if (message['delivered_later'] == true) {
|
||||
delayed = encrypted.text;
|
||||
continue;
|
||||
}
|
||||
final decrypted = KometCrypto.sessionDecrypt(
|
||||
state: fromA ? stateB : stateA,
|
||||
text: encrypted.text,
|
||||
);
|
||||
expect(decrypted.contentType, ContentType.text);
|
||||
expect(utf8.decode(decrypted.plaintext), message['plaintext']);
|
||||
expect(toHex(decrypted.state), message['receiver_state_after']);
|
||||
if (fromA) {
|
||||
stateB = decrypted.state;
|
||||
} else {
|
||||
stateA = decrypted.state;
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
() => KometCrypto.sessionDecrypt(state: stateB, text: delayed!),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.replay,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
() => KometCrypto.sessionDecrypt(state: stateB, text: offer.text),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.handshakeMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
() => KometCrypto.sessionDecrypt(state: stateB, text: 'просто текст'),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.notEncrypted,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('local seal vector', () {
|
||||
final local = vectors['local'] as Map<String, dynamic>;
|
||||
final key = hex(local['key'] as String);
|
||||
final aad = utf8Bytes(local['aad'] as String);
|
||||
final blob = KometCrypto.localSeal(
|
||||
key: key,
|
||||
nonce: hex(local['nonce'] as String),
|
||||
aad: aad,
|
||||
plaintext: utf8Bytes(local['plaintext'] as String),
|
||||
);
|
||||
expect(toHex(blob), local['blob']);
|
||||
expect(
|
||||
utf8.decode(KometCrypto.localOpen(key: key, blob: blob, aad: aad)),
|
||||
local['plaintext'],
|
||||
);
|
||||
expect(
|
||||
() => KometCrypto.localOpen(key: key, blob: blob),
|
||||
throwsA(isA<KometCryptoException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('file vector and round trip', () {
|
||||
final file = vectors['file'] as Map<String, dynamic>;
|
||||
final key = hex(file['key'] as String);
|
||||
final nonce = hex(file['nonce'] as String);
|
||||
final plain = pattern(file['plaintext_len'] as int);
|
||||
final sealer = KometCrypto.fileSealer(key, nonce);
|
||||
final cipher = BytesBuilder(copy: false);
|
||||
var offset = 0;
|
||||
while (true) {
|
||||
final last = plain.length - offset <= KometCryptoSizes.fileChunk;
|
||||
final end = last ? plain.length : offset + KometCryptoSizes.fileChunk;
|
||||
cipher.add(sealer.chunk(plain.sublist(offset, end), last: last));
|
||||
offset = end;
|
||||
if (last) break;
|
||||
}
|
||||
sealer.dispose();
|
||||
final bytes = cipher.toBytes();
|
||||
expect(bytes.length, file['ciphertext_len']);
|
||||
expect(toHex(bytes.sublist(0, 64)), file['ciphertext_head']);
|
||||
|
||||
final opener = KometCrypto.fileOpener(key, nonce);
|
||||
final opened = BytesBuilder(copy: false);
|
||||
offset = 0;
|
||||
const step = KometCryptoSizes.fileChunk + KometCryptoSizes.tag;
|
||||
while (true) {
|
||||
final last = bytes.length - offset <= step;
|
||||
final end = last ? bytes.length : offset + step;
|
||||
opened.add(opener.chunk(bytes.sublist(offset, end), last: last));
|
||||
offset = end;
|
||||
if (last) break;
|
||||
}
|
||||
opener.dispose();
|
||||
expect(opened.toBytes(), plain);
|
||||
});
|
||||
|
||||
test('export vector', () {
|
||||
final export = vectors['export'] as Map<String, dynamic>;
|
||||
final password = utf8Bytes(export['password'] as String);
|
||||
final blob = KometCrypto.exportSeal(
|
||||
password: password,
|
||||
memoryKib: export['memory_kib'] as int,
|
||||
passes: export['passes'] as int,
|
||||
random: hex(export['random'] as String),
|
||||
container: hex(export['container'] as String),
|
||||
);
|
||||
expect(toHex(blob), export['blob']);
|
||||
expect(
|
||||
toHex(KometCrypto.exportOpen(password: password, blob: blob)),
|
||||
export['opened_container'],
|
||||
);
|
||||
expect(
|
||||
() => KometCrypto.exportOpen(password: utf8Bytes('wrong'), blob: blob),
|
||||
throwsA(
|
||||
isA<KometCryptoException>().having(
|
||||
(e) => e.status,
|
||||
'status',
|
||||
CryptoStatus.wrongKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('two live parties', () {
|
||||
final a = KometCrypto.identityCreate();
|
||||
final b = KometCrypto.identityCreate();
|
||||
final offer = KometCrypto.sessionOffer(
|
||||
identity: a,
|
||||
chatId: 7,
|
||||
myId: 1,
|
||||
peerId: 2,
|
||||
);
|
||||
final answer = KometCrypto.sessionAnswer(
|
||||
identity: b,
|
||||
chatId: 7,
|
||||
myId: 2,
|
||||
peerId: 1,
|
||||
offerText: offer.text,
|
||||
);
|
||||
var stateA = KometCrypto.sessionAccept(
|
||||
identity: a,
|
||||
pending: offer.pending,
|
||||
answerText: answer.text,
|
||||
);
|
||||
var stateB = answer.state;
|
||||
for (var round = 0; round < 5; round++) {
|
||||
final toB = KometCrypto.sessionEncrypt(
|
||||
state: stateA,
|
||||
contentType: ContentType.text,
|
||||
plaintext: utf8Bytes('раунд $round'),
|
||||
);
|
||||
stateA = toB.state;
|
||||
final atB = KometCrypto.sessionDecrypt(state: stateB, text: toB.text);
|
||||
stateB = atB.state;
|
||||
expect(utf8.decode(atB.plaintext), 'раунд $round');
|
||||
final toA = KometCrypto.sessionEncrypt(
|
||||
state: stateB,
|
||||
contentType: ContentType.control,
|
||||
plaintext: Uint8List(0),
|
||||
);
|
||||
stateB = toA.state;
|
||||
final atA = KometCrypto.sessionDecrypt(state: stateA, text: toA.text);
|
||||
stateA = atA.state;
|
||||
expect(atA.contentType, ContentType.control);
|
||||
expect(atA.plaintext, isEmpty);
|
||||
}
|
||||
expect(
|
||||
KometCrypto.fingerprint(
|
||||
myId: 1,
|
||||
myPublic: KometCrypto.identityPublicKey(a),
|
||||
peerId: 2,
|
||||
peerPublic: KometCrypto.sessionPeerPublicKey(stateA),
|
||||
),
|
||||
hasLength(60),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(PROJECT_NAME "komet_crypto")
|
||||
project(${PROJECT_NAME} LANGUAGES C CXX)
|
||||
|
||||
set(KC_BUILD_STATIC OFF CACHE BOOL "" FORCE)
|
||||
set(KC_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
get_filename_component(KC_PLUGIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH)
|
||||
get_filename_component(KC_CORE_DIR "${KC_PLUGIN_DIR}/../../.." ABSOLUTE)
|
||||
add_subdirectory("${KC_CORE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/komet_crypto_core")
|
||||
|
||||
set(komet_crypto_bundled_libraries
|
||||
$<TARGET_FILE:komet_crypto>
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,350 @@
|
||||
#ifndef KOMET_CRYPTO_H
|
||||
#define KOMET_CRYPTO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if defined(KC_STATIC)
|
||||
#define KC_EXPORT
|
||||
#elif defined(KC_BUILDING)
|
||||
#define KC_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define KC_EXPORT __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#define KC_EXPORT __attribute__((visibility("default"))) __attribute__((used))
|
||||
#endif
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define KC_KEY_SIZE 32u
|
||||
#define KC_NONCE_SIZE 12u
|
||||
#define KC_TAG_SIZE 16u
|
||||
|
||||
#define KC_SEED_SIZE 32u
|
||||
#define KC_PUBLIC_KEY_SIZE 32u
|
||||
#define KC_IDENTITY_SIZE 64u
|
||||
#define KC_FINGERPRINT_DIGITS 60u
|
||||
#define KC_OFFER_ID_SIZE 8u
|
||||
#define KC_HANDSHAKE_SIZE 138u
|
||||
#define KC_HANDSHAKE_TEXT_BOUND 497u
|
||||
#define KC_PENDING_STATE_SIZE 203u
|
||||
#define KC_SESSION_STATE_MAX 17712u
|
||||
#define KC_SESSION_PLAINTEXT_MAX 414u
|
||||
#define KC_SESSION_MAX_SKIP 256u
|
||||
#define KC_SESSION_MAX_ADVANCE 65536u
|
||||
#define KC_TEXT_TRANSPORT_MAX 1000u
|
||||
|
||||
#define KC_IDENTITY_RANDOM_SIZE 32u
|
||||
#define KC_OFFER_RANDOM_SIZE 40u
|
||||
#define KC_ANSWER_RANDOM_SIZE 32u
|
||||
#define KC_ACCEPT_RANDOM_SIZE 32u
|
||||
#define KC_ENCRYPT_RANDOM_SIZE 32u
|
||||
#define KC_EXPORT_RANDOM_SIZE 40u
|
||||
|
||||
#define KC_CONTENT_TEXT 1u
|
||||
#define KC_CONTENT_FILE 2u
|
||||
#define KC_CONTENT_CONTROL 3u
|
||||
|
||||
#define KC_FILE_KEY_SIZE 32u
|
||||
#define KC_FILE_NONCE_SIZE 12u
|
||||
#define KC_FILE_CHUNK_SIZE 65536u
|
||||
#define KC_FILE_CTX_SIZE 96u
|
||||
|
||||
#define KC_LOCAL_KEY_SIZE 32u
|
||||
#define KC_LOCAL_NONCE_SIZE 24u
|
||||
|
||||
#define KC_TEXT_NONE 0
|
||||
#define KC_TEXT_LEGACY 1
|
||||
#define KC_TEXT_OFFER 2
|
||||
#define KC_TEXT_ANSWER 3
|
||||
#define KC_TEXT_SESSION 4
|
||||
|
||||
typedef enum {
|
||||
KC_OK = 0,
|
||||
KC_ERR_EMPTY_PASSWORD = -1,
|
||||
KC_ERR_BAD_KEY_LENGTH = -2,
|
||||
KC_ERR_NOT_ENCRYPTED = -3,
|
||||
KC_ERR_MALFORMED = -4,
|
||||
KC_ERR_WRONG_KEY = -5,
|
||||
KC_ERR_INTERNAL = -6,
|
||||
KC_ERR_BUFFER_TOO_SMALL = -7,
|
||||
KC_ERR_INVALID_ARGUMENT = -8,
|
||||
KC_ERR_OUT_OF_MEMORY = -9,
|
||||
KC_ERR_BAD_SIGNATURE = -10,
|
||||
KC_ERR_BAD_STATE = -11,
|
||||
KC_ERR_TOO_MANY_SKIPPED = -12,
|
||||
KC_ERR_REPLAY = -13,
|
||||
KC_ERR_BAD_PEER = -14,
|
||||
KC_ERR_UNSUPPORTED_VERSION = -15,
|
||||
KC_ERR_HANDSHAKE_MESSAGE = -16,
|
||||
KC_ERR_TOO_LONG = -17,
|
||||
KC_ERR_AWAITING_PEER = -18
|
||||
} kc_status;
|
||||
|
||||
KC_EXPORT const char *kc_status_string(kc_status status);
|
||||
|
||||
KC_EXPORT void kc_wipe(void *buf, size_t len);
|
||||
|
||||
KC_EXPORT kc_status kc_derive_key(const uint8_t *password,
|
||||
size_t password_len,
|
||||
uint8_t *out_key,
|
||||
size_t out_key_len);
|
||||
|
||||
KC_EXPORT size_t kc_encrypt_message_bound(size_t plaintext_len);
|
||||
|
||||
KC_EXPORT kc_status kc_encrypt_message(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT size_t kc_decrypt_message_bound(size_t text_len);
|
||||
|
||||
KC_EXPORT kc_status kc_decrypt_message(const uint8_t *text,
|
||||
size_t text_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT int kc_looks_encrypted_message(const uint8_t *text, size_t text_len);
|
||||
|
||||
KC_EXPORT size_t kc_encrypt_image_blob_bound(size_t plaintext_len);
|
||||
|
||||
KC_EXPORT kc_status kc_encrypt_image_blob(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT size_t kc_decrypt_image_blob_bound(size_t blob_len);
|
||||
|
||||
KC_EXPORT kc_status kc_decrypt_image_blob(const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT int kc_looks_encrypted_image_blob(const uint8_t *blob,
|
||||
size_t blob_len);
|
||||
|
||||
KC_EXPORT kc_status kc_identity_create(const uint8_t *seed,
|
||||
size_t seed_len,
|
||||
uint8_t *out_identity,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_identity_public_key(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
uint8_t *out_public,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_fingerprint(int64_t my_id,
|
||||
const uint8_t *my_public,
|
||||
size_t my_public_len,
|
||||
int64_t peer_id,
|
||||
const uint8_t *peer_public,
|
||||
size_t peer_public_len,
|
||||
uint8_t *out_digits,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT int kc_classify_text(const uint8_t *text, size_t text_len);
|
||||
|
||||
KC_EXPORT kc_status kc_handshake_peek(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t *out_type,
|
||||
uint8_t *out_offer_id,
|
||||
size_t offer_id_cap,
|
||||
uint8_t *out_public,
|
||||
size_t public_cap);
|
||||
|
||||
KC_EXPORT kc_status kc_session_offer(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
int64_t chat_id,
|
||||
int64_t my_id,
|
||||
int64_t peer_id,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_pending,
|
||||
size_t pending_cap,
|
||||
size_t *pending_len);
|
||||
|
||||
KC_EXPORT kc_status kc_session_answer(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
int64_t chat_id,
|
||||
int64_t my_id,
|
||||
int64_t peer_id,
|
||||
const uint8_t *offer_text,
|
||||
size_t offer_len,
|
||||
const uint8_t *old_state,
|
||||
size_t old_state_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_len);
|
||||
|
||||
KC_EXPORT kc_status kc_session_accept(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
const uint8_t *pending,
|
||||
size_t pending_len,
|
||||
const uint8_t *answer_text,
|
||||
size_t answer_len,
|
||||
const uint8_t *expected_peer,
|
||||
size_t expected_peer_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_len);
|
||||
|
||||
KC_EXPORT size_t kc_session_encrypt_bound(size_t plaintext_len);
|
||||
|
||||
KC_EXPORT kc_status kc_session_encrypt(const uint8_t *state,
|
||||
size_t state_len,
|
||||
uint8_t content_type,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_out_len);
|
||||
|
||||
KC_EXPORT size_t kc_session_decrypt_bound(size_t text_len);
|
||||
|
||||
KC_EXPORT kc_status kc_session_decrypt(const uint8_t *state,
|
||||
size_t state_len,
|
||||
const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t *out_content_type,
|
||||
uint8_t *out_plaintext,
|
||||
size_t plaintext_cap,
|
||||
size_t *plaintext_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_session_peer_public_key(const uint8_t *state,
|
||||
size_t state_len,
|
||||
uint8_t *out_public,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT size_t kc_file_seal_bound(size_t plaintext_len);
|
||||
|
||||
KC_EXPORT size_t kc_file_open_bound(size_t ciphertext_len);
|
||||
|
||||
KC_EXPORT kc_status kc_file_seal_init(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len);
|
||||
|
||||
KC_EXPORT kc_status kc_file_seal_chunk(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
int last,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_file_open_init(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len);
|
||||
|
||||
KC_EXPORT kc_status kc_file_open_chunk(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *chunk,
|
||||
size_t chunk_len,
|
||||
int last,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT size_t kc_local_seal_bound(size_t plaintext_len);
|
||||
|
||||
KC_EXPORT size_t kc_local_open_bound(size_t blob_len);
|
||||
|
||||
KC_EXPORT kc_status kc_local_seal(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
const uint8_t *aad,
|
||||
size_t aad_len,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_local_open(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *aad,
|
||||
size_t aad_len,
|
||||
const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT size_t kc_export_seal_bound(size_t container_len);
|
||||
|
||||
KC_EXPORT size_t kc_export_open_bound(size_t blob_len);
|
||||
|
||||
KC_EXPORT kc_status kc_export_seal(const uint8_t *password,
|
||||
size_t password_len,
|
||||
uint32_t memory_kib,
|
||||
uint32_t passes,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
const uint8_t *container,
|
||||
size_t container_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
KC_EXPORT kc_status kc_export_open(const uint8_t *password,
|
||||
size_t password_len,
|
||||
const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
uint8_t *out_container,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "komet_crypto.c"
|
||||
#include "kc_alphabet.c"
|
||||
#include "kc_sha256.c"
|
||||
#include "kc_status.c"
|
||||
#include "kc_hkdf.c"
|
||||
#include "kc_identity.c"
|
||||
#include "kc_local.c"
|
||||
#include "kc_session_state.c"
|
||||
#include "kc_ratchet.c"
|
||||
#include "kc_envelope_v2.c"
|
||||
#include "kc_handshake.c"
|
||||
#include "kc_file.c"
|
||||
#include "kc_export.c"
|
||||
@@ -0,0 +1,281 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
static int kc_utf8_next(const uint8_t *input,
|
||||
size_t input_len,
|
||||
size_t *offset,
|
||||
uint32_t *codepoint)
|
||||
{
|
||||
uint8_t first;
|
||||
uint32_t value;
|
||||
size_t length;
|
||||
size_t index;
|
||||
|
||||
if (input == NULL || offset == NULL || codepoint == NULL ||
|
||||
*offset >= input_len) {
|
||||
return 0;
|
||||
}
|
||||
first = input[*offset];
|
||||
if (first < 0x80u) {
|
||||
*codepoint = first;
|
||||
*offset += 1u;
|
||||
return 1;
|
||||
}
|
||||
if (first >= 0xC2u && first <= 0xDFu) {
|
||||
value = first & 0x1Fu;
|
||||
length = 2u;
|
||||
} else if (first >= 0xE0u && first <= 0xEFu) {
|
||||
value = first & 0x0Fu;
|
||||
length = 3u;
|
||||
} else if (first >= 0xF0u && first <= 0xF4u) {
|
||||
value = first & 0x07u;
|
||||
length = 4u;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (length > input_len - *offset) {
|
||||
return 0;
|
||||
}
|
||||
for (index = 1u; index < length; index++) {
|
||||
uint8_t next = input[*offset + index];
|
||||
if ((next & 0xC0u) != 0x80u) {
|
||||
return 0;
|
||||
}
|
||||
value = (value << 6u) | (next & 0x3Fu);
|
||||
}
|
||||
if ((length == 3u && value < 0x800u) ||
|
||||
(length == 4u && value < 0x10000u) ||
|
||||
(value >= 0xD800u && value <= 0xDFFFu) || value > 0x10FFFFu) {
|
||||
return 0;
|
||||
}
|
||||
*codepoint = value;
|
||||
*offset += length;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int kc_unicode_whitespace(uint32_t codepoint)
|
||||
{
|
||||
if ((codepoint >= 0x09u && codepoint <= 0x0Du) || codepoint == 0x20u ||
|
||||
codepoint == 0x85u || codepoint == 0xA0u || codepoint == 0x1680u ||
|
||||
(codepoint >= 0x2000u && codepoint <= 0x200Au) ||
|
||||
codepoint == 0x2028u || codepoint == 0x2029u ||
|
||||
codepoint == 0x202Fu || codepoint == 0x205Fu ||
|
||||
codepoint == 0x3000u) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int kc_letter_index(uint32_t codepoint, uint8_t *index)
|
||||
{
|
||||
if (codepoint >= 0x410u && codepoint <= 0x42Fu) {
|
||||
codepoint += 0x20u;
|
||||
}
|
||||
if (codepoint < 0x430u || codepoint > 0x44Fu || index == NULL) {
|
||||
return 0;
|
||||
}
|
||||
*index = (uint8_t)(codepoint - 0x430u);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void kc_write_letter(uint8_t index, uint8_t *out, size_t *offset)
|
||||
{
|
||||
uint32_t codepoint = 0x430u + index;
|
||||
if (codepoint <= 0x43Fu) {
|
||||
out[*offset] = 0xD0u;
|
||||
out[*offset + 1u] = (uint8_t)(0xB0u + index);
|
||||
} else {
|
||||
out[*offset] = 0xD1u;
|
||||
out[*offset + 1u] = (uint8_t)(0x80u + index - 16u);
|
||||
}
|
||||
*offset += 2u;
|
||||
}
|
||||
|
||||
static kc_status kc_write_symbol(uint8_t symbol,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *offset,
|
||||
size_t *run,
|
||||
size_t *word_len)
|
||||
{
|
||||
if (*run == *word_len) {
|
||||
if (*offset >= out_cap) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
out[*offset] = (uint8_t)' ';
|
||||
*offset += 1u;
|
||||
*run = 0u;
|
||||
*word_len = 4u + symbol % 5u;
|
||||
}
|
||||
if (out_cap - *offset < 2u) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
kc_write_letter(symbol, out, offset);
|
||||
*run += 1u;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
size_t kc_alphabet_encode_bound(size_t input_len)
|
||||
{
|
||||
size_t bits;
|
||||
size_t letters;
|
||||
size_t bytes;
|
||||
|
||||
if (input_len > (SIZE_MAX - 4u) / 8u) {
|
||||
return 0u;
|
||||
}
|
||||
bits = input_len * 8u;
|
||||
letters = (bits + 4u) / 5u;
|
||||
if (letters > SIZE_MAX / 2u) {
|
||||
return 0u;
|
||||
}
|
||||
bytes = letters * 2u;
|
||||
if (letters != 0u && !kc_size_add(bytes, (letters - 1u) / 4u, &bytes)) {
|
||||
return 0u;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
kc_status kc_alphabet_encode(const uint8_t *input,
|
||||
size_t input_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint32_t accumulator = 0u;
|
||||
unsigned int bits = 0u;
|
||||
size_t offset = 0u;
|
||||
size_t run = 0u;
|
||||
size_t word_len = 4u;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
if ((input == NULL && input_len != 0u) || out_len == NULL ||
|
||||
(out == NULL && out_cap != 0u)) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
for (index = 0u; index < input_len; index++) {
|
||||
accumulator = (accumulator << 8u) | input[index];
|
||||
bits += 8u;
|
||||
while (bits >= 5u) {
|
||||
uint8_t symbol;
|
||||
bits -= 5u;
|
||||
symbol = (uint8_t)((accumulator >> bits) & 31u);
|
||||
status = kc_write_symbol(symbol, out, out_cap, &offset, &run,
|
||||
&word_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
if (bits == 0u) {
|
||||
accumulator = 0u;
|
||||
} else {
|
||||
accumulator &= (1u << bits) - 1u;
|
||||
}
|
||||
}
|
||||
if (bits != 0u) {
|
||||
uint8_t symbol = (uint8_t)((accumulator << (5u - bits)) & 31u);
|
||||
status = kc_write_symbol(symbol, out, out_cap, &offset, &run,
|
||||
&word_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
*out_len = offset;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_alphabet_decode(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t **out,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t *symbols;
|
||||
uint8_t *decoded;
|
||||
size_t symbol_count = 0u;
|
||||
size_t offset = 0u;
|
||||
size_t decoded_len;
|
||||
size_t decoded_offset = 0u;
|
||||
uint32_t accumulator = 0u;
|
||||
unsigned int bits = 0u;
|
||||
size_t index;
|
||||
size_t remainder;
|
||||
|
||||
if ((text == NULL && text_len != 0u) || out == NULL || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
symbols = (uint8_t *)malloc(text_len == 0u ? 1u : text_len);
|
||||
if (symbols == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
while (offset < text_len) {
|
||||
uint32_t codepoint;
|
||||
uint8_t letter;
|
||||
if (!kc_utf8_next(text, text_len, &offset, &codepoint)) {
|
||||
free(symbols);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
if (kc_unicode_whitespace(codepoint)) {
|
||||
continue;
|
||||
}
|
||||
if (!kc_letter_index(codepoint, &letter)) {
|
||||
free(symbols);
|
||||
return KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
symbols[symbol_count++] = letter;
|
||||
}
|
||||
if (symbol_count == 0u) {
|
||||
free(symbols);
|
||||
return KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
remainder = symbol_count % 8u;
|
||||
if (remainder == 1u || remainder == 3u || remainder == 6u) {
|
||||
free(symbols);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
decoded_len = symbol_count * 5u / 8u;
|
||||
decoded = (uint8_t *)malloc(decoded_len == 0u ? 1u : decoded_len);
|
||||
if (decoded == NULL) {
|
||||
free(symbols);
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
for (index = 0u; index < symbol_count; index++) {
|
||||
accumulator = (accumulator << 5u) | symbols[index];
|
||||
bits += 5u;
|
||||
if (bits >= 8u) {
|
||||
bits -= 8u;
|
||||
decoded[decoded_offset++] =
|
||||
(uint8_t)((accumulator >> bits) & 0xFFu);
|
||||
}
|
||||
if (bits == 0u) {
|
||||
accumulator = 0u;
|
||||
} else {
|
||||
accumulator &= (1u << bits) - 1u;
|
||||
}
|
||||
}
|
||||
free(symbols);
|
||||
if (bits != 0u && accumulator != 0u) {
|
||||
free(decoded);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
*out = decoded;
|
||||
*out_len = decoded_offset;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
int kc_utf8_valid(const uint8_t *input, size_t input_len)
|
||||
{
|
||||
size_t offset = 0u;
|
||||
uint32_t codepoint;
|
||||
|
||||
if (input == NULL && input_len != 0u) {
|
||||
return 0;
|
||||
}
|
||||
while (offset < input_len) {
|
||||
if (!kc_utf8_next(input, input_len, &offset, &codepoint)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef KC_CODEC_H
|
||||
#define KC_CODEC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static inline void kc_store_u32(uint8_t *out, uint32_t value)
|
||||
{
|
||||
out[0] = (uint8_t)(value >> 24u);
|
||||
out[1] = (uint8_t)(value >> 16u);
|
||||
out[2] = (uint8_t)(value >> 8u);
|
||||
out[3] = (uint8_t)value;
|
||||
}
|
||||
|
||||
static inline uint32_t kc_load_u32(const uint8_t *in)
|
||||
{
|
||||
return ((uint32_t)in[0] << 24u) | ((uint32_t)in[1] << 16u) |
|
||||
((uint32_t)in[2] << 8u) | (uint32_t)in[3];
|
||||
}
|
||||
|
||||
static inline void kc_store_u64(uint8_t *out, uint64_t value)
|
||||
{
|
||||
kc_store_u32(out, (uint32_t)(value >> 32u));
|
||||
kc_store_u32(out + 4, (uint32_t)value);
|
||||
}
|
||||
|
||||
static inline uint64_t kc_load_u64(const uint8_t *in)
|
||||
{
|
||||
return ((uint64_t)kc_load_u32(in) << 32u) | (uint64_t)kc_load_u32(in + 4);
|
||||
}
|
||||
|
||||
static inline void kc_store_i64(uint8_t *out, int64_t value)
|
||||
{
|
||||
kc_store_u64(out, (uint64_t)value);
|
||||
}
|
||||
|
||||
static inline int64_t kc_load_i64(const uint8_t *in)
|
||||
{
|
||||
return (int64_t)kc_load_u64(in);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,411 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "kc_hkdf.h"
|
||||
#include "kc_ratchet.h"
|
||||
#include "kc_session_state.h"
|
||||
#include "monocypher.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define KC_V2_AAD_SIZE (1u + 8u + 8u + KC_V2_HEADER_SIZE)
|
||||
|
||||
static size_t kc_env_padded_len(size_t body_len)
|
||||
{
|
||||
size_t with_marker;
|
||||
|
||||
if (!kc_size_add(body_len, 1u, &with_marker)) {
|
||||
return 0u;
|
||||
}
|
||||
return (with_marker + KC_V2_PAD_BLOCK - 1u) / KC_V2_PAD_BLOCK *
|
||||
KC_V2_PAD_BLOCK;
|
||||
}
|
||||
|
||||
static int kc_env_is_shaped(size_t blob_len)
|
||||
{
|
||||
return blob_len >= KC_V2_ENVELOPE_MIN &&
|
||||
(blob_len - KC_V2_HEADER_SIZE - KC_TAG_SIZE) % KC_V2_PAD_BLOCK == 0u;
|
||||
}
|
||||
|
||||
static int kc_env_is_handshake(const uint8_t *blob, size_t blob_len)
|
||||
{
|
||||
return blob_len == KC_HANDSHAKE_SIZE && blob[0] == KC_V2_VERSION &&
|
||||
(blob[1] == KC_V2_OFFER || blob[1] == KC_V2_ANSWER);
|
||||
}
|
||||
|
||||
static void kc_env_aad(uint8_t out[KC_V2_AAD_SIZE],
|
||||
int64_t chat_id,
|
||||
int64_t sender_id,
|
||||
const uint8_t header[KC_V2_HEADER_SIZE])
|
||||
{
|
||||
out[0] = KC_V2_VERSION;
|
||||
kc_store_i64(out + 1, chat_id);
|
||||
kc_store_i64(out + 9, sender_id);
|
||||
memcpy(out + 17, header, KC_V2_HEADER_SIZE);
|
||||
}
|
||||
|
||||
static void kc_env_mask(const uint8_t ok[32],
|
||||
const uint8_t repr[32],
|
||||
uint8_t out[8])
|
||||
{
|
||||
crypto_blake2b_keyed(out, 8u, ok, 32u, repr, 32u);
|
||||
}
|
||||
|
||||
static int kc_env_content_valid(uint8_t content_type)
|
||||
{
|
||||
return content_type == KC_CONTENT_TEXT || content_type == KC_CONTENT_FILE ||
|
||||
content_type == KC_CONTENT_CONTROL;
|
||||
}
|
||||
|
||||
size_t kc_session_encrypt_bound(size_t plaintext_len)
|
||||
{
|
||||
size_t padded;
|
||||
size_t blob_len;
|
||||
|
||||
if (plaintext_len > KC_SESSION_PLAINTEXT_MAX) {
|
||||
return 0u;
|
||||
}
|
||||
padded = kc_env_padded_len(plaintext_len + 1u);
|
||||
if (padded == 0u ||
|
||||
!kc_size_add(padded, KC_V2_HEADER_SIZE + KC_TAG_SIZE, &blob_len)) {
|
||||
return 0u;
|
||||
}
|
||||
return kc_alphabet_encode_bound(blob_len);
|
||||
}
|
||||
|
||||
kc_status kc_session_encrypt(const uint8_t *state,
|
||||
size_t state_len,
|
||||
uint8_t content_type,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_out_len)
|
||||
{
|
||||
kc_session *session;
|
||||
uint8_t mk[32];
|
||||
uint8_t key[32];
|
||||
uint8_t nonce[12];
|
||||
uint8_t mask[8];
|
||||
uint8_t counters[8];
|
||||
uint8_t aad[KC_V2_AAD_SIZE];
|
||||
uint8_t *blob;
|
||||
size_t padded;
|
||||
size_t blob_len;
|
||||
size_t sealed_len;
|
||||
size_t required;
|
||||
uint32_t n;
|
||||
uint32_t pn;
|
||||
kc_status status;
|
||||
|
||||
if (!kc_env_content_valid(content_type) ||
|
||||
(plaintext == NULL && plaintext_len != 0u) || random == NULL ||
|
||||
random_len != KC_ENCRYPT_RANDOM_SIZE || text_len == NULL ||
|
||||
state_out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (plaintext_len > KC_SESSION_PLAINTEXT_MAX) {
|
||||
return KC_ERR_TOO_LONG;
|
||||
}
|
||||
if (content_type == KC_CONTENT_TEXT &&
|
||||
!kc_utf8_valid(plaintext, plaintext_len)) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
required = kc_session_encrypt_bound(plaintext_len);
|
||||
if (required == 0u || out_text == NULL || text_cap < required ||
|
||||
out_state == NULL || state_cap < KC_SESSION_FIXED_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
session = (kc_session *)malloc(sizeof(*session));
|
||||
if (session == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
status = kc_session_parse(state, state_len, session);
|
||||
if (status != KC_OK) {
|
||||
free(session);
|
||||
return status;
|
||||
}
|
||||
padded = kc_env_padded_len(plaintext_len + 1u);
|
||||
blob_len = KC_V2_HEADER_SIZE + padded + KC_TAG_SIZE;
|
||||
blob = (uint8_t *)malloc(blob_len);
|
||||
if (blob == NULL) {
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
status = kc_ratchet_next_send_key(session, random, mk, &n, &pn, blob);
|
||||
if (status == KC_OK) {
|
||||
uint8_t *body = blob + KC_V2_HEADER_SIZE;
|
||||
kc_env_mask(session->ok, blob, mask);
|
||||
kc_store_u32(counters, pn);
|
||||
kc_store_u32(counters + 4, n);
|
||||
for (n = 0u; n < 8u; n++) {
|
||||
blob[32u + n] = counters[n] ^ mask[n];
|
||||
}
|
||||
memset(body, 0, padded);
|
||||
body[0] = content_type;
|
||||
if (plaintext_len != 0u) {
|
||||
memcpy(body + 1, plaintext, plaintext_len);
|
||||
}
|
||||
body[1u + plaintext_len] = 0x80u;
|
||||
kc_env_aad(aad, session->chat_id, session->my_id, blob);
|
||||
kc_kdf_message(mk, key, nonce);
|
||||
status = kc_seal(body, padded, key, sizeof(key), nonce, sizeof(nonce),
|
||||
aad, sizeof(aad), body, padded + KC_TAG_SIZE,
|
||||
&sealed_len);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_alphabet_encode(blob, blob_len, out_text, text_cap,
|
||||
text_len);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_session_serialize(session, out_state, state_cap,
|
||||
state_out_len);
|
||||
}
|
||||
crypto_wipe(mk, sizeof(mk));
|
||||
crypto_wipe(key, sizeof(key));
|
||||
crypto_wipe(nonce, sizeof(nonce));
|
||||
crypto_wipe(blob, blob_len);
|
||||
free(blob);
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
return status;
|
||||
}
|
||||
|
||||
size_t kc_session_decrypt_bound(size_t text_len)
|
||||
{
|
||||
return text_len;
|
||||
}
|
||||
|
||||
kc_status kc_session_decrypt(const uint8_t *state,
|
||||
size_t state_len,
|
||||
const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t *out_content_type,
|
||||
uint8_t *out_plaintext,
|
||||
size_t plaintext_cap,
|
||||
size_t *plaintext_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_out_len)
|
||||
{
|
||||
kc_session *session = NULL;
|
||||
uint8_t *blob = NULL;
|
||||
uint8_t *body = NULL;
|
||||
size_t blob_len = 0u;
|
||||
size_t body_len;
|
||||
size_t opened_len;
|
||||
size_t content_len = 0u;
|
||||
uint8_t mk[32];
|
||||
uint8_t key[32];
|
||||
uint8_t nonce[12];
|
||||
uint8_t mask[8];
|
||||
uint8_t counters[8];
|
||||
uint8_t aad[KC_V2_AAD_SIZE];
|
||||
uint32_t pn;
|
||||
uint32_t n;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
if (out_content_type == NULL || plaintext_len == NULL ||
|
||||
state_out_len == NULL || (out_plaintext == NULL && plaintext_cap != 0u)) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
status = kc_alphabet_decode(text, text_len, &blob, &blob_len);
|
||||
if (status == KC_ERR_OUT_OF_MEMORY) {
|
||||
return status;
|
||||
}
|
||||
if (status != KC_OK) {
|
||||
uint8_t handshake[KC_HANDSHAKE_SIZE];
|
||||
return kc_handshake_decode(text, text_len, handshake) == KC_OK
|
||||
? KC_ERR_HANDSHAKE_MESSAGE
|
||||
: KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
if (kc_env_is_handshake(blob, blob_len)) {
|
||||
free(blob);
|
||||
return KC_ERR_HANDSHAKE_MESSAGE;
|
||||
}
|
||||
if (!kc_env_is_shaped(blob_len)) {
|
||||
free(blob);
|
||||
return KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
body_len = blob_len - KC_V2_HEADER_SIZE - KC_TAG_SIZE;
|
||||
if (plaintext_cap < body_len || out_state == NULL ||
|
||||
state_cap < KC_SESSION_FIXED_SIZE) {
|
||||
free(blob);
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
session = (kc_session *)malloc(sizeof(*session));
|
||||
body = (uint8_t *)malloc(body_len);
|
||||
if (session == NULL || body == NULL) {
|
||||
free(session);
|
||||
free(body);
|
||||
free(blob);
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
status = kc_session_parse(state, state_len, session);
|
||||
if (status == KC_OK) {
|
||||
kc_env_mask(session->ok, blob, mask);
|
||||
for (index = 0u; index < 8u; index++) {
|
||||
counters[index] = blob[32u + index] ^ mask[index];
|
||||
}
|
||||
pn = kc_load_u32(counters);
|
||||
n = kc_load_u32(counters + 4);
|
||||
status = kc_ratchet_receive_key(session, blob, pn, n, mk);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
kc_env_aad(aad, session->chat_id, session->peer_id, blob);
|
||||
kc_kdf_message(mk, key, nonce);
|
||||
status = kc_open(blob + KC_V2_HEADER_SIZE, body_len + KC_TAG_SIZE, key,
|
||||
sizeof(key), nonce, aad, sizeof(aad), body, body_len,
|
||||
&opened_len);
|
||||
crypto_wipe(mk, sizeof(mk));
|
||||
crypto_wipe(key, sizeof(key));
|
||||
crypto_wipe(nonce, sizeof(nonce));
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
content_len = body_len;
|
||||
while (content_len > 0u && body[content_len - 1u] == 0x00u) {
|
||||
content_len--;
|
||||
}
|
||||
if (content_len < 2u || body[content_len - 1u] != 0x80u ||
|
||||
!kc_env_content_valid(body[0])) {
|
||||
status = KC_ERR_MALFORMED;
|
||||
} else {
|
||||
content_len -= 2u;
|
||||
if (body[0] == KC_CONTENT_TEXT &&
|
||||
!kc_utf8_valid(body + 1, content_len)) {
|
||||
status = KC_ERR_MALFORMED;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_session_serialize(session, out_state, state_cap,
|
||||
state_out_len);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
*out_content_type = body[0];
|
||||
if (content_len != 0u) {
|
||||
memcpy(out_plaintext, body + 1, content_len);
|
||||
}
|
||||
*plaintext_len = content_len;
|
||||
}
|
||||
crypto_wipe(body, body_len);
|
||||
free(body);
|
||||
crypto_wipe(blob, blob_len);
|
||||
free(blob);
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
return status;
|
||||
}
|
||||
|
||||
kc_status kc_session_peer_public_key(const uint8_t *state,
|
||||
size_t state_len,
|
||||
uint8_t *out_public,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
if (out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (state == NULL || state_len < KC_SESSION_FIXED_SIZE ||
|
||||
state[0] != KC_SESSION_FMT) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
if (out_public == NULL || out_cap < KC_PUBLIC_KEY_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memcpy(out_public, state + 2u + 24u + KC_OFFER_ID_SIZE, KC_PUBLIC_KEY_SIZE);
|
||||
*out_len = KC_PUBLIC_KEY_SIZE;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static const uint8_t *kc_env_last_line(const uint8_t *text,
|
||||
size_t text_len,
|
||||
size_t *out_len)
|
||||
{
|
||||
size_t index = text_len;
|
||||
|
||||
while (index > 0u) {
|
||||
if (text[index - 1u] == (uint8_t)'\n') {
|
||||
*out_len = text_len - index;
|
||||
return text + index;
|
||||
}
|
||||
index--;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
kc_status kc_handshake_decode(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t out[KC_HANDSHAKE_SIZE])
|
||||
{
|
||||
uint8_t *blob = NULL;
|
||||
size_t blob_len = 0u;
|
||||
const uint8_t *line;
|
||||
size_t line_len = 0u;
|
||||
kc_status status;
|
||||
|
||||
if (text == NULL && text_len != 0u) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
status = kc_alphabet_decode(text, text_len, &blob, &blob_len);
|
||||
if (status != KC_OK || blob_len != KC_HANDSHAKE_SIZE) {
|
||||
free(blob);
|
||||
blob = NULL;
|
||||
line = kc_env_last_line(text, text_len, &line_len);
|
||||
if (line == NULL) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
status = kc_alphabet_decode(line, line_len, &blob, &blob_len);
|
||||
if (status != KC_OK) {
|
||||
return status == KC_ERR_OUT_OF_MEMORY ? status : KC_ERR_MALFORMED;
|
||||
}
|
||||
}
|
||||
if (blob_len != KC_HANDSHAKE_SIZE) {
|
||||
free(blob);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
if (blob[0] != KC_V2_VERSION) {
|
||||
free(blob);
|
||||
return KC_ERR_UNSUPPORTED_VERSION;
|
||||
}
|
||||
if (blob[1] != KC_V2_OFFER && blob[1] != KC_V2_ANSWER) {
|
||||
free(blob);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
memcpy(out, blob, KC_HANDSHAKE_SIZE);
|
||||
free(blob);
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
int kc_classify_text(const uint8_t *text, size_t text_len)
|
||||
{
|
||||
uint8_t *blob = NULL;
|
||||
size_t blob_len = 0u;
|
||||
uint8_t handshake[KC_HANDSHAKE_SIZE];
|
||||
int result = KC_TEXT_NONE;
|
||||
|
||||
if (kc_alphabet_decode(text, text_len, &blob, &blob_len) == KC_OK) {
|
||||
if (kc_env_is_handshake(blob, blob_len)) {
|
||||
result = blob[1] == KC_V2_OFFER ? KC_TEXT_OFFER : KC_TEXT_ANSWER;
|
||||
} else if (blob_len >= KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE +
|
||||
KC_TAG_SIZE &&
|
||||
blob[0] == KC_MAGIC && blob[1] == KC_MESSAGE_VERSION) {
|
||||
result = KC_TEXT_LEGACY;
|
||||
} else if (kc_env_is_shaped(blob_len)) {
|
||||
result = KC_TEXT_SESSION;
|
||||
}
|
||||
free(blob);
|
||||
return result;
|
||||
}
|
||||
if (kc_handshake_decode(text, text_len, handshake) == KC_OK) {
|
||||
return handshake[1] == KC_V2_OFFER ? KC_TEXT_OFFER : KC_TEXT_ANSWER;
|
||||
}
|
||||
return KC_TEXT_NONE;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "kc_session_state.h"
|
||||
#include "monocypher.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define KC_EXPORT_VERSION 0x01u
|
||||
#define KC_EXPORT_SALT_SIZE 16u
|
||||
#define KC_EXPORT_HEADER_SIZE (1u + KC_EXPORT_SALT_SIZE + 4u + 4u + KC_LOCAL_NONCE_SIZE)
|
||||
#define KC_EXPORT_MIN_BLOCKS 8u
|
||||
#define KC_EXPORT_MAX_PASSES 8u
|
||||
#define KC_EXPORT_MAX_KIB 262144u
|
||||
|
||||
static kc_status kc_export_key(const uint8_t *password,
|
||||
size_t password_len,
|
||||
const uint8_t *salt,
|
||||
uint32_t memory_kib,
|
||||
uint32_t passes,
|
||||
uint8_t out_key[32])
|
||||
{
|
||||
crypto_argon2_config config;
|
||||
crypto_argon2_inputs inputs;
|
||||
void *work_area;
|
||||
size_t work_size;
|
||||
|
||||
if (password_len == 0u) {
|
||||
return KC_ERR_EMPTY_PASSWORD;
|
||||
}
|
||||
if (password == NULL || password_len > UINT32_MAX ||
|
||||
memory_kib < KC_EXPORT_MIN_BLOCKS || memory_kib > KC_EXPORT_MAX_KIB ||
|
||||
passes == 0u || passes > KC_EXPORT_MAX_PASSES) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
work_size = (size_t)memory_kib * 1024u;
|
||||
work_area = malloc(work_size);
|
||||
if (work_area == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
config.algorithm = CRYPTO_ARGON2_ID;
|
||||
config.nb_blocks = memory_kib;
|
||||
config.nb_passes = passes;
|
||||
config.nb_lanes = 1u;
|
||||
inputs.pass = password;
|
||||
inputs.pass_size = (uint32_t)password_len;
|
||||
inputs.salt = salt;
|
||||
inputs.salt_size = KC_EXPORT_SALT_SIZE;
|
||||
crypto_argon2(out_key, 32u, work_area, config, inputs,
|
||||
crypto_argon2_no_extras);
|
||||
crypto_wipe(work_area, work_size);
|
||||
free(work_area);
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
size_t kc_export_seal_bound(size_t container_len)
|
||||
{
|
||||
size_t result;
|
||||
|
||||
if (!kc_size_add(container_len, KC_EXPORT_HEADER_SIZE + KC_TAG_SIZE,
|
||||
&result)) {
|
||||
return 0u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t kc_export_open_bound(size_t blob_len)
|
||||
{
|
||||
if (blob_len < KC_EXPORT_HEADER_SIZE + KC_TAG_SIZE) {
|
||||
return 0u;
|
||||
}
|
||||
return blob_len - KC_EXPORT_HEADER_SIZE - KC_TAG_SIZE;
|
||||
}
|
||||
|
||||
kc_status kc_export_seal(const uint8_t *password,
|
||||
size_t password_len,
|
||||
uint32_t memory_kib,
|
||||
uint32_t passes,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
const uint8_t *container,
|
||||
size_t container_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
static const uint8_t empty = 0u;
|
||||
uint8_t key[32];
|
||||
size_t required = kc_export_seal_bound(container_len);
|
||||
kc_status status;
|
||||
|
||||
if (random == NULL || random_len != KC_EXPORT_RANDOM_SIZE ||
|
||||
(container == NULL && container_len != 0u) || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (required == 0u || out == NULL || out_cap < required) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
status = kc_export_key(password, password_len, random, memory_kib, passes,
|
||||
key);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
out[0] = KC_EXPORT_VERSION;
|
||||
memcpy(out + 1, random, KC_EXPORT_SALT_SIZE);
|
||||
kc_store_u32(out + 1 + KC_EXPORT_SALT_SIZE, memory_kib);
|
||||
kc_store_u32(out + 1 + KC_EXPORT_SALT_SIZE + 4, passes);
|
||||
memcpy(out + 1 + KC_EXPORT_SALT_SIZE + 8, random + KC_EXPORT_SALT_SIZE,
|
||||
KC_LOCAL_NONCE_SIZE);
|
||||
crypto_aead_lock(out + KC_EXPORT_HEADER_SIZE,
|
||||
out + KC_EXPORT_HEADER_SIZE + container_len, key,
|
||||
out + 1 + KC_EXPORT_SALT_SIZE + 8, out,
|
||||
KC_EXPORT_HEADER_SIZE,
|
||||
container_len == 0u ? &empty : container, container_len);
|
||||
crypto_wipe(key, sizeof(key));
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static kc_status kc_export_mark_sessions(uint8_t *container,
|
||||
size_t container_len)
|
||||
{
|
||||
kc_session *session;
|
||||
uint32_t count;
|
||||
uint32_t index;
|
||||
size_t offset = 4u;
|
||||
kc_status status = KC_OK;
|
||||
|
||||
if (container_len < 4u) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
count = kc_load_u32(container);
|
||||
session = (kc_session *)malloc(sizeof(*session));
|
||||
if (session == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
for (index = 0u; index < count; index++) {
|
||||
uint32_t item_len;
|
||||
if (container_len - offset < 4u) {
|
||||
status = KC_ERR_MALFORMED;
|
||||
break;
|
||||
}
|
||||
item_len = kc_load_u32(container + offset);
|
||||
offset += 4u;
|
||||
if (container_len - offset < item_len) {
|
||||
status = KC_ERR_MALFORMED;
|
||||
break;
|
||||
}
|
||||
if (index != 0u &&
|
||||
kc_session_parse(container + offset, item_len, session) == KC_OK) {
|
||||
size_t written;
|
||||
session->flags |= KC_FLAG_FORCE_DH;
|
||||
kc_session_serialize(session, container + offset, item_len,
|
||||
&written);
|
||||
kc_session_wipe(session);
|
||||
}
|
||||
offset += item_len;
|
||||
}
|
||||
if (status == KC_OK && offset != container_len) {
|
||||
status = KC_ERR_MALFORMED;
|
||||
}
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
return status;
|
||||
}
|
||||
|
||||
kc_status kc_export_open(const uint8_t *password,
|
||||
size_t password_len,
|
||||
const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
uint8_t *out_container,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t empty = 0u;
|
||||
uint8_t key[32];
|
||||
size_t container_len;
|
||||
uint32_t memory_kib;
|
||||
uint32_t passes;
|
||||
kc_status status;
|
||||
|
||||
if (out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (blob == NULL || blob_len < KC_EXPORT_HEADER_SIZE + KC_TAG_SIZE) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
if (blob[0] != KC_EXPORT_VERSION) {
|
||||
return KC_ERR_UNSUPPORTED_VERSION;
|
||||
}
|
||||
container_len = kc_export_open_bound(blob_len);
|
||||
if (out_cap < container_len || (out_container == NULL && container_len != 0u)) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memory_kib = kc_load_u32(blob + 1 + KC_EXPORT_SALT_SIZE);
|
||||
passes = kc_load_u32(blob + 1 + KC_EXPORT_SALT_SIZE + 4);
|
||||
status = kc_export_key(password, password_len, blob + 1, memory_kib, passes,
|
||||
key);
|
||||
if (status != KC_OK) {
|
||||
return status == KC_ERR_INVALID_ARGUMENT ? KC_ERR_MALFORMED : status;
|
||||
}
|
||||
if (crypto_aead_unlock(container_len == 0u ? &empty : out_container,
|
||||
blob + KC_EXPORT_HEADER_SIZE + container_len, key,
|
||||
blob + 1 + KC_EXPORT_SALT_SIZE + 8, blob,
|
||||
KC_EXPORT_HEADER_SIZE, blob + KC_EXPORT_HEADER_SIZE,
|
||||
container_len) != 0) {
|
||||
crypto_wipe(key, sizeof(key));
|
||||
if (out_container != NULL && container_len != 0u) {
|
||||
crypto_wipe(out_container, container_len);
|
||||
}
|
||||
return KC_ERR_WRONG_KEY;
|
||||
}
|
||||
crypto_wipe(key, sizeof(key));
|
||||
status = kc_export_mark_sessions(out_container, container_len);
|
||||
if (status != KC_OK) {
|
||||
crypto_wipe(out_container, container_len);
|
||||
return status;
|
||||
}
|
||||
*out_len = container_len;
|
||||
return KC_OK;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
#define KC_FILE_MODE_SEAL 1u
|
||||
#define KC_FILE_MODE_OPEN 2u
|
||||
#define KC_FILE_STATE_SIZE 54u
|
||||
#define KC_FILE_AD_SIZE 18u
|
||||
|
||||
static const uint8_t kc_file_label[] = "komet-v2/file";
|
||||
|
||||
typedef struct {
|
||||
crypto_aead_ctx aead;
|
||||
uint32_t index;
|
||||
uint8_t finished;
|
||||
uint8_t mode;
|
||||
} kc_file_state;
|
||||
|
||||
static void kc_file_store(const kc_file_state *state, uint8_t *ctx)
|
||||
{
|
||||
kc_store_u64(ctx, state->aead.counter);
|
||||
memcpy(ctx + 8, state->aead.key, 32u);
|
||||
memcpy(ctx + 40, state->aead.nonce, 8u);
|
||||
kc_store_u32(ctx + 48, state->index);
|
||||
ctx[52] = state->finished;
|
||||
ctx[53] = state->mode;
|
||||
}
|
||||
|
||||
static kc_status kc_file_load(kc_file_state *state,
|
||||
const uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
uint8_t mode)
|
||||
{
|
||||
if (ctx == NULL || ctx_len < KC_FILE_CTX_SIZE) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (ctx[53] != mode) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
state->aead.counter = kc_load_u64(ctx);
|
||||
memcpy(state->aead.key, ctx + 8, 32u);
|
||||
memcpy(state->aead.nonce, ctx + 40, 8u);
|
||||
state->index = kc_load_u32(ctx + 48);
|
||||
state->finished = ctx[52];
|
||||
state->mode = ctx[53];
|
||||
return state->finished ? KC_ERR_BAD_STATE : KC_OK;
|
||||
}
|
||||
|
||||
static kc_status kc_file_init(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
uint8_t mode)
|
||||
{
|
||||
kc_file_state state;
|
||||
|
||||
if (key_len != KC_FILE_KEY_SIZE) {
|
||||
return KC_ERR_BAD_KEY_LENGTH;
|
||||
}
|
||||
if (ctx == NULL || ctx_len < KC_FILE_CTX_SIZE || key == NULL ||
|
||||
nonce == NULL || nonce_len != KC_FILE_NONCE_SIZE) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
crypto_aead_init_ietf(&state.aead, key, nonce);
|
||||
state.index = 0u;
|
||||
state.finished = 0u;
|
||||
state.mode = mode;
|
||||
memset(ctx, 0, ctx_len);
|
||||
kc_file_store(&state, ctx);
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static void kc_file_ad(uint8_t out[KC_FILE_AD_SIZE], uint32_t index, int last)
|
||||
{
|
||||
memcpy(out, kc_file_label, sizeof(kc_file_label) - 1u);
|
||||
kc_store_u32(out + sizeof(kc_file_label) - 1u, index);
|
||||
out[KC_FILE_AD_SIZE - 1u] = last ? 1u : 0u;
|
||||
}
|
||||
|
||||
size_t kc_file_seal_bound(size_t plaintext_len)
|
||||
{
|
||||
size_t chunks = plaintext_len / KC_FILE_CHUNK_SIZE + 1u;
|
||||
size_t overhead;
|
||||
|
||||
if (chunks > SIZE_MAX / KC_TAG_SIZE) {
|
||||
return 0u;
|
||||
}
|
||||
overhead = chunks * KC_TAG_SIZE;
|
||||
if (plaintext_len > SIZE_MAX - overhead) {
|
||||
return 0u;
|
||||
}
|
||||
return plaintext_len + overhead;
|
||||
}
|
||||
|
||||
size_t kc_file_open_bound(size_t ciphertext_len)
|
||||
{
|
||||
return ciphertext_len;
|
||||
}
|
||||
|
||||
kc_status kc_file_seal_init(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len)
|
||||
{
|
||||
return kc_file_init(ctx, ctx_len, key, key_len, nonce, nonce_len,
|
||||
KC_FILE_MODE_SEAL);
|
||||
}
|
||||
|
||||
kc_status kc_file_open_init(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len)
|
||||
{
|
||||
return kc_file_init(ctx, ctx_len, key, key_len, nonce, nonce_len,
|
||||
KC_FILE_MODE_OPEN);
|
||||
}
|
||||
|
||||
kc_status kc_file_seal_chunk(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
int last,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
static const uint8_t empty = 0u;
|
||||
kc_file_state state;
|
||||
uint8_t ad[KC_FILE_AD_SIZE];
|
||||
kc_status status;
|
||||
|
||||
status = kc_file_load(&state, ctx, ctx_len, KC_FILE_MODE_SEAL);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if ((plaintext == NULL && plaintext_len != 0u) || out_len == NULL ||
|
||||
plaintext_len > KC_FILE_CHUNK_SIZE ||
|
||||
(!last && plaintext_len != KC_FILE_CHUNK_SIZE)) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out == NULL || out_cap < plaintext_len + KC_TAG_SIZE) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
kc_file_ad(ad, state.index, last);
|
||||
crypto_aead_write(&state.aead, out, out + plaintext_len, ad, sizeof(ad),
|
||||
plaintext_len == 0u ? &empty : plaintext, plaintext_len);
|
||||
state.index++;
|
||||
state.finished = last ? 1u : 0u;
|
||||
kc_file_store(&state, ctx);
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
*out_len = plaintext_len + KC_TAG_SIZE;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_file_open_chunk(uint8_t *ctx,
|
||||
size_t ctx_len,
|
||||
const uint8_t *chunk,
|
||||
size_t chunk_len,
|
||||
int last,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t empty = 0u;
|
||||
kc_file_state state;
|
||||
uint8_t ad[KC_FILE_AD_SIZE];
|
||||
size_t plaintext_len;
|
||||
kc_status status;
|
||||
|
||||
status = kc_file_load(&state, ctx, ctx_len, KC_FILE_MODE_OPEN);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (out_len == NULL) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (chunk == NULL || chunk_len < KC_TAG_SIZE ||
|
||||
chunk_len > KC_FILE_CHUNK_SIZE + KC_TAG_SIZE ||
|
||||
(!last && chunk_len != KC_FILE_CHUNK_SIZE + KC_TAG_SIZE)) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
plaintext_len = chunk_len - KC_TAG_SIZE;
|
||||
if (out_cap < plaintext_len || (out == NULL && plaintext_len != 0u)) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
kc_file_ad(ad, state.index, last);
|
||||
if (crypto_aead_read(&state.aead, plaintext_len == 0u ? &empty : out,
|
||||
chunk + plaintext_len, ad, sizeof(ad), chunk,
|
||||
plaintext_len) != 0) {
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
if (out != NULL && plaintext_len != 0u) {
|
||||
crypto_wipe(out, plaintext_len);
|
||||
}
|
||||
return KC_ERR_WRONG_KEY;
|
||||
}
|
||||
state.index++;
|
||||
state.finished = last ? 1u : 0u;
|
||||
kc_file_store(&state, ctx);
|
||||
crypto_wipe(&state, sizeof(state));
|
||||
*out_len = plaintext_len;
|
||||
return KC_OK;
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "kc_hkdf.h"
|
||||
#include "kc_identity.h"
|
||||
#include "kc_ratchet.h"
|
||||
#include "kc_session_state.h"
|
||||
#include "monocypher.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define KC_HS_UNSIGNED 74u
|
||||
#define KC_HS_OFFSET_ID 2u
|
||||
#define KC_HS_OFFSET_IK 10u
|
||||
#define KC_HS_OFFSET_EK 42u
|
||||
#define KC_HS_OFFSET_SIG 74u
|
||||
#define KC_HS_SIGN_MAX (16u + 24u + KC_HS_UNSIGNED + 32u)
|
||||
#define KC_HS_PENDING_OFFSET_EK 33u
|
||||
#define KC_HS_PENDING_OFFSET_OFFER 65u
|
||||
|
||||
static const uint8_t kc_hs_offer_label[] = "komet-v2/offer";
|
||||
static const uint8_t kc_hs_answer_label[] = "komet-v2/answer";
|
||||
static const uint8_t kc_hs_sk_label[] = "komet-v2/sk";
|
||||
|
||||
static size_t kc_hs_sign_message(uint8_t *out,
|
||||
const uint8_t *label,
|
||||
size_t label_len,
|
||||
int64_t chat_id,
|
||||
int64_t from_id,
|
||||
int64_t to_id,
|
||||
const uint8_t *handshake,
|
||||
const uint8_t *offer_hash)
|
||||
{
|
||||
size_t offset = 0u;
|
||||
|
||||
memcpy(out, label, label_len);
|
||||
offset += label_len;
|
||||
kc_store_i64(out + offset, chat_id);
|
||||
offset += 8u;
|
||||
kc_store_i64(out + offset, from_id);
|
||||
offset += 8u;
|
||||
kc_store_i64(out + offset, to_id);
|
||||
offset += 8u;
|
||||
memcpy(out + offset, handshake, KC_HS_UNSIGNED);
|
||||
offset += KC_HS_UNSIGNED;
|
||||
if (offer_hash != NULL) {
|
||||
memcpy(out + offset, offer_hash, 32u);
|
||||
offset += 32u;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
static void kc_hs_offer_hash(const uint8_t *offer, uint8_t out[32])
|
||||
{
|
||||
crypto_blake2b(out, 32u, offer, KC_HANDSHAKE_SIZE);
|
||||
}
|
||||
|
||||
static void kc_hs_secret(const uint8_t dh1[32],
|
||||
const uint8_t dh2[32],
|
||||
const uint8_t dh3[32],
|
||||
const uint8_t *offer,
|
||||
const uint8_t *answer,
|
||||
uint8_t out[96])
|
||||
{
|
||||
uint8_t ikm[128];
|
||||
uint8_t info[sizeof(kc_hs_sk_label) - 1u + 32u];
|
||||
crypto_blake2b_ctx ctx;
|
||||
|
||||
memset(ikm, 0xFF, 32u);
|
||||
memcpy(ikm + 32, dh1, 32u);
|
||||
memcpy(ikm + 64, dh2, 32u);
|
||||
memcpy(ikm + 96, dh3, 32u);
|
||||
memcpy(info, kc_hs_sk_label, sizeof(kc_hs_sk_label) - 1u);
|
||||
crypto_blake2b_init(&ctx, 32u);
|
||||
crypto_blake2b_update(&ctx, offer, KC_HANDSHAKE_SIZE);
|
||||
crypto_blake2b_update(&ctx, answer, KC_HANDSHAKE_SIZE);
|
||||
crypto_blake2b_final(&ctx, info + sizeof(kc_hs_sk_label) - 1u);
|
||||
kc_hkdf(NULL, 0u, ikm, sizeof(ikm), info, sizeof(info), out, 96u);
|
||||
crypto_wipe(ikm, sizeof(ikm));
|
||||
crypto_wipe(&ctx, sizeof(ctx));
|
||||
}
|
||||
|
||||
static kc_status kc_hs_encode(const uint8_t *handshake,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len)
|
||||
{
|
||||
if (text_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_text == NULL || text_cap < KC_HANDSHAKE_TEXT_BOUND) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
return kc_alphabet_encode(handshake, KC_HANDSHAKE_SIZE, out_text, text_cap,
|
||||
text_len);
|
||||
}
|
||||
|
||||
kc_status kc_handshake_peek(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t *out_type,
|
||||
uint8_t *out_offer_id,
|
||||
size_t offer_id_cap,
|
||||
uint8_t *out_public,
|
||||
size_t public_cap)
|
||||
{
|
||||
uint8_t handshake[KC_HANDSHAKE_SIZE];
|
||||
kc_status status;
|
||||
|
||||
if (out_type == NULL || out_offer_id == NULL || out_public == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (offer_id_cap < KC_OFFER_ID_SIZE || public_cap < KC_PUBLIC_KEY_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
status = kc_handshake_decode(text, text_len, handshake);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
*out_type = handshake[1];
|
||||
memcpy(out_offer_id, handshake + KC_HS_OFFSET_ID, KC_OFFER_ID_SIZE);
|
||||
memcpy(out_public, handshake + KC_HS_OFFSET_IK, KC_PUBLIC_KEY_SIZE);
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_session_offer(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
int64_t chat_id,
|
||||
int64_t my_id,
|
||||
int64_t peer_id,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_pending,
|
||||
size_t pending_cap,
|
||||
size_t *pending_len)
|
||||
{
|
||||
uint8_t offer[KC_HANDSHAKE_SIZE];
|
||||
uint8_t seed[32];
|
||||
uint8_t ek_secret[32];
|
||||
uint8_t message[KC_HS_SIGN_MAX];
|
||||
size_t message_len;
|
||||
kc_status status;
|
||||
|
||||
status = kc_identity_check(identity, identity_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (random == NULL || random_len != KC_OFFER_RANDOM_SIZE ||
|
||||
text_len == NULL || pending_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_text == NULL || text_cap < KC_HANDSHAKE_TEXT_BOUND ||
|
||||
out_pending == NULL || pending_cap < KC_PENDING_STATE_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memcpy(seed, random, sizeof(seed));
|
||||
offer[0] = KC_V2_VERSION;
|
||||
offer[1] = KC_V2_OFFER;
|
||||
memcpy(offer + KC_HS_OFFSET_ID, random + 32, KC_OFFER_ID_SIZE);
|
||||
memcpy(offer + KC_HS_OFFSET_IK, identity + KC_SEED_SIZE,
|
||||
KC_PUBLIC_KEY_SIZE);
|
||||
crypto_elligator_key_pair(offer + KC_HS_OFFSET_EK, ek_secret, seed);
|
||||
message_len = kc_hs_sign_message(message, kc_hs_offer_label,
|
||||
sizeof(kc_hs_offer_label) - 1u, chat_id,
|
||||
my_id, peer_id, offer, NULL);
|
||||
kc_identity_sign(identity, message, message_len, offer + KC_HS_OFFSET_SIG);
|
||||
|
||||
out_pending[0] = KC_PENDING_FMT;
|
||||
kc_store_i64(out_pending + 1, chat_id);
|
||||
kc_store_i64(out_pending + 9, my_id);
|
||||
kc_store_i64(out_pending + 17, peer_id);
|
||||
memcpy(out_pending + 25, offer + KC_HS_OFFSET_ID, KC_OFFER_ID_SIZE);
|
||||
memcpy(out_pending + KC_HS_PENDING_OFFSET_EK, ek_secret, 32u);
|
||||
memcpy(out_pending + KC_HS_PENDING_OFFSET_OFFER, offer, KC_HANDSHAKE_SIZE);
|
||||
*pending_len = KC_PENDING_STATE_SIZE;
|
||||
status = kc_hs_encode(offer, out_text, text_cap, text_len);
|
||||
crypto_wipe(ek_secret, sizeof(ek_secret));
|
||||
if (status != KC_OK) {
|
||||
crypto_wipe(out_pending, KC_PENDING_STATE_SIZE);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static kc_status kc_hs_verify(const uint8_t *handshake,
|
||||
const uint8_t *label,
|
||||
size_t label_len,
|
||||
int64_t chat_id,
|
||||
int64_t from_id,
|
||||
int64_t to_id,
|
||||
const uint8_t *offer_hash)
|
||||
{
|
||||
uint8_t message[KC_HS_SIGN_MAX];
|
||||
size_t message_len = kc_hs_sign_message(message, label, label_len, chat_id,
|
||||
from_id, to_id, handshake,
|
||||
offer_hash);
|
||||
|
||||
return kc_identity_verify(handshake + KC_HS_OFFSET_IK, message, message_len,
|
||||
handshake + KC_HS_OFFSET_SIG)
|
||||
? KC_OK
|
||||
: KC_ERR_BAD_SIGNATURE;
|
||||
}
|
||||
|
||||
static void kc_hs_init_common(kc_session *session,
|
||||
int64_t chat_id,
|
||||
int64_t my_id,
|
||||
int64_t peer_id,
|
||||
const uint8_t *offer_id,
|
||||
const uint8_t *peer_ik,
|
||||
const uint8_t *secret)
|
||||
{
|
||||
memset(session, 0, sizeof(*session));
|
||||
session->chat_id = chat_id;
|
||||
session->my_id = my_id;
|
||||
session->peer_id = peer_id;
|
||||
memcpy(session->offer_id, offer_id, KC_OFFER_ID_SIZE);
|
||||
memcpy(session->peer_ik, peer_ik, KC_PUBLIC_KEY_SIZE);
|
||||
memcpy(session->rk, secret, 32u);
|
||||
memcpy(session->ok, secret + 64, 32u);
|
||||
}
|
||||
|
||||
kc_status kc_session_answer(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
int64_t chat_id,
|
||||
int64_t my_id,
|
||||
int64_t peer_id,
|
||||
const uint8_t *offer_text,
|
||||
size_t offer_len,
|
||||
const uint8_t *old_state,
|
||||
size_t old_state_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_text,
|
||||
size_t text_cap,
|
||||
size_t *text_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_len)
|
||||
{
|
||||
uint8_t offer[KC_HANDSHAKE_SIZE];
|
||||
uint8_t answer[KC_HANDSHAKE_SIZE];
|
||||
uint8_t offer_hash[32];
|
||||
uint8_t seed[32];
|
||||
uint8_t ek_secret[32];
|
||||
uint8_t ik_secret[32];
|
||||
uint8_t peer_ik_x[32];
|
||||
uint8_t dh1[32];
|
||||
uint8_t dh2[32];
|
||||
uint8_t dh3[32];
|
||||
uint8_t secret[96];
|
||||
uint8_t message[KC_HS_SIGN_MAX];
|
||||
size_t message_len;
|
||||
kc_session *session;
|
||||
kc_status status;
|
||||
|
||||
status = kc_identity_check(identity, identity_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (random == NULL || random_len != KC_ANSWER_RANDOM_SIZE ||
|
||||
text_len == NULL || state_len == NULL ||
|
||||
(old_state == NULL && old_state_len != 0u)) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_text == NULL || text_cap < KC_HANDSHAKE_TEXT_BOUND ||
|
||||
out_state == NULL || state_cap < KC_SESSION_FIXED_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
status = kc_handshake_decode(offer_text, offer_len, offer);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (offer[1] != KC_V2_OFFER) {
|
||||
return KC_ERR_HANDSHAKE_MESSAGE;
|
||||
}
|
||||
status = kc_hs_verify(offer, kc_hs_offer_label,
|
||||
sizeof(kc_hs_offer_label) - 1u, chat_id, peer_id,
|
||||
my_id, NULL);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
session = (kc_session *)malloc(sizeof(*session));
|
||||
if (session == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (old_state_len != 0u) {
|
||||
status = kc_session_parse(old_state, old_state_len, session);
|
||||
if (status == KC_OK &&
|
||||
memcmp(session->offer_id, offer + KC_HS_OFFSET_ID,
|
||||
KC_OFFER_ID_SIZE) == 0) {
|
||||
status = KC_ERR_REPLAY;
|
||||
}
|
||||
kc_session_wipe(session);
|
||||
if (status != KC_OK) {
|
||||
free(session);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(seed, random, sizeof(seed));
|
||||
answer[0] = KC_V2_VERSION;
|
||||
answer[1] = KC_V2_ANSWER;
|
||||
memcpy(answer + KC_HS_OFFSET_ID, offer + KC_HS_OFFSET_ID, KC_OFFER_ID_SIZE);
|
||||
memcpy(answer + KC_HS_OFFSET_IK, identity + KC_SEED_SIZE,
|
||||
KC_PUBLIC_KEY_SIZE);
|
||||
crypto_elligator_key_pair(answer + KC_HS_OFFSET_EK, ek_secret, seed);
|
||||
kc_hs_offer_hash(offer, offer_hash);
|
||||
message_len = kc_hs_sign_message(message, kc_hs_answer_label,
|
||||
sizeof(kc_hs_answer_label) - 1u, chat_id,
|
||||
my_id, peer_id, answer, offer_hash);
|
||||
kc_identity_sign(identity, message, message_len, answer + KC_HS_OFFSET_SIG);
|
||||
|
||||
kc_identity_x25519_secret(identity, ik_secret);
|
||||
kc_identity_x25519_public(offer + KC_HS_OFFSET_IK, peer_ik_x);
|
||||
status = kc_ratchet_dh_public(dh1, ek_secret, peer_ik_x);
|
||||
if (status == KC_OK) {
|
||||
status = kc_ratchet_dh_repr(dh2, ik_secret, offer + KC_HS_OFFSET_EK);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_ratchet_dh_repr(dh3, ek_secret, offer + KC_HS_OFFSET_EK);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
kc_hs_secret(dh1, dh2, dh3, offer, answer, secret);
|
||||
kc_hs_init_common(session, chat_id, my_id, peer_id,
|
||||
offer + KC_HS_OFFSET_ID, offer + KC_HS_OFFSET_IK,
|
||||
secret);
|
||||
memcpy(session->dhs_priv, ek_secret, 32u);
|
||||
memcpy(session->dhs_repr, answer + KC_HS_OFFSET_EK, 32u);
|
||||
memcpy(session->cks, secret + 32, 32u);
|
||||
status = kc_session_serialize(session, out_state, state_cap, state_len);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_hs_encode(answer, out_text, text_cap, text_len);
|
||||
}
|
||||
crypto_wipe(ek_secret, sizeof(ek_secret));
|
||||
crypto_wipe(ik_secret, sizeof(ik_secret));
|
||||
crypto_wipe(dh1, sizeof(dh1));
|
||||
crypto_wipe(dh2, sizeof(dh2));
|
||||
crypto_wipe(dh3, sizeof(dh3));
|
||||
crypto_wipe(secret, sizeof(secret));
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
if (status != KC_OK) {
|
||||
crypto_wipe(out_state, state_cap);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
kc_status kc_session_accept(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
const uint8_t *pending,
|
||||
size_t pending_len,
|
||||
const uint8_t *answer_text,
|
||||
size_t answer_len,
|
||||
const uint8_t *expected_peer,
|
||||
size_t expected_peer_len,
|
||||
const uint8_t *random,
|
||||
size_t random_len,
|
||||
uint8_t *out_state,
|
||||
size_t state_cap,
|
||||
size_t *state_len)
|
||||
{
|
||||
uint8_t answer[KC_HANDSHAKE_SIZE];
|
||||
const uint8_t *offer;
|
||||
uint8_t offer_hash[32];
|
||||
uint8_t ek_secret[32];
|
||||
uint8_t ik_secret[32];
|
||||
uint8_t peer_ik_x[32];
|
||||
uint8_t dh1[32];
|
||||
uint8_t dh2[32];
|
||||
uint8_t dh3[32];
|
||||
uint8_t secret[96];
|
||||
int64_t chat_id;
|
||||
int64_t my_id;
|
||||
int64_t peer_id;
|
||||
kc_session *session;
|
||||
kc_status status;
|
||||
|
||||
status = kc_identity_check(identity, identity_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (random == NULL || random_len != KC_ACCEPT_RANDOM_SIZE ||
|
||||
state_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (expected_peer_len != 0u &&
|
||||
(expected_peer == NULL || expected_peer_len != KC_PUBLIC_KEY_SIZE)) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (pending == NULL || pending_len != KC_PENDING_STATE_SIZE ||
|
||||
pending[0] != KC_PENDING_FMT) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
if (out_state == NULL || state_cap < KC_SESSION_FIXED_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
status = kc_handshake_decode(answer_text, answer_len, answer);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (answer[1] != KC_V2_ANSWER) {
|
||||
return KC_ERR_HANDSHAKE_MESSAGE;
|
||||
}
|
||||
if (expected_peer_len != 0u &&
|
||||
crypto_verify32(answer + KC_HS_OFFSET_IK, expected_peer) != 0) {
|
||||
return KC_ERR_BAD_PEER;
|
||||
}
|
||||
chat_id = kc_load_i64(pending + 1);
|
||||
my_id = kc_load_i64(pending + 9);
|
||||
peer_id = kc_load_i64(pending + 17);
|
||||
offer = pending + KC_HS_PENDING_OFFSET_OFFER;
|
||||
if (memcmp(offer + KC_HS_OFFSET_IK, identity + KC_SEED_SIZE,
|
||||
KC_PUBLIC_KEY_SIZE) != 0) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
if (memcmp(answer + KC_HS_OFFSET_ID, offer + KC_HS_OFFSET_ID,
|
||||
KC_OFFER_ID_SIZE) != 0) {
|
||||
return KC_ERR_REPLAY;
|
||||
}
|
||||
kc_hs_offer_hash(offer, offer_hash);
|
||||
status = kc_hs_verify(answer, kc_hs_answer_label,
|
||||
sizeof(kc_hs_answer_label) - 1u, chat_id, peer_id,
|
||||
my_id, offer_hash);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
session = (kc_session *)malloc(sizeof(*session));
|
||||
if (session == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
memcpy(ek_secret, pending + KC_HS_PENDING_OFFSET_EK, 32u);
|
||||
kc_identity_x25519_secret(identity, ik_secret);
|
||||
kc_identity_x25519_public(answer + KC_HS_OFFSET_IK, peer_ik_x);
|
||||
status = kc_ratchet_dh_repr(dh1, ik_secret, answer + KC_HS_OFFSET_EK);
|
||||
if (status == KC_OK) {
|
||||
status = kc_ratchet_dh_public(dh2, ek_secret, peer_ik_x);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_ratchet_dh_repr(dh3, ek_secret, answer + KC_HS_OFFSET_EK);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
kc_hs_secret(dh1, dh2, dh3, offer, answer, secret);
|
||||
kc_hs_init_common(session, chat_id, my_id, peer_id,
|
||||
offer + KC_HS_OFFSET_ID, answer + KC_HS_OFFSET_IK,
|
||||
secret);
|
||||
session->flags = KC_FLAG_INITIATOR | KC_FLAG_HAS_DHR | KC_FLAG_HAS_CKR;
|
||||
memcpy(session->dhr_repr, answer + KC_HS_OFFSET_EK, 32u);
|
||||
memcpy(session->ckr, secret + 32, 32u);
|
||||
status = kc_ratchet_send_step(session, random);
|
||||
}
|
||||
if (status == KC_OK) {
|
||||
status = kc_session_serialize(session, out_state, state_cap, state_len);
|
||||
}
|
||||
crypto_wipe(ek_secret, sizeof(ek_secret));
|
||||
crypto_wipe(ik_secret, sizeof(ik_secret));
|
||||
crypto_wipe(dh1, sizeof(dh1));
|
||||
crypto_wipe(dh2, sizeof(dh2));
|
||||
crypto_wipe(dh3, sizeof(dh3));
|
||||
crypto_wipe(secret, sizeof(secret));
|
||||
kc_session_wipe(session);
|
||||
free(session);
|
||||
if (status != KC_OK) {
|
||||
crypto_wipe(out_state, state_cap);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "kc_hkdf.h"
|
||||
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
static const uint8_t kc_hkdf_zero_salt[32] = {0u};
|
||||
static const uint8_t kc_hkdf_root_info[] = "komet-v2/rk";
|
||||
static const uint8_t kc_hkdf_message_info[] = "komet-v2/mk";
|
||||
|
||||
void kc_hkdf(const uint8_t *salt,
|
||||
size_t salt_len,
|
||||
const uint8_t *ikm,
|
||||
size_t ikm_len,
|
||||
const uint8_t *info,
|
||||
size_t info_len,
|
||||
uint8_t *out,
|
||||
size_t out_len)
|
||||
{
|
||||
uint8_t prk[32];
|
||||
uint8_t block[32];
|
||||
size_t block_len = 0u;
|
||||
size_t offset = 0u;
|
||||
uint8_t counter = 0u;
|
||||
crypto_blake2b_ctx ctx;
|
||||
|
||||
if (salt_len == 0u) {
|
||||
salt = kc_hkdf_zero_salt;
|
||||
salt_len = sizeof(kc_hkdf_zero_salt);
|
||||
}
|
||||
crypto_blake2b_keyed(prk, sizeof(prk), salt, salt_len, ikm, ikm_len);
|
||||
while (offset < out_len) {
|
||||
size_t take;
|
||||
counter++;
|
||||
crypto_blake2b_keyed_init(&ctx, sizeof(block), prk, sizeof(prk));
|
||||
crypto_blake2b_update(&ctx, block, block_len);
|
||||
crypto_blake2b_update(&ctx, info, info_len);
|
||||
crypto_blake2b_update(&ctx, &counter, 1u);
|
||||
crypto_blake2b_final(&ctx, block);
|
||||
block_len = sizeof(block);
|
||||
take = out_len - offset < block_len ? out_len - offset : block_len;
|
||||
memcpy(out + offset, block, take);
|
||||
offset += take;
|
||||
}
|
||||
crypto_wipe(prk, sizeof(prk));
|
||||
crypto_wipe(block, sizeof(block));
|
||||
crypto_wipe(&ctx, sizeof(ctx));
|
||||
}
|
||||
|
||||
void kc_kdf_root(const uint8_t root[32],
|
||||
const uint8_t dh[32],
|
||||
uint8_t out_root[32],
|
||||
uint8_t out_chain[32])
|
||||
{
|
||||
uint8_t derived[64];
|
||||
|
||||
kc_hkdf(root, 32u, dh, 32u, kc_hkdf_root_info,
|
||||
sizeof(kc_hkdf_root_info) - 1u, derived, sizeof(derived));
|
||||
memcpy(out_root, derived, 32u);
|
||||
memcpy(out_chain, derived + 32, 32u);
|
||||
crypto_wipe(derived, sizeof(derived));
|
||||
}
|
||||
|
||||
void kc_kdf_chain(const uint8_t chain[32],
|
||||
uint8_t out_chain[32],
|
||||
uint8_t out_message[32])
|
||||
{
|
||||
static const uint8_t message_tag = 0x01u;
|
||||
static const uint8_t chain_tag = 0x02u;
|
||||
uint8_t next[32];
|
||||
|
||||
crypto_blake2b_keyed(out_message, 32u, chain, 32u, &message_tag, 1u);
|
||||
crypto_blake2b_keyed(next, 32u, chain, 32u, &chain_tag, 1u);
|
||||
memcpy(out_chain, next, 32u);
|
||||
crypto_wipe(next, sizeof(next));
|
||||
}
|
||||
|
||||
void kc_kdf_message(const uint8_t message_key[32],
|
||||
uint8_t out_key[32],
|
||||
uint8_t out_nonce[12])
|
||||
{
|
||||
uint8_t derived[44];
|
||||
|
||||
kc_hkdf(NULL, 0u, message_key, 32u, kc_hkdf_message_info,
|
||||
sizeof(kc_hkdf_message_info) - 1u, derived, sizeof(derived));
|
||||
memcpy(out_key, derived, 32u);
|
||||
memcpy(out_nonce, derived + 32, 12u);
|
||||
crypto_wipe(derived, sizeof(derived));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef KC_HKDF_H
|
||||
#define KC_HKDF_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void kc_hkdf(const uint8_t *salt,
|
||||
size_t salt_len,
|
||||
const uint8_t *ikm,
|
||||
size_t ikm_len,
|
||||
const uint8_t *info,
|
||||
size_t info_len,
|
||||
uint8_t *out,
|
||||
size_t out_len);
|
||||
|
||||
void kc_kdf_root(const uint8_t root[32],
|
||||
const uint8_t dh[32],
|
||||
uint8_t out_root[32],
|
||||
uint8_t out_chain[32]);
|
||||
|
||||
void kc_kdf_chain(const uint8_t chain[32],
|
||||
uint8_t out_chain[32],
|
||||
uint8_t out_message[32]);
|
||||
|
||||
void kc_kdf_message(const uint8_t message_key[32],
|
||||
uint8_t out_key[32],
|
||||
uint8_t out_nonce[12]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,161 @@
|
||||
#include "kc_identity.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
#define KC_FINGERPRINT_ITERATIONS 5200u
|
||||
#define KC_FINGERPRINT_HALF 30u
|
||||
|
||||
kc_status kc_identity_check(const uint8_t *identity, size_t identity_len)
|
||||
{
|
||||
if (identity == NULL || identity_len != KC_IDENTITY_SIZE) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
void kc_identity_x25519_secret(const uint8_t *identity, uint8_t out[32])
|
||||
{
|
||||
uint8_t digest[64];
|
||||
|
||||
crypto_blake2b(digest, sizeof(digest), identity, KC_SEED_SIZE);
|
||||
crypto_eddsa_trim_scalar(out, digest);
|
||||
crypto_wipe(digest, sizeof(digest));
|
||||
}
|
||||
|
||||
void kc_identity_x25519_public(const uint8_t eddsa_public[32], uint8_t out[32])
|
||||
{
|
||||
crypto_eddsa_to_x25519(out, eddsa_public);
|
||||
}
|
||||
|
||||
void kc_identity_sign(const uint8_t *identity,
|
||||
const uint8_t *message,
|
||||
size_t message_len,
|
||||
uint8_t out_signature[64])
|
||||
{
|
||||
crypto_eddsa_sign(out_signature, identity, message, message_len);
|
||||
}
|
||||
|
||||
int kc_identity_verify(const uint8_t eddsa_public[32],
|
||||
const uint8_t *message,
|
||||
size_t message_len,
|
||||
const uint8_t signature[64])
|
||||
{
|
||||
return crypto_eddsa_check(signature, eddsa_public, message, message_len) ==
|
||||
0;
|
||||
}
|
||||
|
||||
kc_status kc_identity_create(const uint8_t *seed,
|
||||
size_t seed_len,
|
||||
uint8_t *out_identity,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t seed_copy[KC_SEED_SIZE];
|
||||
uint8_t public_key[KC_PUBLIC_KEY_SIZE];
|
||||
|
||||
if (seed == NULL || seed_len != KC_SEED_SIZE || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_identity == NULL || out_cap < KC_IDENTITY_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memcpy(seed_copy, seed, sizeof(seed_copy));
|
||||
crypto_eddsa_key_pair(out_identity, public_key, seed_copy);
|
||||
crypto_wipe(public_key, sizeof(public_key));
|
||||
*out_len = KC_IDENTITY_SIZE;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_identity_public_key(const uint8_t *identity,
|
||||
size_t identity_len,
|
||||
uint8_t *out_public,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
kc_status status = kc_identity_check(identity, identity_len);
|
||||
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_public == NULL || out_cap < KC_PUBLIC_KEY_SIZE) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memcpy(out_public, identity + KC_SEED_SIZE, KC_PUBLIC_KEY_SIZE);
|
||||
*out_len = KC_PUBLIC_KEY_SIZE;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static void kc_fingerprint_half(int64_t user_id,
|
||||
const uint8_t *public_key,
|
||||
uint8_t *out)
|
||||
{
|
||||
static const uint8_t version = 0u;
|
||||
uint8_t id_bytes[8];
|
||||
uint8_t hash[32];
|
||||
crypto_blake2b_ctx ctx;
|
||||
unsigned int iteration;
|
||||
size_t group;
|
||||
|
||||
kc_store_i64(id_bytes, user_id);
|
||||
crypto_blake2b_init(&ctx, sizeof(hash));
|
||||
crypto_blake2b_update(&ctx, &version, 1u);
|
||||
crypto_blake2b_update(&ctx, id_bytes, sizeof(id_bytes));
|
||||
crypto_blake2b_update(&ctx, public_key, KC_PUBLIC_KEY_SIZE);
|
||||
crypto_blake2b_final(&ctx, hash);
|
||||
for (iteration = 1u; iteration < KC_FINGERPRINT_ITERATIONS; iteration++) {
|
||||
crypto_blake2b_init(&ctx, sizeof(hash));
|
||||
crypto_blake2b_update(&ctx, hash, sizeof(hash));
|
||||
crypto_blake2b_update(&ctx, public_key, KC_PUBLIC_KEY_SIZE);
|
||||
crypto_blake2b_final(&ctx, hash);
|
||||
}
|
||||
for (group = 0u; group < 6u; group++) {
|
||||
uint64_t value = 0u;
|
||||
size_t index;
|
||||
size_t digit;
|
||||
for (index = 0u; index < 5u; index++) {
|
||||
value = (value << 8u) | hash[group * 5u + index];
|
||||
}
|
||||
value %= 100000u;
|
||||
for (digit = 5u; digit > 0u; digit--) {
|
||||
out[group * 5u + digit - 1u] = (uint8_t)('0' + value % 10u);
|
||||
value /= 10u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kc_status kc_fingerprint(int64_t my_id,
|
||||
const uint8_t *my_public,
|
||||
size_t my_public_len,
|
||||
int64_t peer_id,
|
||||
const uint8_t *peer_public,
|
||||
size_t peer_public_len,
|
||||
uint8_t *out_digits,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
int mine_first;
|
||||
|
||||
if (my_public == NULL || my_public_len != KC_PUBLIC_KEY_SIZE ||
|
||||
peer_public == NULL || peer_public_len != KC_PUBLIC_KEY_SIZE ||
|
||||
out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_digits == NULL || out_cap < KC_FINGERPRINT_DIGITS) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
mine_first = my_id < peer_id ||
|
||||
(my_id == peer_id &&
|
||||
memcmp(my_public, peer_public, KC_PUBLIC_KEY_SIZE) <= 0);
|
||||
kc_fingerprint_half(mine_first ? my_id : peer_id,
|
||||
mine_first ? my_public : peer_public, out_digits);
|
||||
kc_fingerprint_half(mine_first ? peer_id : my_id,
|
||||
mine_first ? peer_public : my_public,
|
||||
out_digits + KC_FINGERPRINT_HALF);
|
||||
*out_len = KC_FINGERPRINT_DIGITS;
|
||||
return KC_OK;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef KC_IDENTITY_H
|
||||
#define KC_IDENTITY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "komet_crypto.h"
|
||||
|
||||
kc_status kc_identity_check(const uint8_t *identity, size_t identity_len);
|
||||
void kc_identity_x25519_secret(const uint8_t *identity, uint8_t out[32]);
|
||||
void kc_identity_x25519_public(const uint8_t eddsa_public[32],
|
||||
uint8_t out[32]);
|
||||
void kc_identity_sign(const uint8_t *identity,
|
||||
const uint8_t *message,
|
||||
size_t message_len,
|
||||
uint8_t out_signature[64]);
|
||||
int kc_identity_verify(const uint8_t eddsa_public[32],
|
||||
const uint8_t *message,
|
||||
size_t message_len,
|
||||
const uint8_t signature[64]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef KC_INTERNAL_H
|
||||
#define KC_INTERNAL_H
|
||||
|
||||
#include "komet_crypto.h"
|
||||
|
||||
#define KC_MAGIC 0x4Bu
|
||||
#define KC_MESSAGE_VERSION 0x01u
|
||||
#define KC_IMAGE_VERSION 0x02u
|
||||
#define KC_MESSAGE_HEADER_SIZE 2u
|
||||
#define KC_IMAGE_HEADER_SIZE 6u
|
||||
|
||||
#define KC_V2_VERSION 0x02u
|
||||
#define KC_V2_OFFER 0x01u
|
||||
#define KC_V2_ANSWER 0x02u
|
||||
#define KC_V2_HEADER_SIZE 40u
|
||||
#define KC_V2_ENVELOPE_MIN (KC_V2_HEADER_SIZE + 32u + KC_TAG_SIZE)
|
||||
#define KC_V2_PAD_BLOCK 32u
|
||||
#define KC_V2_MAX_SKIP KC_SESSION_MAX_SKIP
|
||||
#define KC_V2_MAX_ADVANCE KC_SESSION_MAX_ADVANCE
|
||||
#define KC_V2_MAX_SKIP_ENTRIES 256u
|
||||
|
||||
size_t kc_alphabet_encode_bound(size_t input_len);
|
||||
kc_status kc_alphabet_encode(const uint8_t *input,
|
||||
size_t input_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
kc_status kc_alphabet_decode(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t **out,
|
||||
size_t *out_len);
|
||||
int kc_utf8_valid(const uint8_t *input, size_t input_len);
|
||||
int kc_size_add(size_t left, size_t right, size_t *out);
|
||||
|
||||
kc_status kc_seal(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
const uint8_t *header,
|
||||
size_t header_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
kc_status kc_open(const uint8_t *sealed,
|
||||
size_t sealed_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
const uint8_t *header,
|
||||
size_t header_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
|
||||
kc_status kc_handshake_decode(const uint8_t *text,
|
||||
size_t text_len,
|
||||
uint8_t out[KC_HANDSHAKE_SIZE]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
void kc_wipe(void *buf, size_t len)
|
||||
{
|
||||
if (buf != NULL && len != 0u) {
|
||||
crypto_wipe(buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
size_t kc_local_seal_bound(size_t plaintext_len)
|
||||
{
|
||||
size_t result;
|
||||
|
||||
if (!kc_size_add(plaintext_len, KC_LOCAL_NONCE_SIZE + KC_TAG_SIZE,
|
||||
&result)) {
|
||||
return 0u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t kc_local_open_bound(size_t blob_len)
|
||||
{
|
||||
if (blob_len < KC_LOCAL_NONCE_SIZE + KC_TAG_SIZE) {
|
||||
return 0u;
|
||||
}
|
||||
return blob_len - KC_LOCAL_NONCE_SIZE - KC_TAG_SIZE;
|
||||
}
|
||||
|
||||
kc_status kc_local_seal(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
const uint8_t *aad,
|
||||
size_t aad_len,
|
||||
const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
static const uint8_t empty = 0u;
|
||||
size_t required = kc_local_seal_bound(plaintext_len);
|
||||
|
||||
if (key_len != KC_LOCAL_KEY_SIZE) {
|
||||
return KC_ERR_BAD_KEY_LENGTH;
|
||||
}
|
||||
if (key == NULL || nonce == NULL || nonce_len != KC_LOCAL_NONCE_SIZE ||
|
||||
(aad == NULL && aad_len != 0u) ||
|
||||
(plaintext == NULL && plaintext_len != 0u) || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (required == 0u || out == NULL || out_cap < required) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
memcpy(out, nonce, KC_LOCAL_NONCE_SIZE);
|
||||
crypto_aead_lock(out + KC_LOCAL_NONCE_SIZE,
|
||||
out + KC_LOCAL_NONCE_SIZE + plaintext_len, key, nonce,
|
||||
aad, aad_len, plaintext_len == 0u ? &empty : plaintext,
|
||||
plaintext_len);
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_local_open(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *aad,
|
||||
size_t aad_len,
|
||||
const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t empty = 0u;
|
||||
size_t plaintext_len;
|
||||
uint8_t *target;
|
||||
|
||||
if (key_len != KC_LOCAL_KEY_SIZE) {
|
||||
return KC_ERR_BAD_KEY_LENGTH;
|
||||
}
|
||||
if (key == NULL || (aad == NULL && aad_len != 0u) || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (blob == NULL || blob_len < KC_LOCAL_NONCE_SIZE + KC_TAG_SIZE) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
plaintext_len = kc_local_open_bound(blob_len);
|
||||
if (out_cap < plaintext_len || (out == NULL && plaintext_len != 0u)) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
target = plaintext_len == 0u ? &empty : out;
|
||||
if (crypto_aead_unlock(target, blob + KC_LOCAL_NONCE_SIZE + plaintext_len,
|
||||
key, blob, aad, aad_len,
|
||||
blob + KC_LOCAL_NONCE_SIZE, plaintext_len) != 0) {
|
||||
if (out != NULL && plaintext_len != 0u) {
|
||||
crypto_wipe(out, plaintext_len);
|
||||
}
|
||||
return KC_ERR_WRONG_KEY;
|
||||
}
|
||||
*out_len = plaintext_len;
|
||||
return KC_OK;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "kc_ratchet.h"
|
||||
|
||||
#include "kc_hkdf.h"
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
static const uint8_t kc_ratchet_zero[32] = {0u};
|
||||
|
||||
static kc_status kc_ratchet_check_dh(const uint8_t out[32])
|
||||
{
|
||||
return crypto_verify32(out, kc_ratchet_zero) == 0 ? KC_ERR_BAD_PEER
|
||||
: KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_ratchet_dh_repr(uint8_t out[32],
|
||||
const uint8_t secret[32],
|
||||
const uint8_t peer_repr[32])
|
||||
{
|
||||
uint8_t curve[32];
|
||||
kc_status status;
|
||||
|
||||
crypto_elligator_map(curve, peer_repr);
|
||||
crypto_x25519(out, secret, curve);
|
||||
status = kc_ratchet_check_dh(out);
|
||||
crypto_wipe(curve, sizeof(curve));
|
||||
return status;
|
||||
}
|
||||
|
||||
kc_status kc_ratchet_dh_public(uint8_t out[32],
|
||||
const uint8_t secret[32],
|
||||
const uint8_t peer_public[32])
|
||||
{
|
||||
crypto_x25519(out, secret, peer_public);
|
||||
return kc_ratchet_check_dh(out);
|
||||
}
|
||||
|
||||
kc_status kc_ratchet_send_step(kc_session *session, const uint8_t *seed)
|
||||
{
|
||||
uint8_t seed_copy[32];
|
||||
uint8_t secret[32];
|
||||
uint8_t hidden[32];
|
||||
uint8_t dh[32];
|
||||
kc_status status;
|
||||
|
||||
if ((session->flags & (KC_FLAG_FORCE_DH | KC_FLAG_INITIATOR)) ==
|
||||
(KC_FLAG_FORCE_DH | KC_FLAG_INITIATOR) &&
|
||||
(session->flags & KC_FLAG_NEEDS_DH) == 0u &&
|
||||
session->ns == 0u && session->pn == 0u) {
|
||||
return KC_ERR_AWAITING_PEER;
|
||||
}
|
||||
|
||||
if ((session->flags & KC_FLAG_HAS_DHR) == 0u) {
|
||||
if ((session->flags & KC_FLAG_FORCE_DH) != 0u) {
|
||||
return KC_ERR_AWAITING_PEER;
|
||||
}
|
||||
session->flags &= (uint8_t)(KC_FLAG_MASK ^ KC_FLAG_NEEDS_DH);
|
||||
return KC_OK;
|
||||
}
|
||||
memcpy(seed_copy, seed, sizeof(seed_copy));
|
||||
crypto_elligator_key_pair(hidden, secret, seed_copy);
|
||||
status = kc_ratchet_dh_repr(dh, secret, session->dhr_repr);
|
||||
if (status == KC_OK) {
|
||||
kc_kdf_root(session->rk, dh, session->rk, session->cks);
|
||||
memcpy(session->dhs_priv, secret, 32u);
|
||||
memcpy(session->dhs_repr, hidden, 32u);
|
||||
session->pn = session->ns;
|
||||
session->ns = 0u;
|
||||
session->flags &=
|
||||
(uint8_t)(KC_FLAG_MASK ^ (KC_FLAG_FORCE_DH | KC_FLAG_NEEDS_DH));
|
||||
}
|
||||
crypto_wipe(secret, sizeof(secret));
|
||||
crypto_wipe(dh, sizeof(dh));
|
||||
return status;
|
||||
}
|
||||
|
||||
kc_status kc_ratchet_next_send_key(kc_session *session,
|
||||
const uint8_t *seed,
|
||||
uint8_t out_mk[32],
|
||||
uint32_t *out_n,
|
||||
uint32_t *out_pn,
|
||||
uint8_t out_repr[32])
|
||||
{
|
||||
if ((session->flags & (KC_FLAG_NEEDS_DH | KC_FLAG_FORCE_DH)) != 0u) {
|
||||
kc_status status = kc_ratchet_send_step(session, seed);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
if (session->ns == UINT32_MAX) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
kc_kdf_chain(session->cks, session->cks, out_mk);
|
||||
*out_n = session->ns;
|
||||
*out_pn = session->pn;
|
||||
memcpy(out_repr, session->dhs_repr, 32u);
|
||||
session->ns++;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static kc_status kc_ratchet_advance(kc_session *session,
|
||||
uint32_t until,
|
||||
int strict)
|
||||
{
|
||||
uint8_t mk[32];
|
||||
|
||||
if ((session->flags & KC_FLAG_HAS_CKR) == 0u || until <= session->nr) {
|
||||
return KC_OK;
|
||||
}
|
||||
if (until - session->nr > KC_V2_MAX_ADVANCE) {
|
||||
return strict ? KC_ERR_TOO_MANY_SKIPPED : KC_OK;
|
||||
}
|
||||
while (session->nr < until) {
|
||||
kc_kdf_chain(session->ckr, session->ckr, mk);
|
||||
if (until - session->nr <= KC_V2_MAX_SKIP) {
|
||||
kc_session_skipped_push(session, session->dhr_repr, session->nr, mk);
|
||||
}
|
||||
session->nr++;
|
||||
}
|
||||
crypto_wipe(mk, sizeof(mk));
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_ratchet_receive_key(kc_session *session,
|
||||
const uint8_t repr[32],
|
||||
uint32_t pn,
|
||||
uint32_t n,
|
||||
uint8_t out_mk[32])
|
||||
{
|
||||
kc_status status;
|
||||
|
||||
if (kc_session_skipped_take(session, repr, n, out_mk)) {
|
||||
return KC_OK;
|
||||
}
|
||||
if ((session->flags & KC_FLAG_HAS_DHR) == 0u ||
|
||||
memcmp(repr, session->dhr_repr, 32u) != 0) {
|
||||
uint8_t dh[32];
|
||||
if (n > KC_V2_MAX_ADVANCE) {
|
||||
return KC_ERR_TOO_MANY_SKIPPED;
|
||||
}
|
||||
(void)kc_ratchet_advance(session, pn, 0);
|
||||
status = kc_ratchet_dh_repr(dh, session->dhs_priv, repr);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
kc_kdf_root(session->rk, dh, session->rk, session->ckr);
|
||||
crypto_wipe(dh, sizeof(dh));
|
||||
memcpy(session->dhr_repr, repr, 32u);
|
||||
session->nr = 0u;
|
||||
session->flags |= KC_FLAG_HAS_DHR | KC_FLAG_HAS_CKR | KC_FLAG_NEEDS_DH;
|
||||
} else if ((session->flags & KC_FLAG_HAS_CKR) == 0u) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
} else if (n < session->nr) {
|
||||
return KC_ERR_REPLAY;
|
||||
}
|
||||
status = kc_ratchet_advance(session, n, 1);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (session->nr == UINT32_MAX) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
kc_kdf_chain(session->ckr, session->ckr, out_mk);
|
||||
session->nr++;
|
||||
return KC_OK;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef KC_RATCHET_H
|
||||
#define KC_RATCHET_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "kc_session_state.h"
|
||||
|
||||
kc_status kc_ratchet_dh_repr(uint8_t out[32],
|
||||
const uint8_t secret[32],
|
||||
const uint8_t peer_repr[32]);
|
||||
kc_status kc_ratchet_dh_public(uint8_t out[32],
|
||||
const uint8_t secret[32],
|
||||
const uint8_t peer_public[32]);
|
||||
kc_status kc_ratchet_send_step(kc_session *session, const uint8_t *seed);
|
||||
kc_status kc_ratchet_next_send_key(kc_session *session,
|
||||
const uint8_t *seed,
|
||||
uint8_t out_mk[32],
|
||||
uint32_t *out_n,
|
||||
uint32_t *out_pn,
|
||||
uint8_t out_repr[32]);
|
||||
kc_status kc_ratchet_receive_key(kc_session *session,
|
||||
const uint8_t repr[32],
|
||||
uint32_t pn,
|
||||
uint32_t n,
|
||||
uint8_t out_mk[32]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "kc_session_state.h"
|
||||
|
||||
#include "kc_codec.h"
|
||||
#include "monocypher.h"
|
||||
#include <string.h>
|
||||
|
||||
kc_status kc_session_parse(const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
kc_session *session)
|
||||
{
|
||||
const uint8_t *cursor;
|
||||
size_t index;
|
||||
|
||||
if (blob == NULL || session == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (blob_len < KC_SESSION_FIXED_SIZE || blob[0] != KC_SESSION_FMT) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
memset(session, 0, sizeof(*session));
|
||||
cursor = blob + 1;
|
||||
session->flags = *cursor++;
|
||||
if ((session->flags & ~KC_FLAG_MASK) != 0u) {
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
session->chat_id = kc_load_i64(cursor);
|
||||
cursor += 8;
|
||||
session->my_id = kc_load_i64(cursor);
|
||||
cursor += 8;
|
||||
session->peer_id = kc_load_i64(cursor);
|
||||
cursor += 8;
|
||||
memcpy(session->offer_id, cursor, KC_OFFER_ID_SIZE);
|
||||
cursor += KC_OFFER_ID_SIZE;
|
||||
memcpy(session->peer_ik, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->rk, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->ok, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->dhs_priv, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->dhs_repr, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->dhr_repr, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->cks, cursor, 32u);
|
||||
cursor += 32;
|
||||
memcpy(session->ckr, cursor, 32u);
|
||||
cursor += 32;
|
||||
session->ns = kc_load_u32(cursor);
|
||||
cursor += 4;
|
||||
session->nr = kc_load_u32(cursor);
|
||||
cursor += 4;
|
||||
session->pn = kc_load_u32(cursor);
|
||||
cursor += 4;
|
||||
session->nskip = (uint16_t)(((uint16_t)cursor[0] << 8u) | cursor[1]);
|
||||
cursor += 2;
|
||||
if (session->nskip > KC_V2_MAX_SKIP_ENTRIES ||
|
||||
blob_len != KC_SESSION_FIXED_SIZE +
|
||||
(size_t)session->nskip * KC_SKIPPED_ENTRY_SIZE) {
|
||||
memset(session, 0, sizeof(*session));
|
||||
return KC_ERR_BAD_STATE;
|
||||
}
|
||||
for (index = 0u; index < session->nskip; index++) {
|
||||
memcpy(session->skipped[index].dhr, cursor, 32u);
|
||||
cursor += 32;
|
||||
session->skipped[index].n = kc_load_u32(cursor);
|
||||
cursor += 4;
|
||||
memcpy(session->skipped[index].mk, cursor, 32u);
|
||||
cursor += 32;
|
||||
}
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
size_t kc_session_serialized_size(const kc_session *session)
|
||||
{
|
||||
return KC_SESSION_FIXED_SIZE +
|
||||
(size_t)session->nskip * KC_SKIPPED_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
kc_status kc_session_serialize(const kc_session *session,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t *cursor;
|
||||
size_t required;
|
||||
size_t index;
|
||||
|
||||
if (session == NULL || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
required = kc_session_serialized_size(session);
|
||||
if (out == NULL || out_cap < required) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
cursor = out;
|
||||
*cursor++ = KC_SESSION_FMT;
|
||||
*cursor++ = session->flags;
|
||||
kc_store_i64(cursor, session->chat_id);
|
||||
cursor += 8;
|
||||
kc_store_i64(cursor, session->my_id);
|
||||
cursor += 8;
|
||||
kc_store_i64(cursor, session->peer_id);
|
||||
cursor += 8;
|
||||
memcpy(cursor, session->offer_id, KC_OFFER_ID_SIZE);
|
||||
cursor += KC_OFFER_ID_SIZE;
|
||||
memcpy(cursor, session->peer_ik, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->rk, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->ok, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->dhs_priv, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->dhs_repr, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->dhr_repr, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->cks, 32u);
|
||||
cursor += 32;
|
||||
memcpy(cursor, session->ckr, 32u);
|
||||
cursor += 32;
|
||||
kc_store_u32(cursor, session->ns);
|
||||
cursor += 4;
|
||||
kc_store_u32(cursor, session->nr);
|
||||
cursor += 4;
|
||||
kc_store_u32(cursor, session->pn);
|
||||
cursor += 4;
|
||||
*cursor++ = (uint8_t)(session->nskip >> 8u);
|
||||
*cursor++ = (uint8_t)session->nskip;
|
||||
for (index = 0u; index < session->nskip; index++) {
|
||||
memcpy(cursor, session->skipped[index].dhr, 32u);
|
||||
cursor += 32;
|
||||
kc_store_u32(cursor, session->skipped[index].n);
|
||||
cursor += 4;
|
||||
memcpy(cursor, session->skipped[index].mk, 32u);
|
||||
cursor += 32;
|
||||
}
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
void kc_session_wipe(kc_session *session)
|
||||
{
|
||||
if (session != NULL) {
|
||||
crypto_wipe(session, sizeof(*session));
|
||||
}
|
||||
}
|
||||
|
||||
int kc_session_skipped_take(kc_session *session,
|
||||
const uint8_t dhr[32],
|
||||
uint32_t n,
|
||||
uint8_t out_mk[32])
|
||||
{
|
||||
size_t index;
|
||||
|
||||
for (index = 0u; index < session->nskip; index++) {
|
||||
kc_skipped_key *entry = &session->skipped[index];
|
||||
if (entry->n == n && memcmp(entry->dhr, dhr, 32u) == 0) {
|
||||
memcpy(out_mk, entry->mk, 32u);
|
||||
crypto_wipe(entry, sizeof(*entry));
|
||||
if (index + 1u < session->nskip) {
|
||||
memmove(entry, entry + 1,
|
||||
(session->nskip - index - 1u) * sizeof(*entry));
|
||||
crypto_wipe(&session->skipped[session->nskip - 1u],
|
||||
sizeof(*entry));
|
||||
}
|
||||
session->nskip--;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void kc_session_skipped_push(kc_session *session,
|
||||
const uint8_t dhr[32],
|
||||
uint32_t n,
|
||||
const uint8_t mk[32])
|
||||
{
|
||||
kc_skipped_key *entry;
|
||||
|
||||
if (session->nskip == KC_V2_MAX_SKIP_ENTRIES) {
|
||||
crypto_wipe(&session->skipped[0], sizeof(*entry));
|
||||
memmove(&session->skipped[0], &session->skipped[1],
|
||||
(KC_V2_MAX_SKIP_ENTRIES - 1u) * sizeof(*entry));
|
||||
session->nskip--;
|
||||
}
|
||||
entry = &session->skipped[session->nskip];
|
||||
memcpy(entry->dhr, dhr, 32u);
|
||||
entry->n = n;
|
||||
memcpy(entry->mk, mk, 32u);
|
||||
session->nskip++;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef KC_SESSION_STATE_H
|
||||
#define KC_SESSION_STATE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "kc_internal.h"
|
||||
|
||||
#define KC_SESSION_FMT 0x01u
|
||||
#define KC_PENDING_FMT 0x01u
|
||||
#define KC_SESSION_FIXED_SIZE 304u
|
||||
#define KC_SKIPPED_ENTRY_SIZE 68u
|
||||
|
||||
#define KC_FLAG_FORCE_DH 0x01u
|
||||
#define KC_FLAG_INITIATOR 0x02u
|
||||
#define KC_FLAG_HAS_DHR 0x04u
|
||||
#define KC_FLAG_HAS_CKR 0x08u
|
||||
#define KC_FLAG_NEEDS_DH 0x10u
|
||||
#define KC_FLAG_MASK 0x1Fu
|
||||
|
||||
typedef struct {
|
||||
uint8_t dhr[32];
|
||||
uint32_t n;
|
||||
uint8_t mk[32];
|
||||
} kc_skipped_key;
|
||||
|
||||
typedef struct {
|
||||
uint8_t flags;
|
||||
int64_t chat_id;
|
||||
int64_t my_id;
|
||||
int64_t peer_id;
|
||||
uint8_t offer_id[KC_OFFER_ID_SIZE];
|
||||
uint8_t peer_ik[32];
|
||||
uint8_t rk[32];
|
||||
uint8_t ok[32];
|
||||
uint8_t dhs_priv[32];
|
||||
uint8_t dhs_repr[32];
|
||||
uint8_t dhr_repr[32];
|
||||
uint8_t cks[32];
|
||||
uint8_t ckr[32];
|
||||
uint32_t ns;
|
||||
uint32_t nr;
|
||||
uint32_t pn;
|
||||
uint16_t nskip;
|
||||
kc_skipped_key skipped[256];
|
||||
} kc_session;
|
||||
|
||||
kc_status kc_session_parse(const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
kc_session *session);
|
||||
size_t kc_session_serialized_size(const kc_session *session);
|
||||
kc_status kc_session_serialize(const kc_session *session,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len);
|
||||
void kc_session_wipe(kc_session *session);
|
||||
int kc_session_skipped_take(kc_session *session,
|
||||
const uint8_t dhr[32],
|
||||
uint32_t n,
|
||||
uint8_t out_mk[32]);
|
||||
void kc_session_skipped_push(kc_session *session,
|
||||
const uint8_t dhr[32],
|
||||
uint32_t n,
|
||||
const uint8_t mk[32]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "kc_sha256.h"
|
||||
|
||||
#include "monocypher.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static const uint32_t kc_sha256_constants[64] = {
|
||||
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu,
|
||||
0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u,
|
||||
0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u,
|
||||
0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu,
|
||||
0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 0x983e5152u,
|
||||
0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u,
|
||||
0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu,
|
||||
0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
|
||||
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u,
|
||||
0xd6990624u, 0xf40e3585u, 0x106aa070u, 0x19a4c116u, 0x1e376c08u,
|
||||
0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu,
|
||||
0x682e6ff3u, 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
|
||||
0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u};
|
||||
|
||||
static uint32_t kc_rotr32(uint32_t value, unsigned int count)
|
||||
{
|
||||
return (value >> count) | (value << (32u - count));
|
||||
}
|
||||
|
||||
static uint32_t kc_choose(uint32_t x, uint32_t y, uint32_t z)
|
||||
{
|
||||
return (x & y) ^ (~x & z);
|
||||
}
|
||||
|
||||
static uint32_t kc_majority(uint32_t x, uint32_t y, uint32_t z)
|
||||
{
|
||||
return (x & y) ^ (x & z) ^ (y & z);
|
||||
}
|
||||
|
||||
static uint32_t kc_big_sigma0(uint32_t value)
|
||||
{
|
||||
return kc_rotr32(value, 2u) ^ kc_rotr32(value, 13u) ^
|
||||
kc_rotr32(value, 22u);
|
||||
}
|
||||
|
||||
static uint32_t kc_big_sigma1(uint32_t value)
|
||||
{
|
||||
return kc_rotr32(value, 6u) ^ kc_rotr32(value, 11u) ^
|
||||
kc_rotr32(value, 25u);
|
||||
}
|
||||
|
||||
static uint32_t kc_small_sigma0(uint32_t value)
|
||||
{
|
||||
return kc_rotr32(value, 7u) ^ kc_rotr32(value, 18u) ^ (value >> 3u);
|
||||
}
|
||||
|
||||
static uint32_t kc_small_sigma1(uint32_t value)
|
||||
{
|
||||
return kc_rotr32(value, 17u) ^ kc_rotr32(value, 19u) ^ (value >> 10u);
|
||||
}
|
||||
|
||||
static void kc_sha256_block(kc_sha256 *context, const uint8_t *block)
|
||||
{
|
||||
uint32_t words[64];
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
uint32_t c;
|
||||
uint32_t d;
|
||||
uint32_t e;
|
||||
uint32_t f;
|
||||
uint32_t g;
|
||||
uint32_t h;
|
||||
size_t index;
|
||||
|
||||
for (index = 0u; index < 16u; index++) {
|
||||
words[index] = ((uint32_t)block[index * 4u] << 24u) |
|
||||
((uint32_t)block[index * 4u + 1u] << 16u) |
|
||||
((uint32_t)block[index * 4u + 2u] << 8u) |
|
||||
(uint32_t)block[index * 4u + 3u];
|
||||
}
|
||||
for (index = 16u; index < 64u; index++) {
|
||||
words[index] = kc_small_sigma1(words[index - 2u]) +
|
||||
words[index - 7u] +
|
||||
kc_small_sigma0(words[index - 15u]) +
|
||||
words[index - 16u];
|
||||
}
|
||||
a = context->state[0];
|
||||
b = context->state[1];
|
||||
c = context->state[2];
|
||||
d = context->state[3];
|
||||
e = context->state[4];
|
||||
f = context->state[5];
|
||||
g = context->state[6];
|
||||
h = context->state[7];
|
||||
for (index = 0u; index < 64u; index++) {
|
||||
uint32_t first = h + kc_big_sigma1(e) + kc_choose(e, f, g) +
|
||||
kc_sha256_constants[index] + words[index];
|
||||
uint32_t second = kc_big_sigma0(a) + kc_majority(a, b, c);
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + first;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = first + second;
|
||||
}
|
||||
context->state[0] += a;
|
||||
context->state[1] += b;
|
||||
context->state[2] += c;
|
||||
context->state[3] += d;
|
||||
context->state[4] += e;
|
||||
context->state[5] += f;
|
||||
context->state[6] += g;
|
||||
context->state[7] += h;
|
||||
crypto_wipe(words, sizeof(words));
|
||||
}
|
||||
|
||||
void kc_sha256_init(kc_sha256 *context)
|
||||
{
|
||||
if (context == NULL) {
|
||||
return;
|
||||
}
|
||||
context->state[0] = 0x6a09e667u;
|
||||
context->state[1] = 0xbb67ae85u;
|
||||
context->state[2] = 0x3c6ef372u;
|
||||
context->state[3] = 0xa54ff53au;
|
||||
context->state[4] = 0x510e527fu;
|
||||
context->state[5] = 0x9b05688cu;
|
||||
context->state[6] = 0x1f83d9abu;
|
||||
context->state[7] = 0x5be0cd19u;
|
||||
context->length = 0u;
|
||||
context->buffered = 0u;
|
||||
memset(context->buffer, 0, sizeof(context->buffer));
|
||||
}
|
||||
|
||||
void kc_sha256_update(kc_sha256 *context,
|
||||
const uint8_t *input,
|
||||
size_t input_len)
|
||||
{
|
||||
size_t offset = 0u;
|
||||
|
||||
if (context == NULL || (input == NULL && input_len != 0u)) {
|
||||
return;
|
||||
}
|
||||
context->length += input_len;
|
||||
if (context->buffered != 0u) {
|
||||
size_t required = 64u - context->buffered;
|
||||
size_t take = input_len < required ? input_len : required;
|
||||
memcpy(context->buffer + context->buffered, input, take);
|
||||
context->buffered += take;
|
||||
offset = take;
|
||||
if (context->buffered < 64u) {
|
||||
return;
|
||||
}
|
||||
kc_sha256_block(context, context->buffer);
|
||||
context->buffered = 0u;
|
||||
}
|
||||
while (input_len - offset >= 64u) {
|
||||
kc_sha256_block(context, input + offset);
|
||||
offset += 64u;
|
||||
}
|
||||
if (offset < input_len) {
|
||||
context->buffered = input_len - offset;
|
||||
memcpy(context->buffer, input + offset, context->buffered);
|
||||
}
|
||||
}
|
||||
|
||||
void kc_sha256_final(kc_sha256 *context, uint8_t out[KC_SHA256_SIZE])
|
||||
{
|
||||
uint64_t bit_length;
|
||||
uint8_t tail[72];
|
||||
size_t padding;
|
||||
size_t index;
|
||||
|
||||
if (context == NULL || out == NULL) {
|
||||
return;
|
||||
}
|
||||
bit_length = context->length * 8u;
|
||||
padding = context->buffered < 56u ? 56u - context->buffered
|
||||
: 120u - context->buffered;
|
||||
memset(tail, 0, sizeof(tail));
|
||||
tail[0] = 0x80u;
|
||||
for (index = 0u; index < 8u; index++) {
|
||||
tail[padding + index] =
|
||||
(uint8_t)(bit_length >> (56u - index * 8u));
|
||||
}
|
||||
kc_sha256_update(context, tail, padding + 8u);
|
||||
for (index = 0u; index < 8u; index++) {
|
||||
out[index * 4u] = (uint8_t)(context->state[index] >> 24u);
|
||||
out[index * 4u + 1u] = (uint8_t)(context->state[index] >> 16u);
|
||||
out[index * 4u + 2u] = (uint8_t)(context->state[index] >> 8u);
|
||||
out[index * 4u + 3u] = (uint8_t)context->state[index];
|
||||
}
|
||||
crypto_wipe(tail, sizeof(tail));
|
||||
crypto_wipe(context, sizeof(*context));
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef KC_SHA256_H
|
||||
#define KC_SHA256_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define KC_SHA256_SIZE 32u
|
||||
|
||||
typedef struct {
|
||||
uint32_t state[8];
|
||||
uint64_t length;
|
||||
uint8_t buffer[64];
|
||||
size_t buffered;
|
||||
} kc_sha256;
|
||||
|
||||
void kc_sha256_init(kc_sha256 *context);
|
||||
void kc_sha256_update(kc_sha256 *context,
|
||||
const uint8_t *input,
|
||||
size_t input_len);
|
||||
void kc_sha256_final(kc_sha256 *context, uint8_t out[KC_SHA256_SIZE]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "kc_internal.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
const char *kc_status_string(kc_status status)
|
||||
{
|
||||
switch (status) {
|
||||
case KC_OK:
|
||||
return "ok";
|
||||
case KC_ERR_EMPTY_PASSWORD:
|
||||
return "empty_password";
|
||||
case KC_ERR_BAD_KEY_LENGTH:
|
||||
return "bad_key_length";
|
||||
case KC_ERR_NOT_ENCRYPTED:
|
||||
return "not_encrypted";
|
||||
case KC_ERR_MALFORMED:
|
||||
return "malformed";
|
||||
case KC_ERR_WRONG_KEY:
|
||||
return "wrong_key";
|
||||
case KC_ERR_INTERNAL:
|
||||
return "internal";
|
||||
case KC_ERR_BUFFER_TOO_SMALL:
|
||||
return "buffer_too_small";
|
||||
case KC_ERR_INVALID_ARGUMENT:
|
||||
return "invalid_argument";
|
||||
case KC_ERR_OUT_OF_MEMORY:
|
||||
return "out_of_memory";
|
||||
case KC_ERR_BAD_SIGNATURE:
|
||||
return "bad_signature";
|
||||
case KC_ERR_BAD_STATE:
|
||||
return "bad_state";
|
||||
case KC_ERR_TOO_MANY_SKIPPED:
|
||||
return "too_many_skipped";
|
||||
case KC_ERR_REPLAY:
|
||||
return "replay";
|
||||
case KC_ERR_BAD_PEER:
|
||||
return "bad_peer";
|
||||
case KC_ERR_UNSUPPORTED_VERSION:
|
||||
return "unsupported_version";
|
||||
case KC_ERR_HANDSHAKE_MESSAGE:
|
||||
return "handshake_message";
|
||||
case KC_ERR_TOO_LONG:
|
||||
return "too_long";
|
||||
case KC_ERR_AWAITING_PEER:
|
||||
return "awaiting_peer";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
int kc_size_add(size_t left, size_t right, size_t *out)
|
||||
{
|
||||
if (out == NULL || left > SIZE_MAX - right) {
|
||||
return 0;
|
||||
}
|
||||
*out = left + right;
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
#include "kc_internal.h"
|
||||
#include "kc_sha256.h"
|
||||
|
||||
#include "monocypher.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const uint8_t kc_salt_context[] = "komet-enc-v1";
|
||||
static const size_t kc_argon_work_size = 65536u * 1024u;
|
||||
|
||||
static kc_status kc_validate_key(const uint8_t *key, size_t key_len)
|
||||
{
|
||||
if (key_len != KC_KEY_SIZE) {
|
||||
return KC_ERR_BAD_KEY_LENGTH;
|
||||
}
|
||||
if (key == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static const uint8_t *kc_nonnull_input(const uint8_t *input,
|
||||
size_t input_len,
|
||||
const uint8_t *fallback)
|
||||
{
|
||||
return input_len == 0u ? fallback : input;
|
||||
}
|
||||
|
||||
kc_status kc_seal(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
const uint8_t *header,
|
||||
size_t header_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
static const uint8_t empty = 0u;
|
||||
crypto_aead_ctx context;
|
||||
size_t required;
|
||||
kc_status status;
|
||||
|
||||
status = kc_validate_key(key, key_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if ((plaintext == NULL && plaintext_len != 0u) || nonce == NULL ||
|
||||
nonce_len != KC_NONCE_SIZE || header == NULL || out_len == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!kc_size_add(plaintext_len, KC_TAG_SIZE, &required) ||
|
||||
out_cap < required || (out == NULL && required != 0u)) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
crypto_aead_init_ietf(&context, key, nonce);
|
||||
crypto_aead_write(&context, out, out + plaintext_len, header, header_len,
|
||||
kc_nonnull_input(plaintext, plaintext_len, &empty),
|
||||
plaintext_len);
|
||||
crypto_wipe(&context, sizeof(context));
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_open(const uint8_t *sealed,
|
||||
size_t sealed_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
const uint8_t *header,
|
||||
size_t header_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t empty = 0u;
|
||||
crypto_aead_ctx context;
|
||||
size_t required;
|
||||
uint8_t *target;
|
||||
kc_status status;
|
||||
|
||||
status = kc_validate_key(key, key_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (sealed == NULL || nonce == NULL || header == NULL || out_len == NULL ||
|
||||
sealed_len < KC_TAG_SIZE) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
required = sealed_len - KC_TAG_SIZE;
|
||||
if (out_cap < required || (out == NULL && required != 0u)) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
target = required == 0u ? &empty : out;
|
||||
crypto_aead_init_ietf(&context, key, nonce);
|
||||
if (crypto_aead_read(&context, target, sealed + required, header,
|
||||
header_len, sealed, required) != 0) {
|
||||
crypto_wipe(&context, sizeof(context));
|
||||
if (out != NULL && required != 0u) {
|
||||
crypto_wipe(out, required);
|
||||
}
|
||||
return KC_ERR_WRONG_KEY;
|
||||
}
|
||||
crypto_wipe(&context, sizeof(context));
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
kc_status kc_derive_key(const uint8_t *password,
|
||||
size_t password_len,
|
||||
uint8_t *out_key,
|
||||
size_t out_key_len)
|
||||
{
|
||||
kc_sha256 hash;
|
||||
crypto_argon2_config config;
|
||||
crypto_argon2_inputs inputs;
|
||||
uint8_t digest[KC_SHA256_SIZE];
|
||||
void *work_area;
|
||||
|
||||
if (password_len == 0u) {
|
||||
return KC_ERR_EMPTY_PASSWORD;
|
||||
}
|
||||
if (password == NULL || out_key == NULL) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out_key_len != KC_KEY_SIZE) {
|
||||
return KC_ERR_BAD_KEY_LENGTH;
|
||||
}
|
||||
if (password_len > UINT32_MAX) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
kc_sha256_init(&hash);
|
||||
kc_sha256_update(&hash, kc_salt_context, sizeof(kc_salt_context) - 1u);
|
||||
kc_sha256_update(&hash, password, password_len);
|
||||
kc_sha256_final(&hash, digest);
|
||||
work_area = malloc(kc_argon_work_size);
|
||||
if (work_area == NULL) {
|
||||
crypto_wipe(digest, sizeof(digest));
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
config.algorithm = CRYPTO_ARGON2_ID;
|
||||
config.nb_blocks = 65536u;
|
||||
config.nb_passes = 3u;
|
||||
config.nb_lanes = 1u;
|
||||
inputs.pass = password;
|
||||
inputs.salt = digest;
|
||||
inputs.pass_size = (uint32_t)password_len;
|
||||
inputs.salt_size = 16u;
|
||||
crypto_argon2(out_key, KC_KEY_SIZE, work_area, config, inputs,
|
||||
crypto_argon2_no_extras);
|
||||
crypto_wipe(work_area, kc_argon_work_size);
|
||||
free(work_area);
|
||||
crypto_wipe(digest, sizeof(digest));
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
size_t kc_encrypt_message_bound(size_t plaintext_len)
|
||||
{
|
||||
size_t blob_len;
|
||||
|
||||
if (!kc_size_add(plaintext_len,
|
||||
KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE,
|
||||
&blob_len)) {
|
||||
return 0u;
|
||||
}
|
||||
return kc_alphabet_encode_bound(blob_len);
|
||||
}
|
||||
|
||||
kc_status kc_encrypt_message(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t header[KC_MESSAGE_HEADER_SIZE] = {KC_MAGIC, KC_MESSAGE_VERSION};
|
||||
uint8_t *blob;
|
||||
size_t blob_len;
|
||||
size_t sealed_len;
|
||||
size_t required;
|
||||
kc_status status;
|
||||
|
||||
if (!kc_utf8_valid(plaintext, plaintext_len)) {
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
required = kc_encrypt_message_bound(plaintext_len);
|
||||
if (required == 0u || out_cap < required || out == NULL ||
|
||||
out_len == NULL) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
if (!kc_size_add(plaintext_len,
|
||||
KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE,
|
||||
&blob_len)) {
|
||||
return KC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
blob = (uint8_t *)malloc(blob_len);
|
||||
if (blob == NULL) {
|
||||
return KC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
memcpy(blob, header, KC_MESSAGE_HEADER_SIZE);
|
||||
if (nonce != NULL && nonce_len == KC_NONCE_SIZE) {
|
||||
memcpy(blob + KC_MESSAGE_HEADER_SIZE, nonce, KC_NONCE_SIZE);
|
||||
}
|
||||
status = kc_seal(plaintext, plaintext_len, key, key_len, nonce, nonce_len,
|
||||
header, KC_MESSAGE_HEADER_SIZE,
|
||||
blob + KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE,
|
||||
blob_len - KC_MESSAGE_HEADER_SIZE - KC_NONCE_SIZE,
|
||||
&sealed_len);
|
||||
if (status == KC_OK) {
|
||||
status = kc_alphabet_encode(blob, blob_len, out, out_cap, out_len);
|
||||
}
|
||||
crypto_wipe(blob, blob_len);
|
||||
free(blob);
|
||||
return status;
|
||||
}
|
||||
|
||||
size_t kc_decrypt_message_bound(size_t text_len)
|
||||
{
|
||||
return text_len;
|
||||
}
|
||||
|
||||
kc_status kc_decrypt_message(const uint8_t *text,
|
||||
size_t text_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t *blob = NULL;
|
||||
size_t blob_len = 0u;
|
||||
size_t plaintext_len;
|
||||
kc_status status;
|
||||
|
||||
status = kc_alphabet_decode(text, text_len, &blob, &blob_len);
|
||||
if (status != KC_OK) {
|
||||
return status;
|
||||
}
|
||||
if (blob_len < KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE ||
|
||||
blob[0] != KC_MAGIC || blob[1] != KC_MESSAGE_VERSION) {
|
||||
free(blob);
|
||||
return KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
plaintext_len =
|
||||
blob_len - KC_MESSAGE_HEADER_SIZE - KC_NONCE_SIZE - KC_TAG_SIZE;
|
||||
if (out_cap < plaintext_len || (out == NULL && plaintext_len != 0u) ||
|
||||
out_len == NULL) {
|
||||
free(blob);
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
status = kc_open(blob + KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE,
|
||||
blob_len - KC_MESSAGE_HEADER_SIZE - KC_NONCE_SIZE, key,
|
||||
key_len, blob + KC_MESSAGE_HEADER_SIZE, blob,
|
||||
KC_MESSAGE_HEADER_SIZE, out, out_cap, out_len);
|
||||
crypto_wipe(blob, blob_len);
|
||||
free(blob);
|
||||
if (status == KC_OK && !kc_utf8_valid(out, *out_len)) {
|
||||
crypto_wipe(out, *out_len);
|
||||
return KC_ERR_MALFORMED;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
int kc_looks_encrypted_message(const uint8_t *text, size_t text_len)
|
||||
{
|
||||
uint8_t *blob = NULL;
|
||||
size_t blob_len = 0u;
|
||||
kc_status status;
|
||||
int result;
|
||||
|
||||
status = kc_alphabet_decode(text, text_len, &blob, &blob_len);
|
||||
if (status != KC_OK) {
|
||||
return 0;
|
||||
}
|
||||
result = blob_len >=
|
||||
KC_MESSAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE &&
|
||||
blob[0] == KC_MAGIC && blob[1] == KC_MESSAGE_VERSION;
|
||||
free(blob);
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t kc_encrypt_image_blob_bound(size_t plaintext_len)
|
||||
{
|
||||
size_t result;
|
||||
|
||||
if (!kc_size_add(plaintext_len,
|
||||
KC_IMAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE,
|
||||
&result) ||
|
||||
result - KC_IMAGE_HEADER_SIZE > UINT32_MAX) {
|
||||
return 0u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
kc_status kc_encrypt_image_blob(const uint8_t *plaintext,
|
||||
size_t plaintext_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *nonce,
|
||||
size_t nonce_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
uint8_t header[KC_IMAGE_HEADER_SIZE];
|
||||
size_t required = kc_encrypt_image_blob_bound(plaintext_len);
|
||||
size_t payload_len;
|
||||
size_t sealed_len;
|
||||
kc_status status;
|
||||
|
||||
if (required == 0u || out == NULL || out_len == NULL ||
|
||||
out_cap < required) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
payload_len = required - KC_IMAGE_HEADER_SIZE;
|
||||
header[0] = KC_MAGIC;
|
||||
header[1] = KC_IMAGE_VERSION;
|
||||
header[2] = (uint8_t)(payload_len >> 24u);
|
||||
header[3] = (uint8_t)(payload_len >> 16u);
|
||||
header[4] = (uint8_t)(payload_len >> 8u);
|
||||
header[5] = (uint8_t)payload_len;
|
||||
memcpy(out, header, KC_IMAGE_HEADER_SIZE);
|
||||
if (nonce != NULL && nonce_len == KC_NONCE_SIZE) {
|
||||
memcpy(out + KC_IMAGE_HEADER_SIZE, nonce, KC_NONCE_SIZE);
|
||||
}
|
||||
status = kc_seal(plaintext, plaintext_len, key, key_len, nonce, nonce_len,
|
||||
header, KC_IMAGE_HEADER_SIZE,
|
||||
out + KC_IMAGE_HEADER_SIZE + KC_NONCE_SIZE,
|
||||
out_cap - KC_IMAGE_HEADER_SIZE - KC_NONCE_SIZE,
|
||||
&sealed_len);
|
||||
if (status != KC_OK) {
|
||||
crypto_wipe(out, required);
|
||||
return status;
|
||||
}
|
||||
*out_len = required;
|
||||
return KC_OK;
|
||||
}
|
||||
|
||||
static int kc_image_payload_len(const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
size_t *payload_len)
|
||||
{
|
||||
uint32_t encoded;
|
||||
|
||||
if (blob == NULL || payload_len == NULL || blob_len < KC_IMAGE_HEADER_SIZE ||
|
||||
blob[0] != KC_MAGIC || blob[1] != KC_IMAGE_VERSION) {
|
||||
return 0;
|
||||
}
|
||||
encoded = ((uint32_t)blob[2] << 24u) | ((uint32_t)blob[3] << 16u) |
|
||||
((uint32_t)blob[4] << 8u) | (uint32_t)blob[5];
|
||||
if (encoded < KC_NONCE_SIZE + KC_TAG_SIZE ||
|
||||
(size_t)encoded > blob_len - KC_IMAGE_HEADER_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
*payload_len = encoded;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t kc_decrypt_image_blob_bound(size_t blob_len)
|
||||
{
|
||||
if (blob_len < KC_IMAGE_HEADER_SIZE + KC_NONCE_SIZE + KC_TAG_SIZE) {
|
||||
return 0u;
|
||||
}
|
||||
return blob_len - KC_IMAGE_HEADER_SIZE - KC_NONCE_SIZE - KC_TAG_SIZE;
|
||||
}
|
||||
|
||||
kc_status kc_decrypt_image_blob(const uint8_t *blob,
|
||||
size_t blob_len,
|
||||
const uint8_t *key,
|
||||
size_t key_len,
|
||||
uint8_t *out,
|
||||
size_t out_cap,
|
||||
size_t *out_len)
|
||||
{
|
||||
size_t payload_len;
|
||||
size_t plaintext_len;
|
||||
|
||||
if (!kc_image_payload_len(blob, blob_len, &payload_len)) {
|
||||
return KC_ERR_NOT_ENCRYPTED;
|
||||
}
|
||||
plaintext_len = payload_len - KC_NONCE_SIZE - KC_TAG_SIZE;
|
||||
if (out_cap < plaintext_len || (out == NULL && plaintext_len != 0u) ||
|
||||
out_len == NULL) {
|
||||
return KC_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
return kc_open(blob + KC_IMAGE_HEADER_SIZE + KC_NONCE_SIZE,
|
||||
payload_len - KC_NONCE_SIZE, key, key_len,
|
||||
blob + KC_IMAGE_HEADER_SIZE, blob, KC_IMAGE_HEADER_SIZE,
|
||||
out, out_cap, out_len);
|
||||
}
|
||||
|
||||
int kc_looks_encrypted_image_blob(const uint8_t *blob, size_t blob_len)
|
||||
{
|
||||
size_t payload_len;
|
||||
return kc_image_payload_len(blob, blob_len, &payload_len);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# кириллический алфавит base32: без него фаззер не пробивается сквозь декодер
|
||||
l00="\xd0\xb0"
|
||||
l01="\xd0\xb1"
|
||||
l02="\xd0\xb2"
|
||||
l03="\xd0\xb3"
|
||||
l04="\xd0\xb4"
|
||||
l05="\xd0\xb5"
|
||||
l06="\xd0\xb6"
|
||||
l07="\xd0\xb7"
|
||||
l08="\xd0\xb8"
|
||||
l09="\xd0\xb9"
|
||||
l10="\xd0\xba"
|
||||
l11="\xd0\xbb"
|
||||
l12="\xd0\xbc"
|
||||
l13="\xd0\xbd"
|
||||
l14="\xd0\xbe"
|
||||
l15="\xd0\xbf"
|
||||
l16="\xd1\x80"
|
||||
l17="\xd1\x81"
|
||||
l18="\xd1\x82"
|
||||
l19="\xd1\x83"
|
||||
l20="\xd1\x84"
|
||||
l21="\xd1\x85"
|
||||
l22="\xd1\x86"
|
||||
l23="\xd1\x87"
|
||||
l24="\xd1\x88"
|
||||
l25="\xd1\x89"
|
||||
l26="\xd1\x8a"
|
||||
l27="\xd1\x8b"
|
||||
l28="\xd1\x8c"
|
||||
l29="\xd1\x8d"
|
||||
l30="\xd1\x8e"
|
||||
l31="\xd1\x8f"
|
||||
space=" "
|
||||
nl="\x0a"
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "komet_crypto.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint8_t fuzz_identity_a[KC_IDENTITY_SIZE];
|
||||
static uint8_t fuzz_identity_b[KC_IDENTITY_SIZE];
|
||||
static uint8_t fuzz_state_b[KC_SESSION_STATE_MAX];
|
||||
static size_t fuzz_state_b_len;
|
||||
static uint8_t fuzz_pending[KC_PENDING_STATE_SIZE];
|
||||
static size_t fuzz_pending_len;
|
||||
static int fuzz_ready;
|
||||
|
||||
static void fuzz_fill(uint8_t *out, size_t len, uint64_t seed)
|
||||
{
|
||||
uint64_t x = seed;
|
||||
size_t index;
|
||||
|
||||
for (index = 0u; index < len; index++) {
|
||||
x ^= x >> 12u;
|
||||
x ^= x << 25u;
|
||||
x ^= x >> 27u;
|
||||
out[index] = (uint8_t)((x * 0x2545F4914F6CDD1Dull) >> 56u);
|
||||
}
|
||||
}
|
||||
|
||||
static void fuzz_setup(void)
|
||||
{
|
||||
uint8_t seed[KC_SEED_SIZE];
|
||||
uint8_t random[KC_OFFER_RANDOM_SIZE];
|
||||
uint8_t offer_text[KC_HANDSHAKE_TEXT_BOUND];
|
||||
uint8_t answer_text[KC_HANDSHAKE_TEXT_BOUND];
|
||||
size_t offer_len = 0u;
|
||||
size_t answer_len = 0u;
|
||||
size_t len = 0u;
|
||||
|
||||
if (fuzz_ready) {
|
||||
return;
|
||||
}
|
||||
fuzz_fill(seed, sizeof(seed), 1u);
|
||||
kc_identity_create(seed, sizeof(seed), fuzz_identity_a,
|
||||
sizeof(fuzz_identity_a), &len);
|
||||
fuzz_fill(seed, sizeof(seed), 2u);
|
||||
kc_identity_create(seed, sizeof(seed), fuzz_identity_b,
|
||||
sizeof(fuzz_identity_b), &len);
|
||||
fuzz_fill(random, KC_OFFER_RANDOM_SIZE, 3u);
|
||||
kc_session_offer(fuzz_identity_a, sizeof(fuzz_identity_a), 1, 10, 20,
|
||||
random, KC_OFFER_RANDOM_SIZE, offer_text,
|
||||
sizeof(offer_text), &offer_len, fuzz_pending,
|
||||
sizeof(fuzz_pending), &fuzz_pending_len);
|
||||
fuzz_fill(random, KC_ANSWER_RANDOM_SIZE, 4u);
|
||||
kc_session_answer(fuzz_identity_b, sizeof(fuzz_identity_b), 1, 20, 10,
|
||||
offer_text, offer_len, NULL, 0u, random,
|
||||
KC_ANSWER_RANDOM_SIZE, answer_text, sizeof(answer_text),
|
||||
&answer_len, fuzz_state_b, sizeof(fuzz_state_b),
|
||||
&fuzz_state_b_len);
|
||||
fuzz_ready = 1;
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
{
|
||||
static const uint8_t key[32] = {7u};
|
||||
static const uint8_t nonce12[12] = {1u};
|
||||
uint8_t *out;
|
||||
uint8_t state_out[KC_SESSION_STATE_MAX];
|
||||
uint8_t text_out[4096];
|
||||
uint8_t ctx[KC_FILE_CTX_SIZE];
|
||||
uint8_t random[KC_OFFER_RANDOM_SIZE] = {0u};
|
||||
uint8_t content_type = 0u;
|
||||
uint8_t peek_type = 0u;
|
||||
uint8_t peek_id[KC_OFFER_ID_SIZE];
|
||||
uint8_t peek_public[KC_PUBLIC_KEY_SIZE];
|
||||
size_t out_len = 0u;
|
||||
size_t state_len = 0u;
|
||||
size_t text_len = 0u;
|
||||
uint8_t op;
|
||||
|
||||
fuzz_setup();
|
||||
if (size == 0u) {
|
||||
return 0;
|
||||
}
|
||||
op = data[0] % 10u;
|
||||
data++;
|
||||
size--;
|
||||
out = (uint8_t *)malloc(size + KC_FILE_CHUNK_SIZE + 64u);
|
||||
if (out == NULL) {
|
||||
return 0;
|
||||
}
|
||||
switch (op) {
|
||||
case 0:
|
||||
kc_classify_text(data, size);
|
||||
break;
|
||||
case 1:
|
||||
kc_decrypt_message(data, size, key, sizeof(key), out, size, &out_len);
|
||||
break;
|
||||
case 2:
|
||||
kc_session_decrypt(fuzz_state_b, fuzz_state_b_len, data, size,
|
||||
&content_type, out, size, &out_len, state_out,
|
||||
sizeof(state_out), &state_len);
|
||||
break;
|
||||
case 3:
|
||||
kc_session_encrypt(data, size, KC_CONTENT_CONTROL, key, 8u, random,
|
||||
KC_ENCRYPT_RANDOM_SIZE, text_out, sizeof(text_out),
|
||||
&text_len, state_out, sizeof(state_out),
|
||||
&state_len);
|
||||
break;
|
||||
case 4:
|
||||
kc_session_answer(fuzz_identity_b, sizeof(fuzz_identity_b), 1, 20, 10,
|
||||
data, size, fuzz_state_b, fuzz_state_b_len, random,
|
||||
KC_ANSWER_RANDOM_SIZE, text_out, sizeof(text_out),
|
||||
&text_len, state_out, sizeof(state_out),
|
||||
&state_len);
|
||||
break;
|
||||
case 5:
|
||||
kc_session_accept(fuzz_identity_a, sizeof(fuzz_identity_a),
|
||||
fuzz_pending, fuzz_pending_len, data, size,
|
||||
fuzz_identity_b + KC_SEED_SIZE, KC_PUBLIC_KEY_SIZE,
|
||||
random, KC_ACCEPT_RANDOM_SIZE, state_out,
|
||||
sizeof(state_out), &state_len);
|
||||
break;
|
||||
case 6:
|
||||
if (kc_file_open_init(ctx, sizeof(ctx), key, sizeof(key), nonce12,
|
||||
sizeof(nonce12)) == KC_OK) {
|
||||
kc_file_open_chunk(ctx, sizeof(ctx), data, size, (int)(size & 1u),
|
||||
out, size + KC_FILE_CHUNK_SIZE, &out_len);
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
kc_export_open((const uint8_t *)"pw", 2u, data, size, out, size,
|
||||
&out_len);
|
||||
break;
|
||||
case 8:
|
||||
kc_local_open(key, sizeof(key), NULL, 0u, data, size, out, size,
|
||||
&out_len);
|
||||
break;
|
||||
default:
|
||||
kc_handshake_peek(data, size, &peek_type, peek_id, sizeof(peek_id),
|
||||
peek_public, sizeof(peek_public));
|
||||
break;
|
||||
}
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "komet_crypto.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
int kc_run_v2_tests(void);
|
||||
|
||||
static void expect(int condition, const char *name)
|
||||
{
|
||||
if (!condition) {
|
||||
fprintf(stderr, "FAIL %s\n", name);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
static int hex_value(char value)
|
||||
{
|
||||
if (value >= '0' && value <= '9') {
|
||||
return value - '0';
|
||||
}
|
||||
return value - 'a' + 10;
|
||||
}
|
||||
|
||||
static void decode_hex(const char *text, uint8_t *out, size_t out_len)
|
||||
{
|
||||
size_t index;
|
||||
for (index = 0u; index < out_len; index++) {
|
||||
out[index] = (uint8_t)((hex_value(text[index * 2u]) << 4) |
|
||||
hex_value(text[index * 2u + 1u]));
|
||||
}
|
||||
}
|
||||
|
||||
static void test_derive_key(void)
|
||||
{
|
||||
static const uint8_t password[] = "fixture-password-01";
|
||||
static const char expected_hex[] =
|
||||
"2acd8a97ecc4690d5d90455991241200bb42e40b5c5efa334709a0eb17ad2e3d";
|
||||
uint8_t expected[KC_KEY_SIZE];
|
||||
uint8_t actual[KC_KEY_SIZE];
|
||||
kc_status status;
|
||||
|
||||
decode_hex(expected_hex, expected, sizeof(expected));
|
||||
status = kc_derive_key(password, sizeof(password) - 1u, actual,
|
||||
sizeof(actual));
|
||||
expect(status == KC_OK, "derive status");
|
||||
expect(memcmp(actual, expected, sizeof(actual)) == 0, "derive vector");
|
||||
expect(kc_derive_key(password, 0u, actual, sizeof(actual)) ==
|
||||
KC_ERR_EMPTY_PASSWORD,
|
||||
"derive empty");
|
||||
}
|
||||
|
||||
static void test_message(void)
|
||||
{
|
||||
static const uint8_t plaintext[] = "fixture-message-01";
|
||||
static const char expected_hex[] =
|
||||
"d0b9d0bcd0b0d18020d0b2d0b3d0b0d187d0b4d0b820d186d183d180d180d18a"
|
||||
"d0be20d0bbd0b5d182d0b6d18e20d18ed180d186d0b820d0b7d189d181d18ed0b7"
|
||||
"d18820d189d0b8d0bcd18c20d0bed0b3d0bdd180d0b0d187d181d18f20d185d0b6"
|
||||
"d0bcd182d0ba20d18ed0bcd0bfd18120d0bfd0bfd0b7d18620d0b9d0b6d0b2d187"
|
||||
"d18fd182d184d0b720d0bad18ed185d18c20d181d0b2d18fd0b7d0b0d18520d188"
|
||||
"d0b5d0b4";
|
||||
uint8_t key[KC_KEY_SIZE];
|
||||
uint8_t other_key[KC_KEY_SIZE];
|
||||
uint8_t nonce[KC_NONCE_SIZE];
|
||||
uint8_t expected[(sizeof(expected_hex) - 1u) / 2u];
|
||||
uint8_t *encrypted;
|
||||
uint8_t *decrypted;
|
||||
size_t encrypted_cap;
|
||||
size_t encrypted_len = 0u;
|
||||
size_t decrypted_len = 0u;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
for (index = 0u; index < sizeof(key); index++) {
|
||||
key[index] = (uint8_t)(index * 7u + 3u);
|
||||
other_key[index] = (uint8_t)(key[index] ^ 0x5Au);
|
||||
}
|
||||
for (index = 0u; index < sizeof(nonce); index++) {
|
||||
nonce[index] = (uint8_t)(index * 11u + 1u);
|
||||
}
|
||||
decode_hex(expected_hex, expected, sizeof(expected));
|
||||
encrypted_cap = kc_encrypt_message_bound(sizeof(plaintext) - 1u);
|
||||
encrypted = (uint8_t *)malloc(encrypted_cap);
|
||||
decrypted = (uint8_t *)malloc(kc_decrypt_message_bound(encrypted_cap));
|
||||
expect(encrypted != NULL && decrypted != NULL, "message allocation");
|
||||
if (encrypted == NULL || decrypted == NULL) {
|
||||
free(encrypted);
|
||||
free(decrypted);
|
||||
return;
|
||||
}
|
||||
status = kc_encrypt_message(
|
||||
plaintext, sizeof(plaintext) - 1u, key, sizeof(key), nonce,
|
||||
sizeof(nonce), encrypted, encrypted_cap, &encrypted_len);
|
||||
expect(status == KC_OK, "message encrypt");
|
||||
expect(encrypted_len == sizeof(expected), "message vector length");
|
||||
expect(memcmp(encrypted, expected, sizeof(expected)) == 0,
|
||||
"message vector");
|
||||
expect(kc_looks_encrypted_message(encrypted, encrypted_len),
|
||||
"message detection");
|
||||
status = kc_decrypt_message(encrypted, encrypted_len, key, sizeof(key),
|
||||
decrypted, encrypted_cap, &decrypted_len);
|
||||
expect(status == KC_OK, "message decrypt");
|
||||
expect(decrypted_len == sizeof(plaintext) - 1u,
|
||||
"message plaintext length");
|
||||
expect(memcmp(decrypted, plaintext, decrypted_len) == 0,
|
||||
"message plaintext");
|
||||
status = kc_decrypt_message(encrypted, encrypted_len, other_key,
|
||||
sizeof(other_key), decrypted, encrypted_cap,
|
||||
&decrypted_len);
|
||||
expect(status == KC_ERR_WRONG_KEY, "message wrong key");
|
||||
expect(!kc_looks_encrypted_message((const uint8_t *)"fixture", 7u),
|
||||
"message plain detection");
|
||||
free(encrypted);
|
||||
free(decrypted);
|
||||
}
|
||||
|
||||
static void test_rust_message_vector(void)
|
||||
{
|
||||
static const char encrypted_hex[] =
|
||||
"d0b9d0bcd0b0d18320d187d0b7d0b9d18bd186d18fd18d20d183d0bad18fd184"
|
||||
"d0bcd0b5d0bed0b820d0b9d186d0b0d0bdd18bd18dd188d18220d0b6d185d181"
|
||||
"d180d0b620d18fd180d0bfd186d18020d189d183d0bad0b720d183d183d0b3d183"
|
||||
"d0bcd187d0b5d0b120d0bbd0b2d181d185d18820d189d180d188d0b520d18cd0b4"
|
||||
"d0b7d184d0b7d187d0ba20d185d182d185d18bd18120d189d0bfd18bd0b820d183"
|
||||
"d187d184d185d185d184d0b3d18520d0b6d188d185d0bed0bd20d181";
|
||||
static const uint8_t plaintext[] = "fixture-message-from-rust";
|
||||
uint8_t encrypted[(sizeof(encrypted_hex) - 1u) / 2u];
|
||||
uint8_t decrypted[sizeof(plaintext)];
|
||||
uint8_t key[KC_KEY_SIZE];
|
||||
size_t decrypted_len = 0u;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
decode_hex(encrypted_hex, encrypted, sizeof(encrypted));
|
||||
for (index = 0u; index < sizeof(key); index++) {
|
||||
key[index] = (uint8_t)(index * 7u + 3u);
|
||||
}
|
||||
status = kc_decrypt_message(encrypted, sizeof(encrypted), key, sizeof(key),
|
||||
decrypted, sizeof(decrypted), &decrypted_len);
|
||||
expect(status == KC_OK, "rust vector decrypt");
|
||||
expect(decrypted_len == sizeof(plaintext) - 1u,
|
||||
"rust vector plaintext length");
|
||||
expect(memcmp(decrypted, plaintext, decrypted_len) == 0,
|
||||
"rust vector plaintext");
|
||||
}
|
||||
|
||||
static void test_message_whitespace(void)
|
||||
{
|
||||
static const uint8_t plaintext[] = "fixture-message-02";
|
||||
uint8_t key[KC_KEY_SIZE] = {0u};
|
||||
uint8_t nonce[KC_NONCE_SIZE] = {0u};
|
||||
uint8_t encrypted[256];
|
||||
uint8_t compact[256];
|
||||
uint8_t decrypted[256];
|
||||
size_t encrypted_len = 0u;
|
||||
size_t compact_len = 0u;
|
||||
size_t decrypted_len = 0u;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
status = kc_encrypt_message(plaintext, sizeof(plaintext) - 1u, key,
|
||||
sizeof(key), nonce, sizeof(nonce), encrypted,
|
||||
sizeof(encrypted), &encrypted_len);
|
||||
expect(status == KC_OK, "whitespace encrypt");
|
||||
for (index = 0u; index < encrypted_len; index++) {
|
||||
if (encrypted[index] != (uint8_t)' ') {
|
||||
compact[compact_len++] = encrypted[index];
|
||||
}
|
||||
}
|
||||
status = kc_decrypt_message(compact, compact_len, key, sizeof(key),
|
||||
decrypted, sizeof(decrypted), &decrypted_len);
|
||||
expect(status == KC_OK, "whitespace decrypt");
|
||||
expect(decrypted_len == sizeof(plaintext) - 1u &&
|
||||
memcmp(decrypted, plaintext, decrypted_len) == 0,
|
||||
"whitespace plaintext");
|
||||
}
|
||||
|
||||
static void test_image_blob(void)
|
||||
{
|
||||
static const char expected_hex[] =
|
||||
"4b020000006502070c11161b20252a2f34396de568dde4b8dad019643fcd8e3ae3"
|
||||
"c8679fde726c273de373cf6da15530341e0774ff422257dce47fe8e050ea0923d8"
|
||||
"c3406fe2644dad0aa3b3954e45e87de9bbe54b40921a8927be100eb73f4e3cd30"
|
||||
"b69c9eb64ffddc7e1";
|
||||
uint8_t plaintext[73];
|
||||
uint8_t key[KC_KEY_SIZE];
|
||||
uint8_t nonce[KC_NONCE_SIZE];
|
||||
uint8_t encrypted[128];
|
||||
uint8_t padded[137];
|
||||
uint8_t decrypted[73];
|
||||
uint8_t expected[(sizeof(expected_hex) - 1u) / 2u];
|
||||
size_t encrypted_len = 0u;
|
||||
size_t decrypted_len = 0u;
|
||||
size_t index;
|
||||
kc_status status;
|
||||
|
||||
for (index = 0u; index < sizeof(plaintext); index++) {
|
||||
plaintext[index] = (uint8_t)(index * 13u + 5u);
|
||||
}
|
||||
for (index = 0u; index < sizeof(key); index++) {
|
||||
key[index] = (uint8_t)(index * 3u + 9u);
|
||||
}
|
||||
for (index = 0u; index < sizeof(nonce); index++) {
|
||||
nonce[index] = (uint8_t)(index * 5u + 2u);
|
||||
}
|
||||
decode_hex(expected_hex, expected, sizeof(expected));
|
||||
status = kc_encrypt_image_blob(
|
||||
plaintext, sizeof(plaintext), key, sizeof(key), nonce, sizeof(nonce),
|
||||
encrypted, sizeof(encrypted), &encrypted_len);
|
||||
expect(status == KC_OK, "image encrypt");
|
||||
expect(encrypted_len == sizeof(expected), "image vector length");
|
||||
expect(memcmp(encrypted, expected, sizeof(expected)) == 0,
|
||||
"image vector");
|
||||
memcpy(padded, encrypted, encrypted_len);
|
||||
memset(padded + encrypted_len, 0xA5, sizeof(padded) - encrypted_len);
|
||||
expect(kc_looks_encrypted_image_blob(padded, sizeof(padded)),
|
||||
"image detection");
|
||||
status = kc_decrypt_image_blob(padded, sizeof(padded), key, sizeof(key),
|
||||
decrypted, sizeof(decrypted),
|
||||
&decrypted_len);
|
||||
expect(status == KC_OK, "image decrypt");
|
||||
expect(decrypted_len == sizeof(plaintext), "image plaintext length");
|
||||
expect(memcmp(decrypted, plaintext, sizeof(plaintext)) == 0,
|
||||
"image plaintext");
|
||||
padded[6u + KC_NONCE_SIZE + 3u] ^= 1u;
|
||||
status = kc_decrypt_image_blob(padded, sizeof(padded), key, sizeof(key),
|
||||
decrypted, sizeof(decrypted),
|
||||
&decrypted_len);
|
||||
expect(status == KC_ERR_WRONG_KEY, "image tamper");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_derive_key();
|
||||
test_message();
|
||||
test_rust_message_vector();
|
||||
test_message_whitespace();
|
||||
test_image_blob();
|
||||
failures += kc_run_v2_tests();
|
||||
if (failures != 0) {
|
||||
fprintf(stderr, "%d failures\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 2,
|
||||
"identity_a": {"seed": "b6f9d7d84912bd38434e59688641d2b2f085d948a8b08f1e1c735d6e1a6ff1cb", "identity": "b6f9d7d84912bd38434e59688641d2b2f085d948a8b08f1e1c735d6e1a6ff1cb4b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f", "public": "4b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f", "x25519_public": "a64c2174d06a34a33de6edad864921c2d46342d20a66609378e98fbe8705e75f"},
|
||||
"identity_b": {"seed": "48d781c1efaf217b5abcfc5299e9baf0c63cb598f1fa91ea541ba2e363becf39", "identity": "48d781c1efaf217b5abcfc5299e9baf0c63cb598f1fa91ea541ba2e363becf39c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac", "public": "c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac"},
|
||||
"fingerprint": "044956595089091352292114863300003150226159695553980617795743",
|
||||
"handshake": {"chat_id": 3003, "a_id": 1001, "b_id": 2002,
|
||||
"offer_random": "bad54b420a3e3ab348148020653a1659323a77524fcefa5d41422b5fc23db58625059025c0a489a4",
|
||||
"offer_text": "аиат кбмр дчакйвнд йомехьдъ сткцфп ъсбжд йбглжщжщ дъыочлпя оиечщгзо эяйкзэфя эоошшнкх бччьш кцъе лжсэж рнщъу тжькмхщ рэгац диеюргфэ щкгы ьгъфппб хсйпв бърхг щпхт щщщу щтмя шчичцъдр маубмж угмцтчцз ишмйямж ыобрхс ашфи ныюгшсч ьчсщтрй аяеф сююньу гсыбучщ таат",
|
||||
"pending": "010000000000000bbb00000000000003e900000000000007d225059025c0a489a4f2696bffd0a922044aa4f48e4a1430daa7f33bfb3a6add5a9bb1bc6bf21b69fb020125059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3fd73b186aaa1bdf9855b4559a3d341b9d4e46e2995cc3a3058882fa03a772a1ef83d51ef0d6297883a854797d659ce679933f8ba2f6d120c04c2c34c6cb4af63a30c4fd86db830ac418a21bbf0f11bf2f1cca0907cb48fbcde4c71d8677cc8009",
|
||||
"answer_random": "6df2b2ba4086d86e37905fffcc00af26c939ebb091388c2c09279b9b4696662d",
|
||||
"answer_text": "аибв кбмр дчакйвнд шчъгрнне члююпцн эшчвяпдш пъйи пгзв жнбью юарч брршм схпйцц емтв тячувам тгрдяжы иъялщфр чофжэцж аврс рызьц члмбожк хкхьэ хщяйу лфпоф мйылще эеюбртьъ кццш сьвшищ чдфзэвг ьядяюшэ ьчлачпу улвзулмк млцсцч эгщанщок бвасч ръбпф жангш цикньл юам",
|
||||
"b_state_after_answer": "01000000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f9e07e2de80b36be98d507a694739ec6ad4026a24c74bae1c47b177f39436211b8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fdb31c9b454e0490af9e91797a15d789b8acfa40bfae215668d62168ea0ff29b564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad81719555570000000000000000000000000000000000000000000000000000000000000000183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"accept_random": "b59d5003f3ddfd4cce796922f75772bf4e5f5801b3f6a76aef5fd3c6248be5ed",
|
||||
"a_state_after_accept": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac49cf231828545471b06af45f61c1b38c8047c1c89a8147f952a49d7a6cb06bcf8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f664258082c5166bc5c716c8849264060f858c81d7c1c118e782abb92f06db1a34edd296c0333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad817195555744d190a60281ecd11f872757ae40e4ce1c18ef7ba24ec7022eb64d2f64b46457183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b60000000000000000000000000000"
|
||||
},
|
||||
"messages": [
|
||||
{"from": "a", "random": "d4ceab116d7dbe85e9c23903290173c54549e1fc65dc46ce38637ddb2d26d79b", "content_type": 1, "plaintext": "первое от А", "text": "амщъ ыиьяци аълй рыавй кбпм оижрюасе кхдв ершг шылпэщдр илэяюрб хлэей гсыфщшй эирдбзйу еъмк бгорт цпфзюь яогюь шамусфьх тлйтиым езкъ тцагпъы аъоь вщризт лзрнщ себээю", "sender_state_after": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac49cf231828545471b06af45f61c1b38c8047c1c89a8147f952a49d7a6cb06bcf8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f664258082c5166bc5c716c8849264060f858c81d7c1c118e782abb92f06db1a34edd296c0333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad8171955557a1583be207914454a6b465f8de88282d9de682d26cadffb59cb46531f605eaf9183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b60000000100000000000000000000", "receiver_state_after": "011c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f49cf231828545471b06af45f61c1b38c8047c1c89a8147f952a49d7a6cb06bcf8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fdb31c9b454e0490af9e91797a15d789b8acfa40bfae215668d62168ea0ff29b564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad81719555570333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b6a1583be207914454a6b465f8de88282d9de682d26cadffb59cb46531f605eaf90000000000000001000000000000"},
|
||||
{"from": "a", "random": "d1474d32db3273d2debb0407905ab84691ff679951d05fa7137e05be354ddf8d", "content_type": 1, "plaintext": "второе от А", "text": "амщъ ыиьяци аълй рыавй кбпм оижрюасе кхдв ершг шылпэщдр илэяюрб хлэей гсыфшця хяацх эщисрцюр тюнйгио двфкыжощ кзоы ррщъб ьжмшоэй отидрясш ажтб тщындкк ърйпэ яакет иф", "sender_state_after": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac49cf231828545471b06af45f61c1b38c8047c1c89a8147f952a49d7a6cb06bcf8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f664258082c5166bc5c716c8849264060f858c81d7c1c118e782abb92f06db1a34edd296c0333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad8171955557c8bb9b7e8b0ce6a1d06274c51e9d7f54247b165342e9c8f4098cfb446824c525183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b60000000200000000000000000000", "receiver_state_after": "011c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f49cf231828545471b06af45f61c1b38c8047c1c89a8147f952a49d7a6cb06bcf8ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fdb31c9b454e0490af9e91797a15d789b8acfa40bfae215668d62168ea0ff29b564852fde6203243813e6da35f5e690bba86ed8c014230d9f96bad81719555570333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f183f363f586b79161ea1ebf49998dee2f79475318d3aeeabddc72fd1ab4982b6c8bb9b7e8b0ce6a1d06274c51e9d7f54247b165342e9c8f4098cfb446824c5250000000000000002000000000000"},
|
||||
{"from": "b", "random": "cf2cfb21576b8cf6e74c3f0a3c15ed0d2f4a5c3dc1bcffc303a2c7ff377d0d9d", "content_type": 1, "plaintext": "ответ от Б", "text": "зъол ъцъьд нжоысбс гивдацо чфяицйк снгидд аюфф жктгд тшнгусч убээжеца йщлшясцг егйу хоиьб ййраъзеь йхчяшсгб юкзэ эимйваиа еоьь лщржз ифргыкч дньццвчь рчьад аышж йдь", "sender_state_after": "010c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3fbcd0dd951e5fb5bb0ef5d94211c9940e12edfaf8d3ea18d467d45bc9d5acd3f38ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fa650c7f00bb8a2a2985eb4edcf63b1f352eca03b41b4064bd6f3971d648b0623e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce30333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f3fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc7c8bb9b7e8b0ce6a1d06274c51e9d7f54247b165342e9c8f4098cfb446824c5250000000100000002000000000000", "receiver_state_after": "011e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dacbcd0dd951e5fb5bb0ef5d94211c9940e12edfaf8d3ea18d467d45bc9d5acd3f38ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f664258082c5166bc5c716c8849264060f858c81d7c1c118e782abb92f06db1a34edd296c0333ada39fb201a5a61b0092a0bd8e41a1e044aaa904586078dadfdc92085f7f3e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce3c8bb9b7e8b0ce6a1d06274c51e9d7f54247b165342e9c8f4098cfb446824c5253fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000200000001000000000000"},
|
||||
{"from": "a", "random": "70eb04cfb4aeffc8ca61158713c6fc058a2ecd765162568d74f81e35aa277042", "content_type": 1, "plaintext": "новая эпоха", "text": "рпаг дифйвщъц къъд зздэкг инъгафл шьржщвуч щуоц зхэнащ йьоухмзз ьфвйудк срхмзс зщйсрс тфэхьпд сьгммк вчсосщ бчэжф лфтэе люшях цйирэю тяеиауп чнчцяэш уиюжчоеж хыыфю", "sender_state_after": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808366d059c6ee0646a12de83e63d2584ded95d59264e0db7b5a107b1ac9e19e7b283c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce3ff933bedd81a3f5dcd9971f5203529b33dfde38ea489f30fc362540fde4186c43fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000100000001000000020000", "receiver_state_after": "011c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fa650c7f00bb8a2a2985eb4edcf63b1f352eca03b41b4064bd6f3971d648b0623e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce383c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc7ff933bedd81a3f5dcd9971f5203529b33dfde38ea489f30fc362540fde4186c40000000100000001000000000000"},
|
||||
{"from": "a", "random": "b84c42af1b2f2ab2ff83be322899cfb58b509fa7345dc71471bfe5351800a57f", "content_type": 1, "plaintext": "пропущенное", "text": "рпаг дифйвщъц къъд зздэкг инъгафл шьржщвуч щуоц зхэнащ йьоухмзз ьфвйудк сслмцн уярлцшчп йлсшдрив ыхгччс гзжшщшл гвюдегн кляп рорбю ъйовл гзщьъяу иикщгпч вззсры оки", "delivered_later": true, "sender_state_after": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808366d059c6ee0646a12de83e63d2584ded95d59264e0db7b5a107b1ac9e19e7b283c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce316c6f2cc5df7d3b71c23e1e01e855172b38c18ff46e1bbd647522447a8a493013fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000200000001000000020000"},
|
||||
{"from": "a", "random": "00ae61b2538a7d7eee5bbba1e51d38df7e34c09b5722604533ad7ef849d6ce07", "content_type": 1, "plaintext": "после пропуска", "text": "рпаг дифйвщъц къъд зздэкг инъгафл шьржщвуч щуоц зхэнащ йьоухмзз ьфвйудк стсгшф яойтх вяжлна взтщжя мтнпгс мыкишш хфекй дбифьъхб гжгкжчш евсэ дссрхщсф тншццяй ияпть", "sender_state_after": "010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808366d059c6ee0646a12de83e63d2584ded95d59264e0db7b5a107b1ac9e19e7b283c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce32fa9b2b71434328b6fd5fe56f7ce5a0ef9ebe5e5ca05dc7898086954d2348eeb3fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000300000001000000020000", "receiver_state_after": "011c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fa650c7f00bb8a2a2985eb4edcf63b1f352eca03b41b4064bd6f3971d648b0623e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce383c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc72fa9b2b71434328b6fd5fe56f7ce5a0ef9ebe5e5ca05dc7898086954d2348eeb000000010000000300000000000183c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a70000000160461815c1cb1921858dae81119b6cea06f62887ce69ab20ab98171a3af1e0d2"},
|
||||
{"from": "a", "replay_of": 4, "receiver_state_after": "011c0000000000000bbb00000000000007d200000000000003e925059025c0a489a44b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808fa650c7f00bb8a2a2985eb4edcf63b1f352eca03b41b4064bd6f3971d648b0623e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce383c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc72fa9b2b71434328b6fd5fe56f7ce5a0ef9ebe5e5ca05dc7898086954d2348eeb0000000100000003000000000000"}
|
||||
],
|
||||
"local": {"key": "2cda4308c3607bafefb02ed6ba5c70ce585bc57380529f8f69f74a6be517a2a3", "nonce": "74d0bc853808ab279e9c0c391f3921441abe769c0fa6c213", "aad": "session/1001/3003", "plaintext": "local plaintext", "blob": "74d0bc853808ab279e9c0c391f3921441abe769c0fa6c213b31164230a40494529151ce0abb84c707f316adfeaf6862f66a46870ac368c"},
|
||||
"file": {"key": "d3742621fbbf709565bdf6eb338eb60093feb069c7ba394b94cb7dbfd95309c5", "nonce": "66088d2b4bf42df3459a8a3c", "plaintext_len": 131073, "plaintext_rule": "byte[i] = (i * 7 + 3) & 0xff", "ciphertext_len": 131121, "ciphertext_head": "3217ce586de198a05809479a61cf77c3a48acc399eb945c1cf8ea7abafab07ccf501c38214df4827498ce3b4dee465823a69df319b4e7cce5b1edc17b725566a", "ciphertext_blake2b256": "b8be7d2d04afdfc50223044b5c3c971ca57be7c975c377a8949ad649514a4822"},
|
||||
"export": {"password": "перенос", "memory_kib": 8, "passes": 1, "random": "f65b9b2765c57910d6aa25199b4733520c04f7d3ac73387311ca8d5a24b882fb9cbaaa4522e1e98f", "container": "0000000200000040b6f9d7d84912bd38434e59688641d2b2f085d948a8b08f1e1c735d6e1a6ff1cb4b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f00000130010e0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808366d059c6ee0646a12de83e63d2584ded95d59264e0db7b5a107b1ac9e19e7b283c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce32fa9b2b71434328b6fd5fe56f7ce5a0ef9ebe5e5ca05dc7898086954d2348eeb3fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000300000001000000020000", "blob": "01f65b9b2765c57910d6aa25199b47335200000008000000010c04f7d3ac73387311ca8d5a24b882fb9cbaaa4522e1e98fb0423d71c84f4bb6183c0d8ff086b851b5cfe1c91fde507cc54af4c7f5c85b0fbc2db85dd535fe4d9f192ab18356a0137e3b2e49e9ed01b480bff4617811022c56db1737abe12edec31f740f7e05297e2c2fb6d3731f4c3361c30fc52ad3b43bc767e0acc53c6605c0970dc9af2fe7089cd126da7759776a0ce580cf0df5df2ae1afb4068e34423df4c3ef4abc8ba83a46df08cd16d8695a9957860a6ed7a1820cf1a0373c8ac2c7c1dc104660dcbec6477dee67b11f8f299cded337557ae6938363b1a7386118f57a80cac784bd751ca52324623803c8cbcd38005f72446b21080f5a8a5903725907cb2f9124183ce9316e6ab744ed553a2ddd4c4c50b40cf22b2a0f7d72e25bdf469ddce23cad8f405158120e26aec967a0761e822a8b34f8cb749215c7639865c0e8080e6eb252ec3b3b71a2550a60e3b4c26045afcd0aa91874fd728611a49eb99ce74d310c757ac7b20ccf30690bdf211ca1e83f2eecafcb1b03c11709812a8c3bea19c083a4b95e33efabf58a775f2510185b99d3dcb78a057922abf8a8eb18ad95a6", "opened_container": "0000000200000040b6f9d7d84912bd38434e59688641d2b2f085d948a8b08f1e1c735d6e1a6ff1cb4b985af09a8c956a3f510988908d66c9b24d6dd75bfee416f919dddfa547ed3f00000130010f0000000000000bbb00000000000003e900000000000007d225059025c0a489a4c5f43835a5bafde7d9bdc5c5f7930fd250f19c466879ef02170c218646af4dac639806ee769371938de2767ade1b4040142c44dd50b73a4f50629f1c61b848c78ac59f5f685b62e65a8d92fd2ff3e723486f7802a966d4ace7e5424f66425808366d059c6ee0646a12de83e63d2584ded95d59264e0db7b5a107b1ac9e19e7b283c03222891675656b4439c9d50d0dd0c145e39036453be66eb1ebd68329e3a73e9cbd5b5c234cedc4311a044059d7a7d164aa2d1a08407a9432a4324b0d1ce32fa9b2b71434328b6fd5fe56f7ce5a0ef9ebe5e5ca05dc7898086954d2348eeb3fdcd0eecd2873d0fac3696d393ab29a310b54351378112ab230c192a9700bc70000000300000001000000020000"}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
Monocypher as a whole is dual-licensed. Choose whichever licence you
|
||||
want from the two licences listed below.
|
||||
|
||||
The first licence is a regular 2-clause BSD licence. The second licence
|
||||
is the CC-0 from Creative Commons. It is intended to release Monocypher
|
||||
to the public domain. The BSD licence serves as a fallback option.
|
||||
|
||||
See the individual files for specific information about who contributed
|
||||
to what file during which years. See below for special notes.
|
||||
|
||||
Licence 1 (2-clause BSD)
|
||||
------------------------
|
||||
|
||||
Copyright (c) 2017-2023, Loup Vaillant
|
||||
Copyright (c) 2017-2019, Michael Savage
|
||||
Copyright (c) 2017-2023, Fabio Scotoni
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
Licence 2 (CC-0)
|
||||
----------------
|
||||
|
||||
> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
> LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
> ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
> INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
> REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
> PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
> THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
> HEREUNDER.
|
||||
|
||||
### Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work
|
||||
of authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without
|
||||
fear of later claims of infringement build upon, modify, incorporate in
|
||||
other works, reuse and redistribute as freely as possible in any form
|
||||
whatsoever and for any purposes, including without limitation commercial
|
||||
purposes. These owners may contribute to the Commons to promote the
|
||||
ideal of a free culture and the further production of creative, cultural
|
||||
and scientific works, or to gain reputation or greater distribution for
|
||||
their Work in part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or
|
||||
she is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under
|
||||
its terms, with knowledge of his or her Copyright and Related Rights in
|
||||
the Work and the meaning and intended legal effect of CC0 on those
|
||||
rights.
|
||||
|
||||
1. **Copyright and Related Rights.** A Work made available under CC0 may
|
||||
be protected by copyright and related or neighboring rights
|
||||
("Copyright and Related Rights"). Copyright and Related Rights
|
||||
include, but are not limited to, the following:
|
||||
|
||||
- the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
- moral rights retained by the original author(s) and/or
|
||||
performer(s); publicity and privacy rights pertaining to a person's
|
||||
image or likeness depicted in a Work;
|
||||
- rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
- rights protecting the extraction, dissemination, use and reuse of
|
||||
data in a Work;
|
||||
- database rights (such as those arising under Directive 96/9/EC of
|
||||
the European Parliament and of the Council of 11 March 1996 on the
|
||||
legal protection of databases, and under any national
|
||||
implementation thereof, including any amended or successor version
|
||||
of such directive); and
|
||||
- other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. **Waiver.** To the greatest extent permitted by, but not in
|
||||
contravention of, applicable law, Affirmer hereby overtly, fully,
|
||||
permanently, irrevocably and unconditionally waives, abandons, and
|
||||
surrenders all of Affirmer's Copyright and Related Rights and
|
||||
associated claims and causes of action, whether now known or unknown
|
||||
(including existing as well as future claims and causes of action),
|
||||
in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"Waiver"). Affirmer makes the Waiver for the benefit of each member
|
||||
of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal
|
||||
or equitable action to disrupt the quiet enjoyment of the Work by the
|
||||
public as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. **Public License Fallback.** Should any part of the Waiver for any
|
||||
reason be judged legally invalid or ineffective under applicable law,
|
||||
then the Waiver shall be preserved to the maximum extent permitted
|
||||
taking into account Affirmer's express Statement of Purpose. In
|
||||
addition, to the extent the Waiver is so judged Affirmer hereby
|
||||
grants to each affected person a royalty-free, non transferable, non
|
||||
sublicensable, non exclusive, irrevocable and unconditional license
|
||||
to exercise Affirmer's Copyright and Related Rights in the Work (i)
|
||||
in all territories worldwide, (ii) for the maximum duration provided
|
||||
by applicable law or treaty (including future time extensions), (iii)
|
||||
in any current or future medium and for any number of copies, and
|
||||
(iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the "License"). The
|
||||
License shall be deemed effective as of the date CC0 was applied by
|
||||
Affirmer to the Work. Should any part of the License for any reason
|
||||
be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the
|
||||
remainder of the License, and in such case Affirmer hereby affirms
|
||||
that he or she will not (i) exercise any of his or her remaining
|
||||
Copyright and Related Rights in the Work or (ii) assert any
|
||||
associated claims and causes of action with respect to the Work, in
|
||||
either case contrary to Affirmer's express Statement of Purpose.
|
||||
|
||||
4. **Limitations and Disclaimers.**
|
||||
|
||||
- No trademark or patent rights held by Affirmer are waived,
|
||||
abandoned, surrendered, licensed or otherwise affected by this
|
||||
document.
|
||||
- Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy,
|
||||
or the present or absence of errors, whether or not discoverable,
|
||||
all to the greatest extent permissible under applicable law.
|
||||
- Affirmer disclaims responsibility for clearing rights of other
|
||||
persons that may apply to the Work or any use thereof, including
|
||||
without limitation any person's Copyright and Related Rights in the
|
||||
Work. Further, Affirmer disclaims responsibility for obtaining any
|
||||
necessary consents, permissions or other rights required for any
|
||||
use of the Work.
|
||||
- Affirmer understands and acknowledges that Creative Commons is not
|
||||
a party to this document and has no duty or obligation with respect
|
||||
to this CC0 or use of the Work.
|
||||
|
||||
Special notes
|
||||
-------------
|
||||
|
||||
The files in `tests/externals/` were placed in the public domain by
|
||||
their respective authors. See the `AUTHORS.md` files in each directory.
|
||||
@@ -0,0 +1,5 @@
|
||||
version=4.0.2
|
||||
source=https://github.com/LoupVaillant/Monocypher/archive/refs/tags/4.0.2.tar.gz
|
||||
archive_sha256=bc1ca30b1b2654e4e7daf2492c0d204200e55137f23fda6b7142fd7d523bd6b4
|
||||
monocypher_c_sha256=02174117935699d418443c75a558a287deb06ef8cf7c1adced61d9047d2f323d
|
||||
monocypher_h_sha256=fcaf6ed771358bb4f40fba016f6518ae86ec02b1b877d2cc35ad92d3a26fd7b3
|
||||
+2956
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,321 @@
|
||||
// Monocypher version __git__
|
||||
//
|
||||
// This file is dual-licensed. Choose whichever licence you want from
|
||||
// the two licences listed below.
|
||||
//
|
||||
// The first licence is a regular 2-clause BSD licence. The second licence
|
||||
// is the CC-0 from Creative Commons. It is intended to release Monocypher
|
||||
// to the public domain. The BSD licence serves as a fallback option.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
|
||||
//
|
||||
// ------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright (c) 2017-2019, Loup Vaillant
|
||||
// All rights reserved.
|
||||
//
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// ------------------------------------------------------------------------
|
||||
//
|
||||
// Written in 2017-2019 by Loup Vaillant
|
||||
//
|
||||
// To the extent possible under law, the author(s) have dedicated all copyright
|
||||
// and related neighboring rights to this software to the public domain
|
||||
// worldwide. This software is distributed without any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication along
|
||||
// with this software. If not, see
|
||||
// <https://creativecommons.org/publicdomain/zero/1.0/>
|
||||
|
||||
#ifndef MONOCYPHER_H
|
||||
#define MONOCYPHER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef MONOCYPHER_CPP_NAMESPACE
|
||||
namespace MONOCYPHER_CPP_NAMESPACE {
|
||||
#elif defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Constant time comparisons
|
||||
// -------------------------
|
||||
|
||||
// Return 0 if a and b are equal, -1 otherwise
|
||||
int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
|
||||
int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
|
||||
int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
|
||||
|
||||
|
||||
// Erase sensitive data
|
||||
// --------------------
|
||||
void crypto_wipe(void *secret, size_t size);
|
||||
|
||||
|
||||
// Authenticated encryption
|
||||
// ------------------------
|
||||
void crypto_aead_lock(uint8_t *cipher_text,
|
||||
uint8_t mac [16],
|
||||
const uint8_t key [32],
|
||||
const uint8_t nonce[24],
|
||||
const uint8_t *ad, size_t ad_size,
|
||||
const uint8_t *plain_text, size_t text_size);
|
||||
int crypto_aead_unlock(uint8_t *plain_text,
|
||||
const uint8_t mac [16],
|
||||
const uint8_t key [32],
|
||||
const uint8_t nonce[24],
|
||||
const uint8_t *ad, size_t ad_size,
|
||||
const uint8_t *cipher_text, size_t text_size);
|
||||
|
||||
// Authenticated stream
|
||||
// --------------------
|
||||
typedef struct {
|
||||
uint64_t counter;
|
||||
uint8_t key[32];
|
||||
uint8_t nonce[8];
|
||||
} crypto_aead_ctx;
|
||||
|
||||
void crypto_aead_init_x(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[24]);
|
||||
void crypto_aead_init_djb(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[8]);
|
||||
void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[12]);
|
||||
|
||||
void crypto_aead_write(crypto_aead_ctx *ctx,
|
||||
uint8_t *cipher_text,
|
||||
uint8_t mac[16],
|
||||
const uint8_t *ad , size_t ad_size,
|
||||
const uint8_t *plain_text, size_t text_size);
|
||||
int crypto_aead_read(crypto_aead_ctx *ctx,
|
||||
uint8_t *plain_text,
|
||||
const uint8_t mac[16],
|
||||
const uint8_t *ad , size_t ad_size,
|
||||
const uint8_t *cipher_text, size_t text_size);
|
||||
|
||||
|
||||
// General purpose hash (BLAKE2b)
|
||||
// ------------------------------
|
||||
|
||||
// Direct interface
|
||||
void crypto_blake2b(uint8_t *hash, size_t hash_size,
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
|
||||
const uint8_t *key, size_t key_size,
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
// Incremental interface
|
||||
typedef struct {
|
||||
// Do not rely on the size or contents of this type,
|
||||
// for they may change without notice.
|
||||
uint64_t hash[8];
|
||||
uint64_t input_offset[2];
|
||||
uint64_t input[16];
|
||||
size_t input_idx;
|
||||
size_t hash_size;
|
||||
} crypto_blake2b_ctx;
|
||||
|
||||
void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
|
||||
void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
|
||||
const uint8_t *key, size_t key_size);
|
||||
void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
|
||||
const uint8_t *message, size_t message_size);
|
||||
void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
|
||||
|
||||
|
||||
// Password key derivation (Argon2)
|
||||
// --------------------------------
|
||||
#define CRYPTO_ARGON2_D 0
|
||||
#define CRYPTO_ARGON2_I 1
|
||||
#define CRYPTO_ARGON2_ID 2
|
||||
|
||||
typedef struct {
|
||||
uint32_t algorithm; // Argon2d, Argon2i, Argon2id
|
||||
uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
|
||||
uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
|
||||
uint32_t nb_lanes; // parallelism level (single threaded anyway)
|
||||
} crypto_argon2_config;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *pass;
|
||||
const uint8_t *salt;
|
||||
uint32_t pass_size;
|
||||
uint32_t salt_size; // 16 bytes recommended
|
||||
} crypto_argon2_inputs;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *key; // may be NULL if no key
|
||||
const uint8_t *ad; // may be NULL if no additional data
|
||||
uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
|
||||
uint32_t ad_size; // 0 if no additional data
|
||||
} crypto_argon2_extras;
|
||||
|
||||
extern const crypto_argon2_extras crypto_argon2_no_extras;
|
||||
|
||||
void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
|
||||
crypto_argon2_config config,
|
||||
crypto_argon2_inputs inputs,
|
||||
crypto_argon2_extras extras);
|
||||
|
||||
|
||||
// Key exchange (X-25519)
|
||||
// ----------------------
|
||||
|
||||
// Shared secrets are not quite random.
|
||||
// Hash them to derive an actual shared key.
|
||||
void crypto_x25519_public_key(uint8_t public_key[32],
|
||||
const uint8_t secret_key[32]);
|
||||
void crypto_x25519(uint8_t raw_shared_secret[32],
|
||||
const uint8_t your_secret_key [32],
|
||||
const uint8_t their_public_key [32]);
|
||||
|
||||
// Conversion to EdDSA
|
||||
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
|
||||
|
||||
// scalar "division"
|
||||
// Used for OPRF. Be aware that exponential blinding is less secure
|
||||
// than Diffie-Hellman key exchange.
|
||||
void crypto_x25519_inverse(uint8_t blind_salt [32],
|
||||
const uint8_t private_key[32],
|
||||
const uint8_t curve_point[32]);
|
||||
|
||||
// "Dirty" versions of x25519_public_key().
|
||||
// Use with crypto_elligator_rev().
|
||||
// Leaks 3 bits of the private key.
|
||||
void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
|
||||
void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
|
||||
|
||||
|
||||
// Signatures
|
||||
// ----------
|
||||
|
||||
// EdDSA with curve25519 + BLAKE2b
|
||||
void crypto_eddsa_key_pair(uint8_t secret_key[64],
|
||||
uint8_t public_key[32],
|
||||
uint8_t seed[32]);
|
||||
void crypto_eddsa_sign(uint8_t signature [64],
|
||||
const uint8_t secret_key[64],
|
||||
const uint8_t *message, size_t message_size);
|
||||
int crypto_eddsa_check(const uint8_t signature [64],
|
||||
const uint8_t public_key[32],
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
// Conversion to X25519
|
||||
void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
|
||||
|
||||
// EdDSA building blocks
|
||||
void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
|
||||
void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
|
||||
void crypto_eddsa_mul_add(uint8_t r[32],
|
||||
const uint8_t a[32],
|
||||
const uint8_t b[32],
|
||||
const uint8_t c[32]);
|
||||
void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
|
||||
int crypto_eddsa_check_equation(const uint8_t signature[64],
|
||||
const uint8_t public_key[32],
|
||||
const uint8_t h_ram[32]);
|
||||
|
||||
|
||||
// Chacha20
|
||||
// --------
|
||||
|
||||
// Specialised hash.
|
||||
// Used to hash X25519 shared secrets.
|
||||
void crypto_chacha20_h(uint8_t out[32],
|
||||
const uint8_t key[32],
|
||||
const uint8_t in [16]);
|
||||
|
||||
// Unauthenticated stream cipher.
|
||||
// Don't forget to add authentication.
|
||||
uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[8],
|
||||
uint64_t ctr);
|
||||
uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[12],
|
||||
uint32_t ctr);
|
||||
uint64_t crypto_chacha20_x(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[24],
|
||||
uint64_t ctr);
|
||||
|
||||
|
||||
// Poly 1305
|
||||
// ---------
|
||||
|
||||
// This is a *one time* authenticator.
|
||||
// Disclosing the mac reveals the key.
|
||||
// See crypto_lock() on how to use it properly.
|
||||
|
||||
// Direct interface
|
||||
void crypto_poly1305(uint8_t mac[16],
|
||||
const uint8_t *message, size_t message_size,
|
||||
const uint8_t key[32]);
|
||||
|
||||
// Incremental interface
|
||||
typedef struct {
|
||||
// Do not rely on the size or contents of this type,
|
||||
// for they may change without notice.
|
||||
uint8_t c[16]; // chunk of the message
|
||||
size_t c_idx; // How many bytes are there in the chunk.
|
||||
uint32_t r [4]; // constant multiplier (from the secret key)
|
||||
uint32_t pad[4]; // random number added at the end (from the secret key)
|
||||
uint32_t h [5]; // accumulated hash
|
||||
} crypto_poly1305_ctx;
|
||||
|
||||
void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
|
||||
void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
|
||||
const uint8_t *message, size_t message_size);
|
||||
void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
|
||||
|
||||
|
||||
// Elligator 2
|
||||
// -----------
|
||||
|
||||
// Elligator mappings proper
|
||||
void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
|
||||
int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
|
||||
uint8_t tweak);
|
||||
|
||||
// Easy to use key pair generation
|
||||
void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
|
||||
uint8_t seed[32]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // MONOCYPHER_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SDK_ROOT=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}
|
||||
NDK_VERSION=${NDK_VERSION:-28.2.13676358}
|
||||
ANDROID_API=${ANDROID_API:-23}
|
||||
|
||||
if [ -z "$SDK_ROOT" ]; then
|
||||
SDK_ROOT="$HOME/Library/Android/sdk"
|
||||
fi
|
||||
|
||||
NDK_ROOT=${ANDROID_NDK_HOME:-"$SDK_ROOT/ndk/$NDK_VERSION"}
|
||||
TOOLCHAIN_FILE="$NDK_ROOT/build/cmake/android.toolchain.cmake"
|
||||
|
||||
if [ ! -f "$TOOLCHAIN_FILE" ]; then
|
||||
echo "NDK toolchain not found: $TOOLCHAIN_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for ABI in arm64-v8a armeabi-v7a x86_64 x86; do
|
||||
OUT="$ROOT/build/android/$ABI"
|
||||
cmake -S "$ROOT" -B "$OUT" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \
|
||||
-DANDROID_ABI="$ABI" \
|
||||
-DANDROID_PLATFORM="android-$ANDROID_API" \
|
||||
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON \
|
||||
-DCMAKE_BUILD_TYPE=MinSizeRel \
|
||||
-DKC_BUILD_TESTS=OFF >/dev/null
|
||||
cmake --build "$OUT" --config MinSizeRel >/dev/null
|
||||
"$NDK_ROOT/toolchains/llvm/prebuilt/"*/bin/llvm-strip --strip-unneeded "$OUT/libkomet_crypto.so"
|
||||
echo "$ABI: $OUT/libkomet_crypto.so"
|
||||
done
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "komet_crypto.h"
|
||||
|
||||
#include "monocypher.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *seed_dir;
|
||||
|
||||
static void emit_seed(unsigned char op, const void *bytes, size_t len)
|
||||
{
|
||||
char path[512];
|
||||
static unsigned int counter;
|
||||
FILE *fh;
|
||||
|
||||
if (seed_dir == NULL) {
|
||||
return;
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s/seed_%02u_%u", seed_dir, (unsigned int)op,
|
||||
counter++);
|
||||
fh = fopen(path, "wb");
|
||||
if (fh == NULL) {
|
||||
return;
|
||||
}
|
||||
fputc((int)op, fh);
|
||||
fwrite(bytes, 1u, len, fh);
|
||||
fclose(fh);
|
||||
}
|
||||
|
||||
static void fill(uint8_t *out, size_t len, uint64_t seed)
|
||||
{
|
||||
uint64_t x = seed;
|
||||
size_t index;
|
||||
|
||||
for (index = 0u; index < len; index++) {
|
||||
x ^= x >> 12u;
|
||||
x ^= x << 25u;
|
||||
x ^= x >> 27u;
|
||||
out[index] = (uint8_t)((x * 0x2545F4914F6CDD1Dull) >> 56u);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_hex(const char *key, const uint8_t *bytes, size_t len)
|
||||
{
|
||||
size_t index;
|
||||
|
||||
printf("\"%s\": \"", key);
|
||||
for (index = 0u; index < len; index++) {
|
||||
printf("%02x", bytes[index]);
|
||||
}
|
||||
printf("\"");
|
||||
}
|
||||
|
||||
static void print_text(const char *key, const uint8_t *text, size_t len)
|
||||
{
|
||||
printf("\"%s\": \"", key);
|
||||
fwrite(text, 1u, len, stdout);
|
||||
printf("\"");
|
||||
}
|
||||
|
||||
static void must(kc_status status, const char *what)
|
||||
{
|
||||
if (status != KC_OK) {
|
||||
fprintf(stderr, "%s: %s\n", what, kc_status_string(status));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int64_t id;
|
||||
uint8_t identity[KC_IDENTITY_SIZE];
|
||||
uint8_t state[KC_SESSION_STATE_MAX];
|
||||
size_t state_len;
|
||||
} party;
|
||||
|
||||
static void emit_message(party *from,
|
||||
party *to,
|
||||
const char *plaintext,
|
||||
uint64_t seed,
|
||||
int comma)
|
||||
{
|
||||
uint8_t random[KC_ENCRYPT_RANDOM_SIZE];
|
||||
uint8_t text[2048];
|
||||
uint8_t opened[512];
|
||||
uint8_t content_type = 0u;
|
||||
size_t text_len = 0u;
|
||||
size_t opened_len = 0u;
|
||||
|
||||
fill(random, sizeof(random), seed);
|
||||
must(kc_session_encrypt(from->state, from->state_len, KC_CONTENT_TEXT,
|
||||
(const uint8_t *)plaintext, strlen(plaintext),
|
||||
random, sizeof(random), text, sizeof(text),
|
||||
&text_len, from->state, sizeof(from->state),
|
||||
&from->state_len),
|
||||
"encrypt");
|
||||
must(kc_session_decrypt(to->state, to->state_len, text, text_len,
|
||||
&content_type, opened, sizeof(opened), &opened_len,
|
||||
to->state, sizeof(to->state), &to->state_len),
|
||||
"decrypt");
|
||||
if (opened_len != strlen(plaintext) ||
|
||||
memcmp(opened, plaintext, opened_len) != 0) {
|
||||
fprintf(stderr, "vector mismatch\n");
|
||||
exit(1);
|
||||
}
|
||||
printf(" {\"from\": \"%s\", ", from->id < to->id ? "a" : "b");
|
||||
print_hex("random", random, sizeof(random));
|
||||
printf(", \"content_type\": 1, ");
|
||||
print_text("plaintext", (const uint8_t *)plaintext, strlen(plaintext));
|
||||
printf(", ");
|
||||
print_text("text", text, text_len);
|
||||
printf(", ");
|
||||
print_hex("sender_state_after", from->state, from->state_len);
|
||||
printf(", ");
|
||||
print_hex("receiver_state_after", to->state, to->state_len);
|
||||
printf("}%s\n", comma ? "," : "");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
party a;
|
||||
party b;
|
||||
uint8_t seed[KC_SEED_SIZE];
|
||||
uint8_t public_a[KC_PUBLIC_KEY_SIZE];
|
||||
uint8_t public_b[KC_PUBLIC_KEY_SIZE];
|
||||
uint8_t x_public_a[32];
|
||||
uint8_t random[KC_OFFER_RANDOM_SIZE];
|
||||
uint8_t offer_text[KC_HANDSHAKE_TEXT_BOUND];
|
||||
uint8_t answer_text[KC_HANDSHAKE_TEXT_BOUND];
|
||||
uint8_t pending[KC_PENDING_STATE_SIZE];
|
||||
uint8_t digits[KC_FINGERPRINT_DIGITS];
|
||||
uint8_t local_key[KC_LOCAL_KEY_SIZE];
|
||||
uint8_t local_nonce[KC_LOCAL_NONCE_SIZE];
|
||||
uint8_t local_blob[128];
|
||||
uint8_t file_key[KC_FILE_KEY_SIZE];
|
||||
uint8_t file_nonce[KC_FILE_NONCE_SIZE];
|
||||
uint8_t file_ctx[KC_FILE_CTX_SIZE];
|
||||
uint8_t *file_plain;
|
||||
uint8_t *file_cipher;
|
||||
uint8_t file_hash[32];
|
||||
uint8_t container[4096];
|
||||
uint8_t export_random[KC_EXPORT_RANDOM_SIZE];
|
||||
uint8_t export_blob[4096];
|
||||
uint8_t opened[4096];
|
||||
uint8_t skipped_text[2048];
|
||||
uint8_t content_type = 0u;
|
||||
size_t len = 0u;
|
||||
size_t offer_len = 0u;
|
||||
size_t answer_len = 0u;
|
||||
size_t pending_len = 0u;
|
||||
size_t file_len = 131073u;
|
||||
size_t cipher_len = 0u;
|
||||
size_t offset;
|
||||
size_t container_len;
|
||||
size_t export_len = 0u;
|
||||
size_t opened_len = 0u;
|
||||
size_t skipped_len = 0u;
|
||||
static const uint8_t local_aad[] = "session/1001/3003";
|
||||
static const uint8_t local_plain[] = "local plaintext";
|
||||
static const uint8_t password[] = "перенос";
|
||||
size_t index;
|
||||
|
||||
if (argc == 3 && strcmp(argv[1], "--seed-corpus") == 0) {
|
||||
seed_dir = argv[2];
|
||||
}
|
||||
|
||||
memset(&a, 0, sizeof(a));
|
||||
memset(&b, 0, sizeof(b));
|
||||
a.id = 1001;
|
||||
b.id = 2002;
|
||||
printf("{\n \"version\": 2,\n");
|
||||
|
||||
fill(seed, sizeof(seed), 101u);
|
||||
must(kc_identity_create(seed, sizeof(seed), a.identity, sizeof(a.identity),
|
||||
&len),
|
||||
"identity a");
|
||||
printf(" \"identity_a\": {");
|
||||
print_hex("seed", seed, sizeof(seed));
|
||||
printf(", ");
|
||||
print_hex("identity", a.identity, sizeof(a.identity));
|
||||
must(kc_identity_public_key(a.identity, sizeof(a.identity), public_a,
|
||||
sizeof(public_a), &len),
|
||||
"public a");
|
||||
printf(", ");
|
||||
print_hex("public", public_a, sizeof(public_a));
|
||||
crypto_eddsa_to_x25519(x_public_a, public_a);
|
||||
printf(", ");
|
||||
print_hex("x25519_public", x_public_a, sizeof(x_public_a));
|
||||
printf("},\n");
|
||||
|
||||
fill(seed, sizeof(seed), 102u);
|
||||
must(kc_identity_create(seed, sizeof(seed), b.identity, sizeof(b.identity),
|
||||
&len),
|
||||
"identity b");
|
||||
must(kc_identity_public_key(b.identity, sizeof(b.identity), public_b,
|
||||
sizeof(public_b), &len),
|
||||
"public b");
|
||||
printf(" \"identity_b\": {");
|
||||
print_hex("seed", seed, sizeof(seed));
|
||||
printf(", ");
|
||||
print_hex("identity", b.identity, sizeof(b.identity));
|
||||
printf(", ");
|
||||
print_hex("public", public_b, sizeof(public_b));
|
||||
printf("},\n");
|
||||
|
||||
must(kc_fingerprint(a.id, public_a, sizeof(public_a), b.id, public_b,
|
||||
sizeof(public_b), digits, sizeof(digits), &len),
|
||||
"fingerprint");
|
||||
printf(" ");
|
||||
print_text("fingerprint", digits, sizeof(digits));
|
||||
printf(",\n");
|
||||
|
||||
printf(" \"handshake\": {\"chat_id\": 3003, \"a_id\": 1001, \"b_id\": 2002,\n");
|
||||
fill(random, KC_OFFER_RANDOM_SIZE, 201u);
|
||||
must(kc_session_offer(a.identity, sizeof(a.identity), 3003, a.id, b.id,
|
||||
random, KC_OFFER_RANDOM_SIZE, offer_text,
|
||||
sizeof(offer_text), &offer_len, pending,
|
||||
sizeof(pending), &pending_len),
|
||||
"offer");
|
||||
printf(" ");
|
||||
print_hex("offer_random", random, KC_OFFER_RANDOM_SIZE);
|
||||
printf(",\n ");
|
||||
print_text("offer_text", offer_text, offer_len);
|
||||
emit_seed(0u, offer_text, offer_len);
|
||||
emit_seed(9u, offer_text, offer_len);
|
||||
emit_seed(4u, offer_text, offer_len);
|
||||
printf(",\n ");
|
||||
print_hex("pending", pending, pending_len);
|
||||
printf(",\n");
|
||||
fill(random, KC_ANSWER_RANDOM_SIZE, 202u);
|
||||
must(kc_session_answer(b.identity, sizeof(b.identity), 3003, b.id, a.id,
|
||||
offer_text, offer_len, NULL, 0u, random,
|
||||
KC_ANSWER_RANDOM_SIZE, answer_text,
|
||||
sizeof(answer_text), &answer_len, b.state,
|
||||
sizeof(b.state), &b.state_len),
|
||||
"answer");
|
||||
printf(" ");
|
||||
print_hex("answer_random", random, KC_ANSWER_RANDOM_SIZE);
|
||||
printf(",\n ");
|
||||
print_text("answer_text", answer_text, answer_len);
|
||||
emit_seed(9u, answer_text, answer_len);
|
||||
emit_seed(5u, answer_text, answer_len);
|
||||
emit_seed(3u, b.state, b.state_len);
|
||||
printf(",\n ");
|
||||
print_hex("b_state_after_answer", b.state, b.state_len);
|
||||
printf(",\n");
|
||||
fill(random, KC_ACCEPT_RANDOM_SIZE, 203u);
|
||||
must(kc_session_accept(a.identity, sizeof(a.identity), pending, pending_len,
|
||||
answer_text, answer_len, NULL, 0u, random,
|
||||
KC_ACCEPT_RANDOM_SIZE, a.state, sizeof(a.state),
|
||||
&a.state_len),
|
||||
"accept");
|
||||
printf(" ");
|
||||
print_hex("accept_random", random, KC_ACCEPT_RANDOM_SIZE);
|
||||
printf(",\n ");
|
||||
print_hex("a_state_after_accept", a.state, a.state_len);
|
||||
printf("\n },\n");
|
||||
|
||||
printf(" \"messages\": [\n");
|
||||
emit_message(&a, &b, "первое от А", 301u, 1);
|
||||
emit_message(&a, &b, "второе от А", 302u, 1);
|
||||
emit_message(&b, &a, "ответ от Б", 303u, 1);
|
||||
emit_message(&a, &b, "новая эпоха", 304u, 1);
|
||||
fill(random, KC_ENCRYPT_RANDOM_SIZE, 305u);
|
||||
must(kc_session_encrypt(a.state, a.state_len, KC_CONTENT_TEXT,
|
||||
(const uint8_t *)"пропущенное", 22u, random,
|
||||
KC_ENCRYPT_RANDOM_SIZE, skipped_text,
|
||||
sizeof(skipped_text), &skipped_len, a.state,
|
||||
sizeof(a.state), &a.state_len),
|
||||
"skipped encrypt");
|
||||
emit_seed(1u, skipped_text, skipped_len);
|
||||
emit_seed(2u, skipped_text, skipped_len);
|
||||
emit_seed(0u, skipped_text, skipped_len);
|
||||
printf(" {\"from\": \"a\", ");
|
||||
print_hex("random", random, KC_ENCRYPT_RANDOM_SIZE);
|
||||
printf(", \"content_type\": 1, ");
|
||||
print_text("plaintext", (const uint8_t *)"пропущенное", 22u);
|
||||
printf(", ");
|
||||
print_text("text", skipped_text, skipped_len);
|
||||
printf(", \"delivered_later\": true, ");
|
||||
print_hex("sender_state_after", a.state, a.state_len);
|
||||
printf("},\n");
|
||||
emit_message(&a, &b, "после пропуска", 306u, 1);
|
||||
must(kc_session_decrypt(b.state, b.state_len, skipped_text, skipped_len,
|
||||
&content_type, opened, sizeof(opened), &opened_len,
|
||||
b.state, sizeof(b.state), &b.state_len),
|
||||
"skipped decrypt");
|
||||
printf(" {\"from\": \"a\", \"replay_of\": 4, ");
|
||||
print_hex("receiver_state_after", b.state, b.state_len);
|
||||
printf("}\n ],\n");
|
||||
|
||||
fill(local_key, sizeof(local_key), 401u);
|
||||
fill(local_nonce, sizeof(local_nonce), 402u);
|
||||
must(kc_local_seal(local_key, sizeof(local_key), local_nonce,
|
||||
sizeof(local_nonce), local_aad, sizeof(local_aad) - 1u,
|
||||
local_plain, sizeof(local_plain) - 1u, local_blob,
|
||||
sizeof(local_blob), &len),
|
||||
"local seal");
|
||||
printf(" \"local\": {");
|
||||
print_hex("key", local_key, sizeof(local_key));
|
||||
printf(", ");
|
||||
print_hex("nonce", local_nonce, sizeof(local_nonce));
|
||||
printf(", ");
|
||||
print_text("aad", local_aad, sizeof(local_aad) - 1u);
|
||||
printf(", ");
|
||||
print_text("plaintext", local_plain, sizeof(local_plain) - 1u);
|
||||
emit_seed(8u, local_blob, len);
|
||||
printf(", ");
|
||||
print_hex("blob", local_blob, len);
|
||||
printf("},\n");
|
||||
|
||||
fill(file_key, sizeof(file_key), 501u);
|
||||
fill(file_nonce, sizeof(file_nonce), 502u);
|
||||
file_plain = (uint8_t *)malloc(file_len);
|
||||
file_cipher = (uint8_t *)malloc(kc_file_seal_bound(file_len));
|
||||
for (index = 0u; index < file_len; index++) {
|
||||
file_plain[index] = (uint8_t)((index * 7u + 3u) & 0xFFu);
|
||||
}
|
||||
must(kc_file_seal_init(file_ctx, sizeof(file_ctx), file_key,
|
||||
sizeof(file_key), file_nonce, sizeof(file_nonce)),
|
||||
"file init");
|
||||
offset = 0u;
|
||||
for (;;) {
|
||||
size_t take = file_len - offset < KC_FILE_CHUNK_SIZE
|
||||
? file_len - offset
|
||||
: KC_FILE_CHUNK_SIZE;
|
||||
int last = file_len - offset <= KC_FILE_CHUNK_SIZE;
|
||||
must(kc_file_seal_chunk(file_ctx, sizeof(file_ctx), file_plain + offset,
|
||||
take, last, file_cipher + cipher_len,
|
||||
take + KC_TAG_SIZE, &len),
|
||||
"file chunk");
|
||||
cipher_len += len;
|
||||
offset += take;
|
||||
if (last) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
crypto_blake2b(file_hash, sizeof(file_hash), file_cipher, cipher_len);
|
||||
printf(" \"file\": {");
|
||||
print_hex("key", file_key, sizeof(file_key));
|
||||
printf(", ");
|
||||
print_hex("nonce", file_nonce, sizeof(file_nonce));
|
||||
printf(", \"plaintext_len\": %u, \"plaintext_rule\": \"byte[i] = (i * 7 + 3) & 0xff\", \"ciphertext_len\": %u, ",
|
||||
(unsigned int)file_len, (unsigned int)cipher_len);
|
||||
emit_seed(6u, file_cipher, KC_FILE_CHUNK_SIZE + KC_TAG_SIZE);
|
||||
print_hex("ciphertext_head", file_cipher, 64u);
|
||||
printf(", ");
|
||||
print_hex("ciphertext_blake2b256", file_hash, sizeof(file_hash));
|
||||
printf("},\n");
|
||||
free(file_plain);
|
||||
free(file_cipher);
|
||||
|
||||
memset(container, 0, sizeof(container));
|
||||
container[3] = 2u;
|
||||
offset = 4u;
|
||||
container[offset + 3u] = (uint8_t)KC_IDENTITY_SIZE;
|
||||
offset += 4u;
|
||||
memcpy(container + offset, a.identity, KC_IDENTITY_SIZE);
|
||||
offset += KC_IDENTITY_SIZE;
|
||||
container[offset + 2u] = (uint8_t)(a.state_len >> 8u);
|
||||
container[offset + 3u] = (uint8_t)a.state_len;
|
||||
offset += 4u;
|
||||
memcpy(container + offset, a.state, a.state_len);
|
||||
offset += a.state_len;
|
||||
container_len = offset;
|
||||
fill(export_random, sizeof(export_random), 601u);
|
||||
must(kc_export_seal(password, sizeof(password) - 1u, 8u, 1u, export_random,
|
||||
sizeof(export_random), container, container_len,
|
||||
export_blob, sizeof(export_blob), &export_len),
|
||||
"export seal");
|
||||
must(kc_export_open(password, sizeof(password) - 1u, export_blob,
|
||||
export_len, opened, sizeof(opened), &opened_len),
|
||||
"export open");
|
||||
printf(" \"export\": {");
|
||||
print_text("password", password, sizeof(password) - 1u);
|
||||
printf(", \"memory_kib\": 8, \"passes\": 1, ");
|
||||
print_hex("random", export_random, sizeof(export_random));
|
||||
printf(", ");
|
||||
print_hex("container", container, container_len);
|
||||
emit_seed(7u, export_blob, export_len);
|
||||
printf(", ");
|
||||
print_hex("blob", export_blob, export_len);
|
||||
printf(", ");
|
||||
print_hex("opened_container", opened, opened_len);
|
||||
printf("}\n}\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.dart_tool/
|
||||
build/
|
||||
rust/target/
|
||||
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026-present dkaraush <dkaraush@gmail.com>
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,29 @@
|
||||
# komet_tlottie
|
||||
|
||||
Local FFI plugin that ships [tlottie](https://github.com/dkaraush/tlottie) — a Lottie
|
||||
renderer written in Rust — to every native Komet build. It carries only the native
|
||||
library and its raw bindings (`TlottieBindings`); playback, the isolate pool and the
|
||||
frame caches live in the app under `lib/core/media/tlottie/`.
|
||||
|
||||
## How it is built
|
||||
|
||||
`rust/` is a wrapper crate that depends on tlottie by an exact git `rev` with the
|
||||
`c-api` feature and builds it as `cdylib` + `staticlib`, so the `tlottie_*` symbols of
|
||||
the C ABI end up in `libkomet_tlottie`. `cargokit/` is vendored from `fjs` 3.3.0 (the
|
||||
copy that also aligns Android arm64/x86_64 ELF segments to 16 KB pages) and compiles the
|
||||
crate during the Flutter build with the same toolchain as `kolibri` and `fjs`;
|
||||
`cargokit_options.yaml` at the app root keeps it building from source.
|
||||
|
||||
| Platform | Artifact | Loaded via |
|
||||
|----------|----------|------------|
|
||||
| Android | `libkomet_tlottie.so` per ABI, packaged into the APK | `DynamicLibrary.open('libkomet_tlottie.so')` |
|
||||
| Linux | `libkomet_tlottie.so` in the bundle's `lib/` | `DynamicLibrary.open('libkomet_tlottie.so')` |
|
||||
| Windows | `komet_tlottie.dll` next to the executable | `DynamicLibrary.open('komet_tlottie.dll')` |
|
||||
| iOS / macOS | `libkomet_tlottie.a`, `-force_load`ed into the pod | `DynamicLibrary.process()`, falling back to `komet_tlottie.framework` |
|
||||
|
||||
Web has no native path; the app plays animations with the pure-Dart `lottie` package there.
|
||||
|
||||
## Bumping tlottie
|
||||
|
||||
Change `rev` in `rust/Cargo.toml`, run `cargo update -p tlottie` in `rust/`, and check
|
||||
that `tlottie.h` upstream still matches the signatures in `lib/komet_tlottie.dart`.
|
||||
@@ -0,0 +1,43 @@
|
||||
group = "pw.komet.komet_tlottie"
|
||||
version = "0.1.0"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
|
||||
android {
|
||||
namespace = "pw.komet.komet_tlottie"
|
||||
compileSdk = 35
|
||||
ndkVersion = android.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../cargokit/gradle/plugin.gradle"
|
||||
cargokit {
|
||||
manifestDir = "../rust"
|
||||
libname = "komet_tlottie"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
Copyright 2022 Matej Knopp
|
||||
|
||||
================================================================================
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
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.
|
||||
|
||||
================================================================================
|
||||
|
||||
APACHE LICENSE, VERSION 2.0
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||
with flutter plugins and packages.
|
||||
|
||||
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||
for a tutorial on how to use Cargokit.
|
||||
|
||||
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||
|
||||
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||
|
||||
export PATH=${NEW_PATH%?} # remove trailing :
|
||||
|
||||
env
|
||||
|
||||
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||
|
||||
# Arctive architectures (arm64, armv7, x86_64), space separated.
|
||||
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||
|
||||
# Current build configuration (Debug, Release)
|
||||
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||
|
||||
# Path to directory containing Cargo.toml.
|
||||
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||
|
||||
# Temporary directory for build artifacts.
|
||||
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||
|
||||
# Output directory for final artifacts.
|
||||
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||
|
||||
# Directory to store built tool artifacts.
|
||||
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||
|
||||
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||
|
||||
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||
)
|
||||
|
||||
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||
do
|
||||
if [[ -f "$path" ]]; then
|
||||
source "$path"
|
||||
fi
|
||||
done
|
||||
|
||||
sh "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||
|
||||
# Make a symlink from built framework to phony file, which will be used as input to
|
||||
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||
# attribute on custom build phase)
|
||||
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||
@@ -0,0 +1,123 @@
|
||||
# fjs Cargokit Build Tool
|
||||
|
||||
This package is the local Cargokit build tool used by fjs. It is based on the
|
||||
Flutter Rust Bridge Cargokit integration and is vendored here so the Flutter
|
||||
plugin can build Rust artifacts without depending on a globally installed Dart
|
||||
package.
|
||||
|
||||
The entrypoint is normally invoked through:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh <command> ...
|
||||
```
|
||||
|
||||
`run_build_tool.sh` creates a temporary Dart runner, compiles it to a kernel
|
||||
snapshot, and invalidates that snapshot when this package changes.
|
||||
|
||||
## Commands
|
||||
|
||||
### build-pod
|
||||
|
||||
Used by the iOS, macOS, and shared Darwin podspecs:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh build-pod libfjs fjs
|
||||
```
|
||||
|
||||
Flutter and CocoaPods provide the required `CARGOKIT_*` and Xcode deployment
|
||||
environment variables. The command builds the Rust crate for the active Darwin
|
||||
platform and copies `libfjs.a` into the configured build output directory.
|
||||
|
||||
### build-gradle
|
||||
|
||||
Used by Android builds. Flutter/Gradle provide Android SDK, NDK, target platform,
|
||||
and output directories through `CARGOKIT_*` environment variables.
|
||||
|
||||
### build-cmake
|
||||
|
||||
Used by desktop CMake-based builds. The command builds the Rust crate for the
|
||||
requested desktop target and copies the produced dynamic library into the
|
||||
configured output directory.
|
||||
|
||||
### gen-key
|
||||
|
||||
Generates an Ed25519 key pair for signing precompiled Cargokit artifacts:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh gen-key
|
||||
```
|
||||
|
||||
Store the private key in CI secrets only. Put the public key in
|
||||
`libfjs/cargokit.yaml`.
|
||||
|
||||
### precompile-binaries
|
||||
|
||||
Builds release Rust artifacts, signs them, and uploads them to a GitHub Release
|
||||
tag named `precompiled_<crate-hash>`:
|
||||
|
||||
```sh
|
||||
PRIVATE_KEY=<hex-private-key> \
|
||||
GITHUB_TOKEN=<github-token> \
|
||||
cargokit/run_build_tool.sh precompile-binaries \
|
||||
--repository fluttercandies/fjs \
|
||||
--manifest-dir libfjs
|
||||
```
|
||||
|
||||
Pass `--target <rust-triple>` one or more times to publish a subset of targets.
|
||||
When omitted, the tool builds targets that are buildable on the current host and
|
||||
adds Android targets when Android SDK options are provided.
|
||||
|
||||
### verify-binaries
|
||||
|
||||
Checks that every configured precompiled artifact exists and that its signature
|
||||
matches `libfjs/cargokit.yaml`:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh verify-binaries --manifest-dir libfjs
|
||||
```
|
||||
|
||||
This command verifies per-target Cargokit artifacts. It does not validate the
|
||||
SwiftPM XCFramework zip; use `tool/prepare_darwin_release.sh` for that release
|
||||
path.
|
||||
|
||||
## Configuration
|
||||
|
||||
Crate-level settings live in `libfjs/cargokit.yaml`:
|
||||
|
||||
```yaml
|
||||
precompiled_binaries:
|
||||
url_prefix: https://github.com/fluttercandies/fjs/releases/download/precompiled_
|
||||
public_key: <32-byte-ed25519-public-key-hex>
|
||||
```
|
||||
|
||||
User-level settings are read from the nearest `cargokit_options.yaml` found by
|
||||
walking upward from `CARGOKIT_ROOT_PROJECT_DIR`:
|
||||
|
||||
```yaml
|
||||
use_precompiled_binaries: true
|
||||
verbose_logging: false
|
||||
```
|
||||
|
||||
Default behavior:
|
||||
|
||||
- If Rustup is available, builds use local Rust source.
|
||||
- If Rustup is missing, builds try signed precompiled binaries.
|
||||
- `use_precompiled_binaries: true` opts in to precompiled downloads even when
|
||||
Rustup is available.
|
||||
- `use_precompiled_binaries: false` forces local Rust builds.
|
||||
|
||||
## Darwin XCFramework Release Flow
|
||||
|
||||
CocoaPods and the Swift Package Manager artifact share the same Rust source and
|
||||
Cargokit build inputs, but the XCFramework is packaged by repository scripts:
|
||||
|
||||
```sh
|
||||
tool/build_fjs_xcframework.sh --configuration Release
|
||||
tool/prepare_darwin_release.sh --configuration Release
|
||||
```
|
||||
|
||||
`tool/build_fjs_xcframework.sh` writes a temporary options file that sets
|
||||
`use_precompiled_binaries: false`, so the release XCFramework is built from the
|
||||
current local Rust source. `tool/prepare_darwin_release.sh` then creates
|
||||
`build/darwin-release/fjs.xcframework.zip`, writes the SwiftPM checksum, checks
|
||||
Darwin package metadata, and runs `flutter pub publish --dry-run`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# This is copied from Cargokit (which is the official way to use it currently)
|
||||
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
linter:
|
||||
rules:
|
||||
- prefer_relative_imports
|
||||
- directives_ordering
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,8 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
build_tool.runMain(arguments);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'src/build_tool.dart' as build_tool;
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
return build_tool.runMain(args);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class AndroidEnvironment {
|
||||
AndroidEnvironment({
|
||||
required this.sdkPath,
|
||||
required this.ndkVersion,
|
||||
required this.minSdkVersion,
|
||||
required this.targetTempDir,
|
||||
required this.target,
|
||||
});
|
||||
|
||||
static void clangLinkerWrapper(List<String> args) {
|
||||
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||
if (clang == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||
}
|
||||
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||
}
|
||||
|
||||
runCommand(clang, [
|
||||
target,
|
||||
...args,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Full path to Android SDK.
|
||||
final String sdkPath;
|
||||
|
||||
/// Full version of Android NDK.
|
||||
final String ndkVersion;
|
||||
|
||||
/// Minimum supported SDK version.
|
||||
final int minSdkVersion;
|
||||
|
||||
/// Target directory for build artifacts.
|
||||
final String targetTempDir;
|
||||
|
||||
/// Target being built.
|
||||
final Target target;
|
||||
|
||||
bool ndkIsInstalled() {
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||
return ndkPackageXml.existsSync();
|
||||
}
|
||||
|
||||
void installNdk({
|
||||
required String javaHome,
|
||||
}) {
|
||||
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||
final sdkManager = path.join(
|
||||
sdkPath,
|
||||
'cmdline-tools',
|
||||
'latest',
|
||||
'bin',
|
||||
'sdkmanager$sdkManagerExtension',
|
||||
);
|
||||
|
||||
log.info('Installing NDK $ndkVersion');
|
||||
runCommand(sdkManager, [
|
||||
'--install',
|
||||
'ndk;$ndkVersion',
|
||||
], environment: {
|
||||
'JAVA_HOME': javaHome,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, String>> buildEnvironment() async {
|
||||
final hostArch = Platform.isMacOS
|
||||
? "darwin-x86_64"
|
||||
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final toolchainPath = path.join(
|
||||
ndkPath,
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
hostArch,
|
||||
'bin',
|
||||
);
|
||||
|
||||
final minSdkVersion =
|
||||
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||
|
||||
final exe = Platform.isWindows ? '.exe' : '';
|
||||
|
||||
final arKey = 'AR_${target.rust}';
|
||||
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||
.map((e) => path.join(toolchainPath, e))
|
||||
.firstWhereOrNull((element) => File(element).existsSync());
|
||||
if (arValue == null) {
|
||||
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||
}
|
||||
|
||||
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||
|
||||
final ccKey = 'CC_${target.rust}';
|
||||
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||
final cFlagsValue = targetArg;
|
||||
|
||||
final cxxKey = 'CXX_${target.rust}';
|
||||
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||
final cxxFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||
final cxxFlagsValue = targetArg;
|
||||
|
||||
final linkerKey =
|
||||
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||
|
||||
final ranlibKey = 'RANLIB_${target.rust}';
|
||||
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||
|
||||
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||
|
||||
final runRustTool =
|
||||
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||
|
||||
final packagePath = (await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||
.toFilePath();
|
||||
final selfPath = path.canonicalize(path.join(
|
||||
packagePath,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
runRustTool,
|
||||
));
|
||||
|
||||
// Make sure that run_build_tool is working properly even initially launched directly
|
||||
// through dart run.
|
||||
final toolTempDir =
|
||||
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||
|
||||
final sysroot = path.join(
|
||||
ndkPath,
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
hostArch,
|
||||
'sysroot',
|
||||
);
|
||||
|
||||
final bindgenKey = "BINDGEN_EXTRA_CLANG_ARGS_${target.rust}";
|
||||
final sysrootForClang = _clangPath(sysroot);
|
||||
final includeForClang =
|
||||
_clangPath(path.join(sysroot, 'usr', 'include', target.rust));
|
||||
final bindgenValue =
|
||||
"$targetArg --sysroot=$sysrootForClang -I$includeForClang";
|
||||
|
||||
final env = {
|
||||
arKey: arValue,
|
||||
ccKey: ccValue,
|
||||
cfFlagsKey: cFlagsValue,
|
||||
cxxKey: cxxValue,
|
||||
cxxFlagsKey: cxxFlagsValue,
|
||||
ranlibKey: ranlibValue,
|
||||
rustFlagsKey: rustFlagsValue,
|
||||
linkerKey: selfPath,
|
||||
bindgenKey: bindgenValue,
|
||||
// Recognized by main() so we know when we're acting as a wrapper
|
||||
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||
};
|
||||
|
||||
log.info(JsonEncoder.withIndent(' ').convert(env));
|
||||
return env;
|
||||
}
|
||||
|
||||
String _clangPath(String value) {
|
||||
return value.replaceAll(r'\', '/');
|
||||
}
|
||||
|
||||
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||
final workaroundDir = path.join(
|
||||
buildDir,
|
||||
'cargokit',
|
||||
'libgcc_workaround',
|
||||
'${ndkVersion.major}',
|
||||
);
|
||||
Directory(workaroundDir).createSync(recursive: true);
|
||||
if (ndkVersion.major >= 23) {
|
||||
File(path.join(workaroundDir, 'libgcc.a'))
|
||||
.writeAsStringSync('INPUT(-lunwind)');
|
||||
} else {
|
||||
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||
// gets updated for NDK23+.
|
||||
File(path.join(workaroundDir, 'libunwind.a'))
|
||||
.writeAsStringSync('INPUT(-lgcc)');
|
||||
}
|
||||
|
||||
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||
if (rustFlags.isNotEmpty) {
|
||||
rustFlags = '$rustFlags\x1f';
|
||||
}
|
||||
|
||||
if (["arm64-v8a", "x86_64"].contains(target.android)) {
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir\x1f';
|
||||
|
||||
const pageSizeArgs = [
|
||||
"-C",
|
||||
"link-arg=-Wl,--hash-style=both",
|
||||
"-C",
|
||||
"link-arg=-Wl,-z,max-page-size=16384"
|
||||
];
|
||||
final pageSizeArgsString = pageSizeArgs.join("\x1f");
|
||||
|
||||
rustFlags = '$rustFlags$pageSizeArgsString';
|
||||
} else {
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||
}
|
||||
return rustFlags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'builder.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'precompiled_asset_store.dart';
|
||||
import 'precompiled_generation.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class Artifact {
|
||||
Artifact({
|
||||
required this.path,
|
||||
required this.finalFileName,
|
||||
});
|
||||
|
||||
/// File system location of the artifact.
|
||||
final String path;
|
||||
|
||||
/// Actual file name that the artifact should have in destination folder.
|
||||
final String finalFileName;
|
||||
|
||||
AritifactType get type {
|
||||
if (finalFileName.endsWith('.dll') ||
|
||||
finalFileName.endsWith('.dll.lib') ||
|
||||
finalFileName.endsWith('.pdb') ||
|
||||
finalFileName.endsWith('.so') ||
|
||||
finalFileName.endsWith('.dylib')) {
|
||||
return AritifactType.dylib;
|
||||
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||
return AritifactType.staticlib;
|
||||
}
|
||||
throw Exception('Unknown artifact type for $finalFileName');
|
||||
}
|
||||
}
|
||||
|
||||
typedef ArtifactHttpGet = Future<Response> Function(Uri url);
|
||||
typedef ArtifactGenerationHash = String Function(BuildEnvironment environment);
|
||||
typedef ArtifactLocalBuilder = Future<List<Artifact>> Function(
|
||||
Target target, BuildEnvironment environment);
|
||||
typedef ArtifactTransportFactory = PrecompiledAssetTransport Function();
|
||||
|
||||
final _log = Logger('artifacts_provider');
|
||||
|
||||
class ArtifactProvider {
|
||||
ArtifactProvider({
|
||||
required this.environment,
|
||||
required this.userOptions,
|
||||
ArtifactHttpGet? httpGet,
|
||||
PrecompiledHttpSend? httpSend,
|
||||
ArtifactGenerationHash? generationHash,
|
||||
ArtifactLocalBuilder? localBuilder,
|
||||
ArtifactTransportFactory? transportFactory,
|
||||
PrecompiledTransportPolicy transportPolicy =
|
||||
const PrecompiledTransportPolicy(),
|
||||
this.storePolicy = const PrecompiledAssetStorePolicy(),
|
||||
}) : generationHash = generationHash ??
|
||||
((environment) => CrateHash.compute(
|
||||
environment.manifestDir,
|
||||
tempStorage: environment.targetTempDir,
|
||||
)),
|
||||
localBuilder = localBuilder ?? _defaultLocalBuilder,
|
||||
transportFactory = transportFactory ??
|
||||
(() => PrecompiledAssetTransport(
|
||||
send: httpSend ??
|
||||
(httpGet == null ? null : _sendFromGet(httpGet)),
|
||||
policy: transportPolicy,
|
||||
));
|
||||
|
||||
final BuildEnvironment environment;
|
||||
final CargokitUserOptions userOptions;
|
||||
final ArtifactGenerationHash generationHash;
|
||||
final ArtifactLocalBuilder localBuilder;
|
||||
final ArtifactTransportFactory transportFactory;
|
||||
final PrecompiledAssetStorePolicy storePolicy;
|
||||
|
||||
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||
final result = await _getPrecompiledArtifacts(targets);
|
||||
final pendingTargets = List.of(targets)
|
||||
..removeWhere((target) => result.containsKey(target));
|
||||
|
||||
if (pendingTargets.isEmpty) return result;
|
||||
if (!userOptions.allowLocalBuild) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Required precompiled generation did not provide all requested targets.');
|
||||
}
|
||||
for (final target in pendingTargets) {
|
||||
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||
result[target] = await localBuilder(target, environment);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||
List<Target> targets) async {
|
||||
if (userOptions.precompiledBinariesMode ==
|
||||
PrecompiledBinariesMode.disabled) {
|
||||
_log.info('Precompiled binaries are disabled');
|
||||
return {};
|
||||
}
|
||||
final precompiled = environment.crateOptions.precompiledBinaries;
|
||||
if (precompiled == null) return {};
|
||||
final recipe = precompiled.buildRecipe;
|
||||
if (recipe == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Configured precompiled binaries need a complete build recipe.');
|
||||
}
|
||||
final pinnedTargets = recipe.rustTargets.toSet();
|
||||
if (targets.any((target) => !pinnedTargets.contains(target.rust))) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Requested Rust target is not included in the signed build recipe.');
|
||||
}
|
||||
|
||||
final requiredForTargets = <Target, List<String>>{
|
||||
for (final target in targets)
|
||||
target: getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
).map((name) => PrecompileBinaries.fileName(target, name)).toList(),
|
||||
};
|
||||
final generation = generationHash(environment);
|
||||
final uriPrefix = Uri.parse(precompiled.uriPrefix);
|
||||
final expectedAssetNames = _expectedAssetNames(recipe, precompiled);
|
||||
final expectedCompositeChecksums = _expectedCompositeChecksums(precompiled);
|
||||
final transport = transportFactory();
|
||||
try {
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: path.join(environment.targetTempDir, 'precompiled'),
|
||||
uriPrefix: uriPrefix,
|
||||
publicKey: precompiled.publicKey,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: expectedAssetNames,
|
||||
expectedCompositeChecksums: expectedCompositeChecksums,
|
||||
transport: transport,
|
||||
policy: storePolicy,
|
||||
logger: _log.info,
|
||||
);
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: generation,
|
||||
requestedAssetNames:
|
||||
requiredForTargets.values.expand((names) => names).toSet(),
|
||||
);
|
||||
if (snapshot == null) {
|
||||
if (userOptions.precompiledBinariesMode ==
|
||||
PrecompiledBinariesMode.auto) {
|
||||
if (!store.hasAnyState(generation)) return {};
|
||||
throw PrecompiledGenerationException(
|
||||
'Precompiled generation manifest is unavailable for existing cache state.');
|
||||
}
|
||||
throw PrecompiledGenerationException(
|
||||
'Required precompiled generation manifest is unavailable.');
|
||||
}
|
||||
|
||||
return {
|
||||
for (final entry in requiredForTargets.entries)
|
||||
entry.key: [
|
||||
for (final remoteName in entry.value)
|
||||
Artifact(
|
||||
path: snapshot.pathFor(remoteName),
|
||||
finalFileName:
|
||||
remoteName.substring('${entry.key.rust}_'.length),
|
||||
),
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> _expectedAssetNames(
|
||||
PrecompiledBuildRecipe recipe, PrecompiledBinaries precompiled) {
|
||||
final names = <String>{};
|
||||
for (final rustTarget in recipe.rustTargets) {
|
||||
final target = Target.forRustTriple(rustTarget);
|
||||
if (target == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest recipe has an unsupported Rust target.');
|
||||
}
|
||||
for (final type in AritifactType.values) {
|
||||
for (final name in getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
aritifactType: type,
|
||||
)) {
|
||||
names.add(PrecompileBinaries.fileName(target, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final group in precompiled.compositeGroups) {
|
||||
names.addAll(group.outputs);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
static Set<String> _expectedCompositeChecksums(
|
||||
PrecompiledBinaries precompiled) {
|
||||
final result = <String>{};
|
||||
for (final group in precompiled.compositeGroups) {
|
||||
for (final output in group.outputs) {
|
||||
if (!output.endsWith('.checksum')) continue;
|
||||
final archive = output.substring(0, output.length - '.checksum'.length);
|
||||
if (group.outputs.contains(archive)) {
|
||||
result.add('$archive\u0000$output');
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static PrecompiledHttpSend _sendFromGet(ArtifactHttpGet get) {
|
||||
return (request) async {
|
||||
final response = await get(request.url);
|
||||
return StreamedResponse(
|
||||
Stream.value(response.bodyBytes),
|
||||
response.statusCode,
|
||||
contentLength: response.bodyBytes.length,
|
||||
headers: response.headers,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
request: request,
|
||||
isRedirect: response.isRedirect,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Artifact>> _defaultLocalBuilder(
|
||||
Target target, BuildEnvironment environment) async {
|
||||
final rustup = Rustup();
|
||||
final builder = RustBuilder(target: target, environment: environment);
|
||||
builder.prepare(rustup);
|
||||
final targetDir = await builder.build();
|
||||
final artifactNames = <String>{
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.dylib,
|
||||
remote: false,
|
||||
),
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.staticlib,
|
||||
remote: false,
|
||||
),
|
||||
};
|
||||
return artifactNames
|
||||
.map((artifactName) => Artifact(
|
||||
path: path.join(targetDir, artifactName),
|
||||
finalFileName: artifactName,
|
||||
))
|
||||
.where((artifact) => File(artifact.path).existsSync())
|
||||
.toList();
|
||||
}
|
||||
|
||||
enum AritifactType {
|
||||
staticlib,
|
||||
dylib,
|
||||
}
|
||||
|
||||
AritifactType artifactTypeForTarget(Target target) {
|
||||
if (target.darwinPlatform != null) {
|
||||
return AritifactType.staticlib;
|
||||
}
|
||||
return AritifactType.dylib;
|
||||
}
|
||||
|
||||
List<String> getArtifactNames({
|
||||
required Target target,
|
||||
required String libraryName,
|
||||
required bool remote,
|
||||
AritifactType? aritifactType,
|
||||
}) {
|
||||
aritifactType ??= artifactTypeForTarget(target);
|
||||
if (target.darwinArch != null) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
}
|
||||
return ['lib$libraryName.dylib'];
|
||||
} else if (target.rust.contains('-windows-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['$libraryName.lib'];
|
||||
}
|
||||
return [
|
||||
'$libraryName.dll',
|
||||
'$libraryName.dll.lib',
|
||||
if (!remote) '$libraryName.pdb',
|
||||
];
|
||||
} else if (target.rust.contains('-linux-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
}
|
||||
return ['lib$libraryName.so'];
|
||||
}
|
||||
throw Exception('Unsupported target: ${target.rust}');
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class BuildCMake {
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
BuildCMake({required this.userOptions});
|
||||
|
||||
Future<void> build() async {
|
||||
final targetPlatform = Environment.targetPlatform;
|
||||
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||
if (target == null) {
|
||||
throw Exception("Unknown target platform: $targetPlatform");
|
||||
}
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts([target]);
|
||||
|
||||
final libs = artifacts[target]!;
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path)
|
||||
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final log = Logger('build_gradle');
|
||||
|
||||
class BuildGradle {
|
||||
BuildGradle({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.targetPlatforms.map((arch) {
|
||||
final target = Target.forFlutterName(arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
for (final target in targets) {
|
||||
final libs = artifacts[target]!;
|
||||
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class BuildPod {
|
||||
BuildPod({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.darwinArchs.map((arch) {
|
||||
final target = Target.forDarwin(
|
||||
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||
runCommand("lipo", [
|
||||
'-create',
|
||||
...sourceFiles,
|
||||
'-output',
|
||||
targetFile,
|
||||
]);
|
||||
}
|
||||
|
||||
final outputDir = Environment.outputDir;
|
||||
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
final staticLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.staticlib)
|
||||
.toList();
|
||||
final dynamicLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.dylib)
|
||||
.toList();
|
||||
|
||||
final libName = environment.crateInfo.packageName;
|
||||
|
||||
// If there is static lib, use it and link it with pod
|
||||
if (staticLibs.isNotEmpty) {
|
||||
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||
} else {
|
||||
// Otherwise try to replace bundle dylib with our dylib
|
||||
final bundlePaths = [
|
||||
'$libName.framework/Versions/A/$libName',
|
||||
'$libName.framework/$libName',
|
||||
];
|
||||
|
||||
for (final bundlePath in bundlePaths) {
|
||||
final targetFile = path.join(outputDir, bundlePath);
|
||||
if (File(targetFile).existsSync()) {
|
||||
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||
|
||||
// Replace absolute id with @rpath one so that it works properly
|
||||
// when moved to Frameworks.
|
||||
runCommand("install_name_tool", [
|
||||
'-id',
|
||||
'@rpath/$bundlePath',
|
||||
targetFile,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception('Unable to find bundle for dynamic library');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'build_cmake.dart';
|
||||
import 'build_gradle.dart';
|
||||
import 'build_pod.dart';
|
||||
import 'logging.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
import 'verify_binaries.dart';
|
||||
|
||||
final log = Logger('build_tool');
|
||||
|
||||
abstract class BuildCommand extends Command {
|
||||
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final options = CargokitUserOptions.load();
|
||||
|
||||
if (options.verboseLogging ||
|
||||
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
await runBuildCommand(options);
|
||||
}
|
||||
}
|
||||
|
||||
class BuildPodCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-pod';
|
||||
|
||||
@override
|
||||
final description = 'Build cocoa pod library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildPod(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildGradleCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-gradle';
|
||||
|
||||
@override
|
||||
final description = 'Build android library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildGradle(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildCMakeCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-cmake';
|
||||
|
||||
@override
|
||||
final description = 'Build CMake library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildCMake(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class GenKeyCommand extends Command {
|
||||
@override
|
||||
final name = 'gen-key';
|
||||
|
||||
@override
|
||||
final description = 'Generate key pair for signing precompiled binaries';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
final kp = generateKey();
|
||||
final private = HEX.encode(kp.privateKey.bytes);
|
||||
final public = HEX.encode(kp.publicKey.bytes);
|
||||
print("Private Key: $private");
|
||||
print("Public Key: $public");
|
||||
}
|
||||
}
|
||||
|
||||
class PrecompileBinariesCommand extends Command {
|
||||
PrecompileBinariesCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'repository',
|
||||
mandatory: true,
|
||||
help: 'Github repository slug in format owner/name',
|
||||
)
|
||||
..addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
)
|
||||
..addMultiOption('target',
|
||||
help: 'Rust target triple of artifact to build.\n'
|
||||
'Can be specified multiple times or omitted in which case\n'
|
||||
'all targets for current platform will be built.')
|
||||
..addOption(
|
||||
'android-sdk-location',
|
||||
help: 'Location of Android SDK (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-ndk-version',
|
||||
help: 'Android NDK version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-min-sdk-version',
|
||||
help: 'Android minimum rquired version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'temp-dir',
|
||||
help: 'Directory to store temporary build artifacts',
|
||||
)
|
||||
..addFlag(
|
||||
"verbose",
|
||||
abbr: "v",
|
||||
defaultsTo: false,
|
||||
help: "Enable verbose logging",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'precompile-binaries';
|
||||
|
||||
@override
|
||||
final description = 'Prebuild and upload binaries\n'
|
||||
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||
'Use gen_key through generate priave key.\n'
|
||||
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final verbose = argResults!['verbose'] as bool;
|
||||
if (verbose) {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||
if (privateKeyString == null) {
|
||||
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||
}
|
||||
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||
if (githubToken == null) {
|
||||
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||
}
|
||||
final privateKey = HEX.decode(privateKeyString);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
if (!Directory(manifestDir).existsSync()) {
|
||||
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||
}
|
||||
String? androidMinSdkVersionString =
|
||||
argResults!['android-min-sdk-version'] as String?;
|
||||
int? androidMinSdkVersion;
|
||||
if (androidMinSdkVersionString != null) {
|
||||
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||
if (androidMinSdkVersion == null) {
|
||||
throw ArgumentError(
|
||||
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||
}
|
||||
}
|
||||
final targetStrigns = argResults!['target'] as List<String>;
|
||||
final targets = targetStrigns.map((target) {
|
||||
final res = Target.forRustTriple(target);
|
||||
if (res == null) {
|
||||
throw ArgumentError('Invalid target: $target');
|
||||
}
|
||||
return res;
|
||||
}).toList(growable: false);
|
||||
final precompileBinaries = PrecompileBinaries(
|
||||
privateKey: PrivateKey(privateKey),
|
||||
githubToken: githubToken,
|
||||
manifestDir: manifestDir,
|
||||
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||
targets: targets,
|
||||
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||
androidMinSdkVersion: androidMinSdkVersion,
|
||||
tempDir: argResults!['temp-dir'] as String?,
|
||||
);
|
||||
|
||||
await precompileBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildPrecompiledGenerationCommand extends Command {
|
||||
BuildPrecompiledGenerationCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml and cargokit.yaml',
|
||||
)
|
||||
..addOption(
|
||||
'output-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory to publish the unsigned local generation',
|
||||
)
|
||||
..addOption(
|
||||
'temp-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory for Rust build intermediates',
|
||||
)
|
||||
..addMultiOption(
|
||||
'target',
|
||||
help: 'Pinned Rust target triple. Repeat for each target.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'build-precompiled-generation';
|
||||
|
||||
@override
|
||||
final description =
|
||||
'Builds an unsigned local precompiled generation from the pinned recipe.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final generation = LocalPrecompiledGeneration(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
outputDir: argResults!['output-dir'] as String,
|
||||
tempDir: argResults!['temp-dir'] as String,
|
||||
targetTriples: argResults!['target'] as List<String>,
|
||||
);
|
||||
await generation.run();
|
||||
}
|
||||
}
|
||||
|
||||
class FinalizeLocalGenerationCommand extends Command {
|
||||
FinalizeLocalGenerationCommand() {
|
||||
argParser
|
||||
..addOption('manifest-dir', mandatory: true)
|
||||
..addOption('generation-dir', mandatory: true)
|
||||
..addOption(
|
||||
'scope',
|
||||
defaultsTo: 'full',
|
||||
allowed: ['full', 'darwin-acceptance'],
|
||||
)
|
||||
..addOption(
|
||||
'private-key',
|
||||
help: 'Ed25519 private key hex; defaults to PRIVATE_KEY.',
|
||||
)
|
||||
..addOption('source-commit');
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'finalize-local-generation';
|
||||
|
||||
@override
|
||||
final description =
|
||||
'Signs and validates an unsigned local precompiled generation.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final encodedKey = argResults!['private-key'] as String? ??
|
||||
Platform.environment['PRIVATE_KEY'];
|
||||
if (encodedKey == null) {
|
||||
throw ArgumentError('Missing --private-key or PRIVATE_KEY.');
|
||||
}
|
||||
final privateKey = HEX.decode(encodedKey);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final scope = switch (argResults!['scope'] as String) {
|
||||
'full' => LocalGenerationFinalizationScope.full,
|
||||
'darwin-acceptance' => LocalGenerationFinalizationScope.darwinAcceptance,
|
||||
_ => throw ArgumentError('Unsupported generation scope.'),
|
||||
};
|
||||
final finalized = await LocalGenerationFinalizer(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
generationDir: argResults!['generation-dir'] as String,
|
||||
privateKey: PrivateKey(privateKey),
|
||||
scope: scope,
|
||||
sourceCommit: argResults!['source-commit'] as String?,
|
||||
).run();
|
||||
stdout.writeln(finalized.manifest.generationHash);
|
||||
}
|
||||
}
|
||||
|
||||
class PublishPrecompiledGenerationCommand extends Command {
|
||||
PublishPrecompiledGenerationCommand() {
|
||||
argParser
|
||||
..addOption('manifest-dir', mandatory: true)
|
||||
..addOption('generation-dir', mandatory: true)
|
||||
..addOption('repository', mandatory: true)
|
||||
..addOption('private-key', help: 'Ed25519 private key hex.')
|
||||
..addOption('github-token',
|
||||
help: 'GitHub token; defaults to GITHUB_TOKEN.')
|
||||
..addOption('source-commit');
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'publish-precompiled-generation';
|
||||
|
||||
@override
|
||||
final description = 'Finalizes and publishes one complete v2 generation.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final encodedKey = argResults!['private-key'] as String? ??
|
||||
Platform.environment['PRIVATE_KEY'];
|
||||
final token = argResults!['github-token'] as String? ??
|
||||
Platform.environment['GITHUB_TOKEN'];
|
||||
if (encodedKey == null || token == null) {
|
||||
throw ArgumentError('Missing private key or GitHub token.');
|
||||
}
|
||||
final privateKey = HEX.decode(encodedKey);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final github = GitHub(auth: Authentication.withToken(token));
|
||||
final finalized = await LocalGenerationFinalizer(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
generationDir: argResults!['generation-dir'] as String,
|
||||
privateKey: PrivateKey(privateKey),
|
||||
scope: LocalGenerationFinalizationScope.full,
|
||||
sourceCommit: argResults!['source-commit'] as String?,
|
||||
).run();
|
||||
await PrecompiledGenerationPublisher(
|
||||
generation: finalized,
|
||||
gateway: GitHubPrecompiledReleaseGateway(
|
||||
repositories: github.repositories,
|
||||
repository: RepositorySlug.full(argResults!['repository'] as String),
|
||||
),
|
||||
).publish();
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyBinariesCommand extends Command {
|
||||
VerifyBinariesCommand() {
|
||||
argParser.addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = "verify-binaries";
|
||||
|
||||
@override
|
||||
final description = 'Verifies published binaries\n'
|
||||
'Checks the complete v2 generation, assets, signatures, and checksums.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
final verifyBinaries = VerifyBinaries(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
await verifyBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
try {
|
||||
// Init logging before options are loaded
|
||||
initLogging();
|
||||
|
||||
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||
}
|
||||
|
||||
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||
..addCommand(BuildPodCommand())
|
||||
..addCommand(BuildGradleCommand())
|
||||
..addCommand(BuildCMakeCommand())
|
||||
..addCommand(GenKeyCommand())
|
||||
..addCommand(PrecompileBinariesCommand())
|
||||
..addCommand(BuildPrecompiledGenerationCommand())
|
||||
..addCommand(FinalizeLocalGenerationCommand())
|
||||
..addCommand(PublishPrecompiledGenerationCommand())
|
||||
..addCommand(VerifyBinariesCommand());
|
||||
|
||||
await runner.run(args);
|
||||
} on ArgumentError catch (e) {
|
||||
stderr.writeln(e.toString());
|
||||
exit(1);
|
||||
} catch (e, s) {
|
||||
log.severe(kDoubleSeparator);
|
||||
log.severe('Cargokit BuildTool failed with error:');
|
||||
log.severe(kSeparator);
|
||||
log.severe(e);
|
||||
// This tells user to install Rust, there's no need to pollute the log with
|
||||
// stack trace.
|
||||
if (e is! RustupNotFoundException) {
|
||||
log.severe(kSeparator);
|
||||
log.severe(s);
|
||||
log.severe(kSeparator);
|
||||
log.severe('BuildTool arguments: $args');
|
||||
}
|
||||
log.severe(kDoubleSeparator);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'cargo.dart';
|
||||
import 'darwin_environment.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
final _log = Logger('builder');
|
||||
|
||||
enum BuildConfiguration {
|
||||
debug,
|
||||
release,
|
||||
profile,
|
||||
}
|
||||
|
||||
extension on BuildConfiguration {
|
||||
bool get isDebug => this == BuildConfiguration.debug;
|
||||
String get rustName => switch (this) {
|
||||
BuildConfiguration.debug => 'debug',
|
||||
BuildConfiguration.release => 'release',
|
||||
BuildConfiguration.profile => 'release',
|
||||
};
|
||||
}
|
||||
|
||||
class BuildException implements Exception {
|
||||
final String message;
|
||||
|
||||
BuildException(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BuildException: $message';
|
||||
}
|
||||
}
|
||||
|
||||
class BuildEnvironment {
|
||||
final BuildConfiguration configuration;
|
||||
final CargokitCrateOptions crateOptions;
|
||||
final String targetTempDir;
|
||||
final String manifestDir;
|
||||
final CrateInfo crateInfo;
|
||||
|
||||
final bool isAndroid;
|
||||
final String? androidSdkPath;
|
||||
final String? androidNdkVersion;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? javaHome;
|
||||
final String? iosDeploymentTarget;
|
||||
final String? macosDeploymentTarget;
|
||||
|
||||
BuildEnvironment({
|
||||
required this.configuration,
|
||||
required this.crateOptions,
|
||||
required this.targetTempDir,
|
||||
required this.manifestDir,
|
||||
required this.crateInfo,
|
||||
required this.isAndroid,
|
||||
this.androidSdkPath,
|
||||
this.androidNdkVersion,
|
||||
this.androidMinSdkVersion,
|
||||
this.javaHome,
|
||||
this.iosDeploymentTarget,
|
||||
this.macosDeploymentTarget,
|
||||
});
|
||||
|
||||
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||
// XCode configuration adds the flavor to configuration name.
|
||||
final firstSegment = value.split('-').first;
|
||||
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||
(e) => e.name == firstSegment,
|
||||
);
|
||||
if (buildConfiguration == null) {
|
||||
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||
return BuildConfiguration.release;
|
||||
}
|
||||
return buildConfiguration;
|
||||
}
|
||||
|
||||
static BuildEnvironment fromEnvironment({
|
||||
required bool isAndroid,
|
||||
}) {
|
||||
final buildConfiguration =
|
||||
parseBuildConfiguration(Environment.configuration);
|
||||
final manifestDir = Environment.manifestDir;
|
||||
final crateOptions = CargokitCrateOptions.load(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
return BuildEnvironment(
|
||||
configuration: buildConfiguration,
|
||||
crateOptions: crateOptions,
|
||||
targetTempDir: Environment.targetTempDir,
|
||||
manifestDir: manifestDir,
|
||||
crateInfo: crateInfo,
|
||||
isAndroid: isAndroid,
|
||||
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||
androidMinSdkVersion:
|
||||
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||
javaHome: isAndroid ? Environment.javaHome : null,
|
||||
iosDeploymentTarget: isAndroid ? null : Environment.iosDeploymentTarget,
|
||||
macosDeploymentTarget:
|
||||
isAndroid ? null : Platform.environment['MACOSX_DEPLOYMENT_TARGET'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RustBuilder {
|
||||
final Target target;
|
||||
final BuildEnvironment environment;
|
||||
|
||||
RustBuilder({
|
||||
required this.target,
|
||||
required this.environment,
|
||||
this.toolchain,
|
||||
});
|
||||
|
||||
final String? toolchain;
|
||||
|
||||
void prepare(
|
||||
Rustup rustup,
|
||||
) {
|
||||
final toolchain = _toolchain;
|
||||
if (rustup.installedTargets(toolchain) == null) {
|
||||
rustup.installToolchain(toolchain);
|
||||
}
|
||||
if (toolchain == 'nightly') {
|
||||
rustup.installRustSrcForNightly();
|
||||
}
|
||||
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||
}
|
||||
}
|
||||
|
||||
CargoBuildOptions? get _buildOptions =>
|
||||
environment.crateOptions.cargo[environment.configuration];
|
||||
|
||||
String get _toolchain =>
|
||||
toolchain ?? _buildOptions?.toolchain.name ?? 'stable';
|
||||
|
||||
/// Returns the path of directory containing build artifacts.
|
||||
Future<String> build() async {
|
||||
final extraArgs = _buildOptions?.flags ?? [];
|
||||
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||
final buildEnvironment = await _buildEnvironment();
|
||||
buildEnvironment['RUSTC'] = _toolchainRustc();
|
||||
runCommand(
|
||||
'rustup',
|
||||
[
|
||||
'run',
|
||||
_toolchain,
|
||||
'cargo',
|
||||
'build',
|
||||
...extraArgs,
|
||||
'--manifest-path',
|
||||
manifestPath,
|
||||
'-p',
|
||||
environment.crateInfo.packageName,
|
||||
if (!environment.configuration.isDebug) '--release',
|
||||
'--target',
|
||||
target.rust,
|
||||
'--target-dir',
|
||||
environment.targetTempDir,
|
||||
],
|
||||
environment: buildEnvironment,
|
||||
);
|
||||
return path.join(
|
||||
environment.targetTempDir,
|
||||
target.rust,
|
||||
environment.configuration.rustName,
|
||||
);
|
||||
}
|
||||
|
||||
String _toolchainRustc() {
|
||||
final result = runCommand('rustup', [
|
||||
'which',
|
||||
'--toolchain',
|
||||
_toolchain,
|
||||
'rustc',
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _buildEnvironment() async {
|
||||
if (target.android == null) {
|
||||
if (target.darwinPlatform == 'macosx') {
|
||||
final deploymentTarget = environment.macosDeploymentTarget;
|
||||
return deploymentTarget == null
|
||||
? {}
|
||||
: {'MACOSX_DEPLOYMENT_TARGET': deploymentTarget};
|
||||
}
|
||||
if (target.darwinPlatform == 'iphonesimulator' ||
|
||||
target.darwinPlatform == 'iphoneos') {
|
||||
return DarwinEnvironment(
|
||||
target: target,
|
||||
deploymentTarget: environment.iosDeploymentTarget ??
|
||||
Environment.iosDeploymentTarget,
|
||||
).buildEnvironment();
|
||||
}
|
||||
return {};
|
||||
} else {
|
||||
final sdkPath = environment.androidSdkPath;
|
||||
final ndkVersion = environment.androidNdkVersion;
|
||||
final minSdkVersion = environment.androidMinSdkVersion;
|
||||
if (sdkPath == null) {
|
||||
throw BuildException('androidSdkPath is not set');
|
||||
}
|
||||
if (ndkVersion == null) {
|
||||
throw BuildException('androidNdkVersion is not set');
|
||||
}
|
||||
if (minSdkVersion == null) {
|
||||
throw BuildException('androidMinSdkVersion is not set');
|
||||
}
|
||||
final env = AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: ndkVersion,
|
||||
minSdkVersion: minSdkVersion,
|
||||
targetTempDir: environment.targetTempDir,
|
||||
target: target,
|
||||
);
|
||||
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||
env.installNdk(javaHome: environment.javaHome!);
|
||||
}
|
||||
return env.buildEnvironment();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class ManifestException {
|
||||
ManifestException(this.message, {required this.fileName});
|
||||
|
||||
final String? fileName;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (fileName != null) {
|
||||
return 'Failed to parse package manifest at $fileName: $message';
|
||||
} else {
|
||||
return 'Failed to parse package manifest: $message';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CrateInfo {
|
||||
CrateInfo({required this.packageName});
|
||||
|
||||
final String packageName;
|
||||
|
||||
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||
final toml = TomlDocument.parse(manifest);
|
||||
final package = toml.toMap()['package'];
|
||||
if (package == null) {
|
||||
throw ManifestException('Missing package section', fileName: fileName);
|
||||
}
|
||||
final name = package['name'];
|
||||
if (name == null) {
|
||||
throw ManifestException('Missing package name', fileName: fileName);
|
||||
}
|
||||
return CrateInfo(packageName: name);
|
||||
}
|
||||
|
||||
static CrateInfo load(String manifestDir) {
|
||||
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||
final manifest = manifestFile.readAsStringSync();
|
||||
return parseManifest(manifest, fileName: manifestFile.path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
class CompositeArtifactException implements Exception {
|
||||
CompositeArtifactException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => 'CompositeArtifactException: $message';
|
||||
}
|
||||
|
||||
class CompositeProcessInvocation {
|
||||
const CompositeProcessInvocation({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
required this.workingDirectory,
|
||||
required this.environment,
|
||||
required this.timeout,
|
||||
});
|
||||
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final String workingDirectory;
|
||||
final Map<String, String> environment;
|
||||
final Duration timeout;
|
||||
}
|
||||
|
||||
class CompositeProcessResult {
|
||||
const CompositeProcessResult({
|
||||
required this.exitCode,
|
||||
this.stdout = '',
|
||||
this.stderr = '',
|
||||
this.timedOut = false,
|
||||
});
|
||||
|
||||
final int exitCode;
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
final bool timedOut;
|
||||
}
|
||||
|
||||
typedef CompositeProcessRunner = Future<CompositeProcessResult> Function(
|
||||
CompositeProcessInvocation invocation,
|
||||
);
|
||||
|
||||
class CompositeArtifactResult {
|
||||
CompositeArtifactResult(this.outputs);
|
||||
|
||||
final Map<String, String> outputs;
|
||||
}
|
||||
|
||||
class CompositeArtifactBuilder {
|
||||
CompositeArtifactBuilder({
|
||||
required String workspaceRoot,
|
||||
required this.generationHash,
|
||||
required String targetOutputRoot,
|
||||
required String outputRoot,
|
||||
CompositeHost? currentHost,
|
||||
CompositeProcessRunner? processRunner,
|
||||
}) : workspaceRoot = path.normalize(path.absolute(workspaceRoot)),
|
||||
targetOutputRoot = path.normalize(path.absolute(targetOutputRoot)),
|
||||
outputRoot = path.normalize(path.absolute(outputRoot)),
|
||||
currentHost = currentHost ?? _platformHost(),
|
||||
processRunner = processRunner ?? _runProcess;
|
||||
|
||||
final String workspaceRoot;
|
||||
final String generationHash;
|
||||
final String targetOutputRoot;
|
||||
final String outputRoot;
|
||||
final CompositeHost currentHost;
|
||||
final CompositeProcessRunner processRunner;
|
||||
|
||||
bool supports(CompositeGroup group, Set<String> targets) =>
|
||||
group.host == currentHost &&
|
||||
group.requiredTargets.every(targets.contains);
|
||||
|
||||
Future<CompositeArtifactResult> build(CompositeGroup group) async {
|
||||
if (group.host != currentHost) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} requires host ${group.host.name}.',
|
||||
);
|
||||
}
|
||||
_requireDirectory(workspaceRoot, 'Workspace root');
|
||||
_requireDirectory(targetOutputRoot, 'Target output root');
|
||||
for (final target in group.requiredTargets) {
|
||||
final targetDirectory = path.join(targetOutputRoot, target);
|
||||
_requireContainedDirectory(
|
||||
targetOutputRoot,
|
||||
targetDirectory,
|
||||
'Required target output $target',
|
||||
);
|
||||
}
|
||||
|
||||
final executable = path.normalize(path.joinAll([
|
||||
workspaceRoot,
|
||||
...path.posix.split(group.argv.first),
|
||||
]));
|
||||
_requireContainedFile(workspaceRoot, executable, 'Composite executable');
|
||||
|
||||
_ensureNoSymlinkComponents(outputRoot, 'Composite output root');
|
||||
Directory(outputRoot).createSync(recursive: true);
|
||||
_ensureNoSymlinkComponents(outputRoot, 'Composite output root');
|
||||
final staging = Directory(path.join(
|
||||
outputRoot,
|
||||
'.${group.name}.$pid.${DateTime.now().microsecondsSinceEpoch}',
|
||||
))
|
||||
..createSync();
|
||||
final finalDirectory = Directory(
|
||||
path.join(outputRoot, 'composites', group.name),
|
||||
);
|
||||
|
||||
try {
|
||||
final environment = <String, String>{
|
||||
...group.environment,
|
||||
'CARGOKIT_GENERATION_HASH': generationHash,
|
||||
'CARGOKIT_WORKSPACE_ROOT': workspaceRoot,
|
||||
'CARGOKIT_TARGET_OUTPUT_ROOT': targetOutputRoot,
|
||||
'CARGOKIT_OUTPUT_ROOT': staging.path,
|
||||
'CARGOKIT_DART_EXECUTABLE': Platform.resolvedExecutable,
|
||||
};
|
||||
final result = await processRunner(CompositeProcessInvocation(
|
||||
executable: executable,
|
||||
arguments: List.unmodifiable(group.argv.skip(1)),
|
||||
workingDirectory: workspaceRoot,
|
||||
environment: Map.unmodifiable(environment),
|
||||
timeout: group.timeout,
|
||||
));
|
||||
if (result.timedOut) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} timed out.',
|
||||
);
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} failed with exit code '
|
||||
'${result.exitCode}: ${result.stderr.trim()}',
|
||||
);
|
||||
}
|
||||
|
||||
for (final output in group.outputs) {
|
||||
_requireContainedFile(
|
||||
staging.path,
|
||||
path.join(staging.path, output),
|
||||
'Composite output $output',
|
||||
);
|
||||
}
|
||||
if (FileSystemEntity.typeSync(finalDirectory.path, followLinks: false) !=
|
||||
FileSystemEntityType.notFound) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite output already exists for ${group.name}.',
|
||||
);
|
||||
}
|
||||
finalDirectory.parent.createSync(recursive: true);
|
||||
staging.renameSync(finalDirectory.path);
|
||||
return CompositeArtifactResult(Map.unmodifiable({
|
||||
for (final output in group.outputs)
|
||||
output: path.join(finalDirectory.path, output),
|
||||
}));
|
||||
} on CompositeArtifactException {
|
||||
rethrow;
|
||||
} on Object catch (error) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} could not be built: $error',
|
||||
);
|
||||
} finally {
|
||||
if (staging.existsSync()) staging.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<CompositeProcessResult> _runProcess(
|
||||
CompositeProcessInvocation invocation,
|
||||
) async {
|
||||
final process = await Process.start(
|
||||
invocation.executable,
|
||||
invocation.arguments,
|
||||
workingDirectory: invocation.workingDirectory,
|
||||
environment: invocation.environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: false,
|
||||
);
|
||||
final stdout = process.stdout.transform(utf8.decoder).join();
|
||||
final stderr = process.stderr.transform(utf8.decoder).join();
|
||||
var timedOut = false;
|
||||
int exitCode;
|
||||
try {
|
||||
exitCode = await process.exitCode.timeout(invocation.timeout);
|
||||
} on TimeoutException {
|
||||
timedOut = true;
|
||||
process.kill();
|
||||
exitCode = await process.exitCode;
|
||||
}
|
||||
return CompositeProcessResult(
|
||||
exitCode: exitCode,
|
||||
stdout: await stdout,
|
||||
stderr: await stderr,
|
||||
timedOut: timedOut,
|
||||
);
|
||||
}
|
||||
|
||||
static CompositeHost _platformHost() {
|
||||
if (Platform.isMacOS) return CompositeHost.macos;
|
||||
if (Platform.isWindows) return CompositeHost.windows;
|
||||
return CompositeHost.linux;
|
||||
}
|
||||
}
|
||||
|
||||
void _requireDirectory(String value, String description) {
|
||||
_ensureNoSymlinkComponents(value, description);
|
||||
if (FileSystemEntity.typeSync(value, followLinks: false) !=
|
||||
FileSystemEntityType.directory) {
|
||||
throw CompositeArtifactException('$description is not a directory: $value');
|
||||
}
|
||||
}
|
||||
|
||||
void _requireContainedDirectory(
|
||||
String root,
|
||||
String value,
|
||||
String description,
|
||||
) {
|
||||
_requireContained(root, value, description);
|
||||
_requireDirectory(value, description);
|
||||
}
|
||||
|
||||
void _requireContainedFile(String root, String value, String description) {
|
||||
_requireContained(root, value, description);
|
||||
_ensureNoSymlinkComponents(value, description);
|
||||
if (FileSystemEntity.typeSync(value, followLinks: false) !=
|
||||
FileSystemEntityType.file) {
|
||||
throw CompositeArtifactException('$description is not a file: $value');
|
||||
}
|
||||
}
|
||||
|
||||
void _requireContained(String root, String value, String description) {
|
||||
final normalizedRoot = path.normalize(path.absolute(root));
|
||||
final normalizedValue = path.normalize(path.absolute(value));
|
||||
if (!path.isWithin(normalizedRoot, normalizedValue)) {
|
||||
throw CompositeArtifactException('$description escapes $normalizedRoot.');
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureNoSymlinkComponents(String value, String description) {
|
||||
final absolute = path.normalize(path.absolute(value));
|
||||
var current = path.rootPrefix(absolute);
|
||||
final relative = path.relative(absolute, from: current);
|
||||
if (relative == '.') return;
|
||||
for (final component in path.split(relative)) {
|
||||
current = path.join(current, component);
|
||||
if (FileSystemEntity.typeSync(current, followLinks: false) ==
|
||||
FileSystemEntityType.link) {
|
||||
throw CompositeArtifactException(
|
||||
'$description must not contain symbolic links: $current',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
enum _HashEntryType {
|
||||
file,
|
||||
directory,
|
||||
}
|
||||
|
||||
class _HashEntry {
|
||||
const _HashEntry({
|
||||
required this.relativePath,
|
||||
required this.type,
|
||||
this.file,
|
||||
});
|
||||
|
||||
final String relativePath;
|
||||
final _HashEntryType type;
|
||||
final File? file;
|
||||
}
|
||||
|
||||
class CrateHash {
|
||||
static const _textExtensions = {
|
||||
'.rs',
|
||||
'.toml',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'.dart',
|
||||
'.sh',
|
||||
'.swift',
|
||||
'.json',
|
||||
'.md',
|
||||
'.lock',
|
||||
};
|
||||
|
||||
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||
///
|
||||
/// [tempStorage] is retained for call compatibility but is no longer used;
|
||||
/// file content remains authoritative on every call.
|
||||
static String compute(String manifestDir, {String? tempStorage}) {
|
||||
return CrateHash._(
|
||||
manifestDir: manifestDir,
|
||||
)._compute();
|
||||
}
|
||||
|
||||
CrateHash._({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
String _compute() {
|
||||
return _computeHash(_getInputs());
|
||||
}
|
||||
|
||||
String _computeHash(List<_HashEntry> inputs) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
final data = ByteData(8);
|
||||
|
||||
for (final hashInput in inputs) {
|
||||
_addFramedBytes(input, [hashInput.type.index], data);
|
||||
_addFramedBytes(input, utf8.encode(hashInput.relativePath), data);
|
||||
_addFramedBytes(
|
||||
input,
|
||||
hashInput.file == null ? const [] : _canonicalContent(hashInput.file!),
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
input.close();
|
||||
final res = output.events.single;
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
static List<int> _canonicalContent(File file) {
|
||||
final bytes = file.readAsBytesSync();
|
||||
final extension = path.extension(file.path).toLowerCase();
|
||||
if (!_textExtensions.contains(extension)) {
|
||||
return bytes;
|
||||
}
|
||||
final text = utf8.decode(bytes);
|
||||
return utf8.encode(text.replaceAll('\r\n', '\n'));
|
||||
}
|
||||
|
||||
static void _addUint64(
|
||||
ByteConversionSink input,
|
||||
ByteData data,
|
||||
int value,
|
||||
) {
|
||||
data.setUint64(0, value);
|
||||
input.add(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
static void _addFramedBytes(
|
||||
ByteConversionSink input,
|
||||
List<int> bytes,
|
||||
ByteData data,
|
||||
) {
|
||||
_addUint64(input, data, bytes.length);
|
||||
input.add(bytes);
|
||||
}
|
||||
|
||||
List<_HashEntry> _getInputs() {
|
||||
final manifestPath = path.normalize(path.absolute(manifestDir));
|
||||
_rejectAbsoluteSymlinkComponents(manifestPath, 'Crate manifest path');
|
||||
final precompiled = CargokitCrateOptions.load(manifestDir: manifestPath)
|
||||
.precompiledBinaries;
|
||||
final configuredWorkspaceRoot = path.normalize(path.absolute(path.join(
|
||||
manifestPath,
|
||||
precompiled?.workspaceRoot ?? '.',
|
||||
)));
|
||||
_rejectAbsoluteSymlinkComponents(
|
||||
configuredWorkspaceRoot,
|
||||
'Workspace root',
|
||||
);
|
||||
|
||||
final workspaceRoot =
|
||||
Directory(configuredWorkspaceRoot).resolveSymbolicLinksSync();
|
||||
final resolvedManifest = Directory(manifestPath).resolveSymbolicLinksSync();
|
||||
if (workspaceRoot != resolvedManifest &&
|
||||
!path.isWithin(workspaceRoot, resolvedManifest)) {
|
||||
throw FileSystemException(
|
||||
'Workspace root must contain the crate manifest.',
|
||||
configuredWorkspaceRoot,
|
||||
);
|
||||
}
|
||||
|
||||
final inputs = <String, _HashEntry>{};
|
||||
|
||||
void addFile(File file) {
|
||||
final absolute = path.normalize(path.absolute(file.path));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Hash input escapes the workspace root.',
|
||||
absolute,
|
||||
);
|
||||
}
|
||||
final relative = _normalizedRelativePath(absolute, workspaceRoot);
|
||||
inputs[relative] = _HashEntry(
|
||||
relativePath: relative,
|
||||
type: _HashEntryType.file,
|
||||
file: file,
|
||||
);
|
||||
}
|
||||
|
||||
void addDirectory(Directory directory) {
|
||||
final absolute = path.normalize(path.absolute(directory.path));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Hash input escapes the workspace root.',
|
||||
absolute,
|
||||
);
|
||||
}
|
||||
final relative = _normalizedRelativePath(absolute, workspaceRoot);
|
||||
inputs[relative] = _HashEntry(
|
||||
relativePath: relative,
|
||||
type: _HashEntryType.directory,
|
||||
);
|
||||
}
|
||||
|
||||
void addEntity(FileSystemEntity entity) {
|
||||
final type = FileSystemEntity.typeSync(entity.path, followLinks: false);
|
||||
if (type == FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'Symbolic links are not allowed in generation hash inputs.',
|
||||
entity.path,
|
||||
);
|
||||
}
|
||||
if (type == FileSystemEntityType.file) {
|
||||
addFile(File(entity.path));
|
||||
return;
|
||||
}
|
||||
if (type == FileSystemEntityType.directory) {
|
||||
addDirectory(Directory(entity.path));
|
||||
final children = Directory(entity.path).listSync(followLinks: false)
|
||||
..sort((left, right) => left.path.compareTo(right.path));
|
||||
for (final child in children) {
|
||||
addEntity(child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Generation hash input does not exist.', entity.path);
|
||||
}
|
||||
throw FileSystemException(
|
||||
'Unsupported generation hash input type.',
|
||||
entity.path,
|
||||
);
|
||||
}
|
||||
|
||||
final src = Directory(path.join(resolvedManifest, 'src'));
|
||||
addEntity(src);
|
||||
|
||||
void addOptionalManifestFile(String relative) {
|
||||
final filePath = path.join(resolvedManifest, relative);
|
||||
final type = FileSystemEntity.typeSync(filePath, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
return;
|
||||
}
|
||||
addEntity(File(filePath));
|
||||
}
|
||||
|
||||
addOptionalManifestFile('Cargo.toml');
|
||||
addOptionalManifestFile('Cargo.lock');
|
||||
addOptionalManifestFile('build.rs');
|
||||
addOptionalManifestFile('cargokit.yaml');
|
||||
|
||||
for (final declaredInput in precompiled?.hashInputs ?? const <String>[]) {
|
||||
final absolute = path.normalize(path.joinAll([
|
||||
workspaceRoot,
|
||||
...path.posix.split(declaredInput),
|
||||
]));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Declared hash input escapes the workspace root.',
|
||||
declaredInput,
|
||||
);
|
||||
}
|
||||
_rejectSymlinkComponents(workspaceRoot, absolute);
|
||||
addEntity(FileSystemEntity.isDirectorySync(absolute)
|
||||
? Directory(absolute)
|
||||
: File(absolute));
|
||||
}
|
||||
|
||||
final result = inputs.values.toList()
|
||||
..sort((left, right) => left.relativePath.compareTo(right.relativePath));
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _normalizedRelativePath(String absolute, String root) {
|
||||
final relative = path.relative(absolute, from: root);
|
||||
return path.posix.joinAll(path.split(relative));
|
||||
}
|
||||
|
||||
static void _rejectSymlinkComponents(String root, String target) {
|
||||
var current = root;
|
||||
for (final component in path.split(path.relative(target, from: root))) {
|
||||
current = path.join(current, component);
|
||||
_rejectLink(current, 'Generation hash input');
|
||||
}
|
||||
}
|
||||
|
||||
static void _rejectAbsoluteSymlinkComponents(
|
||||
String target,
|
||||
String description,
|
||||
) {
|
||||
final root = path.rootPrefix(target);
|
||||
var current = root;
|
||||
final relative = path.relative(target, from: root);
|
||||
if (relative == '.') {
|
||||
_rejectLink(current, description);
|
||||
return;
|
||||
}
|
||||
for (final component in path.split(relative)) {
|
||||
current = path.join(current, component);
|
||||
_rejectLink(current, description);
|
||||
}
|
||||
}
|
||||
|
||||
static void _rejectLink(String entityPath, String description) {
|
||||
if (FileSystemEntity.typeSync(entityPath, followLinks: false) ==
|
||||
FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'$description must not contain symbolic links.',
|
||||
entityPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<File> getFiles() {
|
||||
return _getInputs()
|
||||
.map((input) => input.file)
|
||||
.whereType<File>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
final String manifestDir;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class DarwinEnvironment {
|
||||
DarwinEnvironment({
|
||||
required this.target,
|
||||
required this.deploymentTarget,
|
||||
});
|
||||
|
||||
final Target target;
|
||||
final String deploymentTarget;
|
||||
|
||||
Map<String, String> buildEnvironment() {
|
||||
final platform = target.darwinPlatform;
|
||||
if (platform == null || platform == 'macosx') {
|
||||
return {};
|
||||
}
|
||||
|
||||
final sdkPath = _resolveSdkPath(platform);
|
||||
final flags = _clangFlags(sdkPath);
|
||||
final rustTarget = target.rust;
|
||||
|
||||
return {
|
||||
'SDKROOT': sdkPath,
|
||||
'IPHONEOS_DEPLOYMENT_TARGET': deploymentTarget,
|
||||
'CC_$rustTarget': _findTool(platform, 'clang'),
|
||||
'CFLAGS_$rustTarget': flags,
|
||||
'CXX_$rustTarget': _findTool(platform, 'clang++'),
|
||||
'CXXFLAGS_$rustTarget': flags,
|
||||
'AR_$rustTarget': _findTool(platform, 'ar'),
|
||||
'RANLIB_$rustTarget': _findTool(platform, 'ranlib'),
|
||||
'BINDGEN_EXTRA_CLANG_ARGS_$rustTarget': flags,
|
||||
};
|
||||
}
|
||||
|
||||
String _resolveSdkPath(String platform) {
|
||||
final result = runCommand('xcrun', [
|
||||
'--sdk',
|
||||
platform,
|
||||
'--show-sdk-path',
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
String _findTool(String platform, String tool) {
|
||||
final result = runCommand('xcrun', [
|
||||
'--sdk',
|
||||
platform,
|
||||
'--find',
|
||||
tool,
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
String _clangFlags(String sdkPath) {
|
||||
final flags = <String>[];
|
||||
final clangTarget = _clangTarget();
|
||||
if (clangTarget != null) {
|
||||
flags.add('--target=$clangTarget');
|
||||
}
|
||||
flags.add('-isysroot $sdkPath');
|
||||
flags.add(_deploymentFlag());
|
||||
return flags.join(' ');
|
||||
}
|
||||
|
||||
String _deploymentFlag() {
|
||||
return switch (target.darwinPlatform) {
|
||||
'iphonesimulator' => '-mios-simulator-version-min=$deploymentTarget',
|
||||
'iphoneos' => '-miphoneos-version-min=$deploymentTarget',
|
||||
_ => throw ArgumentError(
|
||||
'Unsupported Darwin platform: ${target.darwinPlatform}'),
|
||||
};
|
||||
}
|
||||
|
||||
String? _clangTarget() {
|
||||
return switch (target.rust) {
|
||||
'aarch64-apple-ios-sim' => 'arm64-apple-ios-simulator',
|
||||
'x86_64-apple-ios' => 'x86_64-apple-ios-simulator',
|
||||
'aarch64-apple-ios' => 'arm64-apple-ios',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
extension on String {
|
||||
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||
}
|
||||
|
||||
class Environment {
|
||||
/// Current build configuration (debug or release).
|
||||
static String get configuration =>
|
||||
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||
|
||||
static bool get isDebug => configuration == 'debug';
|
||||
static bool get isRelease => configuration == 'release';
|
||||
|
||||
/// Temporary directory where Rust build artifacts are placed.
|
||||
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||
|
||||
/// Final output directory where the build artifacts are placed.
|
||||
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||
|
||||
/// Path to the crate manifest (containing Cargo.toml).
|
||||
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||
|
||||
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||
/// not resolved on purpose.
|
||||
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||
|
||||
// Pod
|
||||
|
||||
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||
static String get darwinPlatformName =>
|
||||
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||
|
||||
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||
static List<String> get darwinArchs =>
|
||||
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||
|
||||
static String get iosDeploymentTarget =>
|
||||
Platform.environment['IPHONEOS_DEPLOYMENT_TARGET'] ?? '12.0';
|
||||
|
||||
// Gradle
|
||||
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||
static List<String> get targetPlatforms =>
|
||||
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||
|
||||
// CMAKE
|
||||
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||
|
||||
static String _getEnv(String key) {
|
||||
final res = Platform.environment[key];
|
||||
print("Environment variable $key: $res");
|
||||
if (res == null) {
|
||||
throw Exception("Missing environment variable $key");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static String _getEnvPath(String key) {
|
||||
final res = _getEnv(key);
|
||||
if (Directory(res).existsSync()) {
|
||||
return res.resolveSymlink();
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const String kSeparator = "--";
|
||||
const String kDoubleSeparator = "==";
|
||||
|
||||
bool _lastMessageWasSeparator = false;
|
||||
|
||||
void _log(LogRecord rec) {
|
||||
final prefix = '${rec.level.name}: ';
|
||||
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||
if (rec.message == kSeparator) {
|
||||
if (!_lastMessageWasSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('-' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
}
|
||||
return;
|
||||
} else if (rec.message == kDoubleSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('=' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
return;
|
||||
}
|
||||
out.write(prefix);
|
||||
out.writeln(rec.message);
|
||||
_lastMessageWasSeparator = false;
|
||||
}
|
||||
|
||||
void initLogging() {
|
||||
Logger.root.level = Level.INFO;
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
final lines = rec.message.split('\n');
|
||||
for (final line in lines) {
|
||||
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||
_log(LogRecord(
|
||||
rec.level,
|
||||
line,
|
||||
rec.loggerName,
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void enableVerboseLogging() {
|
||||
Logger.root.level = Level.ALL;
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final _log = Logger('options');
|
||||
|
||||
/// A class for exceptions that have source span information attached.
|
||||
class SourceSpanException implements Exception {
|
||||
// This is a getter so that subclasses can override it.
|
||||
/// A message describing the exception.
|
||||
String get message => _message;
|
||||
final String _message;
|
||||
|
||||
// This is a getter so that subclasses can override it.
|
||||
/// The span associated with this exception.
|
||||
///
|
||||
/// This may be `null` if the source location can't be determined.
|
||||
SourceSpan? get span => _span;
|
||||
final SourceSpan? _span;
|
||||
|
||||
SourceSpanException(this._message, this._span);
|
||||
|
||||
/// Returns a string representation of `this`.
|
||||
///
|
||||
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||||
/// it indicates an ANSI terminal color escape that should be used to
|
||||
/// highlight the span's text. If it's `true`, it indicates that the text
|
||||
/// should be highlighted using the default color. If it's `false` or `null`,
|
||||
/// it indicates that the text shouldn't be highlighted.
|
||||
@override
|
||||
String toString({Object? color}) {
|
||||
if (span == null) return message;
|
||||
return 'Error on ${span!.message(message, color: color)}';
|
||||
}
|
||||
}
|
||||
|
||||
enum Toolchain {
|
||||
stable,
|
||||
beta,
|
||||
nightly,
|
||||
}
|
||||
|
||||
class CargoBuildOptions {
|
||||
final Toolchain toolchain;
|
||||
final List<String> flags;
|
||||
|
||||
CargoBuildOptions({
|
||||
required this.toolchain,
|
||||
required this.flags,
|
||||
});
|
||||
|
||||
static Toolchain _toolchainFromNode(YamlNode node) {
|
||||
if (node case YamlScalar(value: String name)) {
|
||||
final toolchain =
|
||||
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||||
if (toolchain != null) {
|
||||
return toolchain;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||||
node.span);
|
||||
}
|
||||
|
||||
static CargoBuildOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||
}
|
||||
Toolchain toolchain = Toolchain.stable;
|
||||
List<String> flags = [];
|
||||
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||
if (key case YamlScalar(value: 'toolchain')) {
|
||||
toolchain = _toolchainFromNode(value);
|
||||
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||||
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||||
if (list.every((element) {
|
||||
if (element case YamlScalar(value: String _)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
flags = list.map((e) => e.value as String).toList();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Extra flags must be a list of strings', value.span);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||||
key.span);
|
||||
}
|
||||
}
|
||||
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||||
}
|
||||
}
|
||||
|
||||
void _rejectUnknownFields(
|
||||
YamlMap map,
|
||||
Set<String> allowedFields,
|
||||
String context,
|
||||
) {
|
||||
for (final key in map.nodes.keys) {
|
||||
if (key is! YamlScalar ||
|
||||
key.value is! String ||
|
||||
!allowedFields.contains(key.value)) {
|
||||
throw SourceSpanException(
|
||||
'Unknown $context field "${key.value}".', key.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
YamlMap _mapFromNode(YamlNode node, String context) {
|
||||
if (node is YamlMap) {
|
||||
return node;
|
||||
}
|
||||
throw SourceSpanException('$context must be a map.', node.span);
|
||||
}
|
||||
|
||||
YamlNode _requiredNode(YamlMap map, String key, String context) {
|
||||
final node = map.nodes[YamlScalar.wrap(key)];
|
||||
if (node != null) {
|
||||
return node;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$context must contain "$key".',
|
||||
map.span,
|
||||
);
|
||||
}
|
||||
|
||||
String _stringFromNode(YamlNode node, String description,
|
||||
{bool allowEmpty = false}) {
|
||||
if (node case YamlScalar(value: String value)) {
|
||||
if (allowEmpty || value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a non-empty string.', node.span);
|
||||
}
|
||||
|
||||
List<String> _stringListFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowEmpty = false,
|
||||
}) {
|
||||
if (node is YamlList) {
|
||||
final result = <String>[];
|
||||
for (final item in node.nodes) {
|
||||
result.add(_stringFromNode(item, '$description entry'));
|
||||
}
|
||||
if (allowEmpty || result.isNotEmpty) {
|
||||
return List.unmodifiable(result);
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a non-empty list of strings.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _stringMapFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowEmpty = false,
|
||||
bool allowEmptyValues = false,
|
||||
}) {
|
||||
if (node is YamlMap) {
|
||||
final result = <String, String>{};
|
||||
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||
final name = _stringFromNode(key, '$description key');
|
||||
result[name] = _stringFromNode(
|
||||
value,
|
||||
'$description value',
|
||||
allowEmpty: allowEmptyValues,
|
||||
);
|
||||
}
|
||||
if (allowEmpty || result.isNotEmpty) {
|
||||
return Map.unmodifiable(result);
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a map of strings to strings.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
|
||||
String _relativePathFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowParent = false,
|
||||
}) {
|
||||
final value = _stringFromNode(node, description);
|
||||
if (value.contains('\\') ||
|
||||
path.posix.isAbsolute(value) ||
|
||||
path.windows.isAbsolute(value)) {
|
||||
throw SourceSpanException(
|
||||
'$description must be a relative path.', node.span);
|
||||
}
|
||||
final normalized = path.posix.normalize(value);
|
||||
if (!allowParent && (normalized == '..' || normalized.startsWith('../'))) {
|
||||
throw SourceSpanException(
|
||||
'$description must not escape the workspace root.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
List<String> _rustTargetsFromNode(YamlNode node, String description) {
|
||||
final targets = _stringListFromNode(node, description);
|
||||
final seen = <String>{};
|
||||
for (final target in targets) {
|
||||
if (Target.forRustTriple(target) == null) {
|
||||
throw SourceSpanException('Invalid Rust target "$target".', node.span);
|
||||
}
|
||||
if (!seen.add(target)) {
|
||||
throw SourceSpanException('Duplicate Rust target "$target".', node.span);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
enum CompositeHost {
|
||||
linux,
|
||||
macos,
|
||||
windows,
|
||||
}
|
||||
|
||||
class PrecompiledBuildRecipe {
|
||||
PrecompiledBuildRecipe({
|
||||
required this.rustToolchain,
|
||||
required this.flutterVersion,
|
||||
required this.xcodeVersion,
|
||||
required this.sdkVersions,
|
||||
required this.deploymentTargets,
|
||||
required this.rustTargets,
|
||||
});
|
||||
|
||||
static PrecompiledBuildRecipe parse(YamlNode node) {
|
||||
final map = _mapFromNode(node, 'Build recipe');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'rust_toolchain',
|
||||
'flutter_version',
|
||||
'xcode_version',
|
||||
'sdk_versions',
|
||||
'deployment_targets',
|
||||
'rust_targets',
|
||||
},
|
||||
'build recipe');
|
||||
return PrecompiledBuildRecipe(
|
||||
rustToolchain: _stringFromNode(
|
||||
_requiredNode(map, 'rust_toolchain', 'Build recipe'),
|
||||
'Rust toolchain',
|
||||
),
|
||||
flutterVersion: _stringFromNode(
|
||||
_requiredNode(map, 'flutter_version', 'Build recipe'),
|
||||
'Flutter version',
|
||||
),
|
||||
xcodeVersion: _stringFromNode(
|
||||
_requiredNode(map, 'xcode_version', 'Build recipe'),
|
||||
'Xcode version',
|
||||
),
|
||||
sdkVersions: _stringMapFromNode(
|
||||
_requiredNode(map, 'sdk_versions', 'Build recipe'),
|
||||
'SDK versions',
|
||||
),
|
||||
deploymentTargets: _stringMapFromNode(
|
||||
_requiredNode(map, 'deployment_targets', 'Build recipe'),
|
||||
'Deployment targets',
|
||||
),
|
||||
rustTargets: _rustTargetsFromNode(
|
||||
_requiredNode(map, 'rust_targets', 'Build recipe'),
|
||||
'Build recipe Rust targets',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String rustToolchain;
|
||||
final String flutterVersion;
|
||||
final String xcodeVersion;
|
||||
final Map<String, String> sdkVersions;
|
||||
final Map<String, String> deploymentTargets;
|
||||
final List<String> rustTargets;
|
||||
}
|
||||
|
||||
class CompositeGroup {
|
||||
CompositeGroup({
|
||||
required this.name,
|
||||
required this.host,
|
||||
required this.requiredTargets,
|
||||
required this.argv,
|
||||
required this.environment,
|
||||
required this.timeout,
|
||||
required this.outputs,
|
||||
});
|
||||
|
||||
static CompositeGroup parse(YamlNode node) {
|
||||
final map = _mapFromNode(node, 'Composite group');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'name',
|
||||
'host',
|
||||
'required_targets',
|
||||
'argv',
|
||||
'environment',
|
||||
'timeout_seconds',
|
||||
'outputs',
|
||||
},
|
||||
'composite group');
|
||||
|
||||
final nameNode = _requiredNode(map, 'name', 'Composite group');
|
||||
final name = _safeNameFromNode(nameNode, 'Composite group name');
|
||||
final hostNode = _requiredNode(map, 'host', 'Composite group');
|
||||
final hostName = _stringFromNode(hostNode, 'Composite host');
|
||||
final host = CompositeHost.values
|
||||
.firstWhereOrNull((element) => element.name == hostName);
|
||||
if (host == null) {
|
||||
throw SourceSpanException(
|
||||
'Invalid composite host "$hostName". Must be one of '
|
||||
'${CompositeHost.values.map((host) => host.name)}.',
|
||||
hostNode.span,
|
||||
);
|
||||
}
|
||||
|
||||
final argvNode = _requiredNode(map, 'argv', 'Composite group');
|
||||
final argv = _stringListFromNode(argvNode, 'Composite argv');
|
||||
final argvList = argvNode as YamlList;
|
||||
final command = _relativePathFromNode(argvList.nodes.first, 'Command path');
|
||||
final normalizedArgv =
|
||||
List<String>.unmodifiable([command, ...argv.skip(1)]);
|
||||
|
||||
final timeoutNode =
|
||||
_requiredNode(map, 'timeout_seconds', 'Composite group');
|
||||
final timeoutSeconds = switch (timeoutNode) {
|
||||
YamlScalar(value: int value) when value > 0 => value,
|
||||
_ => throw SourceSpanException(
|
||||
'Composite timeout must be a positive integer.', timeoutNode.span),
|
||||
};
|
||||
|
||||
final outputsNode = _requiredNode(map, 'outputs', 'Composite group');
|
||||
final outputNodes = switch (outputsNode) {
|
||||
YamlList(nodes: final nodes) when nodes.isNotEmpty => nodes,
|
||||
_ => throw SourceSpanException(
|
||||
'Composite outputs must be a non-empty list.', outputsNode.span),
|
||||
};
|
||||
final outputs = <String>[];
|
||||
final seenOutputs = <String>{};
|
||||
for (final outputNode in outputNodes) {
|
||||
final output = _safeNameFromNode(outputNode, 'Composite output name');
|
||||
if (!seenOutputs.add(output)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite output "$output".',
|
||||
outputNode.span,
|
||||
);
|
||||
}
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
final environmentNode = map.nodes[YamlScalar.wrap('environment')];
|
||||
final environment = environmentNode == null
|
||||
? const <String, String>{}
|
||||
: _stringMapFromNode(
|
||||
environmentNode,
|
||||
'Composite environment',
|
||||
allowEmpty: true,
|
||||
allowEmptyValues: true,
|
||||
);
|
||||
for (final key in environment.keys) {
|
||||
if (!RegExp(r'^[A-Za-z_][A-Za-z0-9_]*$').hasMatch(key)) {
|
||||
throw SourceSpanException(
|
||||
'Invalid composite environment variable "$key".',
|
||||
environmentNode?.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return CompositeGroup(
|
||||
name: name,
|
||||
host: host,
|
||||
requiredTargets: _rustTargetsFromNode(
|
||||
_requiredNode(map, 'required_targets', 'Composite group'),
|
||||
'Composite required targets',
|
||||
),
|
||||
argv: normalizedArgv,
|
||||
environment: environment,
|
||||
timeout: Duration(seconds: timeoutSeconds),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
);
|
||||
}
|
||||
|
||||
static String _safeNameFromNode(YamlNode node, String description) {
|
||||
final value = _stringFromNode(node, description);
|
||||
if (!RegExp(r'^[A-Za-z0-9][A-Za-z0-9._-]*$').hasMatch(value) ||
|
||||
value == '.' ||
|
||||
value == '..') {
|
||||
throw SourceSpanException('$description is unsafe.', node.span);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
final String name;
|
||||
final CompositeHost host;
|
||||
final List<String> requiredTargets;
|
||||
final List<String> argv;
|
||||
final Map<String, String> environment;
|
||||
final Duration timeout;
|
||||
final List<String> outputs;
|
||||
}
|
||||
|
||||
class PrecompiledBinaries {
|
||||
final String uriPrefix;
|
||||
final PublicKey publicKey;
|
||||
final String workspaceRoot;
|
||||
final List<String> hashInputs;
|
||||
final PrecompiledBuildRecipe? buildRecipe;
|
||||
final List<CompositeGroup> compositeGroups;
|
||||
|
||||
PrecompiledBinaries({
|
||||
required this.uriPrefix,
|
||||
required this.publicKey,
|
||||
this.workspaceRoot = '.',
|
||||
this.hashInputs = const [],
|
||||
this.buildRecipe,
|
||||
this.compositeGroups = const [],
|
||||
});
|
||||
|
||||
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||||
final bytes = HEX.decode(key);
|
||||
if (bytes.length != 32) {
|
||||
throw SourceSpanException(
|
||||
'Invalid public key. Must be 32 bytes long.', span);
|
||||
}
|
||||
return PublicKey(bytes);
|
||||
}
|
||||
|
||||
static PrecompiledBinaries parse(YamlNode node) {
|
||||
final map = _mapFromNode(node, 'Precompiled binaries');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'url_prefix',
|
||||
'public_key',
|
||||
'workspace_root',
|
||||
'hash_inputs',
|
||||
'build_recipe',
|
||||
'composite_groups',
|
||||
},
|
||||
'precompiled binaries');
|
||||
final urlPrefixNode =
|
||||
_requiredNode(map, 'url_prefix', 'Precompiled binaries');
|
||||
final publicKeyNode =
|
||||
_requiredNode(map, 'public_key', 'Precompiled binaries');
|
||||
final workspaceRootNode = map.nodes[YamlScalar.wrap('workspace_root')];
|
||||
final hashInputsNode = map.nodes[YamlScalar.wrap('hash_inputs')];
|
||||
final buildRecipeNode = map.nodes[YamlScalar.wrap('build_recipe')];
|
||||
final compositeGroupsNode = map.nodes[YamlScalar.wrap('composite_groups')];
|
||||
|
||||
final hashInputs = <String>[];
|
||||
final seenHashInputs = <String>{};
|
||||
if (hashInputsNode != null) {
|
||||
if (hashInputsNode is! YamlList) {
|
||||
throw SourceSpanException(
|
||||
'Hash inputs must be a list of relative paths.',
|
||||
hashInputsNode.span,
|
||||
);
|
||||
}
|
||||
for (final inputNode in hashInputsNode.nodes) {
|
||||
final input = _relativePathFromNode(inputNode, 'Hash input');
|
||||
if (!seenHashInputs.add(input)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate hash input "$input".', inputNode.span);
|
||||
}
|
||||
hashInputs.add(input);
|
||||
}
|
||||
}
|
||||
|
||||
final compositeGroups = <CompositeGroup>[];
|
||||
final groupNames = <String>{};
|
||||
final outputNames = <String>{};
|
||||
if (compositeGroupsNode != null) {
|
||||
if (compositeGroupsNode is! YamlList) {
|
||||
throw SourceSpanException(
|
||||
'Composite groups must be a list.',
|
||||
compositeGroupsNode.span,
|
||||
);
|
||||
}
|
||||
for (final groupNode in compositeGroupsNode.nodes) {
|
||||
final group = CompositeGroup.parse(groupNode);
|
||||
if (!groupNames.add(group.name)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite group "${group.name}".',
|
||||
groupNode.span,
|
||||
);
|
||||
}
|
||||
for (final output in group.outputs) {
|
||||
if (!outputNames.add(output)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite output "$output".',
|
||||
groupNode.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
compositeGroups.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
final publicKeyValue = _stringFromNode(publicKeyNode, 'Public key');
|
||||
return PrecompiledBinaries(
|
||||
uriPrefix: _stringFromNode(urlPrefixNode, 'URL prefix'),
|
||||
publicKey: _publicKeyFromHex(publicKeyValue, publicKeyNode.span),
|
||||
workspaceRoot: workspaceRootNode == null
|
||||
? '.'
|
||||
: _relativePathFromNode(
|
||||
workspaceRootNode,
|
||||
'Workspace root',
|
||||
allowParent: true,
|
||||
),
|
||||
hashInputs: List.unmodifiable(hashInputs),
|
||||
buildRecipe: buildRecipeNode == null
|
||||
? null
|
||||
: PrecompiledBuildRecipe.parse(buildRecipeNode),
|
||||
compositeGroups: List.unmodifiable(compositeGroups),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cargokit options specified for Rust crate.
|
||||
class CargokitCrateOptions {
|
||||
CargokitCrateOptions({
|
||||
this.cargo = const {},
|
||||
this.precompiledBinaries,
|
||||
});
|
||||
|
||||
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||||
final PrecompiledBinaries? precompiledBinaries;
|
||||
|
||||
static CargokitCrateOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||
}
|
||||
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||||
PrecompiledBinaries? precompiledBinaries;
|
||||
|
||||
for (final entry in node.nodes.entries) {
|
||||
if (entry
|
||||
case MapEntry(
|
||||
key: YamlScalar(value: 'cargo'),
|
||||
value: YamlNode node,
|
||||
)) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||
}
|
||||
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||||
if (key case YamlScalar(value: String name)) {
|
||||
final configuration = BuildConfiguration.values
|
||||
.firstWhereOrNull((element) => element.name == name);
|
||||
if (configuration != null) {
|
||||
options[configuration] = CargoBuildOptions.parse(value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||||
key.span);
|
||||
}
|
||||
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||||
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||||
entry.key.span);
|
||||
}
|
||||
}
|
||||
return CargokitCrateOptions(
|
||||
cargo: options,
|
||||
precompiledBinaries: precompiledBinaries,
|
||||
);
|
||||
}
|
||||
|
||||
static CargokitCrateOptions load({
|
||||
required String manifestDir,
|
||||
}) {
|
||||
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||||
final file = File.fromUri(uri);
|
||||
if (file.existsSync()) {
|
||||
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||||
return parse(contents);
|
||||
} else {
|
||||
return CargokitCrateOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PrecompiledBinariesMode {
|
||||
auto,
|
||||
disabled,
|
||||
required,
|
||||
}
|
||||
|
||||
class CargokitUserOptions {
|
||||
// When Rustup is installed always build locally unless user opts into
|
||||
// using precompiled binaries.
|
||||
static bool defaultUsePrecompiledBinaries() {
|
||||
return Rustup.executablePath() == null;
|
||||
}
|
||||
|
||||
factory CargokitUserOptions({
|
||||
bool? usePrecompiledBinaries,
|
||||
PrecompiledBinariesMode? precompiledBinariesMode,
|
||||
required bool verboseLogging,
|
||||
}) {
|
||||
if (usePrecompiledBinaries != null && precompiledBinariesMode != null) {
|
||||
throw ArgumentError(
|
||||
'usePrecompiledBinaries and precompiledBinariesMode are mutually exclusive.',
|
||||
);
|
||||
}
|
||||
return CargokitUserOptions._values(
|
||||
precompiledBinariesMode: precompiledBinariesMode ??
|
||||
(usePrecompiledBinaries == null
|
||||
? _defaultMode()
|
||||
: _legacyMode(usePrecompiledBinaries)),
|
||||
verboseLogging: verboseLogging,
|
||||
);
|
||||
}
|
||||
|
||||
CargokitUserOptions._defaults()
|
||||
: precompiledBinariesMode = _defaultMode(),
|
||||
verboseLogging = false;
|
||||
|
||||
CargokitUserOptions._values({
|
||||
required this.precompiledBinariesMode,
|
||||
required this.verboseLogging,
|
||||
});
|
||||
|
||||
static PrecompiledBinariesMode _defaultMode() =>
|
||||
defaultUsePrecompiledBinaries()
|
||||
? PrecompiledBinariesMode.auto
|
||||
: PrecompiledBinariesMode.disabled;
|
||||
|
||||
static PrecompiledBinariesMode _legacyMode(bool value) =>
|
||||
value ? PrecompiledBinariesMode.auto : PrecompiledBinariesMode.disabled;
|
||||
|
||||
static CargokitUserOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||
}
|
||||
var precompiledBinariesMode = _defaultMode();
|
||||
bool verboseLogging = false;
|
||||
SourceSpan? legacySettingSpan;
|
||||
SourceSpan? modeSettingSpan;
|
||||
|
||||
for (final entry in node.nodes.entries) {
|
||||
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||
if (entry.value case YamlScalar(value: bool value)) {
|
||||
precompiledBinariesMode = _legacyMode(value);
|
||||
legacySettingSpan = entry.key.span;
|
||||
continue;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||
entry.value.span);
|
||||
} else if (entry.key
|
||||
case YamlScalar(value: 'precompiled_binaries_mode')) {
|
||||
final modeName = switch (entry.value) {
|
||||
YamlScalar(value: String value) => value,
|
||||
_ => throw SourceSpanException(
|
||||
'Invalid value for "precompiled_binaries_mode". Must be one of '
|
||||
'${PrecompiledBinariesMode.values.map((mode) => mode.name)}.',
|
||||
entry.value.span,
|
||||
),
|
||||
};
|
||||
final mode = PrecompiledBinariesMode.values
|
||||
.firstWhereOrNull((element) => element.name == modeName);
|
||||
if (mode == null) {
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "precompiled_binaries_mode". Must be one of '
|
||||
'${PrecompiledBinariesMode.values.map((mode) => mode.name)}.',
|
||||
entry.value.span,
|
||||
);
|
||||
}
|
||||
precompiledBinariesMode = mode;
|
||||
modeSettingSpan = entry.key.span;
|
||||
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||||
if (entry.value case YamlScalar(value: bool value)) {
|
||||
verboseLogging = value;
|
||||
continue;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "verbose_logging". Must be a boolean.',
|
||||
entry.value.span);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargokit option type. Must be "precompiled_binaries_mode", '
|
||||
'"use_precompiled_binaries", or "verbose_logging".',
|
||||
entry.key.span);
|
||||
}
|
||||
}
|
||||
if (legacySettingSpan != null && modeSettingSpan != null) {
|
||||
throw SourceSpanException(
|
||||
'"precompiled_binaries_mode" and legacy '
|
||||
'"use_precompiled_binaries" cannot both be set.',
|
||||
modeSettingSpan,
|
||||
);
|
||||
}
|
||||
return CargokitUserOptions(
|
||||
precompiledBinariesMode: precompiledBinariesMode,
|
||||
verboseLogging: verboseLogging,
|
||||
);
|
||||
}
|
||||
|
||||
static CargokitUserOptions load() {
|
||||
String fileName = "cargokit_options.yaml";
|
||||
var userProjectDir = Directory(Environment.rootProjectDir);
|
||||
|
||||
while (userProjectDir.parent.path != userProjectDir.path) {
|
||||
final configFile = File(path.join(userProjectDir.path, fileName));
|
||||
if (configFile.existsSync()) {
|
||||
final contents = loadYamlNode(
|
||||
configFile.readAsStringSync(),
|
||||
sourceUrl: configFile.uri,
|
||||
);
|
||||
final res = parse(contents);
|
||||
if (res.verboseLogging) {
|
||||
_log.info('Found user options file at ${configFile.path}');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
userProjectDir = userProjectDir.parent;
|
||||
}
|
||||
return CargokitUserOptions._defaults();
|
||||
}
|
||||
|
||||
final PrecompiledBinariesMode precompiledBinariesMode;
|
||||
|
||||
bool get usePrecompiledBinaries =>
|
||||
precompiledBinariesMode != PrecompiledBinariesMode.disabled;
|
||||
|
||||
bool get allowLocalBuild =>
|
||||
precompiledBinariesMode != PrecompiledBinariesMode.required;
|
||||
|
||||
final bool verboseLogging;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,785 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
/// The manifest authenticates release bytes. Apple code signing remains a
|
||||
/// separate platform concern and is intentionally outside this boundary.
|
||||
const precompiledGenerationManifestFileName = 'completion.json';
|
||||
const precompiledGenerationManifestSignatureFileName = 'completion.json.sig';
|
||||
|
||||
const _manifestSchemaVersion = 2;
|
||||
const _manifestScope = 'cargokit-precompiled-generation';
|
||||
const precompiledAssetSignatureScheme = 'ed25519-cargokit-v2';
|
||||
const _assetSignatureDomain = 'cargokit-precompiled-asset-signature';
|
||||
final _sha256Pattern = RegExp(r'^[0-9a-f]{64}$');
|
||||
final _commitPattern = RegExp(r'^[0-9a-f]{40}(?:[0-9a-f]{24})?$');
|
||||
final _assetNamePattern = RegExp(r'^[A-Za-z0-9][A-Za-z0-9._-]*$');
|
||||
|
||||
class PrecompiledGenerationException implements Exception {
|
||||
PrecompiledGenerationException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => 'PrecompiledGenerationException: $message';
|
||||
}
|
||||
|
||||
class PrecompiledAsset {
|
||||
const PrecompiledAsset({
|
||||
required this.name,
|
||||
required this.length,
|
||||
required this.sha256,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int length;
|
||||
final String sha256;
|
||||
}
|
||||
|
||||
class PrecompiledCompositeChecksum {
|
||||
const PrecompiledCompositeChecksum({
|
||||
required this.archive,
|
||||
required this.checksum,
|
||||
this.algorithm = 'sha256',
|
||||
});
|
||||
|
||||
final String archive;
|
||||
final String checksum;
|
||||
final String algorithm;
|
||||
}
|
||||
|
||||
class PrecompiledBuildIdentity {
|
||||
PrecompiledBuildIdentity({
|
||||
required this.rustToolchain,
|
||||
required this.flutterVersion,
|
||||
required this.xcodeVersion,
|
||||
required Map<String, String> sdkVersions,
|
||||
required Map<String, String> deploymentTargets,
|
||||
required Iterable<String> rustTargets,
|
||||
}) : sdkVersions = Map.unmodifiable(Map.from(sdkVersions)),
|
||||
deploymentTargets = Map.unmodifiable(Map.from(deploymentTargets)),
|
||||
rustTargets = List.unmodifiable(rustTargets);
|
||||
|
||||
factory PrecompiledBuildIdentity.fromRecipe(PrecompiledBuildRecipe recipe) {
|
||||
return PrecompiledBuildIdentity(
|
||||
rustToolchain: recipe.rustToolchain,
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: recipe.rustTargets,
|
||||
);
|
||||
}
|
||||
|
||||
final String rustToolchain;
|
||||
final String flutterVersion;
|
||||
final String xcodeVersion;
|
||||
final Map<String, String> sdkVersions;
|
||||
final Map<String, String> deploymentTargets;
|
||||
final List<String> rustTargets;
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'deployment_targets': _sortedStringMap(deploymentTargets),
|
||||
'flutter_version': flutterVersion,
|
||||
'rust_targets': [...rustTargets]..sort(),
|
||||
'rust_toolchain': rustToolchain,
|
||||
'sdk_versions': _sortedStringMap(sdkVersions),
|
||||
'xcode_version': xcodeVersion,
|
||||
};
|
||||
|
||||
bool matches(PrecompiledBuildIdentity other) =>
|
||||
_canonicalJson(_json()) == _canonicalJson(other._json());
|
||||
}
|
||||
|
||||
class PrecompiledGenerationProvenance {
|
||||
PrecompiledGenerationProvenance({
|
||||
required this.pinned,
|
||||
required this.actual,
|
||||
});
|
||||
|
||||
factory PrecompiledGenerationProvenance.fromRecipe(
|
||||
PrecompiledBuildRecipe recipe) {
|
||||
final identity = PrecompiledBuildIdentity.fromRecipe(recipe);
|
||||
return PrecompiledGenerationProvenance(pinned: identity, actual: identity);
|
||||
}
|
||||
|
||||
final PrecompiledBuildIdentity pinned;
|
||||
final PrecompiledBuildIdentity actual;
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'actual': actual._json(),
|
||||
'pinned': pinned._json(),
|
||||
};
|
||||
}
|
||||
|
||||
class PrecompiledGenerationManifest {
|
||||
PrecompiledGenerationManifest({
|
||||
required this.generationHash,
|
||||
required this.sourceCommit,
|
||||
required this.provenance,
|
||||
required Iterable<PrecompiledAsset> assets,
|
||||
required Iterable<PrecompiledCompositeChecksum> compositeChecksums,
|
||||
}) : assets = List.unmodifiable(assets),
|
||||
compositeChecksums = List.unmodifiable(compositeChecksums) {
|
||||
_validateCompositeMetadata(this.assets, this.compositeChecksums);
|
||||
}
|
||||
|
||||
final String generationHash;
|
||||
final String sourceCommit;
|
||||
final PrecompiledGenerationProvenance provenance;
|
||||
final List<PrecompiledAsset> assets;
|
||||
final List<PrecompiledCompositeChecksum> compositeChecksums;
|
||||
|
||||
PrecompiledAsset? asset(String name) {
|
||||
for (final item in assets) {
|
||||
if (item.name == name) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<int> canonicalBytes() => utf8.encode(_canonicalJson(_json()));
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'asset_signature_scheme': precompiledAssetSignatureScheme,
|
||||
'assets': [
|
||||
...assets.map((asset) => {
|
||||
'length': asset.length,
|
||||
'name': asset.name,
|
||||
'sha256': asset.sha256,
|
||||
})
|
||||
]..sort((a, b) => (a['name'] as String).compareTo(b['name'] as String)),
|
||||
'composite_checksums': [
|
||||
...compositeChecksums.map((binding) => {
|
||||
'algorithm': binding.algorithm,
|
||||
'archive': binding.archive,
|
||||
'checksum': binding.checksum,
|
||||
})
|
||||
]..sort((a, b) {
|
||||
final archive =
|
||||
(a['archive'] as String).compareTo(b['archive'] as String);
|
||||
return archive != 0
|
||||
? archive
|
||||
: (a['checksum'] as String).compareTo(b['checksum'] as String);
|
||||
}),
|
||||
'generation_hash': generationHash,
|
||||
'provenance': provenance._json(),
|
||||
'schema_version': _manifestSchemaVersion,
|
||||
'scope': _manifestScope,
|
||||
'source_commit': sourceCommit,
|
||||
};
|
||||
|
||||
Uint8List sign(PrivateKey privateKey) =>
|
||||
ed25519Sign(privateKey, Uint8List.fromList(canonicalBytes()));
|
||||
|
||||
void verifySignature(PublicKey publicKey, List<int> signature) {
|
||||
_verifySignature(publicKey, canonicalBytes(), signature, 'manifest');
|
||||
}
|
||||
|
||||
void validateFor({
|
||||
required String generationHash,
|
||||
required PrecompiledBuildRecipe recipe,
|
||||
required Set<String> expectedAssetNames,
|
||||
}) {
|
||||
if (this.generationHash != generationHash) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest generation hash does not match the current generation.');
|
||||
}
|
||||
final expected = PrecompiledBuildIdentity.fromRecipe(recipe);
|
||||
if (!provenance.pinned.matches(expected) ||
|
||||
!provenance.actual.matches(provenance.pinned)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest provenance does not match the build recipe.');
|
||||
}
|
||||
final actualNames = assets.map((asset) => asset.name).toSet();
|
||||
if (actualNames.length != assets.length ||
|
||||
actualNames.length != expectedAssetNames.length ||
|
||||
!actualNames.containsAll(expectedAssetNames) ||
|
||||
!expectedAssetNames.containsAll(actualNames)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest asset inventory is incomplete or unexpected.');
|
||||
}
|
||||
}
|
||||
|
||||
void verifyAsset({
|
||||
required String name,
|
||||
required List<int> bytes,
|
||||
required List<int> signature,
|
||||
required PublicKey publicKey,
|
||||
}) {
|
||||
final expected = asset(name);
|
||||
if (expected == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" is not in the manifest inventory.');
|
||||
}
|
||||
if (bytes.length != expected.length ||
|
||||
sha256.convert(bytes).toString() != expected.sha256) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" does not match its manifest digest or length.');
|
||||
}
|
||||
verifyAssetMetadataSignature(
|
||||
name: name,
|
||||
signature: signature,
|
||||
publicKey: publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
void verifyAssetMetadataSignature({
|
||||
required String name,
|
||||
required List<int> signature,
|
||||
required PublicKey publicKey,
|
||||
}) {
|
||||
final expected = asset(name);
|
||||
if (expected == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" is not in the manifest inventory.');
|
||||
}
|
||||
verifyPrecompiledAssetMetadataSignature(
|
||||
publicKey,
|
||||
generationHash: generationHash,
|
||||
name: expected.name,
|
||||
length: expected.length,
|
||||
sha256: expected.sha256,
|
||||
signature: signature,
|
||||
);
|
||||
}
|
||||
|
||||
void validateCompositeChecksums(Map<String, List<int>> bytesByName) {
|
||||
for (final binding in compositeChecksums) {
|
||||
if (binding.algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
final archive = bytesByName[binding.archive];
|
||||
final checksum = bytesByName[binding.checksum];
|
||||
if (archive == null || checksum == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum assets are incomplete.');
|
||||
}
|
||||
final expected = ascii.encode('${sha256.convert(archive)}\n');
|
||||
if (checksum.length != expected.length ||
|
||||
!List<int>.generate(
|
||||
expected.length,
|
||||
(index) => checksum[index] ^ expected[index],
|
||||
).every((difference) => difference == 0)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum does not match its archive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PrecompiledGenerationManifest parse(List<int> bytes) {
|
||||
final text = _decodeUtf8(bytes);
|
||||
final value = _StrictJsonParser(text).parse();
|
||||
final map = _object(value, 'manifest');
|
||||
_requireFields(
|
||||
map,
|
||||
const {
|
||||
'schema_version',
|
||||
'scope',
|
||||
'asset_signature_scheme',
|
||||
'generation_hash',
|
||||
'source_commit',
|
||||
'provenance',
|
||||
'assets',
|
||||
'composite_checksums',
|
||||
},
|
||||
'manifest',
|
||||
);
|
||||
if (map['schema_version'] != _manifestSchemaVersion) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Unsupported manifest schema version.');
|
||||
}
|
||||
if (map['scope'] != _manifestScope) {
|
||||
throw PrecompiledGenerationException('Unsupported manifest scope.');
|
||||
}
|
||||
if (map['asset_signature_scheme'] != precompiledAssetSignatureScheme) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Unsupported asset signature scheme.');
|
||||
}
|
||||
final generationHash = _string(map['generation_hash'], 'generation hash');
|
||||
if (!_sha256Pattern.hasMatch(generationHash)) {
|
||||
throw PrecompiledGenerationException('Malformed generation hash.');
|
||||
}
|
||||
final sourceCommit = _string(map['source_commit'], 'source commit');
|
||||
if (!_commitPattern.hasMatch(sourceCommit)) {
|
||||
throw PrecompiledGenerationException('Malformed source commit.');
|
||||
}
|
||||
|
||||
final provenanceMap = _object(map['provenance'], 'provenance');
|
||||
_requireFields(provenanceMap, const {'pinned', 'actual'}, 'provenance');
|
||||
final provenance = PrecompiledGenerationProvenance(
|
||||
pinned: _identity(provenanceMap['pinned'], 'pinned provenance'),
|
||||
actual: _identity(provenanceMap['actual'], 'actual provenance'),
|
||||
);
|
||||
|
||||
final assetsValue = map['assets'];
|
||||
if (assetsValue is! List || assetsValue.isEmpty) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest assets must be a non-empty list.');
|
||||
}
|
||||
final assets = <PrecompiledAsset>[];
|
||||
String? previousName;
|
||||
final seenAssets = <String>{};
|
||||
for (final value in assetsValue) {
|
||||
final assetMap = _object(value, 'asset');
|
||||
_requireFields(assetMap, const {'name', 'length', 'sha256'}, 'asset');
|
||||
final name =
|
||||
_safeName(_string(assetMap['name'], 'asset name'), 'asset name');
|
||||
if (!seenAssets.add(name)) {
|
||||
throw PrecompiledGenerationException('Duplicate asset "$name".');
|
||||
}
|
||||
if (previousName != null && name.compareTo(previousName) <= 0) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest assets must be sorted by name.');
|
||||
}
|
||||
previousName = name;
|
||||
final length = _nonNegativeInt(assetMap['length'], 'asset length');
|
||||
final hash = _string(assetMap['sha256'], 'asset hash');
|
||||
if (!_sha256Pattern.hasMatch(hash)) {
|
||||
throw PrecompiledGenerationException('Malformed asset hash.');
|
||||
}
|
||||
assets.add(PrecompiledAsset(name: name, length: length, sha256: hash));
|
||||
}
|
||||
|
||||
final compositesValue = map['composite_checksums'];
|
||||
if (compositesValue is! List) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum relationships must be a list.');
|
||||
}
|
||||
final composites = <PrecompiledCompositeChecksum>[];
|
||||
final seenComposites = <String>{};
|
||||
String? previousComposite;
|
||||
for (final value in compositesValue) {
|
||||
final bindingMap = _object(value, 'composite checksum');
|
||||
_requireFields(bindingMap, const {'algorithm', 'archive', 'checksum'},
|
||||
'composite checksum');
|
||||
final algorithm = _string(bindingMap['algorithm'], 'checksum algorithm');
|
||||
final archive = _safeName(
|
||||
_string(bindingMap['archive'], 'archive asset'), 'archive asset');
|
||||
final checksum = _safeName(
|
||||
_string(bindingMap['checksum'], 'checksum asset'), 'checksum asset');
|
||||
if (algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
final identity = '$archive\u0000$checksum';
|
||||
if (!seenComposites.add(identity)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Duplicate composite checksum relationship.');
|
||||
}
|
||||
if (previousComposite != null &&
|
||||
identity.compareTo(previousComposite) <= 0) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum relationships must be sorted.');
|
||||
}
|
||||
previousComposite = identity;
|
||||
if (!seenAssets.contains(archive) || !seenAssets.contains(checksum)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum references an unknown asset.');
|
||||
}
|
||||
composites.add(PrecompiledCompositeChecksum(
|
||||
archive: archive,
|
||||
checksum: checksum,
|
||||
algorithm: algorithm,
|
||||
));
|
||||
}
|
||||
return PrecompiledGenerationManifest(
|
||||
generationHash: generationHash,
|
||||
sourceCommit: sourceCommit,
|
||||
provenance: provenance,
|
||||
assets: assets,
|
||||
compositeChecksums: composites,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List signPrecompiledAsset(PrivateKey privateKey, List<int> bytes) =>
|
||||
ed25519Sign(privateKey, Uint8List.fromList(bytes));
|
||||
|
||||
Uint8List precompiledAssetSignaturePayload({
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required Object length,
|
||||
required String sha256,
|
||||
}) {
|
||||
if (!_sha256Pattern.hasMatch(generationHash) ||
|
||||
!_sha256Pattern.hasMatch(sha256)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset signature metadata contains a malformed digest.');
|
||||
}
|
||||
_safeName(name, 'asset name');
|
||||
final lengthValue = switch (length) {
|
||||
int value => BigInt.from(value),
|
||||
BigInt value => value,
|
||||
_ => throw PrecompiledGenerationException(
|
||||
'Asset signature metadata length is not an integer.'),
|
||||
};
|
||||
if (lengthValue < BigInt.zero ||
|
||||
lengthValue > (BigInt.one << 64) - BigInt.one) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset signature metadata length is outside u64.');
|
||||
}
|
||||
final nameBytes = utf8.encode(name);
|
||||
if (nameBytes.length > 0xffffffff) {
|
||||
throw PrecompiledGenerationException('Asset name is too long.');
|
||||
}
|
||||
final nameLength = ByteData(4)..setUint32(0, nameBytes.length, Endian.big);
|
||||
final wordMask = BigInt.from(0xffffffff);
|
||||
final assetLength = ByteData(8)
|
||||
..setUint32(0, (lengthValue >> 32).toInt(), Endian.big)
|
||||
..setUint32(4, (lengthValue & wordMask).toInt(), Endian.big);
|
||||
return Uint8List.fromList([
|
||||
...ascii.encode(_assetSignatureDomain),
|
||||
0,
|
||||
2,
|
||||
..._decodeSha256(generationHash),
|
||||
...nameLength.buffer.asUint8List(),
|
||||
...nameBytes,
|
||||
...assetLength.buffer.asUint8List(),
|
||||
..._decodeSha256(sha256),
|
||||
]);
|
||||
}
|
||||
|
||||
Uint8List signPrecompiledAssetMetadata(
|
||||
PrivateKey privateKey, {
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required int length,
|
||||
required String sha256,
|
||||
}) =>
|
||||
ed25519Sign(
|
||||
privateKey,
|
||||
precompiledAssetSignaturePayload(
|
||||
generationHash: generationHash,
|
||||
name: name,
|
||||
length: length,
|
||||
sha256: sha256,
|
||||
),
|
||||
);
|
||||
|
||||
void verifyPrecompiledAssetMetadataSignature(
|
||||
PublicKey publicKey, {
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required int length,
|
||||
required String sha256,
|
||||
required List<int> signature,
|
||||
}) {
|
||||
_verifySignature(
|
||||
publicKey,
|
||||
precompiledAssetSignaturePayload(
|
||||
generationHash: generationHash,
|
||||
name: name,
|
||||
length: length,
|
||||
sha256: sha256,
|
||||
),
|
||||
signature,
|
||||
'asset "$name" metadata',
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List ed25519Sign(PrivateKey privateKey, Uint8List bytes) =>
|
||||
sign(privateKey, bytes);
|
||||
|
||||
void _verifySignature(
|
||||
PublicKey publicKey, List<int> bytes, List<int> signature, String subject) {
|
||||
if (signature.length != 64 ||
|
||||
!verify(publicKey, Uint8List.fromList(bytes),
|
||||
Uint8List.fromList(signature))) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Invalid Ed25519 signature for $subject.');
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _decodeSha256(String value) => Uint8List.fromList([
|
||||
for (var index = 0; index < value.length; index += 2)
|
||||
int.parse(value.substring(index, index + 2), radix: 16),
|
||||
]);
|
||||
|
||||
void _validateCompositeMetadata(
|
||||
List<PrecompiledAsset> assets,
|
||||
List<PrecompiledCompositeChecksum> composites,
|
||||
) {
|
||||
final byName = {for (final asset in assets) asset.name: asset};
|
||||
final boundAssets = <String>{};
|
||||
for (final binding in composites) {
|
||||
if (binding.algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
if (binding.archive == binding.checksum) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite archive and checksum must be distinct assets.');
|
||||
}
|
||||
if (!boundAssets.add(binding.archive) ||
|
||||
!boundAssets.add(binding.checksum)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'An asset may appear in at most one composite binding.');
|
||||
}
|
||||
final archive = byName[binding.archive];
|
||||
final checksum = byName[binding.checksum];
|
||||
if (archive == null || checksum == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum references an unknown asset.');
|
||||
}
|
||||
final expectedBytes = ascii.encode('${archive.sha256}\n');
|
||||
if (checksum.length != 65 ||
|
||||
checksum.sha256 != sha256.convert(expectedBytes).toString()) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum metadata is inconsistent with its archive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrecompiledBuildIdentity _identity(Object? value, String context) {
|
||||
final map = _object(value, context);
|
||||
_requireFields(
|
||||
map,
|
||||
const {
|
||||
'rust_toolchain',
|
||||
'flutter_version',
|
||||
'xcode_version',
|
||||
'sdk_versions',
|
||||
'deployment_targets',
|
||||
'rust_targets',
|
||||
},
|
||||
context);
|
||||
return PrecompiledBuildIdentity(
|
||||
rustToolchain: _string(map['rust_toolchain'], '$context Rust toolchain'),
|
||||
flutterVersion: _string(map['flutter_version'], '$context Flutter version'),
|
||||
xcodeVersion: _string(map['xcode_version'], '$context Xcode version'),
|
||||
sdkVersions: _stringMap(map['sdk_versions'], '$context SDK versions'),
|
||||
deploymentTargets:
|
||||
_stringMap(map['deployment_targets'], '$context deployment targets'),
|
||||
rustTargets:
|
||||
_sortedUniqueStrings(map['rust_targets'], '$context Rust targets'),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _stringMap(Object? value, String context) {
|
||||
final map = _object(value, context);
|
||||
final result = <String, String>{};
|
||||
for (final entry in map.entries) {
|
||||
if (entry.key.isEmpty) {
|
||||
throw PrecompiledGenerationException('$context contains an empty key.');
|
||||
}
|
||||
result[entry.key] = _string(entry.value, '$context value');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<String> _sortedUniqueStrings(Object? value, String context) {
|
||||
if (value is! List || value.isEmpty) {
|
||||
throw PrecompiledGenerationException('$context must be a non-empty list.');
|
||||
}
|
||||
final result = <String>[];
|
||||
final seen = <String>{};
|
||||
for (final item in value) {
|
||||
final string = _string(item, '$context entry');
|
||||
if (!seen.add(string)) {
|
||||
throw PrecompiledGenerationException('Duplicate $context entry.');
|
||||
}
|
||||
result.add(string);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _object(Object? value, String context) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
throw PrecompiledGenerationException('$context must be an object.');
|
||||
}
|
||||
|
||||
void _requireFields(
|
||||
Map<String, dynamic> map, Set<String> required, String context) {
|
||||
if (map.keys.length != required.length ||
|
||||
!map.keys.toSet().containsAll(required)) {
|
||||
final unknown = map.keys.where((key) => !required.contains(key));
|
||||
final missing = required.where((key) => !map.containsKey(key));
|
||||
throw PrecompiledGenerationException(
|
||||
'$context fields are invalid (unknown: ${unknown.join(', ')}, missing: ${missing.join(', ')}).');
|
||||
}
|
||||
}
|
||||
|
||||
String _string(Object? value, String context) {
|
||||
if (value is String && value.isNotEmpty) return value;
|
||||
throw PrecompiledGenerationException('$context must be a non-empty string.');
|
||||
}
|
||||
|
||||
int _nonNegativeInt(Object? value, String context) {
|
||||
if (value is int && value >= 0) return value;
|
||||
throw PrecompiledGenerationException(
|
||||
'$context must be a non-negative integer.');
|
||||
}
|
||||
|
||||
String _safeName(String value, String context) {
|
||||
if (value.contains('\\') ||
|
||||
path.posix.isAbsolute(value) ||
|
||||
path.windows.isAbsolute(value) ||
|
||||
path.posix.normalize(value) != value ||
|
||||
value.split('/').any((part) =>
|
||||
part.isEmpty ||
|
||||
part == '.' ||
|
||||
part == '..' ||
|
||||
!_assetNamePattern.hasMatch(part))) {
|
||||
throw PrecompiledGenerationException('$context is unsafe.');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Map<String, String> _sortedStringMap(Map<String, String> map) {
|
||||
final keys = map.keys.toList()..sort();
|
||||
return {for (final key in keys) key: map[key]!};
|
||||
}
|
||||
|
||||
String _canonicalJson(Object? value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
final keys = value.keys.toList()..sort();
|
||||
return '{${keys.map((key) => '${jsonEncode(key)}:${_canonicalJson(value[key])}').join(',')}}';
|
||||
}
|
||||
if (value is List) return '[${value.map(_canonicalJson).join(',')}]';
|
||||
return jsonEncode(value);
|
||||
}
|
||||
|
||||
String _decodeUtf8(List<int> bytes) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} on FormatException {
|
||||
throw PrecompiledGenerationException('Manifest is not valid UTF-8.');
|
||||
}
|
||||
}
|
||||
|
||||
class _StrictJsonParser {
|
||||
_StrictJsonParser(this.input);
|
||||
|
||||
final String input;
|
||||
int index = 0;
|
||||
|
||||
Object? parse() {
|
||||
_space();
|
||||
final value = _value();
|
||||
_space();
|
||||
if (index != input.length) _fail('Trailing data.');
|
||||
return value;
|
||||
}
|
||||
|
||||
Object? _value() {
|
||||
_space();
|
||||
if (index >= input.length) _fail('Unexpected end of JSON.');
|
||||
return switch (input.codeUnitAt(index)) {
|
||||
0x7b => _objectValue(),
|
||||
0x5b => _arrayValue(),
|
||||
0x22 => _stringValue(),
|
||||
0x74 => _literal('true', true),
|
||||
0x66 => _literal('false', false),
|
||||
0x6e => _literal('null', null),
|
||||
_ => _numberValue(),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _objectValue() {
|
||||
index++;
|
||||
final result = <String, dynamic>{};
|
||||
_space();
|
||||
if (_take(0x7d)) return result;
|
||||
while (true) {
|
||||
_space();
|
||||
if (index >= input.length || input.codeUnitAt(index) != 0x22) {
|
||||
_fail('Object key must be a string.');
|
||||
}
|
||||
final key = _stringValue();
|
||||
if (result.containsKey(key)) _fail('Duplicate object key "$key".');
|
||||
_space();
|
||||
if (!_take(0x3a)) _fail('Expected colon after object key.');
|
||||
result[key] = _value();
|
||||
_space();
|
||||
if (_take(0x7d)) return result;
|
||||
if (!_take(0x2c)) _fail('Expected comma between object fields.');
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> _arrayValue() {
|
||||
index++;
|
||||
final result = <dynamic>[];
|
||||
_space();
|
||||
if (_take(0x5d)) return result;
|
||||
while (true) {
|
||||
result.add(_value());
|
||||
_space();
|
||||
if (_take(0x5d)) return result;
|
||||
if (!_take(0x2c)) _fail('Expected comma between array values.');
|
||||
}
|
||||
}
|
||||
|
||||
String _stringValue() {
|
||||
final start = index;
|
||||
index++;
|
||||
while (index < input.length) {
|
||||
final code = input.codeUnitAt(index++);
|
||||
if (code == 0x22) {
|
||||
final raw = input.substring(start, index);
|
||||
try {
|
||||
final value = jsonDecode(raw);
|
||||
if (value is String) return value;
|
||||
} on FormatException {
|
||||
_fail('Malformed string.');
|
||||
}
|
||||
_fail('Malformed string.');
|
||||
}
|
||||
if (code == 0x5c) {
|
||||
if (index >= input.length) _fail('Malformed escape.');
|
||||
final escaped = input.codeUnitAt(index++);
|
||||
if (escaped == 0x75) {
|
||||
if (index + 4 > input.length ||
|
||||
!RegExp(r'^[0-9a-fA-F]{4}$')
|
||||
.hasMatch(input.substring(index, index + 4))) {
|
||||
_fail('Malformed unicode escape.');
|
||||
}
|
||||
index += 4;
|
||||
} else if (!const [0x22, 0x5c, 0x2f, 0x62, 0x66, 0x6e, 0x72, 0x74]
|
||||
.contains(escaped)) {
|
||||
_fail('Malformed escape.');
|
||||
}
|
||||
} else if (code < 0x20) {
|
||||
_fail('Unescaped control character.');
|
||||
}
|
||||
}
|
||||
_fail('Unterminated string.');
|
||||
}
|
||||
|
||||
Object? _literal(String literal, Object? value) {
|
||||
if (!input.startsWith(literal, index)) _fail('Malformed literal.');
|
||||
index += literal.length;
|
||||
return value;
|
||||
}
|
||||
|
||||
Object _numberValue() {
|
||||
final match =
|
||||
RegExp(r'-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?')
|
||||
.matchAsPrefix(input.substring(index));
|
||||
if (match == null) _fail('Malformed JSON value.');
|
||||
index += match.end;
|
||||
final text = match.group(0)!;
|
||||
return text.contains('.') || text.contains('e') || text.contains('E')
|
||||
? double.parse(text)
|
||||
: int.parse(text);
|
||||
}
|
||||
|
||||
bool _take(int code) {
|
||||
if (index < input.length && input.codeUnitAt(index) == code) {
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _space() {
|
||||
while (index < input.length &&
|
||||
const [0x20, 0x09, 0x0a, 0x0d].contains(input.codeUnitAt(index))) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
Never _fail(String message) => throw PrecompiledGenerationException(message);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
String? Function()? testRustupExecutablePathOverride;
|
||||
|
||||
class _Toolchain {
|
||||
_Toolchain(
|
||||
this.name,
|
||||
this.targets,
|
||||
);
|
||||
|
||||
final String name;
|
||||
final List<String> targets;
|
||||
}
|
||||
|
||||
class Rustup {
|
||||
List<String>? installedTargets(String toolchain) {
|
||||
final targets = _installedTargets(toolchain);
|
||||
return targets != null ? List.unmodifiable(targets) : null;
|
||||
}
|
||||
|
||||
void installToolchain(String toolchain) {
|
||||
log.info("Installing Rust toolchain: $toolchain");
|
||||
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||
_installedToolchains
|
||||
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||
}
|
||||
|
||||
void installTarget(
|
||||
String target, {
|
||||
required String toolchain,
|
||||
}) {
|
||||
log.info("Installing Rust target: $target");
|
||||
runCommand("rustup", [
|
||||
'target',
|
||||
'add',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
target,
|
||||
]);
|
||||
_installedTargets(toolchain)?.add(target);
|
||||
}
|
||||
|
||||
final List<_Toolchain> _installedToolchains;
|
||||
|
||||
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||
|
||||
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||
.firstWhereOrNull(
|
||||
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||
?.targets;
|
||||
|
||||
static List<_Toolchain> _getInstalledToolchains() {
|
||||
String extractToolchainName(String line) {
|
||||
// ignore (default) after toolchain name
|
||||
final parts = line.split(' ');
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||
|
||||
// To list all non-custom toolchains, we need to filter out lines that
|
||||
// don't start with "stable", "beta", or "nightly".
|
||||
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||
.map(extractToolchainName)
|
||||
.toList(growable: true);
|
||||
|
||||
return lines
|
||||
.map(
|
||||
(name) => _Toolchain(
|
||||
name,
|
||||
_getInstalledTargets(name),
|
||||
),
|
||||
)
|
||||
.toList(growable: true);
|
||||
}
|
||||
|
||||
static List<String> _getInstalledTargets(String toolchain) {
|
||||
final res = runCommand("rustup", [
|
||||
'target',
|
||||
'list',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
'--installed',
|
||||
]);
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList(growable: true);
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool _didInstallRustSrcForNightly = false;
|
||||
|
||||
void installRustSrcForNightly() {
|
||||
if (_didInstallRustSrcForNightly) {
|
||||
return;
|
||||
}
|
||||
// Useful for -Z build-std
|
||||
runCommand(
|
||||
"rustup",
|
||||
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||
);
|
||||
_didInstallRustSrcForNightly = true;
|
||||
}
|
||||
|
||||
static String? executablePath() {
|
||||
if (testRustupExecutablePathOverride != null) {
|
||||
return testRustupExecutablePathOverride!();
|
||||
}
|
||||
|
||||
final envPath = Platform.environment['PATH'];
|
||||
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||
final home = Platform.isWindows
|
||||
? Platform.environment['USERPROFILE']
|
||||
: Platform.environment['HOME'];
|
||||
final paths = [
|
||||
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||
];
|
||||
for (final p in paths) {
|
||||
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||
final rustupPath = path.join(p, rustup);
|
||||
if (File(rustupPath).existsSync()) {
|
||||
return rustupPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
class Target {
|
||||
Target({
|
||||
required this.rust,
|
||||
this.flutter,
|
||||
this.android,
|
||||
this.androidMinSdkVersion,
|
||||
this.darwinPlatform,
|
||||
this.darwinArch,
|
||||
});
|
||||
|
||||
static final all = [
|
||||
Target(
|
||||
rust: 'armv7-linux-androideabi',
|
||||
flutter: 'android-arm',
|
||||
android: 'armeabi-v7a',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-linux-android',
|
||||
flutter: 'android-arm64',
|
||||
android: 'arm64-v8a',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'i686-linux-android',
|
||||
flutter: 'android-x86',
|
||||
android: 'x86',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-linux-android',
|
||||
flutter: 'android-x64',
|
||||
android: 'x86_64',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-pc-windows-msvc',
|
||||
flutter: 'windows-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-unknown-linux-gnu',
|
||||
flutter: 'linux-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-unknown-linux-gnu',
|
||||
flutter: 'linux-arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios-sim',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-ios',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios',
|
||||
darwinPlatform: 'iphoneos',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
];
|
||||
|
||||
static Target? forFlutterName(String flutterName) {
|
||||
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||
}
|
||||
|
||||
static Target? forDarwin({
|
||||
required String platformName,
|
||||
required String darwinAarch,
|
||||
}) {
|
||||
return all.firstWhereOrNull((element) => //
|
||||
element.darwinPlatform == platformName &&
|
||||
element.darwinArch == darwinAarch);
|
||||
}
|
||||
|
||||
static Target? forRustTriple(String triple) {
|
||||
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||
}
|
||||
|
||||
static List<Target> androidTargets() {
|
||||
return all
|
||||
.where((element) => element.android != null)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||
static List<Target> buildableTargets() {
|
||||
if (Platform.isLinux) {
|
||||
// Right now we don't support cross-compiling on Linux. So we just return
|
||||
// the host target.
|
||||
final arch = runCommand('arch', []).stdout as String;
|
||||
if (arch.trim() == 'aarch64') {
|
||||
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||
} else {
|
||||
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||
}
|
||||
}
|
||||
return all.where((target) {
|
||||
if (Platform.isWindows) {
|
||||
return target.rust.contains('-windows-');
|
||||
} else if (Platform.isMacOS) {
|
||||
return target.darwinPlatform != null;
|
||||
}
|
||||
return false;
|
||||
}).toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return rust;
|
||||
}
|
||||
|
||||
final String? flutter;
|
||||
final String rust;
|
||||
final String? android;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? darwinPlatform;
|
||||
final String? darwinArch;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'logging.dart';
|
||||
import 'rustup.dart';
|
||||
|
||||
final log = Logger("process");
|
||||
|
||||
class CommandFailedException implements Exception {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final ProcessResult result;
|
||||
|
||||
CommandFailedException({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
required this.result,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final stdout = result.stdout.toString().trim();
|
||||
final stderr = result.stderr.toString().trim();
|
||||
return [
|
||||
"External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}",
|
||||
"Returned Exit Code: ${result.exitCode}",
|
||||
kSeparator,
|
||||
"STDOUT:",
|
||||
if (stdout.isNotEmpty) stdout,
|
||||
kSeparator,
|
||||
"STDERR:",
|
||||
if (stderr.isNotEmpty) stderr,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunCommandArgs {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final String? workingDirectory;
|
||||
final Map<String, String>? environment;
|
||||
final bool includeParentEnvironment;
|
||||
final bool runInShell;
|
||||
final Encoding? stdoutEncoding;
|
||||
final Encoding? stderrEncoding;
|
||||
|
||||
TestRunCommandArgs({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
this.workingDirectory,
|
||||
this.environment,
|
||||
this.includeParentEnvironment = true,
|
||||
this.runInShell = false,
|
||||
this.stdoutEncoding,
|
||||
this.stderrEncoding,
|
||||
});
|
||||
}
|
||||
|
||||
class TestRunCommandResult {
|
||||
TestRunCommandResult({
|
||||
this.pid = 1,
|
||||
this.exitCode = 0,
|
||||
this.stdout = '',
|
||||
this.stderr = '',
|
||||
});
|
||||
|
||||
final int pid;
|
||||
final int exitCode;
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
}
|
||||
|
||||
TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride;
|
||||
|
||||
ProcessResult runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
Map<String, String>? environment,
|
||||
bool includeParentEnvironment = true,
|
||||
bool runInShell = false,
|
||||
Encoding? stdoutEncoding = systemEncoding,
|
||||
Encoding? stderrEncoding = systemEncoding,
|
||||
}) {
|
||||
if (testRunCommandOverride != null) {
|
||||
final result = testRunCommandOverride!(TestRunCommandArgs(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
stderrEncoding: stderrEncoding,
|
||||
));
|
||||
return ProcessResult(
|
||||
result.pid,
|
||||
result.exitCode,
|
||||
result.stdout,
|
||||
result.stderr,
|
||||
);
|
||||
}
|
||||
log.finer('Running command $executable ${arguments.join(' ')}');
|
||||
final res = Process.runSync(
|
||||
_resolveExecutable(executable),
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stderrEncoding: stderrEncoding,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
);
|
||||
if (res.exitCode != 0) {
|
||||
throw CommandFailedException(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
result: res,
|
||||
);
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
class RustupNotFoundException implements Exception {
|
||||
@override
|
||||
String toString() {
|
||||
return [
|
||||
' ',
|
||||
'rustup not found in PATH.',
|
||||
' ',
|
||||
'Maybe you need to install Rust? It only takes a minute:',
|
||||
' ',
|
||||
if (Platform.isWindows) 'https://www.rust-lang.org/tools/install',
|
||||
if (hasHomebrewRustInPath()) ...[
|
||||
'\$ brew unlink rust # Unlink homebrew Rust from PATH',
|
||||
],
|
||||
if (!Platform.isWindows)
|
||||
"\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
||||
' ',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
static bool hasHomebrewRustInPath() {
|
||||
if (!Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
final envPath = Platform.environment['PATH'] ?? '';
|
||||
final paths = envPath.split(':');
|
||||
return paths.any((p) {
|
||||
return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _resolveExecutable(String executable) {
|
||||
if (executable == 'rustup') {
|
||||
final resolved = Rustup.executablePath();
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
throw RustupNotFoundException();
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'precompiled_asset_store.dart';
|
||||
import 'precompiled_generation.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class VerifyBinaries {
|
||||
VerifyBinaries({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
final String manifestDir;
|
||||
|
||||
Future<void> run() async {
|
||||
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||
final precompiledBinaries = config.precompiledBinaries;
|
||||
if (precompiledBinaries == null) {
|
||||
stdout.writeln('Crate does not support precompiled binaries.');
|
||||
return;
|
||||
}
|
||||
final recipe = precompiledBinaries.buildRecipe;
|
||||
if (recipe == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Configured precompiled binaries need a complete build recipe.');
|
||||
}
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
final expectedNames = <String>{};
|
||||
for (final triple in recipe.rustTargets) {
|
||||
final target = Target.forRustTriple(triple);
|
||||
if (target == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Build recipe contains an unsupported Rust target.');
|
||||
}
|
||||
for (final type in AritifactType.values) {
|
||||
for (final artifact in getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
aritifactType: type,
|
||||
)) {
|
||||
expectedNames.add(PrecompileBinaries.fileName(target, artifact));
|
||||
}
|
||||
}
|
||||
}
|
||||
final compositeChecksums = <String>{};
|
||||
for (final group in precompiledBinaries.compositeGroups) {
|
||||
for (final checksum
|
||||
in group.outputs.where((output) => output.endsWith('.checksum'))) {
|
||||
final archive =
|
||||
checksum.substring(0, checksum.length - '.checksum'.length);
|
||||
if (group.outputs.contains(archive)) {
|
||||
expectedNames.add(archive);
|
||||
expectedNames.add(checksum);
|
||||
compositeChecksums.add('$archive\u0000$checksum');
|
||||
}
|
||||
}
|
||||
}
|
||||
final generationHash = CrateHash.compute(manifestDir);
|
||||
final cacheRoot = Directory.systemTemp.createTempSync('cargokit-verify-');
|
||||
final transport = PrecompiledAssetTransport();
|
||||
try {
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: path.join(cacheRoot.path, 'cache'),
|
||||
uriPrefix: Uri.parse(precompiledBinaries.uriPrefix),
|
||||
publicKey: precompiledBinaries.publicKey,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: expectedNames,
|
||||
expectedCompositeChecksums: compositeChecksums,
|
||||
transport: transport,
|
||||
);
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: generationHash,
|
||||
requestedAssetNames: expectedNames,
|
||||
);
|
||||
if (snapshot == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Completed v2 precompiled generation is unavailable.');
|
||||
}
|
||||
stdout.writeln('Verified generation $generationHash');
|
||||
} finally {
|
||||
transport.close();
|
||||
if (cacheRoot.existsSync()) cacheRoot.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
ed25519_edwards:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.17.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.8"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: version
|
||||
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
@@ -0,0 +1,33 @@
|
||||
# This is copied from Cargokit (which is the official way to use it currently)
|
||||
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
name: build_tool
|
||||
description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build.
|
||||
publish_to: none
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
# these are pinned on purpose because the bundle_tool_runner doesn't have
|
||||
# pubspec.lock. See run_build_tool.sh
|
||||
logging: 1.2.0
|
||||
path: 1.8.0
|
||||
version: 3.0.0
|
||||
collection: 1.18.0
|
||||
ed25519_edwards: 0.3.1
|
||||
hex: 0.2.0
|
||||
yaml: 3.1.2
|
||||
source_span: 1.10.0
|
||||
github: 9.17.0
|
||||
args: 2.4.2
|
||||
crypto: 3.0.3
|
||||
convert: 3.1.1
|
||||
http: 1.1.0
|
||||
toml: 0.14.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.1.0
|
||||
test: ^1.24.0
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/android_environment.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('arm64 and x64 android targets use 16 KB page size linker flags',
|
||||
() async {
|
||||
final sdkPath = _createFakeAndroidSdk();
|
||||
final targets = [
|
||||
Target.forRustTriple('aarch64-linux-android')!,
|
||||
Target.forRustTriple('x86_64-linux-android')!,
|
||||
];
|
||||
|
||||
for (final target in targets) {
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 21,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
contains('link-arg=-Wl,-z,max-page-size=16384'),
|
||||
);
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
contains('link-arg=-Wl,--hash-style=both'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('32-bit android targets keep the libgcc workaround without 16 KB flags',
|
||||
() async {
|
||||
final sdkPath = _createFakeAndroidSdk();
|
||||
final target = Target.forRustTriple('armv7-linux-androideabi')!;
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 16,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['CARGO_ENCODED_RUSTFLAGS'], contains('libgcc_workaround'));
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
isNot(contains('link-arg=-Wl,-z,max-page-size=16384')),
|
||||
);
|
||||
});
|
||||
|
||||
test('bindgen clang args use libclang-safe android paths', () async {
|
||||
final sdkPath = _createFakeAndroidSdk(
|
||||
sdkPathSegment: r'C:\Users\test\AppData\Local\Android\Sdk',
|
||||
);
|
||||
final target = Target.forRustTriple('aarch64-linux-android')!;
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 24,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
final bindgenArgs = env['BINDGEN_EXTRA_CLANG_ARGS_aarch64-linux-android'];
|
||||
expect(bindgenArgs, isNotNull);
|
||||
expect(bindgenArgs, startsWith('--target=aarch64-linux-android24 '));
|
||||
expect(bindgenArgs, isNot(contains(r'\')));
|
||||
expect(
|
||||
bindgenArgs,
|
||||
contains('/C:/Users/test/AppData/Local/Android/Sdk/ndk/26.3.11579264/'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _createFakeAndroidSdk({String? sdkPathSegment}) {
|
||||
final temp = Directory.systemTemp.createTempSync('fjs-cargokit-android-sdk-');
|
||||
final sdk = sdkPathSegment == null
|
||||
? temp
|
||||
: Directory(path.join(temp.path, sdkPathSegment))
|
||||
..createSync(recursive: true);
|
||||
final toolchainBin = Directory(path.join(
|
||||
sdk.path,
|
||||
'ndk',
|
||||
'26.3.11579264',
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
Platform.isWindows
|
||||
? 'windows-x86_64'
|
||||
: Platform.isLinux
|
||||
? 'linux-x86_64'
|
||||
: 'darwin-x86_64',
|
||||
'bin',
|
||||
))
|
||||
..createSync(recursive: true);
|
||||
|
||||
for (final tool in [
|
||||
'llvm-ar',
|
||||
'llvm-ranlib',
|
||||
'clang',
|
||||
'clang++',
|
||||
if (Platform.isWindows) ...[
|
||||
'llvm-ar.exe',
|
||||
'llvm-ranlib.exe',
|
||||
'clang.exe',
|
||||
'clang++.exe',
|
||||
],
|
||||
]) {
|
||||
File(path.join(toolchainBin.path, tool)).writeAsStringSync('');
|
||||
}
|
||||
|
||||
return sdk.path;
|
||||
}
|
||||
@@ -0,0 +1,912 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:build_tool/src/artifacts_provider.dart';
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/cargo.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompile_binaries.dart';
|
||||
import 'package:build_tool/src/precompiled_asset_store.dart';
|
||||
import 'package:build_tool/src/precompiled_generation.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _hash =
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const _sourceCommit = '1111111111111111111111111111111111111111';
|
||||
const _privateSeed = <int>[
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
|
||||
void main() {
|
||||
late Directory temp;
|
||||
late Target target;
|
||||
late KeyPair keyPair;
|
||||
late PrecompiledBuildRecipe recipe;
|
||||
late BuildEnvironment environment;
|
||||
|
||||
setUp(() {
|
||||
temp = Directory.systemTemp.createTempSync('artifacts-provider-test-');
|
||||
target = Target.forRustTriple('x86_64-unknown-linux-gnu')!;
|
||||
keyPair = newKeyPair();
|
||||
recipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.88.0',
|
||||
flutterVersion: '3.32.8',
|
||||
xcodeVersion: '16.4',
|
||||
sdkVersions: const {'linux': '6.8'},
|
||||
deploymentTargets: const {'linux': 'glibc-2.35'},
|
||||
rustTargets: [target.rust],
|
||||
);
|
||||
environment = BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: CargokitCrateOptions(
|
||||
precompiledBinaries: PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: recipe,
|
||||
),
|
||||
),
|
||||
targetTempDir: temp.path,
|
||||
manifestDir: temp.path,
|
||||
crateInfo: CrateInfo(packageName: 'fjs'),
|
||||
isAndroid: false,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => temp.deleteSync(recursive: true));
|
||||
|
||||
test('disabled mode uses local Rust and never contacts remote', () async {
|
||||
var localCalls = 0;
|
||||
var remoteCalls = 0;
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.disabled,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
});
|
||||
|
||||
test('auto mode falls back locally only when completion manifest is absent',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
var requested = <String>[];
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
requested.add(url.path);
|
||||
return Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(requested, hasLength(1));
|
||||
expect(requested.single, contains('completion.json'));
|
||||
});
|
||||
|
||||
test('auto mode fails closed when completion is missing for existing cache',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final installProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
await installProvider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
final requested = <String>[];
|
||||
var localCalls = 0;
|
||||
final orphanedProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
requested.add(url.path);
|
||||
return Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
orphanedProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(requested, [
|
||||
'/precompiled/$_hash/completion.json',
|
||||
]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('auto 404 fails closed for legacy staging previous and malformed v2',
|
||||
() async {
|
||||
final precompiledRoot = Directory(path.join(temp.path, 'precompiled'))
|
||||
..createSync();
|
||||
final states = <FileSystemEntity>[
|
||||
Directory(path.join(precompiledRoot.path, '$_hash.staging-foreign')),
|
||||
Directory(path.join(precompiledRoot.path, '$_hash.previous-foreign')),
|
||||
File(path.join(precompiledRoot.path, 'v2')),
|
||||
];
|
||||
for (final state in states) {
|
||||
if (state is Directory) {
|
||||
state.createSync();
|
||||
} else if (state is File) {
|
||||
state.writeAsStringSync('malformed');
|
||||
}
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async => Response('', 404),
|
||||
localBuilder: (_, __) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
if (state.existsSync()) state.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'auto mode distinguishes unsupported precompilation from incomplete config',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
var remoteCalls = 0;
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final noPrecompiled = _withPrecompiled(environment, null);
|
||||
final unsupportedProvider = _provider(
|
||||
noPrecompiled,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final localResult = await unsupportedProvider.getArtifacts([target]);
|
||||
|
||||
expect(localResult[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final incompleteConfig = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
);
|
||||
final incompleteProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
incompleteProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final disabledProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.disabled,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
final disabledResult = await disabledProvider.getArtifacts([target]);
|
||||
expect(disabledResult[target]!.single.path, local.path);
|
||||
expect(localCalls, 2);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final requiredProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
requiredProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 2);
|
||||
expect(remoteCalls, 0);
|
||||
});
|
||||
|
||||
test('required mode fails on missing completion and never invokes local Rust',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('fetches signed completion first and only exact requested target assets',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final calls = <String>[];
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target], hasLength(1));
|
||||
expect(result[target]!.single.finalFileName, 'libfjs.so');
|
||||
expect(calls.first, contains('completion.json'));
|
||||
expect(calls[1], contains('completion.json.sig'));
|
||||
expect(calls.where((call) => call.endsWith('.sig')).length, 2);
|
||||
expect(calls, everyElement(isNot(contains('other-target'))));
|
||||
});
|
||||
|
||||
test(
|
||||
'rejects a requested target outside the signed recipe before asset requests',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final unsupportedTarget =
|
||||
Target.forRustTriple('aarch64-unknown-linux-gnu')!;
|
||||
final calls = <String>[];
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([unsupportedTarget]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(calls, isEmpty);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('target-only request does not download unrelated composite assets',
|
||||
() async {
|
||||
final group = _compositeGroup(target);
|
||||
final compositeEnvironment = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: recipe,
|
||||
compositeGroups: [group],
|
||||
),
|
||||
);
|
||||
final fixture = _fixture(
|
||||
recipe,
|
||||
target: target,
|
||||
keyPair: keyPair,
|
||||
includeComposite: true,
|
||||
);
|
||||
final calls = <String>[];
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
compositeEnvironment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target], hasLength(1));
|
||||
expect(calls, isNot(contains(endsWith('/fjs.xcframework.zip'))));
|
||||
expect(calls, isNot(contains(endsWith('/fjs.xcframework.zip.checksum'))));
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('revalidates immutable snapshot on every use and never repairs tamper',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
var assetRequests = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
if (url.path.endsWith('.so') || url.path.endsWith('.so.sig')) {
|
||||
assetRequests++;
|
||||
}
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
);
|
||||
await provider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(assetRequests, 2);
|
||||
});
|
||||
|
||||
test(
|
||||
'tampered cache plus invalid remote repair fails closed without local Rust',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final initialProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
await initialProvider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
final invalidRemote = Map<String, Response>.from(fixture.responses);
|
||||
final signaturePath = invalidRemote.keys
|
||||
.firstWhere((key) => key.endsWith('${fixture.assetName}.sig'));
|
||||
invalidRemote[signaturePath] = Response.bytes(Uint8List(64), 200);
|
||||
var localCalls = 0;
|
||||
final repairProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => invalidRemote[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
repairProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('tampered manifest anchor remains unchanged and never builds locally',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
await _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
).getArtifacts([target]);
|
||||
final anchor = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'anchor',
|
||||
precompiledGenerationManifestFileName,
|
||||
))
|
||||
..writeAsBytesSync([0]);
|
||||
var localCalls = 0;
|
||||
|
||||
await expectLater(
|
||||
_provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async =>
|
||||
fixture.responses[url.path] ?? Response('', 404),
|
||||
localBuilder: (_, __) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
).getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(anchor.readAsBytesSync(), [0]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
'partial or signature-invalid remote generation fails closed without local fallback',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
var localCalls = 0;
|
||||
final missingAssetResponses = Map<String, Response>.from(fixture.responses)
|
||||
..removeWhere((key, _) => key.endsWith('.so'));
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async =>
|
||||
missingAssetResponses[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
|
||||
final badSignatureResponses = Map<String, Response>.from(fixture.responses);
|
||||
final signaturePath =
|
||||
badSignatureResponses.keys.firstWhere((key) => key.endsWith('.so.sig'));
|
||||
badSignatureResponses[signaturePath] = Response.bytes(Uint8List(64), 200);
|
||||
final badProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async =>
|
||||
badSignatureResponses[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
badProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('download failure leaves anchor but no partial snapshot staging',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final responses = Map<String, Response>.from(fixture.responses);
|
||||
final assetPath = responses.keys.firstWhere((key) => key.endsWith('.so'));
|
||||
responses[assetPath] = Response.bytes([1, 2, 3], 200);
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
final generation =
|
||||
Directory(path.join(temp.path, 'precompiled', 'v2', _hash));
|
||||
expect(
|
||||
Directory(path.join(generation.path, 'anchor')).existsSync(), isTrue);
|
||||
expect(
|
||||
Directory(path.join(generation.path, 'snapshots'))
|
||||
.listSync(followLinks: false)
|
||||
.where((entity) => path.basename(entity.path).contains('.staging-')),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates and closes a fresh transport for every remote lookup',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final transports = <_TrackingTransport>[];
|
||||
final provider = ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () {
|
||||
final transport = _TrackingTransport(
|
||||
(request) async => _streamedResponse(
|
||||
fixture.responses[request.url.path] ?? Response('', 404),
|
||||
request,
|
||||
),
|
||||
);
|
||||
transports.add(transport);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
|
||||
await provider.getArtifacts([target]);
|
||||
await provider.getArtifacts([target]);
|
||||
|
||||
expect(transports, hasLength(2));
|
||||
expect(transports.map((transport) => transport.closeCalls), [1, 1]);
|
||||
});
|
||||
|
||||
test('closes its transport when a remote lookup fails', () async {
|
||||
_TrackingTransport? transport;
|
||||
final provider = ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () => transport = _TrackingTransport(
|
||||
(_) async => StreamedResponse(
|
||||
const Stream<List<int>>.empty(),
|
||||
500,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(transport!.closeCalls, 1);
|
||||
});
|
||||
|
||||
test('validates the complete recipe before creating a transport', () async {
|
||||
final invalidRecipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: recipe.rustToolchain,
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: [target.rust, 'unsupported-target'],
|
||||
);
|
||||
final invalidEnvironment = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: invalidRecipe,
|
||||
),
|
||||
);
|
||||
var transports = 0;
|
||||
final provider = ArtifactProvider(
|
||||
environment: invalidEnvironment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () {
|
||||
transports++;
|
||||
return PrecompiledAssetTransport(
|
||||
send: (_) async =>
|
||||
StreamedResponse(const Stream<List<int>>.empty(), 500),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(transports, 0);
|
||||
});
|
||||
}
|
||||
|
||||
class _TrackingTransport extends PrecompiledAssetTransport {
|
||||
_TrackingTransport(PrecompiledHttpSend send) : super(send: send);
|
||||
|
||||
var closeCalls = 0;
|
||||
|
||||
@override
|
||||
void close() {
|
||||
closeCalls++;
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
||||
StreamedResponse _streamedResponse(Response response, BaseRequest request) {
|
||||
return StreamedResponse(
|
||||
Stream.value(response.bodyBytes),
|
||||
response.statusCode,
|
||||
contentLength: response.bodyBytes.length,
|
||||
headers: response.headers,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
request: request,
|
||||
isRedirect: response.isRedirect,
|
||||
);
|
||||
}
|
||||
|
||||
ArtifactProvider _provider(
|
||||
BuildEnvironment environment,
|
||||
PrecompiledBinariesMode mode, {
|
||||
required ArtifactHttpGet httpGet,
|
||||
ArtifactLocalBuilder? localBuilder,
|
||||
}) {
|
||||
return ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: mode,
|
||||
verboseLogging: false,
|
||||
),
|
||||
httpGet: httpGet,
|
||||
generationHash: (_) => _hash,
|
||||
localBuilder: localBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Artifact> _localArtifact(
|
||||
Directory temp, Target target, String name) async {
|
||||
final file = File(path.join(temp.path, name))..writeAsBytesSync([1]);
|
||||
return Artifact(path: file.path, finalFileName: name);
|
||||
}
|
||||
|
||||
class _Fixture {
|
||||
_Fixture(
|
||||
{required this.responses,
|
||||
required this.assetName,
|
||||
required this.assetBytes});
|
||||
final Map<String, Response> responses;
|
||||
final String assetName;
|
||||
final Uint8List assetBytes;
|
||||
}
|
||||
|
||||
_Fixture _fixture(
|
||||
PrecompiledBuildRecipe recipe, {
|
||||
required Target target,
|
||||
required KeyPair keyPair,
|
||||
bool includeComposite = false,
|
||||
}) {
|
||||
final assetName = PrecompileBinaries.fileName(target, 'libfjs.so');
|
||||
final assetBytes = Uint8List.fromList([5, 4, 3, 2]);
|
||||
final staticAssetName = PrecompileBinaries.fileName(target, 'libfjs.a');
|
||||
final staticAssetBytes = Uint8List.fromList([4, 3, 2, 1]);
|
||||
final archiveBytes = Uint8List.fromList([8, 7, 6, 5]);
|
||||
final checksumText = sha256Bytes(archiveBytes);
|
||||
final checksumBytes = Uint8List.fromList(utf8.encode('$checksumText\n'));
|
||||
final assets = <PrecompiledAsset>[
|
||||
PrecompiledAsset(
|
||||
name: assetName,
|
||||
length: assetBytes.length,
|
||||
sha256: sha256Bytes(assetBytes),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: staticAssetName,
|
||||
length: staticAssetBytes.length,
|
||||
sha256: sha256Bytes(staticAssetBytes),
|
||||
),
|
||||
if (includeComposite) ...[
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archiveBytes.length,
|
||||
sha256: sha256Bytes(archiveBytes),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksumBytes.length,
|
||||
sha256: sha256Bytes(checksumBytes),
|
||||
),
|
||||
],
|
||||
]..sort((a, b) => a.name.compareTo(b.name));
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _hash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: assets,
|
||||
compositeChecksums: includeComposite
|
||||
? const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
);
|
||||
final prefix = 'https://assets.example/precompiled/$_hash/';
|
||||
return _Fixture(
|
||||
assetName: assetName,
|
||||
assetBytes: assetBytes,
|
||||
responses: {
|
||||
'/precompiled/$_hash/completion.json.sig':
|
||||
Response.bytes(manifest.sign(keyPair.privateKey), 200),
|
||||
'/precompiled/$_hash/completion.json':
|
||||
Response.bytes(manifest.canonicalBytes(), 200),
|
||||
'/precompiled/$_hash/$assetName.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: assetName,
|
||||
length: assetBytes.length,
|
||||
sha256: sha256Bytes(assetBytes),
|
||||
),
|
||||
200),
|
||||
'/precompiled/$_hash/$assetName': Response.bytes(assetBytes, 200),
|
||||
if (includeComposite) ...{
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archiveBytes.length,
|
||||
sha256: sha256Bytes(archiveBytes),
|
||||
),
|
||||
200,
|
||||
),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip':
|
||||
Response.bytes(archiveBytes, 200),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.checksum.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksumBytes.length,
|
||||
sha256: sha256Bytes(checksumBytes),
|
||||
),
|
||||
200,
|
||||
),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.checksum':
|
||||
Response.bytes(checksumBytes, 200),
|
||||
},
|
||||
prefix: Response('', 404),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
BuildEnvironment _withPrecompiled(
|
||||
BuildEnvironment environment,
|
||||
PrecompiledBinaries? precompiled,
|
||||
) {
|
||||
return BuildEnvironment(
|
||||
configuration: environment.configuration,
|
||||
crateOptions: CargokitCrateOptions(precompiledBinaries: precompiled),
|
||||
targetTempDir: environment.targetTempDir,
|
||||
manifestDir: environment.manifestDir,
|
||||
crateInfo: environment.crateInfo,
|
||||
isAndroid: environment.isAndroid,
|
||||
androidSdkPath: environment.androidSdkPath,
|
||||
androidNdkVersion: environment.androidNdkVersion,
|
||||
androidMinSdkVersion: environment.androidMinSdkVersion,
|
||||
javaHome: environment.javaHome,
|
||||
);
|
||||
}
|
||||
|
||||
CompositeGroup _compositeGroup(Target target) {
|
||||
return CompositeGroup(
|
||||
name: 'swiftpm',
|
||||
host: CompositeHost.linux,
|
||||
requiredTargets: [target.rust],
|
||||
argv: const ['tool/build.sh'],
|
||||
environment: const {},
|
||||
timeout: const Duration(minutes: 1),
|
||||
outputs: const [
|
||||
'fjs.xcframework.zip',
|
||||
'fjs.xcframework.zip.checksum',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String sha256Bytes(List<int> bytes) => sha256.convert(bytes).toString();
|
||||
|
||||
KeyPair newKeyPair() {
|
||||
final private = newKeyFromSeed(Uint8List.fromList(_privateSeed));
|
||||
return KeyPair(private, public(private));
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/cargo.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('cargo build uses rustc from the selected rustup toolchain', () async {
|
||||
Map<String, String>? cargoEnvironment;
|
||||
testRunCommandOverride = (args) {
|
||||
if (args.executable == 'rustup' &&
|
||||
args.arguments.length == 4 &&
|
||||
args.arguments[0] == 'which' &&
|
||||
args.arguments[1] == '--toolchain' &&
|
||||
args.arguments[2] == 'stable' &&
|
||||
args.arguments[3] == 'rustc') {
|
||||
return TestRunCommandResult(stdout: '/rustup/stable/bin/rustc\n');
|
||||
}
|
||||
|
||||
if (args.executable == 'rustup' &&
|
||||
args.arguments.length >= 4 &&
|
||||
args.arguments[0] == 'run' &&
|
||||
args.arguments[1] == 'stable' &&
|
||||
args.arguments[2] == 'cargo' &&
|
||||
args.arguments[3] == 'build') {
|
||||
cargoEnvironment = args.environment;
|
||||
return TestRunCommandResult();
|
||||
}
|
||||
|
||||
throw StateError(
|
||||
'Unexpected command: ${args.executable} ${args.arguments}');
|
||||
};
|
||||
|
||||
final builder = RustBuilder(
|
||||
target: Target.forRustTriple('x86_64-pc-windows-msvc')!,
|
||||
environment: BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: CargokitCrateOptions(),
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
manifestDir: '/tmp/fjs-cargokit-manifest',
|
||||
crateInfo: CrateInfo(packageName: 'fjs'),
|
||||
isAndroid: false,
|
||||
),
|
||||
);
|
||||
|
||||
await builder.build();
|
||||
|
||||
expect(cargoEnvironment?['RUSTC'], '/rustup/stable/bin/rustc');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/build_tool.dart';
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/composite_artifacts.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompile_binaries.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Directory temp;
|
||||
late Directory workspace;
|
||||
late Directory targetRoot;
|
||||
late CompositeGroup group;
|
||||
|
||||
setUp(() {
|
||||
final systemTemp =
|
||||
Directory(Directory.systemTemp.resolveSymbolicLinksSync());
|
||||
temp = systemTemp.createTempSync('composite-artifacts-test.');
|
||||
workspace = Directory(path.join(temp.path, 'workspace'))..createSync();
|
||||
targetRoot = Directory(path.join(temp.path, 'targets'))..createSync();
|
||||
File(path.join(workspace.path, 'assemble.sh'))
|
||||
.writeAsStringSync('#!/bin/sh\n');
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios')).createSync();
|
||||
group = CompositeGroup(
|
||||
name: 'swiftpm',
|
||||
host: CompositeHost.macos,
|
||||
requiredTargets: const ['aarch64-apple-ios'],
|
||||
argv: const ['assemble.sh', r'$literal'],
|
||||
environment: const {'LC_ALL': 'C', 'PATH': '/usr/bin:/bin'},
|
||||
timeout: const Duration(seconds: 3),
|
||||
outputs: const ['bundle.zip', 'bundle.zip.checksum'],
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => temp.deleteSync(recursive: true));
|
||||
|
||||
test('local generation command requires explicit paths and repeats targets',
|
||||
() {
|
||||
final command = BuildPrecompiledGenerationCommand();
|
||||
final arguments = command.argParser.parse(const [
|
||||
'--manifest-dir',
|
||||
'/manifest',
|
||||
'--output-dir',
|
||||
'/output',
|
||||
'--temp-dir',
|
||||
'/temp',
|
||||
'--target',
|
||||
'aarch64-apple-ios',
|
||||
'--target',
|
||||
'aarch64-apple-darwin',
|
||||
]);
|
||||
|
||||
expect(command.name, 'build-precompiled-generation');
|
||||
expect(arguments['manifest-dir'], '/manifest');
|
||||
expect(arguments['output-dir'], '/output');
|
||||
expect(arguments['temp-dir'], '/temp');
|
||||
expect(arguments['target'], [
|
||||
'aarch64-apple-ios',
|
||||
'aarch64-apple-darwin',
|
||||
]);
|
||||
});
|
||||
|
||||
test('composite uses controlled argv/env and atomically retains outputs',
|
||||
() async {
|
||||
CompositeProcessInvocation? invocation;
|
||||
final outputRoot = path.join(temp.path, 'output');
|
||||
final builder = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: outputRoot,
|
||||
currentHost: CompositeHost.macos,
|
||||
processRunner: (value) async {
|
||||
invocation = value;
|
||||
final output = Directory(value.environment['CARGOKIT_OUTPUT_ROOT']!);
|
||||
File(path.join(output.path, 'bundle.zip')).writeAsBytesSync([1, 2, 3]);
|
||||
File(path.join(output.path, 'bundle.zip.checksum'))
|
||||
.writeAsStringSync('checksum\n');
|
||||
return const CompositeProcessResult(exitCode: 0);
|
||||
},
|
||||
);
|
||||
|
||||
final result = await builder.build(group);
|
||||
|
||||
expect(invocation!.executable, path.join(workspace.path, 'assemble.sh'));
|
||||
expect(invocation!.arguments, [r'$literal']);
|
||||
expect(invocation!.workingDirectory, workspace.path);
|
||||
expect(invocation!.timeout, const Duration(seconds: 3));
|
||||
expect(invocation!.environment, {
|
||||
'LC_ALL': 'C',
|
||||
'PATH': '/usr/bin:/bin',
|
||||
'CARGOKIT_GENERATION_HASH': 'generation-hash',
|
||||
'CARGOKIT_WORKSPACE_ROOT': workspace.path,
|
||||
'CARGOKIT_TARGET_OUTPUT_ROOT': targetRoot.path,
|
||||
'CARGOKIT_OUTPUT_ROOT': isNotEmpty,
|
||||
'CARGOKIT_DART_EXECUTABLE': Platform.resolvedExecutable,
|
||||
});
|
||||
expect(File(result.outputs['bundle.zip']!).readAsBytesSync(), [1, 2, 3]);
|
||||
expect(
|
||||
Directory(outputRoot).listSync(recursive: true).where(
|
||||
(entity) => path.basename(entity.path).startsWith('.swiftpm.')),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('composite rejects host/target, timeout, nonzero, and missing output',
|
||||
() async {
|
||||
Future<void> expectFailure(
|
||||
String name,
|
||||
CompositeProcessResult result,
|
||||
) async {
|
||||
final builder = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, name),
|
||||
currentHost: CompositeHost.macos,
|
||||
processRunner: (_) async => result,
|
||||
);
|
||||
await expectLater(
|
||||
builder.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
}
|
||||
|
||||
final wrongHost = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, 'wrong-host'),
|
||||
currentHost: CompositeHost.linux,
|
||||
);
|
||||
await expectLater(
|
||||
wrongHost.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios'))
|
||||
.deleteSync(recursive: true);
|
||||
final missingTarget = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, 'missing-target'),
|
||||
currentHost: CompositeHost.macos,
|
||||
);
|
||||
await expectLater(
|
||||
missingTarget.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios')).createSync();
|
||||
|
||||
await expectFailure(
|
||||
'timeout',
|
||||
const CompositeProcessResult(exitCode: -1, timedOut: true),
|
||||
);
|
||||
await expectFailure(
|
||||
'nonzero',
|
||||
const CompositeProcessResult(exitCode: 9, stderr: 'failed'),
|
||||
);
|
||||
await expectFailure(
|
||||
'missing-output',
|
||||
const CompositeProcessResult(exitCode: 0),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'local generation builds targets once, retains both forms, and is deterministic',
|
||||
() async {
|
||||
_writeCrate(workspace);
|
||||
final buildCalls = <String, int>{};
|
||||
var compositeCalls = 0;
|
||||
|
||||
Future<String> buildTarget(
|
||||
Target target,
|
||||
BuildEnvironment environment,
|
||||
String toolchain,
|
||||
) async {
|
||||
expect(toolchain, '1.88.0');
|
||||
buildCalls.update(target.rust, (value) => value + 1, ifAbsent: () => 1);
|
||||
final output =
|
||||
Directory(path.join(environment.targetTempDir, target.rust))
|
||||
..createSync(recursive: true);
|
||||
File(path.join(output.path, 'libfjs.a'))
|
||||
.writeAsStringSync('static:${target.rust}');
|
||||
File(path.join(output.path, 'libfjs.dylib'))
|
||||
.writeAsStringSync('dynamic:${target.rust}');
|
||||
return output.path;
|
||||
}
|
||||
|
||||
Future<CompositeProcessResult> assemble(
|
||||
CompositeProcessInvocation invocation) async {
|
||||
compositeCalls++;
|
||||
final output = Directory(invocation.environment['CARGOKIT_OUTPUT_ROOT']!);
|
||||
File(path.join(output.path, 'bundle.zip')).writeAsStringSync('archive');
|
||||
File(path.join(output.path, 'bundle.zip.checksum'))
|
||||
.writeAsStringSync('checksum\n');
|
||||
return const CompositeProcessResult(exitCode: 0);
|
||||
}
|
||||
|
||||
final output = path.join(temp.path, 'generation');
|
||||
final generator = LocalPrecompiledGeneration(
|
||||
manifestDir: workspace.path,
|
||||
outputDir: output,
|
||||
tempDir: path.join(temp.path, 'build'),
|
||||
targetTriples: const [
|
||||
'aarch64-apple-ios',
|
||||
'aarch64-apple-darwin',
|
||||
],
|
||||
targetBuilder: buildTarget,
|
||||
currentHost: CompositeHost.macos,
|
||||
compositeProcessRunner: assemble,
|
||||
);
|
||||
|
||||
final firstFragment = await generator.run();
|
||||
final firstBytes = firstFragment.readAsBytesSync();
|
||||
expect(buildCalls, {
|
||||
'aarch64-apple-ios': 1,
|
||||
'aarch64-apple-darwin': 1,
|
||||
});
|
||||
expect(compositeCalls, 1);
|
||||
final secondFragment = await generator.run();
|
||||
|
||||
expect(buildCalls, {
|
||||
'aarch64-apple-ios': 2,
|
||||
'aarch64-apple-darwin': 2,
|
||||
});
|
||||
expect(compositeCalls, 2);
|
||||
expect(secondFragment.readAsBytesSync(), firstBytes);
|
||||
for (final target in buildCalls.keys) {
|
||||
expect(
|
||||
File(path.join(output, 'targets', target, '${target}_libfjs.a'))
|
||||
.existsSync(),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
File(path.join(output, 'targets', target, '${target}_libfjs.dylib'))
|
||||
.existsSync(),
|
||||
isTrue,
|
||||
);
|
||||
}
|
||||
final fragment =
|
||||
jsonDecode(utf8.decode(firstBytes)) as Map<String, dynamic>;
|
||||
expect(fragment['schema_version'], 1);
|
||||
expect(fragment['scope'], 'cargokit-local-precompiled-generation');
|
||||
expect(fragment['generation_hash'], matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
expect((fragment['recipe'] as Map)['rust_toolchain'], '1.88.0');
|
||||
expect(fragment['assets'], hasLength(6));
|
||||
expect(fragment.containsKey('signature'), isFalse);
|
||||
});
|
||||
|
||||
test('local generation rejects duplicate, unknown, and unpinned targets',
|
||||
() async {
|
||||
_writeCrate(workspace);
|
||||
|
||||
Future<void> rejects(List<String> targets, String suffix) async {
|
||||
final generator = LocalPrecompiledGeneration(
|
||||
manifestDir: workspace.path,
|
||||
outputDir: path.join(temp.path, 'invalid-$suffix'),
|
||||
tempDir: path.join(temp.path, 'invalid-build-$suffix'),
|
||||
targetTriples: targets,
|
||||
targetBuilder: (_, __, ___) => throw StateError('must not build'),
|
||||
currentHost: CompositeHost.macos,
|
||||
);
|
||||
await expectLater(
|
||||
generator.run(),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
}
|
||||
|
||||
await rejects(
|
||||
const ['aarch64-apple-ios', 'aarch64-apple-ios'],
|
||||
'duplicate',
|
||||
);
|
||||
await rejects(const ['invented-target'], 'unknown');
|
||||
await rejects(const ['x86_64-apple-ios'], 'unpinned');
|
||||
await rejects(const ['aarch64-apple-ios'], 'incomplete-composite');
|
||||
});
|
||||
}
|
||||
|
||||
void _writeCrate(Directory workspace) {
|
||||
File(path.join(workspace.path, 'Cargo.toml')).writeAsStringSync('''
|
||||
[package]
|
||||
name = "fjs"
|
||||
version = "1.0.0"
|
||||
''');
|
||||
Directory(path.join(workspace.path, 'src')).createSync();
|
||||
File(path.join(workspace.path, 'src/lib.rs'))
|
||||
.writeAsStringSync('pub fn fjs() {}\n');
|
||||
File(path.join(workspace.path, 'cargokit.yaml')).writeAsStringSync('''
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
workspace_root: .
|
||||
build_recipe:
|
||||
rust_toolchain: '1.88.0'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
macosx: '15.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
macos: '10.14'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- assemble.sh
|
||||
- --retained
|
||||
environment:
|
||||
LC_ALL: C
|
||||
PATH: /usr/bin:/bin
|
||||
timeout_seconds: 3
|
||||
outputs:
|
||||
- bundle.zip
|
||||
- bundle.zip.checksum
|
||||
''');
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/crate_hash.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _publicKey =
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445';
|
||||
|
||||
void main() {
|
||||
late Directory temporaryDirectory;
|
||||
late Directory workspace;
|
||||
late Directory manifest;
|
||||
|
||||
setUp(() {
|
||||
final resolvedSystemTemp =
|
||||
Directory(Directory.systemTemp.resolveSymbolicLinksSync());
|
||||
temporaryDirectory = resolvedSystemTemp.createTempSync('crate_hash_test.');
|
||||
workspace = Directory(path.join(temporaryDirectory.path, 'workspace'))
|
||||
..createSync();
|
||||
manifest = Directory(path.join(workspace.path, 'crate'))..createSync();
|
||||
_write(manifest, 'Cargo.toml', '[package]\nname = "test_crate"\n');
|
||||
_write(manifest, 'src/lib.rs', 'pub fn answer() -> i32 { 42 }\n');
|
||||
_writeConfig(manifest);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
temporaryDirectory.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('existing Rust input names and contents affect the hash', () {
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(manifest, 'src/lib.rs', 'pub fn answer() -> i32 { 43 }\n');
|
||||
final contentChanged = CrateHash.compute(manifest.path);
|
||||
expect(contentChanged, isNot(original));
|
||||
|
||||
final source = File(path.join(manifest.path, 'src/lib.rs'));
|
||||
source.renameSync(path.join(manifest.path, 'src/renamed.rs'));
|
||||
final pathChanged = CrateHash.compute(manifest.path);
|
||||
expect(pathChanged, isNot(contentChanged));
|
||||
});
|
||||
|
||||
const textProducerPaths = [
|
||||
'producer/source.rs',
|
||||
'producer/Cargo.toml',
|
||||
'producer/config.yaml',
|
||||
'producer/config.yml',
|
||||
'producer/source.dart',
|
||||
'producer/build.sh',
|
||||
'producer/Plugin.swift',
|
||||
'producer/data.json',
|
||||
'producer/README.md',
|
||||
'producer/Cargo.lock',
|
||||
];
|
||||
for (final producerPath in textProducerPaths) {
|
||||
test('normalizes Git newlines in $producerPath', () {
|
||||
_write(workspace, producerPath, 'first line\nsecond line\n');
|
||||
_writeConfig(manifest, hashInputs: [producerPath]);
|
||||
final lfHash = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, producerPath, 'first line\r\nsecond line\r\n');
|
||||
|
||||
expect(CrateHash.compute(manifest.path), lfHash);
|
||||
});
|
||||
}
|
||||
|
||||
test('meaningful text changes affect the canonical hash', () {
|
||||
const producerPath = 'producer/source.dart';
|
||||
_write(workspace, producerPath, 'const answer = 42;\n');
|
||||
_writeConfig(manifest, hashInputs: const [producerPath]);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, producerPath, 'const answer = 43;\r\n');
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('binary inputs preserve CRLF and invalid UTF-8 bytes', () {
|
||||
const producerPath = 'producer/artifact.bin';
|
||||
final producer = File(path.join(workspace.path, producerPath));
|
||||
producer.parent.createSync(recursive: true);
|
||||
producer.writeAsBytesSync([0xff, 0x0d, 0x0a]);
|
||||
_writeConfig(manifest, hashInputs: const [producerPath]);
|
||||
final crlfHash = CrateHash.compute(manifest.path);
|
||||
|
||||
producer.writeAsBytesSync([0xff, 0x0a]);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(crlfHash));
|
||||
});
|
||||
|
||||
test('declared files and directories are hashed recursively', () {
|
||||
_write(workspace, 'producer.txt', 'producer-v1');
|
||||
_write(workspace, 'tool/nested/build.sh', 'build-v1');
|
||||
_writeConfig(
|
||||
manifest,
|
||||
hashInputs: const ['producer.txt', 'tool'],
|
||||
);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, 'producer.txt', 'producer-v2');
|
||||
final fileChanged = CrateHash.compute(manifest.path);
|
||||
expect(fileChanged, isNot(original));
|
||||
|
||||
_write(workspace, 'tool/nested/build.sh', 'build-v2');
|
||||
final nestedFileChanged = CrateHash.compute(manifest.path);
|
||||
expect(nestedFileChanged, isNot(fileChanged));
|
||||
|
||||
File(path.join(workspace.path, 'tool/nested/build.sh')).renameSync(
|
||||
path.join(workspace.path, 'tool/nested/assemble.sh'),
|
||||
);
|
||||
final nestedPathChanged = CrateHash.compute(manifest.path);
|
||||
expect(nestedPathChanged, isNot(nestedFileChanged));
|
||||
});
|
||||
|
||||
test('adding empty nested declared directories affects the hash', () {
|
||||
Directory(path.join(workspace.path, 'tool')).createSync();
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
.createSync(recursive: true);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('removing empty nested declared directories affects the hash', () {
|
||||
final nested = Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
..createSync(recursive: true);
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
nested.parent.deleteSync(recursive: true);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('renaming empty nested declared directories affects the hash', () {
|
||||
final empty = Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
..createSync(recursive: true);
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
empty.renameSync(path.join(workspace.path, 'tool/nested/renamed'));
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('undeclared workspace files do not affect the hash', () {
|
||||
_write(workspace, 'producer.txt', 'producer');
|
||||
_writeConfig(manifest, hashInputs: const ['producer.txt']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, 'unrelated.txt', 'first');
|
||||
expect(CrateHash.compute(manifest.path), original);
|
||||
|
||||
_write(workspace, 'unrelated.txt', 'second');
|
||||
expect(CrateHash.compute(manifest.path), original);
|
||||
});
|
||||
|
||||
test('pinned build recipe changes affect the hash', () {
|
||||
_writeConfig(manifest, rustToolchain: '1.88.0');
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_writeConfig(manifest, rustToolchain: '1.89.0');
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('temp storage compatibility does not create persistent markers', () {
|
||||
final cachePath = path.join(temporaryDirectory.path, 'cache');
|
||||
final source = File(path.join(manifest.path, 'src/lib.rs'));
|
||||
final original = CrateHash.compute(manifest.path, tempStorage: cachePath);
|
||||
expect(CrateHash.compute(manifest.path, tempStorage: cachePath), original);
|
||||
|
||||
source.writeAsStringSync('pub fn answer() -> i32 { 43 }\n');
|
||||
expect(CrateHash.compute(manifest.path, tempStorage: cachePath),
|
||||
isNot(original));
|
||||
expect(FileSystemEntity.typeSync(cachePath), FileSystemEntityType.notFound);
|
||||
});
|
||||
|
||||
test('missing declared inputs fail closed', () {
|
||||
_writeConfig(manifest, hashInputs: const ['missing.txt']);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('declared symlinks fail closed', () {
|
||||
_write(workspace, 'producer.txt', 'producer');
|
||||
Link(path.join(workspace.path, 'producer-link'))
|
||||
.createSync(path.join(workspace.path, 'producer.txt'));
|
||||
_writeConfig(manifest, hashInputs: const ['producer-link']);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
|
||||
test('workspace root must contain the crate manifest', () {
|
||||
_writeConfig(manifest, workspaceRoot: '../other');
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('intermediate workspace root symlinks fail closed', () {
|
||||
final realWorkspace =
|
||||
Directory(path.join(temporaryDirectory.path, 'real/workspace'))
|
||||
..createSync(recursive: true);
|
||||
final realManifest = Directory(path.join(realWorkspace.path, 'crate'))
|
||||
..createSync();
|
||||
_write(realManifest, 'Cargo.toml', '[package]\nname = "linked_crate"\n');
|
||||
_write(realManifest, 'src/lib.rs', 'pub fn linked() {}\n');
|
||||
Link(path.join(temporaryDirectory.path, 'workspace-link'))
|
||||
.createSync(path.join(temporaryDirectory.path, 'real'));
|
||||
_writeConfig(
|
||||
realManifest,
|
||||
workspaceRoot: '../../../workspace-link/workspace',
|
||||
);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(realManifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
|
||||
test('intermediate manifest symlinks fail closed', () {
|
||||
final realWorkspace =
|
||||
Directory(path.join(temporaryDirectory.path, 'real/workspace'))
|
||||
..createSync(recursive: true);
|
||||
final realManifest = Directory(path.join(realWorkspace.path, 'crate'))
|
||||
..createSync();
|
||||
_write(realManifest, 'Cargo.toml', '[package]\nname = "linked_crate"\n');
|
||||
_write(realManifest, 'src/lib.rs', 'pub fn linked() {}\n');
|
||||
_writeConfig(realManifest, workspaceRoot: '../..');
|
||||
Link(path.join(temporaryDirectory.path, 'workspace-link'))
|
||||
.createSync(realWorkspace.path);
|
||||
final linkedManifest =
|
||||
Directory(path.join(temporaryDirectory.path, 'workspace-link/crate'));
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(linkedManifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
}
|
||||
|
||||
void _write(Directory root, String relativePath, String contents) {
|
||||
final file = File(path.join(root.path, relativePath));
|
||||
file.parent.createSync(recursive: true);
|
||||
file.writeAsStringSync(contents);
|
||||
}
|
||||
|
||||
void _writeConfig(
|
||||
Directory manifest, {
|
||||
String workspaceRoot = '..',
|
||||
List<String> hashInputs = const [],
|
||||
String? rustToolchain,
|
||||
}) {
|
||||
final recipe = rustToolchain == null
|
||||
? ''
|
||||
: '''
|
||||
build_recipe:
|
||||
rust_toolchain: '$rustToolchain'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
''';
|
||||
final inputs = hashInputs.isEmpty
|
||||
? ''
|
||||
: 'hash_inputs:\n${hashInputs.map((input) => ' - $input').join('\n')}\n';
|
||||
_write(manifest, 'cargokit.yaml', '''
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: $_publicKey
|
||||
workspace_root: $workspaceRoot
|
||||
${_indent(inputs, 2)}${_indent(recipe, 2)}''');
|
||||
}
|
||||
|
||||
String _indent(String value, int spaces) {
|
||||
if (value.isEmpty) {
|
||||
return value;
|
||||
}
|
||||
final prefix = ' ' * spaces;
|
||||
return value
|
||||
.split('\n')
|
||||
.map((line) => line.isEmpty ? line : '$prefix$line')
|
||||
.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:build_tool/src/darwin_environment.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
testRunCommandOverride = (args) {
|
||||
if (args.executable != 'xcrun') {
|
||||
throw StateError('Unexpected executable: ${args.executable}');
|
||||
}
|
||||
|
||||
final sdk = args.arguments[1];
|
||||
if (args.arguments[2] == '--show-sdk-path') {
|
||||
return TestRunCommandResult(stdout: '/sdk/$sdk\n');
|
||||
}
|
||||
if (args.arguments[2] == '--find') {
|
||||
final tool = args.arguments[3];
|
||||
return TestRunCommandResult(stdout: '/toolchain/$sdk/$tool\n');
|
||||
}
|
||||
|
||||
throw StateError('Unexpected xcrun args: ${args.arguments}');
|
||||
};
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('builds simulator environment for arm64 ios sim target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('aarch64-apple-ios-sim')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphonesimulator');
|
||||
expect(env['CC_aarch64-apple-ios-sim'], '/toolchain/iphonesimulator/clang');
|
||||
expect(
|
||||
env['CFLAGS_aarch64-apple-ios-sim'],
|
||||
'--target=arm64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
expect(
|
||||
env['BINDGEN_EXTRA_CLANG_ARGS_aarch64-apple-ios-sim'],
|
||||
'--target=arm64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
});
|
||||
|
||||
test('builds simulator environment for x86_64 ios target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('x86_64-apple-ios')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphonesimulator');
|
||||
expect(
|
||||
env['CFLAGS_x86_64-apple-ios'],
|
||||
'--target=x86_64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
});
|
||||
|
||||
test('builds device environment for arm64 ios target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('aarch64-apple-ios')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphoneos');
|
||||
expect(
|
||||
env['CFLAGS_aarch64-apple-ios'],
|
||||
'--target=arm64-apple-ios -isysroot /sdk/iphoneos -miphoneos-version-min=12.0',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/rustup.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
testRustupExecutablePathOverride = null;
|
||||
});
|
||||
|
||||
test('parse cargo build options', () {
|
||||
final yaml = '''
|
||||
toolchain: nightly
|
||||
extra_flags:
|
||||
- -Z
|
||||
- build-std=panic_abort,std
|
||||
''';
|
||||
|
||||
final options = CargoBuildOptions.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(options.toolchain, Toolchain.nightly);
|
||||
expect(options.flags, ['-Z', 'build-std=panic_abort,std']);
|
||||
});
|
||||
|
||||
test('parse precompiled binaries config', () {
|
||||
final yaml = '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
''';
|
||||
|
||||
final config = PrecompiledBinaries.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(config.uriPrefix, 'https://example.com/precompiled_');
|
||||
expect(
|
||||
config.publicKey.bytes,
|
||||
HEX.decode(
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('parse complete precompiled generation config', () {
|
||||
final yaml = '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
workspace_root: ..
|
||||
hash_inputs:
|
||||
- pubspec.yaml
|
||||
- tool
|
||||
build_recipe:
|
||||
rust_toolchain: '1.88.0'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
macosx: '15.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
macos: '10.14'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/assemble_fjs_xcframework.sh
|
||||
- --release
|
||||
environment:
|
||||
LC_ALL: C
|
||||
timeout_seconds: 900
|
||||
outputs:
|
||||
- fjs.xcframework.zip
|
||||
- fjs.xcframework.zip.checksum
|
||||
''';
|
||||
|
||||
final config = PrecompiledBinaries.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(config.workspaceRoot, '..');
|
||||
expect(config.hashInputs, ['pubspec.yaml', 'tool']);
|
||||
expect(config.buildRecipe?.rustToolchain, '1.88.0');
|
||||
expect(config.buildRecipe?.flutterVersion, '3.32.8');
|
||||
expect(config.buildRecipe?.xcodeVersion, '16.4');
|
||||
expect(config.buildRecipe?.sdkVersions,
|
||||
{'iphoneos': '18.5', 'macosx': '15.5'});
|
||||
expect(config.buildRecipe?.deploymentTargets,
|
||||
{'ios': '12.0', 'macos': '10.14'});
|
||||
expect(config.buildRecipe?.rustTargets,
|
||||
['aarch64-apple-ios', 'aarch64-apple-darwin']);
|
||||
expect(config.compositeGroups, hasLength(1));
|
||||
final group = config.compositeGroups.single;
|
||||
expect(group.name, 'swiftpm');
|
||||
expect(group.host, CompositeHost.macos);
|
||||
expect(
|
||||
group.requiredTargets, ['aarch64-apple-ios', 'aarch64-apple-darwin']);
|
||||
expect(group.argv, ['tool/assemble_fjs_xcframework.sh', '--release']);
|
||||
expect(group.environment, {'LC_ALL': 'C'});
|
||||
expect(group.timeout, const Duration(seconds: 900));
|
||||
expect(
|
||||
group.outputs, ['fjs.xcframework.zip', 'fjs.xcframework.zip.checksum']);
|
||||
});
|
||||
|
||||
group('precompiled generation config rejects', () {
|
||||
const publicKey =
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445';
|
||||
|
||||
String config(String body) => '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: $publicKey
|
||||
$body
|
||||
''';
|
||||
|
||||
void expectInvalid(String body) {
|
||||
expect(
|
||||
() => PrecompiledBinaries.parse(loadYamlNode(config(body))),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
}
|
||||
|
||||
test('unknown fields', () {
|
||||
expectInvalid('unexpected: true');
|
||||
expectInvalid('''
|
||||
build_recipe:
|
||||
rust_toolchain: 1.88.0
|
||||
flutter_version: 3.32.8
|
||||
xcode_version: 16.4
|
||||
sdk_versions: {}
|
||||
deployment_targets: {}
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
unexpected: true
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
unexpected: true
|
||||
''');
|
||||
});
|
||||
|
||||
test('absolute and escaping paths', () {
|
||||
expectInvalid('workspace_root: /tmp/workspace');
|
||||
expectInvalid('''
|
||||
hash_inputs:
|
||||
- ../outside
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- /tmp/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- ../build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('invalid hosts and targets', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: darwin
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- invented-rust-target
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('empty commands and outputs and invalid timeouts', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv: []
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs: []
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 0
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('unsafe and duplicate output names', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- ../output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: first
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
- name: second
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('duplicate groups and targets', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- first.zip
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- second.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
});
|
||||
|
||||
test('parse crate options with cargo and precompiled binaries', () {
|
||||
final yaml = '''
|
||||
cargo:
|
||||
debug:
|
||||
toolchain: nightly
|
||||
extra_flags:
|
||||
- -Z
|
||||
- build-std=panic_abort,std
|
||||
release:
|
||||
toolchain: beta
|
||||
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
''';
|
||||
|
||||
final options = CargokitCrateOptions.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.debug]!.toolchain, Toolchain.nightly);
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.debug]!.flags,
|
||||
['-Z', 'build-std=panic_abort,std'],
|
||||
);
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.release]!.toolchain, Toolchain.beta);
|
||||
expect(options.precompiledBinaries?.uriPrefix,
|
||||
'https://example.com/precompiled_');
|
||||
});
|
||||
|
||||
test('default user options build locally when rustup is available', () {
|
||||
testRustupExecutablePathOverride = () => '/usr/bin/rustup';
|
||||
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('{}'));
|
||||
|
||||
expect(options.usePrecompiledBinaries, false);
|
||||
expect(options.verboseLogging, false);
|
||||
});
|
||||
|
||||
test('default user options use precompiled binaries when rustup is missing',
|
||||
() {
|
||||
testRustupExecutablePathOverride = () => null;
|
||||
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('{}'));
|
||||
|
||||
expect(options.usePrecompiledBinaries, true);
|
||||
expect(options.verboseLogging, false);
|
||||
});
|
||||
|
||||
test('explicit user options override default precompiled binary behavior',
|
||||
() {
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('''
|
||||
use_precompiled_binaries: true
|
||||
verbose_logging: true
|
||||
'''));
|
||||
|
||||
expect(options.usePrecompiledBinaries, true);
|
||||
expect(options.verboseLogging, true);
|
||||
});
|
||||
|
||||
test('parse precompiled binary modes', () {
|
||||
for (final mode in PrecompiledBinariesMode.values) {
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('''
|
||||
precompiled_binaries_mode: ${mode.name}
|
||||
'''));
|
||||
|
||||
expect(options.precompiledBinariesMode, mode);
|
||||
expect(options.usePrecompiledBinaries,
|
||||
mode != PrecompiledBinariesMode.disabled);
|
||||
expect(options.allowLocalBuild, mode != PrecompiledBinariesMode.required);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy precompiled boolean maps to auto or disabled', () {
|
||||
final enabled = CargokitUserOptions.parse(
|
||||
loadYamlNode('use_precompiled_binaries: true'));
|
||||
final disabled = CargokitUserOptions.parse(
|
||||
loadYamlNode('use_precompiled_binaries: false'));
|
||||
|
||||
expect(enabled.precompiledBinariesMode, PrecompiledBinariesMode.auto);
|
||||
expect(disabled.precompiledBinariesMode, PrecompiledBinariesMode.disabled);
|
||||
});
|
||||
|
||||
test('precompiled mode rejects legacy setting conflict', () {
|
||||
expect(
|
||||
() => CargokitUserOptions.parse(loadYamlNode('''
|
||||
precompiled_binaries_mode: required
|
||||
use_precompiled_binaries: true
|
||||
''')),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('precompiled mode rejects unknown values', () {
|
||||
expect(
|
||||
() => CargokitUserOptions.parse(
|
||||
loadYamlNode('precompiled_binaries_mode: sometimes')),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompiled_generation.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _generationHash =
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const _archiveHash =
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
const _checksumHash =
|
||||
'5c6449da335badb355097ca2a7898509bce8feebfe31dedce47798825fdd9a11';
|
||||
const _sourceCommit = '1111111111111111111111111111111111111111';
|
||||
|
||||
void main() {
|
||||
late KeyPair keyPair;
|
||||
late PrecompiledBuildRecipe recipe;
|
||||
|
||||
setUp(() {
|
||||
keyPair = KeyPair(
|
||||
newKeyFromSeed(Uint8List.fromList(List<int>.generate(32, (i) => i))),
|
||||
public(
|
||||
newKeyFromSeed(Uint8List.fromList(List<int>.generate(32, (i) => i)))),
|
||||
);
|
||||
recipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.88.0',
|
||||
flutterVersion: '3.32.8',
|
||||
xcodeVersion: '16.4',
|
||||
sdkVersions: const {'macosx': '15.5', 'iphoneos': '18.5'},
|
||||
deploymentTargets: const {'macos': '10.14', 'ios': '12.0'},
|
||||
rustTargets: const [
|
||||
'x86_64-apple-darwin',
|
||||
'aarch64-apple-darwin',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('canonical encoding is deterministic and recursively sorted', () {
|
||||
final first = _manifest(recipe, reverseAssets: true);
|
||||
final second = _manifest(recipe);
|
||||
|
||||
expect(first.canonicalBytes(), second.canonicalBytes());
|
||||
expect(
|
||||
utf8.decode(first.canonicalBytes()),
|
||||
'{"asset_signature_scheme":"ed25519-cargokit-v2",'
|
||||
'"assets":[{"length":3,"name":"aarch64-apple-darwin_libfjs.a",'
|
||||
'"sha256":"$_checksumHash"},{"length":4,"name":"fjs.xcframework.zip",'
|
||||
'"sha256":"$_archiveHash"},{"length":65,'
|
||||
'"name":"fjs.xcframework.zip.checksum","sha256":"$_checksumHash"}],'
|
||||
'"composite_checksums":[{"algorithm":"sha256",'
|
||||
'"archive":"fjs.xcframework.zip",'
|
||||
'"checksum":"fjs.xcframework.zip.checksum"}],'
|
||||
'"generation_hash":"$_generationHash","provenance":{"actual":'
|
||||
'{"deployment_targets":{"ios":"12.0","macos":"10.14"},'
|
||||
'"flutter_version":"3.32.8","rust_targets":'
|
||||
'["aarch64-apple-darwin","x86_64-apple-darwin"],'
|
||||
'"rust_toolchain":"1.88.0","sdk_versions":'
|
||||
'{"iphoneos":"18.5","macosx":"15.5"},"xcode_version":"16.4"},'
|
||||
'"pinned":{"deployment_targets":{"ios":"12.0","macos":"10.14"},'
|
||||
'"flutter_version":"3.32.8","rust_targets":'
|
||||
'["aarch64-apple-darwin","x86_64-apple-darwin"],'
|
||||
'"rust_toolchain":"1.88.0","sdk_versions":'
|
||||
'{"iphoneos":"18.5","macosx":"15.5"},"xcode_version":"16.4"}},'
|
||||
'"schema_version":2,"scope":"cargokit-precompiled-generation",'
|
||||
'"source_commit":"$_sourceCommit"}',
|
||||
);
|
||||
});
|
||||
|
||||
test('canonical manifest round trips through strict parsing', () {
|
||||
final manifest = _manifest(recipe);
|
||||
|
||||
final parsed =
|
||||
PrecompiledGenerationManifest.parse(manifest.canonicalBytes());
|
||||
|
||||
expect(parsed.canonicalBytes(), manifest.canonicalBytes());
|
||||
expect(parsed.asset('fjs.xcframework.zip')?.length, 4);
|
||||
});
|
||||
|
||||
group('strict manifest parsing rejects', () {
|
||||
test('unknown, missing, and duplicate fields', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'unexpected': true}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
final missing = {...valid}..remove('scope');
|
||||
expect(
|
||||
() => _parse(missing),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => PrecompiledGenerationManifest.parse(utf8.encode(
|
||||
'{"schema_version":2,"schema_version":2}',
|
||||
)),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('v1, unsupported schema, scheme, and scope', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'schema_version': 1}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'schema_version': 3}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'asset_signature_scheme': 'ed25519'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'scope': 'github-release'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('unsafe paths, malformed hashes, duplicates, and unsorted inventory',
|
||||
() {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
final assets = (valid['assets'] as List).cast<Map<String, dynamic>>();
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'generation_hash': 'not-a-sha256'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
{...assets.first, 'name': '../escape'},
|
||||
...assets.skip(1),
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [assets.first, assets.first]
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'assets': assets.reversed.toList()}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('validation rejects incomplete inventory and provenance mismatch', () {
|
||||
final manifest = _manifest(recipe);
|
||||
|
||||
expect(
|
||||
() => manifest.validateFor(
|
||||
generationHash: _generationHash,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: const {
|
||||
'aarch64-apple-darwin_libfjs.a',
|
||||
'x86_64-apple-darwin_libfjs.a',
|
||||
'fjs.xcframework.zip',
|
||||
'fjs.xcframework.zip.checksum',
|
||||
},
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
final differentRecipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.89.0',
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: recipe.rustTargets,
|
||||
);
|
||||
expect(
|
||||
() => manifest.validateFor(
|
||||
generationHash: _generationHash,
|
||||
recipe: differentRecipe,
|
||||
expectedAssetNames: manifest.assets.map((asset) => asset.name).toSet(),
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('asset signature payload has the exact v2 metadata encoding', () {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
|
||||
expect(
|
||||
_hex(payload),
|
||||
'636172676f6b69742d707265636f6d70696c65642d61737365742d7369676e6174757265'
|
||||
'0002'
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
'0000000961737365742e62696e'
|
||||
'0000000000000003'
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
});
|
||||
|
||||
test('asset signature payload encodes the full unsigned u64 range', () {
|
||||
String encodedLength(BigInt length) {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: length,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
final offset = ascii
|
||||
.encode(
|
||||
'cargokit-precompiled-asset-signature',
|
||||
)
|
||||
.length +
|
||||
2 +
|
||||
32 +
|
||||
4 +
|
||||
ascii.encode('asset.bin').length;
|
||||
return _hex(payload.sublist(offset, offset + 8));
|
||||
}
|
||||
|
||||
expect(encodedLength(BigInt.one << 63), '8000000000000000');
|
||||
expect(encodedLength((BigInt.one << 64) - BigInt.one), 'ffffffffffffffff');
|
||||
});
|
||||
|
||||
test('asset signatures reject protocol substitution and u64 overflow', () {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
final signature = sign(keyPair.privateKey, payload);
|
||||
final substituted = Uint8List.fromList(payload)..[payload.indexOf(2)] = 3;
|
||||
|
||||
expect(verify(keyPair.publicKey, payload, signature), isTrue);
|
||||
expect(verify(keyPair.publicKey, substituted, signature), isFalse);
|
||||
expect(
|
||||
() => precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: BigInt.one << 64,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Ed25519 binds asset generation, name, length, and digest metadata', () {
|
||||
final assetBytes = Uint8List.fromList([9, 8, 7]);
|
||||
final assetHash = sha256.convert(assetBytes).toString();
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
);
|
||||
|
||||
final manifestSignature = manifest.sign(keyPair.privateKey);
|
||||
final assetSignature = signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: assetBytes.length,
|
||||
sha256: assetHash,
|
||||
);
|
||||
|
||||
manifest.verifySignature(keyPair.publicKey, manifestSignature);
|
||||
manifest.verifyAsset(
|
||||
name: 'asset.bin',
|
||||
bytes: assetBytes,
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
);
|
||||
expect(
|
||||
() => manifest.verifyAsset(
|
||||
name: 'asset.bin',
|
||||
bytes: Uint8List.fromList([9, 8, 6]),
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
for (final substitution in <PrecompiledGenerationManifest>[
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash:
|
||||
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'other.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 4, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: const [
|
||||
PrecompiledAsset(
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
]) {
|
||||
expect(
|
||||
() => substitution.verifyAssetMetadataSignature(
|
||||
name: substitution.assets.single.name,
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
}
|
||||
final badManifestSignature = Uint8List.fromList(manifestSignature)
|
||||
..[0] ^= 1;
|
||||
expect(
|
||||
() => manifest.verifySignature(keyPair.publicKey, badManifestSignature),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('zip/checksum relationship binds checksum contents to archive digest',
|
||||
() {
|
||||
final archive = Uint8List.fromList([1, 2, 3, 4]);
|
||||
final checksum =
|
||||
Uint8List.fromList(utf8.encode('${sha256.convert(archive)}\n'));
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archive.length,
|
||||
sha256: sha256.convert(archive).toString(),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksum.length,
|
||||
sha256: sha256.convert(checksum).toString(),
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manifest.validateCompositeChecksums({
|
||||
'fjs.xcframework.zip': archive,
|
||||
'fjs.xcframework.zip.checksum': checksum,
|
||||
});
|
||||
expect(
|
||||
() => manifest.validateCompositeChecksums({
|
||||
'fjs.xcframework.zip': archive,
|
||||
'fjs.xcframework.zip.checksum': Uint8List.fromList(utf8.encode(
|
||||
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\n',
|
||||
)),
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('composite checksum content is exact lowercase sha256 plus LF', () {
|
||||
final archive = Uint8List.fromList([1, 2, 3, 4]);
|
||||
final digest = sha256.convert(archive).toString();
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(
|
||||
name: 'archive.zip',
|
||||
length: archive.length,
|
||||
sha256: digest,
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'archive.zip.checksum',
|
||||
length: 65,
|
||||
sha256: sha256.convert(ascii.encode('$digest\n')).toString(),
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'archive.zip',
|
||||
checksum: 'archive.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manifest.validateCompositeChecksums({
|
||||
'archive.zip': archive,
|
||||
'archive.zip.checksum': Uint8List.fromList(ascii.encode('$digest\n')),
|
||||
});
|
||||
final invalid = <String, List<int>>{
|
||||
'no newline': ascii.encode(digest),
|
||||
'CRLF': ascii.encode('$digest\r\n'),
|
||||
'extra newline': ascii.encode('$digest\n\n'),
|
||||
'trailing space': ascii.encode('$digest \n'),
|
||||
'uppercase': ascii.encode('${digest.toUpperCase()}\n'),
|
||||
'invalid UTF-8': [...ascii.encode(digest), 0xff],
|
||||
};
|
||||
for (final entry in invalid.entries) {
|
||||
expect(
|
||||
() => manifest.validateCompositeChecksums({
|
||||
'archive.zip': archive,
|
||||
'archive.zip.checksum': entry.value,
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
reason: entry.key,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('composite metadata is normative without downloading assets', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
final assets = (valid['assets'] as List).cast<Map<String, dynamic>>();
|
||||
final bindings =
|
||||
(valid['composite_checksums'] as List).cast<Map<String, dynamic>>();
|
||||
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'composite_checksums': [
|
||||
{...bindings.single, 'checksum': bindings.single['archive']},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'composite_checksums': [
|
||||
bindings.single,
|
||||
{
|
||||
...bindings.single,
|
||||
'archive': 'aarch64-apple-darwin_libfjs.a',
|
||||
},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
...assets.take(2),
|
||||
{...assets.last, 'length': 64},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
...assets.take(2),
|
||||
{...assets.last, 'sha256': _archiveHash},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
PrecompiledGenerationManifest _manifest(
|
||||
PrecompiledBuildRecipe recipe, {
|
||||
bool reverseAssets = false,
|
||||
}) {
|
||||
final assets = [
|
||||
const PrecompiledAsset(
|
||||
name: 'aarch64-apple-darwin_libfjs.a',
|
||||
length: 3,
|
||||
sha256: _checksumHash,
|
||||
),
|
||||
const PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: 4,
|
||||
sha256: _archiveHash,
|
||||
),
|
||||
const PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: 65,
|
||||
sha256: _checksumHash,
|
||||
),
|
||||
];
|
||||
return PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: reverseAssets ? assets.reversed.toList() : assets,
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PrecompiledGenerationManifest _parse(Map<String, dynamic> value) =>
|
||||
PrecompiledGenerationManifest.parse(utf8.encode(jsonEncode(value)));
|
||||
|
||||
String _hex(List<int> bytes) =>
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user