Import IKAR messenger client and server

This commit is contained in:
sevenhill
2026-10-07 22:06:07 +03:00
commit 69ea5091eb
1951 changed files with 424546 additions and 0 deletions
+64
View File
@@ -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
+15
View File
@@ -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/
+90
View File
@@ -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()
+26
View File
@@ -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.
+78
View File
@@ -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)
+15
View File
@@ -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`.
+54
View File
@@ -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
)
+350
View File
@@ -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
+13
View File
@@ -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"
+281
View File
@@ -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;
}
+42
View File
@@ -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
+411
View File
@@ -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;
}
+221
View File
@@ -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;
}
+216
View File
@@ -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;
}
+468
View File
@@ -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;
}
+88
View File
@@ -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));
}
+29
View File
@@ -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
+161
View File
@@ -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;
}
+22
View File
@@ -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
+61
View File
@@ -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
+105
View File
@@ -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;
}
+165
View File
@@ -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;
}
+28
View File
@@ -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
+194
View File
@@ -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++;
}
+66
View File
@@ -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
+194
View File
@@ -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));
}
+22
View File
@@ -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
+58
View File
@@ -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;
}
+401
View File
@@ -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);
}
+35
View File
@@ -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"
+146
View File
@@ -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;
}
+249
View File
@@ -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
+28
View File
@@ -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"}
}
+173
View File
@@ -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.
+5
View File
@@ -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
File diff suppressed because it is too large Load Diff
+321
View File
@@ -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
+33
View File
@@ -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
+386
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
.dart_tool/
build/
rust/target/
+18
View File
@@ -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.
+29
View File
@@ -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`.
+43
View File
@@ -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"
}
+42
View File
@@ -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.
+11
View File
@@ -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