diff --git a/wrappers/kotlin/.gitignore b/wrappers/kotlin/.gitignore new file mode 100644 index 00000000..91449ef4 --- /dev/null +++ b/wrappers/kotlin/.gitignore @@ -0,0 +1,15 @@ +local.properties +build +.gradle +.DS_STORE +.idea/ +.gradle/ +*.iml +*.hprof +.cxx/ + +*.db* +*.db +*.db-shm +*.db-wal +target \ No newline at end of file diff --git a/wrappers/kotlin/build-targets.sh b/wrappers/kotlin/build-targets.sh new file mode 100644 index 00000000..bb6728e6 --- /dev/null +++ b/wrappers/kotlin/build-targets.sh @@ -0,0 +1,24 @@ +# BUILD IOS TARGETS +rustup toolchain install 1.64.0 --target aarch64-apple-ios --profile minimal --no-self-update +cargo build --release --target aarch64-apple-ios & +rustup toolchain install 1.64.0 --target aarch64-apple-ios-sim --profile minimal --no-self-update +cargo build --release --target aarch64-apple-ios-sim & +rustup toolchain install 1.64.0 --target x86_64-apple-ios --profile minimal --no-self-update +cargo build --release --target x86_64-apple-ios & + +# BUILD ANDROID TARGETS + +#cargo install --bins --git https://github.com/rust-embedded/cross --tag v0.2.4 cross +cargo install cross --git https://github.com/cross-rs/cross + +rustup toolchain install 1.64.0 --target aarch64-linux-android --profile minimal --no-self-update +cross build --release --target aarch64-linux-android & +rustup toolchain install 1.64.0 --target armv7-linux-androideabi --profile minimal --no-self-update +cross build --release --target armv7-linux-androideabi & +rustup toolchain install 1.64.0 --target i686-linux-android --profile minimal --no-self-update +cross build --release --target i686-linux-android & +rustup toolchain install 1.64.0 --target x86_64-linux-android --profile minimal --no-self-update +cross build --release --target x86_64-linux-android & + +# BUILD MAC OS TARGETS +../../build-universal.sh \ No newline at end of file diff --git a/wrappers/kotlin/build.gradle.kts b/wrappers/kotlin/build.gradle.kts new file mode 100644 index 00000000..b5a5b83d --- /dev/null +++ b/wrappers/kotlin/build.gradle.kts @@ -0,0 +1,142 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import java.util.* + +plugins { + kotlin("multiplatform") version "1.8.21" + kotlin("plugin.serialization") version "1.8.21" + id("maven-publish") +} + +repositories { + mavenCentral() +} + +// Stub secrets to let the project sync and build without the publication values set up +ext["githubUsername"] = null +ext["githubToken"] = null +ext["askarVersion"] = "0.2.9-dev.3" +ext["wrapperVersion"] = "3.4" + +val secretPropsFile = project.rootProject.file("local.properties") +if(secretPropsFile.exists()) { + secretPropsFile.reader().use { + Properties().apply { + load(it) + } + }.onEach{ (name, value) -> + ext[name.toString()] = value + } +} else { + ext["githubUsername"] = System.getenv("GITHUB_USERNAME") + ext["githubToken"] = System.getenv("GITHUB_TOKEN") +} + +fun getExtraString(name: String) = ext[name]?.toString() + +group = "org.hyperledger.aries-askar" +version = "${getExtraString("askarVersion")}-wrapper.${getExtraString("wrapperVersion")}" + +publishing{ + repositories{ + maven{ + name = "github" + setUrl("https://maven.pkg.github.com/indicio-tech/aries-askar") + credentials { + username = getExtraString("githubUsername") + password = getExtraString("githubToken") + } + } + } + + publications.withType { + pom { + name.set("Aries Askar Kotlin") + description.set("Kotlin MPP wrapper around aries-askar") + url.set("https://github.com/indicio-tech/aries-askar") + + scm{ + url.set("https://github.com/indicio-tech/aries-askar") + } + } + } +} + +private enum class PlatformType { + APPLE, + ANDROID +} + +kotlin { + + fun addLibs(libDirectory: String, target: KotlinNativeTarget) { + target.compilations.getByName("main") { + val aries_askar by cinterops.creating { + this.includeDirs("libraries/headers/") + packageName("aries_askar") + } + } + + target.binaries.all { + linkerOpts("-L${libDirectory}", "-laries_askar") + linkerOpts("-Wl,-framework,Security") + } + + } + + macosX64{ + val libDirectory = "${projectDir}/../../target/x86_64-apple-darwin/release" + addLibs(libDirectory, this) + } + + macosArm64{ + val libDirectory = "${projectDir}/../../target/aarch64-apple-darwin/release" + addLibs(libDirectory, this) + } + + iosX64 { + val libDirectory = "${projectDir}/../../target/x86_64-apple-ios/release" + addLibs(libDirectory, this) + } + + iosSimulatorArm64 { + val libDirectory = "${projectDir}/../../target/aarch64-apple-ios-sim/release" + addLibs(libDirectory, this) + } + + iosArm64 { + val libDirectory = "${projectDir}/../../target/aarch64-apple-ios/release" + addLibs(libDirectory, this) + } + + androidNativeArm64(){ + val libDirectory = "${projectDir}/../../target/aarch64-linux-android/release" + addLibs(libDirectory, this) + } + + androidNativeX64(){ + val libDirectory = "${projectDir}/../../target/i686-linux-android/release" + addLibs(libDirectory, this) + } + + androidNativeX86(){ + val libDirectory = "${projectDir}/../../target/x86_64-linux-android/release" + addLibs(libDirectory, this) + } + + androidNativeArm32(){ + val libDirectory = "${projectDir}/../../target/armv7-linux-androideabi/release" + addLibs(libDirectory, this) + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC") + } + } + val commonTest by getting { + this.dependsOn(commonMain) + } + } +} diff --git a/wrappers/kotlin/gradle.properties b/wrappers/kotlin/gradle.properties new file mode 100644 index 00000000..fca70253 --- /dev/null +++ b/wrappers/kotlin/gradle.properties @@ -0,0 +1,5 @@ +kotlin.code.style=official +kotlin.native.cacheKind.macosX64=none +kotlin.native.cacheKind.iosX64=none +kotlin.mpp.enableCInteropCommonization=true +kotlin.native.cacheKind.androidNativeArm32=none \ No newline at end of file diff --git a/wrappers/kotlin/gradle/wrapper/gradle-wrapper.jar b/wrappers/kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..41d9927a Binary files /dev/null and b/wrappers/kotlin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/wrappers/kotlin/gradle/wrapper/gradle-wrapper.properties b/wrappers/kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..aa991fce --- /dev/null +++ b/wrappers/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wrappers/kotlin/gradlew b/wrappers/kotlin/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/wrappers/kotlin/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wrappers/kotlin/gradlew.bat b/wrappers/kotlin/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/wrappers/kotlin/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wrappers/kotlin/libraries/headers/aries_askar.h b/wrappers/kotlin/libraries/headers/aries_askar.h new file mode 100644 index 00000000..669481b4 --- /dev/null +++ b/wrappers/kotlin/libraries/headers/aries_askar.h @@ -0,0 +1,574 @@ +#pragma once + +/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ + +#include +#include +#include +#include +#include + +typedef size_t ScanHandle; +typedef size_t StoreHandle; +typedef size_t SessionHandle; + + +enum ErrorCodeEnum +#ifdef __cplusplus + : int64_t +#endif // __cplusplus + { + Success = 0, + Backend = 1, + Busy = 2, + Duplicate = 3, + Encryption = 4, + Input = 5, + NotFound = 6, + Unexpected = 7, + Unsupported = 8, + Custom = 100, +}; +#ifndef __cplusplus +typedef int64_t ErrorCode; +#endif // __cplusplus + +typedef struct FfiResultList_Entry FfiResultList_Entry; + +typedef struct FfiResultList_KeyEntry FfiResultList_KeyEntry; + +/** + * A stored key entry + */ +typedef struct LocalKey LocalKey; + +typedef struct Option_EnabledCallback Option_EnabledCallback; + +typedef struct Option_FlushCallback Option_FlushCallback; + +typedef struct SecretBuffer { + int64_t len; + uint8_t *data; +} SecretBuffer; + +typedef struct FfiResultList_Entry FfiEntryList; + +typedef struct ArcHandle_FfiEntryList { + const FfiEntryList *_0; +} ArcHandle_FfiEntryList; + +typedef struct ArcHandle_FfiEntryList EntryListHandle; + +typedef struct ArcHandle_LocalKey { + const struct LocalKey *_0; +} ArcHandle_LocalKey; + +typedef struct ArcHandle_LocalKey LocalKeyHandle; + +/** + * ByteBuffer is a struct that represents an array of bytes to be sent over the FFI boundaries. + * There are several cases when you might want to use this, but the primary one for us + * is for returning protobuf-encoded data to Swift and Java. The type is currently rather + * limited (implementing almost no functionality), however in the future it may be + * more expanded. + * + * ## Caveats + * + * Note that the order of the fields is `len` (an i64) then `data` (a `*mut u8`), getting + * this wrong on the other side of the FFI will cause memory corruption and crashes. + * `i64` is used for the length instead of `u64` and `usize` because JNA has interop + * issues with both these types. + * + * ### `Drop` is not implemented + * + * ByteBuffer does not implement Drop. This is intentional. Memory passed into it will + * be leaked if it is not explicitly destroyed by calling [`ByteBuffer::destroy`], or + * [`ByteBuffer::destroy_into_vec`]. This is for two reasons: + * + * 1. In the future, we may allow it to be used for data that is not managed by + * the Rust allocator\*, and `ByteBuffer` assuming it's okay to automatically + * deallocate this data with the Rust allocator. + * + * 2. Automatically running destructors in unsafe code is a + * [frequent footgun](https://without.boats/blog/two-memory-bugs-from-ringbahn/) + * (among many similar issues across many crates). + * + * Note that calling `destroy` manually is often not needed, as usually you should + * be passing these to the function defined by [`define_bytebuffer_destructor!`] from + * the other side of the FFI. + * + * Because this type is essentially *only* useful in unsafe or FFI code (and because + * the most common usage pattern does not require manually managing the memory), it + * does not implement `Drop`. + * + * \* Note: in the case of multiple Rust shared libraries loaded at the same time, + * there may be multiple instances of "the Rust allocator" (one per shared library), + * in which case we're referring to whichever instance is active for the code using + * the `ByteBuffer`. Note that this doesn't occur on all platforms or build + * configurations, but treating allocators in different shared libraries as fully + * independent is always safe. + * + * ## Layout/fields + * + * This struct's field are not `pub` (mostly so that we can soundly implement `Send`, but also so + * that we can verify rust users are constructing them appropriately), the fields, their types, and + * their order are *very much* a part of the public API of this type. Consumers on the other side + * of the FFI will need to know its layout. + * + * If this were a C struct, it would look like + * + * ```c,no_run + * struct ByteBuffer { + * // Note: This should never be negative, but values above + * // INT64_MAX / i64::MAX are not allowed. + * int64_t len; + * // Note: nullable! + * uint8_t *data; + * }; + * ``` + * + * In rust, there are two fields, in this order: `len: i64`, and `data: *mut u8`. + * + * For clarity, the fact that the data pointer is nullable means that `Option` is not + * the same size as ByteBuffer, and additionally is not FFI-safe (the latter point is not + * currently guaranteed anyway as of the time of writing this comment). + * + * ### Description of fields + * + * `data` is a pointer to an array of `len` bytes. Note that data can be a null pointer and therefore + * should be checked. + * + * The bytes array is allocated on the heap and must be freed on it as well. Critically, if there + * are multiple rust shared libraries using being used in the same application, it *must be freed + * on the same heap that allocated it*, or you will corrupt both heaps. + * + * Typically, this object is managed on the other side of the FFI (on the "FFI consumer"), which + * means you must expose a function to release the resources of `data` which can be done easily + * using the [`define_bytebuffer_destructor!`] macro provided by this crate. + */ +typedef struct ByteBuffer { + int64_t len; + uint8_t *data; +} ByteBuffer; + +typedef struct EncryptedBuffer { + struct SecretBuffer buffer; + int64_t tag_pos; + int64_t nonce_pos; +} EncryptedBuffer; + +typedef struct AeadParams { + int32_t nonce_length; + int32_t tag_length; +} AeadParams; + +/** + * `FfiStr<'a>` is a safe (`#[repr(transparent)]`) wrapper around a + * nul-terminated `*const c_char` (e.g. a C string). Conceptually, it is + * similar to [`std::ffi::CStr`], except that it may be used in the signatures + * of extern "C" functions. + * + * Functions accepting strings should use this instead of accepting a C string + * directly. This allows us to write those functions using safe code without + * allowing safe Rust to cause memory unsafety. + * + * A single function for constructing these from Rust ([`FfiStr::from_raw`]) + * has been provided. Most of the time, this should not be necessary, and users + * should accept `FfiStr` in the parameter list directly. + * + * ## Caveats + * + * An effort has been made to make this struct hard to misuse, however it is + * still possible, if the `'static` lifetime is manually specified in the + * struct. E.g. + * + * ```rust,no_run + * # use ffi_support::FfiStr; + * // NEVER DO THIS + * #[no_mangle] + * extern "C" fn never_do_this(s: FfiStr<'static>) { + * // save `s` somewhere, and access it after this + * // function returns. + * } + * ``` + * + * Instead, one of the following patterns should be used: + * + * ``` + * # use ffi_support::FfiStr; + * #[no_mangle] + * extern "C" fn valid_use_1(s: FfiStr<'_>) { + * // Use of `s` after this function returns is impossible + * } + * // Alternative: + * #[no_mangle] + * extern "C" fn valid_use_2(s: FfiStr) { + * // Use of `s` after this function returns is impossible + * } + * ``` + */ +typedef const char *FfiStr; + +typedef struct FfiResultList_KeyEntry FfiKeyEntryList; + +typedef struct ArcHandle_FfiKeyEntryList { + const FfiKeyEntryList *_0; +} ArcHandle_FfiKeyEntryList; + +typedef struct ArcHandle_FfiKeyEntryList KeyEntryListHandle; + +typedef int64_t CallbackId; + +typedef void (*LogCallback)(const void *context, int32_t level, const char *target, const char *message, const char *module_path, const char *file, int32_t line); + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void askar_buffer_free(struct SecretBuffer buffer); + +void askar_clear_custom_logger(void); + +ErrorCode askar_entry_list_count(EntryListHandle handle, int32_t *count); + +void askar_entry_list_free(EntryListHandle handle); + +ErrorCode askar_entry_list_get_category(EntryListHandle handle, + int32_t index, + const char **category); + +ErrorCode askar_entry_list_get_name(EntryListHandle handle, int32_t index, const char **name); + +ErrorCode askar_entry_list_get_tags(EntryListHandle handle, int32_t index, const char **tags); + +ErrorCode askar_entry_list_get_value(EntryListHandle handle, + int32_t index, + struct SecretBuffer *value); + +ErrorCode askar_get_current_error(const char **error_json_p); + +ErrorCode askar_key_aead_decrypt(LocalKeyHandle handle, + struct ByteBuffer ciphertext, + struct ByteBuffer nonce, + struct ByteBuffer tag, + struct ByteBuffer aad, + struct SecretBuffer *out); + +ErrorCode askar_key_aead_encrypt(LocalKeyHandle handle, + struct ByteBuffer message, + struct ByteBuffer nonce, + struct ByteBuffer aad, + struct EncryptedBuffer *out); + +ErrorCode askar_key_aead_get_padding(LocalKeyHandle handle, int64_t msg_len, int32_t *out); + +ErrorCode askar_key_aead_get_params(LocalKeyHandle handle, struct AeadParams *out); + +ErrorCode askar_key_aead_random_nonce(LocalKeyHandle handle, struct SecretBuffer *out); + +ErrorCode askar_key_convert(LocalKeyHandle handle, FfiStr alg, LocalKeyHandle *out); + +ErrorCode askar_key_crypto_box(LocalKeyHandle recip_key, + LocalKeyHandle sender_key, + struct ByteBuffer message, + struct ByteBuffer nonce, + struct SecretBuffer *out); + +ErrorCode askar_key_crypto_box_open(LocalKeyHandle recip_key, + LocalKeyHandle sender_key, + struct ByteBuffer message, + struct ByteBuffer nonce, + struct SecretBuffer *out); + +ErrorCode askar_key_crypto_box_random_nonce(struct SecretBuffer *out); + +ErrorCode askar_key_crypto_box_seal(LocalKeyHandle handle, + struct ByteBuffer message, + struct SecretBuffer *out); + +ErrorCode askar_key_crypto_box_seal_open(LocalKeyHandle handle, + struct ByteBuffer ciphertext, + struct SecretBuffer *out); + +ErrorCode askar_key_derive_ecdh_1pu(FfiStr alg, + LocalKeyHandle ephem_key, + LocalKeyHandle sender_key, + LocalKeyHandle recip_key, + struct ByteBuffer alg_id, + struct ByteBuffer apu, + struct ByteBuffer apv, + struct ByteBuffer cc_tag, + int8_t receive, + LocalKeyHandle *out); + +ErrorCode askar_key_derive_ecdh_es(FfiStr alg, + LocalKeyHandle ephem_key, + LocalKeyHandle recip_key, + struct ByteBuffer alg_id, + struct ByteBuffer apu, + struct ByteBuffer apv, + int8_t receive, + LocalKeyHandle *out); + +ErrorCode askar_key_entry_list_count(KeyEntryListHandle handle, int32_t *count); + +void askar_key_entry_list_free(KeyEntryListHandle handle); + +ErrorCode askar_key_entry_list_get_algorithm(KeyEntryListHandle handle, + int32_t index, + const char **alg); + +ErrorCode askar_key_entry_list_get_metadata(KeyEntryListHandle handle, + int32_t index, + const char **metadata); + +ErrorCode askar_key_entry_list_get_name(KeyEntryListHandle handle, + int32_t index, + const char **name); + +ErrorCode askar_key_entry_list_get_tags(KeyEntryListHandle handle, + int32_t index, + const char **tags); + +ErrorCode askar_key_entry_list_load_local(KeyEntryListHandle handle, + int32_t index, + LocalKeyHandle *out); + +void askar_key_free(LocalKeyHandle handle); + +ErrorCode askar_key_from_jwk(struct ByteBuffer jwk, LocalKeyHandle *out); + +ErrorCode askar_key_from_key_exchange(FfiStr alg, + LocalKeyHandle sk_handle, + LocalKeyHandle pk_handle, + LocalKeyHandle *out); + +ErrorCode askar_key_from_public_bytes(FfiStr alg, struct ByteBuffer public_, LocalKeyHandle *out); + +ErrorCode askar_key_from_secret_bytes(FfiStr alg, struct ByteBuffer secret, LocalKeyHandle *out); + +ErrorCode askar_key_from_seed(FfiStr alg, + struct ByteBuffer seed, + FfiStr method, + LocalKeyHandle *out); + +ErrorCode askar_key_generate(FfiStr alg, int8_t ephemeral, LocalKeyHandle *out); + +ErrorCode askar_key_get_algorithm(LocalKeyHandle handle, const char **out); + +ErrorCode askar_key_get_ephemeral(LocalKeyHandle handle, int8_t *out); + +ErrorCode askar_key_get_jwk_public(LocalKeyHandle handle, FfiStr alg, const char **out); + +ErrorCode askar_key_get_jwk_secret(LocalKeyHandle handle, struct SecretBuffer *out); + +ErrorCode askar_key_get_jwk_thumbprint(LocalKeyHandle handle, FfiStr alg, const char **out); + +ErrorCode askar_key_get_public_bytes(LocalKeyHandle handle, struct SecretBuffer *out); + +ErrorCode askar_key_get_secret_bytes(LocalKeyHandle handle, struct SecretBuffer *out); + +ErrorCode askar_key_sign_message(LocalKeyHandle handle, + struct ByteBuffer message, + FfiStr sig_type, + struct SecretBuffer *out); + +ErrorCode askar_key_unwrap_key(LocalKeyHandle handle, + FfiStr alg, + struct ByteBuffer ciphertext, + struct ByteBuffer nonce, + struct ByteBuffer tag, + LocalKeyHandle *out); + +ErrorCode askar_key_verify_signature(LocalKeyHandle handle, + struct ByteBuffer message, + struct ByteBuffer signature, + FfiStr sig_type, + int8_t *out); + +ErrorCode askar_key_wrap_key(LocalKeyHandle handle, + LocalKeyHandle other, + struct ByteBuffer nonce, + struct EncryptedBuffer *out); + +/** + * Migrate an sqlite wallet from an indy-sdk structure to an aries-askar structure. + * It is important to note that this does not do any post-processing. If the record values, tags, + * names, etc. have changed, it must be processed manually afterwards. This script does the following: + * + * 1. Create and rename the required tables + * 2. Fetch the indy key from the wallet + * 3. Create a new configuration + * 4. Initialize a profile + * 5. Update the items from the indy-sdk + * 6. Clean up (drop tables and add a version of "1") + */ +ErrorCode askar_migrate_indy_sdk(FfiStr spec_uri, + FfiStr wallet_name, + FfiStr wallet_key, + FfiStr kdf_level, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_scan_free(ScanHandle handle); + +ErrorCode askar_scan_next(ScanHandle handle, + void (*cb)(CallbackId cb_id, ErrorCode err, EntryListHandle results), + CallbackId cb_id); + +ErrorCode askar_scan_start(StoreHandle handle, + FfiStr profile, + FfiStr category, + FfiStr tag_filter, + int64_t offset, + int64_t limit, + void (*cb)(CallbackId cb_id, ErrorCode err, ScanHandle handle), + CallbackId cb_id); + +ErrorCode askar_session_close(SessionHandle handle, + int8_t commit, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_session_count(SessionHandle handle, + FfiStr category, + FfiStr tag_filter, + void (*cb)(CallbackId cb_id, ErrorCode err, int64_t count), + CallbackId cb_id); + +ErrorCode askar_session_fetch(SessionHandle handle, + FfiStr category, + FfiStr name, + int8_t for_update, + void (*cb)(CallbackId cb_id, ErrorCode err, EntryListHandle results), + CallbackId cb_id); + +ErrorCode askar_session_fetch_all(SessionHandle handle, + FfiStr category, + FfiStr tag_filter, + int64_t limit, + int8_t for_update, + void (*cb)(CallbackId cb_id, ErrorCode err, EntryListHandle results), + CallbackId cb_id); + +ErrorCode askar_session_fetch_all_keys(SessionHandle handle, + FfiStr alg, + FfiStr thumbprint, + FfiStr tag_filter, + int64_t limit, + int8_t for_update, + void (*cb)(CallbackId cb_id, ErrorCode err, KeyEntryListHandle results), + CallbackId cb_id); + +ErrorCode askar_session_fetch_key(SessionHandle handle, + FfiStr name, + int8_t for_update, + void (*cb)(CallbackId cb_id, ErrorCode err, KeyEntryListHandle results), + CallbackId cb_id); + +ErrorCode askar_session_insert_key(SessionHandle handle, + LocalKeyHandle key_handle, + FfiStr name, + FfiStr metadata, + FfiStr tags, + int64_t expiry_ms, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_session_remove_all(SessionHandle handle, + FfiStr category, + FfiStr tag_filter, + void (*cb)(CallbackId cb_id, ErrorCode err, int64_t removed), + CallbackId cb_id); + +ErrorCode askar_session_remove_key(SessionHandle handle, + FfiStr name, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_session_start(StoreHandle handle, + FfiStr profile, + int8_t as_transaction, + void (*cb)(CallbackId cb_id, ErrorCode err, SessionHandle handle), + CallbackId cb_id); + +ErrorCode askar_session_update(SessionHandle handle, + int8_t operation, + FfiStr category, + FfiStr name, + struct ByteBuffer value, + FfiStr tags, + int64_t expiry_ms, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_session_update_key(SessionHandle handle, + FfiStr name, + FfiStr metadata, + FfiStr tags, + int64_t expiry_ms, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_set_custom_logger(const void *context, + LogCallback log, + struct Option_EnabledCallback enabled, + struct Option_FlushCallback flush, + int32_t max_level); + +ErrorCode askar_set_default_logger(void); + +ErrorCode askar_set_max_log_level(int32_t max_level); + +ErrorCode askar_store_close(StoreHandle handle, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_store_create_profile(StoreHandle handle, + FfiStr profile, + void (*cb)(CallbackId cb_id, ErrorCode err, const char *result_p), + CallbackId cb_id); + +ErrorCode askar_store_generate_raw_key(struct ByteBuffer seed, const char **out); + +ErrorCode askar_store_get_profile_name(StoreHandle handle, + void (*cb)(CallbackId cb_id, ErrorCode err, const char *name), + CallbackId cb_id); + +ErrorCode askar_store_open(FfiStr spec_uri, + FfiStr key_method, + FfiStr pass_key, + FfiStr profile, + void (*cb)(CallbackId cb_id, ErrorCode err, StoreHandle handle), + CallbackId cb_id); + +ErrorCode askar_store_provision(FfiStr spec_uri, + FfiStr key_method, + FfiStr pass_key, + FfiStr profile, + int8_t recreate, + void (*cb)(CallbackId cb_id, ErrorCode err, StoreHandle handle), + CallbackId cb_id); + +ErrorCode askar_store_rekey(StoreHandle handle, + FfiStr key_method, + FfiStr pass_key, + void (*cb)(CallbackId cb_id, ErrorCode err), + CallbackId cb_id); + +ErrorCode askar_store_remove(FfiStr spec_uri, + void (*cb)(CallbackId cb_id, ErrorCode err, int8_t), + CallbackId cb_id); + +ErrorCode askar_store_remove_profile(StoreHandle handle, + FfiStr profile, + void (*cb)(CallbackId cb_id, ErrorCode err, int8_t removed), + CallbackId cb_id); + +void askar_terminate(void); + +char *askar_version(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus \ No newline at end of file diff --git a/wrappers/kotlin/settings.gradle.kts b/wrappers/kotlin/settings.gradle.kts new file mode 100644 index 00000000..8ef286f6 --- /dev/null +++ b/wrappers/kotlin/settings.gradle.kts @@ -0,0 +1,3 @@ + +rootProject.name = "aries-askar-kotlin" + diff --git a/wrappers/kotlin/src/commonMain/kotlin/Askar.kt b/wrappers/kotlin/src/commonMain/kotlin/Askar.kt new file mode 100644 index 00000000..9e10e90e --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Askar.kt @@ -0,0 +1,128 @@ +@file:OptIn(UnsafeNumber::class, UnsafeNumber::class) + +package askar + + +import aries_askar.* +import askar.wrappers.* +import kotlinx.cinterop.* +import kotlinx.serialization.json.* +import kotlin.coroutines.Continuation + +class Askar { + + enum class ErrorCodes(val errorCode: ErrorCode) { + Success(0), + Backend(1), + Busy(2), + Duplicate(3), + Encryption(4), + Input(5), + NotFound(6), + Unexpected(7), + Unsupported(8), + Custom(100), + } + + companion object { + fun version(): String { + return askar_version()!!.toKString() + } + + private fun getErrorJson(): AskarError { + memScoped { + val jsonPointer = alloc>() + askar_get_current_error(jsonPointer.ptr) + val json = jsonPointer.value!!.toKString() + return Json.decodeFromString(json) + } + } + + fun assertNoError(errorCode: ErrorCode){ + if (errorCode > 0L) { + throw getErrorJson() + } + } + + fun assertNoError(errorCode: ErrorCode, continuation: Continuation): Boolean { + if(errorCode > 0L) { + continuation.resumeWith(Result.failure(getErrorJson())) + return false + } + return true + } + + fun getErrorCode(errorCode: ErrorCode): ErrorCodes { + for(errCode in ErrorCodes.values()) + if(errCode.errorCode.equals(errorCode)) + return errCode; + throw Error("Could not find matching error code for $errorCode") + } + + fun secretBufferToString(secretBuffer: SecretBuffer): String { + val buffer = ByteArray(secretBuffer.len.toInt()){ + secretBuffer.data?.get(it)?.toByte()!! + } + return buffer.toKString() + } + + + fun secretBufferToByteArray(secretBuffer: SecretBuffer): ByteArray { + val buffer = ByteArray(secretBuffer.len.toInt()) { + secretBuffer.data!![it].toByte() + } + return buffer + } + + fun stringToByteBuffer(string: String, scope: MemScope): CValue { + val cArr = scope.allocArray(string.length){ + this.value = string[it].code.toUByte() + } + val byteBuffer = cValue { + data = cArr + len = string.length.toLong() + } + return byteBuffer + } + + fun byteArrayToByteBuffer(buffer: ByteArray, scope: MemScope): CValue { + val cArr = scope.allocArray(buffer.size){ + this.value = buffer[it].toUByte() + } + val byteBuffer = cValue { + data = cArr + len = buffer.size.toLong() + } + return byteBuffer + } + + fun Map.mapToJsonObject(): JsonObject { + val map = this + val json = buildJsonObject { + map.forEach { entry -> + put(entry.key, entry.value) + } + } + return json + } + + + + + val store = StoreWrapper() + + val keyEntryList = KeyEntryListWrapper() + + val entryList = EntryListWrapper() + + val scan = ScanWrapper() + + val session = SessionWrapper() + + val key = KeyWrapper() + + val cryptoBox = CryptoBoxWrapper() + + } +} + diff --git a/wrappers/kotlin/src/commonMain/kotlin/AskarError.kt b/wrappers/kotlin/src/commonMain/kotlin/AskarError.kt new file mode 100644 index 00000000..57982d5d --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/AskarError.kt @@ -0,0 +1,11 @@ +package askar + +import kotlinx.serialization.Serializable + +@Serializable +class AskarError( + val code: Long, + override val message: String +) : Exception( + "Askar Error: $code; $message" +) \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/Entry.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/Entry.kt new file mode 100644 index 00000000..898ae404 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/Entry.kt @@ -0,0 +1,64 @@ +package askar.Store + +import askar.crypto.EntryListHandle +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +/*** + * @param category the string name of the category + * @param name the string name of the entry + * @param value the value passed in when the entry was created + * @param tags a json formatted string of tags + */ +@Serializable +class EntryObject(val category: String, val name: String, val value: String, val tags: String) { + + override fun toString(): String { + val temp = buildJsonObject { + put("category", category) + put("name", name) + put("value", value) + val tagsJson = Json.decodeFromString(tags) + put("tags", tagsJson) + } + return temp.toString() + } + + override fun equals(other: Any?): Boolean { + if(other == null) return false + val o = other as EntryObject + val tags = Json.decodeFromString(this.tags) + val otherTags = Json.decodeFromString(o.tags) + return o.category == this.category && o.name == this.name && o.value == this.value && tags == otherTags + } + +} +class Entry (private val list: EntryListHandle, private val pos: Int) { + + fun category(): String { + return this.list.getCategory(this.pos) + } + + fun name(): String { + return this.list.getName(this.pos) + } + + fun value(): String { + return this.list.getValue(this.pos) + } + + //Revisit if needed later +// private fun rawValue(): String { +// return this.list.getValue(this.pos) +// } + + fun tags(): String { + return list.getTags(pos) ?: "{}" + } + + fun toJson(): EntryObject { + val entry = EntryObject(category(), name(), tags = tags(), value = value()) + return entry + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/EntryList.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/EntryList.kt new file mode 100644 index 00000000..b3c1d6e1 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/EntryList.kt @@ -0,0 +1,45 @@ +package askar.Store + +import askar.Askar +import askar.crypto.EntryListHandle + +class EntryList(private val handle: EntryListHandle, length: Int? = null) { + private val length: Int + + init { + this.length = length?: Askar.entryList.entryListCount(handle.handle) + } + + fun handle(): EntryListHandle { + return handle + } + + fun length(): Int { + return length + } + + fun getEntryByIndex(index: Int): Entry { + return Entry(this.handle, index) + } + + private fun forEach(cb: (entry: Entry, index: Int) -> Any){ + for(i in 0 until this.length){ + cb(getEntryByIndex(i), i) + } + } + + fun find(cb: (entry: Entry, index: Int) -> Boolean): Entry? { + for(i in 0 until this.length){ + if(cb(this.getEntryByIndex(i), i)) + return this.getEntryByIndex(i) + } + return null + } + + fun toArray(): ArrayList { + val list = ArrayList(this.length) + this.forEach { entry, _ -> list.add(entry.toJson()) } + return list + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntry.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntry.kt new file mode 100644 index 00000000..ccbd68c0 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntry.kt @@ -0,0 +1,79 @@ +package askar.Store + +import askar.crypto.Key +import askar.crypto.KeyEntryListHandle + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.* + +/*** + * @param algorithm the string name of the category + * @param name the string name of the entry + * @param metadata the metadata passed in when the entry was created + * @param tags a json formatted string of tags + */ +@Serializable +class KeyEntryObject( + val algorithm: String, + val name: String, + val metadata: String?, + val tags: String, + @Transient + val key: Key? = null +) { + + override fun toString(): String { + val temp = buildJsonObject { + put("algorithm", algorithm) + put("name", name) + put("metadata", metadata) + val tagsJson = Json.decodeFromString(tags) + put("tags", tagsJson) + } + + return temp.toString() + } + + override fun equals(other: Any?): Boolean { + if (other == null) return false + val o = other as KeyEntryObject + val tags = Json.decodeFromString(this.tags) + val otherTags = Json.decodeFromString(o.tags) + return o.algorithm == this.algorithm && o.name == this.name && tags == otherTags && o.metadata == this.metadata + } + +} + +class KeyEntry( + private val list: KeyEntryListHandle, + private val pos: Int +) { + + fun algorithm(): String { + return list.getAlgorithm(pos) + } + + fun name(): String { + return list.getName(pos) + } + + fun metadata(): String? { + return list.getMetadata(pos) + } + + fun tags(): String { + return list.getTags(pos) ?: "{}" + } + + fun key(): Key { + return Key(list.loadKey(pos)) + } + + fun toJson(): KeyEntryObject { + val entry = KeyEntryObject(algorithm(), name(), metadata(), tags(), key()) + + return entry + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntryList.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntryList.kt new file mode 100644 index 00000000..6701f5a2 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/KeyEntryList.kt @@ -0,0 +1,35 @@ +package askar.Store + +import askar.Askar +import askar.crypto.KeyEntryListHandle + +class KeyEntryList(private val handle: KeyEntryListHandle) { + private val length: Int = Askar.keyEntryList.count(handle.handle) + + + fun handle(): KeyEntryListHandle { + return handle + } + + fun length(): Int { + return length + } + + fun getEntryByIndex(index: Int): KeyEntry { + return KeyEntry(handle, index) + } + + fun forEach(cb: (entry: KeyEntry, index: Int) -> Any) { + for(i in 0 until length) { + cb(getEntryByIndex(i), i) + } + } + + fun toArray(): ArrayList { + val list = ArrayList(length) + forEach{ entry, _ -> list.add(entry.toJson()) } + return list + } + + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/OpenSession.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/OpenSession.kt new file mode 100644 index 00000000..01f62299 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/OpenSession.kt @@ -0,0 +1,22 @@ +@file:OptIn(UnsafeNumber::class) + +package askar.Store + +import askar.Askar +import askar.crypto.SessionHandle +import askar.crypto.StoreHandle +import kotlinx.cinterop.UnsafeNumber +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking + +@OptIn(UnsafeNumber::class) +class OpenSession(private val store: StoreHandle, private val profile: String? = null, private val isTxn: Boolean) { + //TODO: Implement session not done in javascript yet + private var session: SessionHandle? = null + + suspend fun open(): Session { + if (this.session != null) throw Error("Session already opened") + val sessionHandle = Askar.session.sessionStart(store.handle, profile, isTxn) + return Session( sessionHandle, this.isTxn ) + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/Scan.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/Scan.kt new file mode 100644 index 00000000..b42083e4 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/Scan.kt @@ -0,0 +1,63 @@ +package askar.Store + +import aries_askar.askar_get_current_error +import askar.Askar +import askar.crypto.EntryListHandle +import askar.crypto.ScanHandle +import kotlinx.cinterop.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject + +@OptIn(UnsafeNumber::class) +class Scan( + private val profile: String? = null, + private val category: String, + private val tagFilter: JsonObject = buildJsonObject {}, + private val offset: Int = 0, + private val limit: Int = -1, + private val store: Store +) { + private var handle: ScanHandle? = null + private var listHandle: EntryListHandle? = null + + fun handle(): ScanHandle? { + return this.handle + } + + suspend fun forEach(cb: (row: Entry, index: Int) -> Unit) { + memScoped { + if (handle == null) { + handle = Askar.scan.scanStart(store.handle().handle, limit, offset, tagFilter, profile, category) + } + try { + var recordCount = 0 + while (limit == -1 || recordCount < limit) { + val list = Askar.scan.scanNext(handle!!.handle, this) ?: break + listHandle = list + + val entryList = EntryList(list) + + recordCount += entryList.length() + for (i in 0 until entryList.length()) { + val entry = entryList.getEntryByIndex(i) + cb(entry, i) + } + } + + } finally { + Askar.scan.free(handle!!.handle) + } + } + + } + + suspend fun fetchAll(): ArrayList { + val rows = ArrayList() + forEach { row: Entry, _ -> rows.add(row.toJson()) } + return rows + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/Session.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/Session.kt new file mode 100644 index 00000000..f469d47d --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/Session.kt @@ -0,0 +1,204 @@ +package askar.Store + +import askar.Askar +import askar.Askar.Companion.mapToJsonObject +import askar.crypto.Key +import askar.crypto.SessionHandle +import askar.enums.EntryOperation +import askar.enums.KeyAlgs +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.memScoped +import kotlinx.serialization.json.* + +@OptIn(UnsafeNumber::class) +class Session(private var handle: SessionHandle?, private val isTxn: Boolean) { + + fun isTransaction(): Boolean { + return this.isTxn + } + + fun handle(): SessionHandle? { + return this.handle + } + + suspend fun count(category: String, tagFilter: String): Long { + if (this.handle == null) throw Error("Cannot get count of closed session") + val handle = this.handle + var count = Askar.session.sessionCount(handle!!.handle, category, tagFilter) + return count + } + + suspend fun fetch( + category: String, + name: String, + forUpdate: Boolean = false, + ): EntryObject? { + if (this.handle == null) throw Error("Cannot fetch from a close session") + val h = Askar.session.sessionFetch(handle!!.handle, category, name, forUpdate) ?: return null + val entry = Entry(h, 0) + return entry.toJson() + } + + suspend fun fetchAll( + category: String, + tagFilter: String = "{}", + forUpdate: Boolean = false, + limit: Long = -1L, + ): ArrayList { + if (this.handle == null) throw Error("Cannot fetch from a closed session") + val handle = + Askar.session.fetchAll(handle!!.handle, category, tagFilter, limit, forUpdate) + ?: return arrayListOf() + val entryList = EntryList(handle) + return entryList.toArray() + } + + suspend fun insert( + category: String, + name: String, + expiryMs: Long = -1, + tags: String = "{}", + value: String + ): Boolean { + if (this.handle == null) throw Error("Cannot insert into a closed session") + val code = Askar.session.sessionUpdate( + handle!!.handle, + category, + name, + expiryMs, + tags, + value, + EntryOperation.Insert + ) + return code == 0L + } + + suspend fun replace( + category: String, + name: String, + expiryMs: Long = -1, + tags: String = "{}", + value: String + ): Boolean { + if (this.handle == null) throw Error("Cannot replace in a closed session") + val code = + Askar.session.sessionUpdate( + handle!!.handle, + category, + name, + expiryMs, + tags, + value, + EntryOperation.Replace + ) + return code == 0L + } + + suspend fun remove(category: String, name: String): Boolean { + if (this.handle == null) throw Error("Cannot remove from a closed session") + val code = + Askar.session.sessionUpdate( + handle!!.handle, + category, + name, + 0, + "{}", + "", + EntryOperation.Remove + ) + return code == 0L + } + + suspend fun removeAll(category: String, tagFilter: String = "{}"): Boolean { + if (this.handle == null) throw Error("Cannot remove from a closed session") + val code = Askar.session.sessionRemoveAll(handle!!.handle, category, tagFilter) + return code == 0L + } + + suspend fun insertKey( + name: String, + key: Key, + expiryMs: Long = -1, + metadata: String? = null, + tags: String = "{}" + ): Boolean { + if (this.handle == null) throw Error("Cannot insert a key with a closed session") + val code = Askar.session.sessionInsertKey(handle!!.handle, name, key, metadata, tags, expiryMs) + return code == 0L + } + + + suspend fun fetchKey(name: String, forUpdate: Boolean = false): KeyEntryObject? { + if (this.handle == null) throw Error("Cannot fetch key from closed session") + val handle = Askar.session.sessionFetchKey(handle!!.handle, name, forUpdate) ?: return null + val keyEntryList = KeyEntryList(handle) + + return keyEntryList.getEntryByIndex(0).toJson() + } + + suspend fun fetchAllKeys( + algorithm: KeyAlgs? = null, + thumbprint: String? = null, + tagFilter: String? = null, + limit: Long = -1, + forUpdate: Boolean = false, + ): ArrayList { + if (this.handle == null) throw Error("Cannot fetch keys from a closed session") + val handle = Askar.session.sessionFetchAllKeys( + handle!!.handle, + algorithm, + thumbprint, + tagFilter, + limit, + forUpdate, + ) ?: return ArrayList() + val keyEntryList = KeyEntryList(handle) + return keyEntryList.toArray() + } + + suspend fun updateKey( + name: String, + metadata: String? = null, + tags: String = "{}", + expiryMs: Long = -1 + ): Boolean { + if (this.handle == null) throw Error("Cannot update key from a closed session") + val code = Askar.session.sessionUpdateKey(handle!!.handle, name, metadata, tags, expiryMs) + return code == 0L + } + + suspend fun removeKey(name: String): Boolean { + if (this.handle == null) throw Error("Cannot remove key from a closed session") + val code = Askar.session.sessionRemoveKey(handle!!.handle, name) + return code == 0L + } + + /** + * @Note also closes the session + */ + suspend fun commit(): Boolean { + if (!this.isTxn) throw Error("Session is not a transaction") + if (this.handle == null) throw Error("Cannot commit a closed session") + val code = handle!!.close(true) + this.handle = null + return code == 0L + } + + suspend fun rollback(): Boolean { + if (!this.isTxn) throw Error("Session is not a transaction") + if (this.handle == null) throw Error("Cannot rollback a closed session") + val code = handle!!.close(false) + this.handle = null + return code == 0L + } + + suspend fun close(): Boolean { + if (this.handle == null) throw Error("Cannot close a closed session") + val code = handle!!.close(false) + this.handle = null + return code == 0L + } + + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/Store.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/Store.kt new file mode 100644 index 00000000..01a87f96 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/Store.kt @@ -0,0 +1,172 @@ +package askar.Store + +import askar.Askar +import askar.crypto.StoreHandle +import kotlinx.cinterop.UnsafeNumber +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject + +@OptIn(UnsafeNumber::class) +class Store(private val handle: StoreHandle, private val uri: String) { + private var opener: OpenSession? = null + + /** + * The store handle object for this store + */ + fun handle(): StoreHandle { + return handle + } + + /** + * The uri for this store + */ + fun uri(): String { + return uri + } + + /** + * Creates a new profile in the store + * @param name the name of the profile to create + * @return A string of the profile that was created + */ + suspend fun createProfile(name: String = ""): String? { + return Askar.store.storeCreateProfile(handle.handle, name) + } + + /** + * Removes the given profile from the store + * @param name the name of the profile to remove + * @return true if successful false otherwise + */ + suspend fun removeProfile(name: String): Boolean { + return Askar.store.storeRemoveProfile(this.handle.handle, name) + } + + /** + * Changes the passkey to the store + * @param keyMethod StoreKeyMethod object denoting the encryption key method to use. Takes in a KdfKeyMethod + * @param passkey the password to open this store. Recommended to use generate raw key create a strong key + * @return True if successful false otherwise + */ + suspend fun rekey(keyMethod: StoreKeyMethod = StoreKeyMethod(KdfMethod.Argon2IInt), passkey: String): Boolean { + val code = Askar.store.storeRekey(handle.handle, keyMethod, passkey) + return code == 0L + } + + /** + * closes the current store for any operations + * @param remove whether the store should be removed from storage + * @return This function only returns true if the uri has been removed from the store + */ + suspend fun close(remove: Boolean = false): Boolean { + this.opener = null + Askar.store.storeClose(handle.handle) + if (remove) return remove(uri) + return false + } + + /** + * Creates a session on this store with the given profile + * @param profile the profile with which to create the session + * @return the OpenSession object to the newly created session + */ + fun session(profile: String? = null): OpenSession { + return OpenSession(this.handle, profile, false) + } + + /** + * Creates a transaction on this store with the given profile + * @param profile the profile with which to create the transaction + * @return the OpenSession object to the newly created transaction + */ + fun transaction(profile: String = "local"): OpenSession { + return OpenSession(this.handle, profile, true) + } + + /** + * Creates and opens a session on this store + * @throws Error if this store has been closed or is in invalid state + * @param isTxn whether this session should be a transaction + * @return The created session + */ + suspend fun openSession(isTxn: Boolean = false): Session { + this.opener = OpenSession(this.handle, isTxn = isTxn) + return opener!!.open() + } + + fun scan( + category: String, + tagFilter: JsonObject = buildJsonObject { }, + offset: Int = 0, + limit: Int = -1, + profile: String? = null + ): Scan { + return Scan(profile, category, tagFilter, offset, limit, this) + } + + + companion object { + /** + * Generates a raw key for use else where + * @param seed input to consistently generate same key + * @return The raw key as a string + */ + fun generateRawKey(seed: String): String? { + return Askar.store.storeGenerateRawKey(seed) + } + + /** + * Initializes a store and returns the created store + * @throws Error if uri is invalid + * @param uri String mapping to storage location i.e. sqlite://local.db + * @param keyMethod StoreKeyMethod object denoting the encryption key method to use. Takes in a KdfKeyMethod + * @param profile string identifying this store in storage + * @param passkey the password to open this store. Recommended to use generate raw key create a strong key + * @param recreate whether this should recreate a removed store + * @return The newly created Store object + */ + suspend fun provision( + uri: String, + keyMethod: StoreKeyMethod = StoreKeyMethod(KdfMethod.None), + passkey: String = "1234", + profile: String? = null, + recreate: Boolean + ): Store { + val h = Askar.store.storeProvision(uri, passkey, profile, keyMethod, recreate) + val handle = StoreHandle(h.storeHandle) + return Store(handle, uri) + } + + /** + * Opens a store that has already been provisioned with given parameters + * @throws Error if store has not been provisioned with given parameters + * @throws Error if passkey is incorrect for store + * @param uri String mapping to storage location i.e. sqlite://local.db + * @param keyMethod StoreKeyMethod object denoting the encryption key method to use. Takes in a KdfKeyMethod + * @param profile string identifying this store in storage + * @return A opened store object + */ + suspend fun open( + uri: String, + keyMethod: StoreKeyMethod = StoreKeyMethod(KdfMethod.Argon2IInt), + passkey: String = "1234", + profile: String? = null + ): Store { + val h = Askar.store.storeOpen(uri, passkey, profile, keyMethod) + val handle = StoreHandle(h.storeHandle) + return Store(handle, uri) + } + + /** + * Removes a store from the local system + * @throws Error if provided invalid uri + * @param uri: String mapping to storage location i.e. sqlite://local.db + * @return true if successful false otherwise + */ + suspend fun remove(uri: String): Boolean { + return Askar.store.storeRemove(uri) + } + } + + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/Store/StoreKeyMethod.kt b/wrappers/kotlin/src/commonMain/kotlin/Store/StoreKeyMethod.kt new file mode 100644 index 00000000..e3140702 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/Store/StoreKeyMethod.kt @@ -0,0 +1,17 @@ +package askar.Store + +enum class KdfMethod(val method: String){ + Raw("raw"), + None("None"), + Argon2IMod("kdf:argon2i:mod"), + Argon2IInt("kdf:argon2i:int") + +} + +class StoreKeyMethod(private val method: KdfMethod) { + + fun toUri(): String { + return this.method.method + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/CryptoBox.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/CryptoBox.kt new file mode 100644 index 00000000..7825b995 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/CryptoBox.kt @@ -0,0 +1,37 @@ +package askar.crypto + +import askar.Askar + +class CryptoBox { + + companion object { + fun randomNonce(): ByteArray { + return Askar.cryptoBox.keyCryptoBoxRandomNonce() + } + + fun cryptoBox(recipientKey: Key, senderKey: Key, message: String, nonce: ByteArray): ByteArray { + return Askar.cryptoBox.keyCryptoBox(recipientKey.handle(), senderKey.handle(), message, nonce) + } + + fun cryptoBox(recipientKey: Key, senderKey: Key, message: ByteArray, nonce: ByteArray): ByteArray { + return Askar.cryptoBox.keyCryptoBox(recipientKey.handle(), senderKey.handle(), message, nonce) + } + + fun open(recipientKey: Key, senderKey: Key, message: ByteArray, nonce: ByteArray): ByteArray { + return Askar.cryptoBox.cryptoBoxOpen(recipientKey.handle(), senderKey.handle(), message, nonce) + } + + fun seal(recipientKey: Key, message: String): ByteArray { + return Askar.cryptoBox.cryptoBoxSeal(recipientKey.handle(), message) + } + + fun seal(recipientKey: Key, message: ByteArray): ByteArray { + return Askar.cryptoBox.cryptoBoxSeal(recipientKey.handle(), message) + } + + fun sealOpen(recipientKey: Key, cipherText: ByteArray): ByteArray { + return Askar.cryptoBox.cryptoBoxSealOpen(recipientKey.handle(), cipherText) + } + + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/Ecdh1PU.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/Ecdh1PU.kt new file mode 100644 index 00000000..b3d1e36a --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/Ecdh1PU.kt @@ -0,0 +1,99 @@ +package askar.crypto + +import aries_askar.ByteBuffer +import aries_askar.EncryptedBuffer +import askar.Askar +import askar.Askar.Companion.stringToByteBuffer +import askar.enums.KeyAlgs +import kotlinx.cinterop.CValue +import kotlinx.cinterop.MemScope + +class Ecdh1PU(private val algId: String, private val apu: String, private val apv: String) { + + + fun deriveKey( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + senderKey: Key, + receive: Boolean, + ccTag: ByteArray = byteArrayOf() + ): Key { + return Key( + Askar.cryptoBox.keyDeriveEcdh1pu( + algId, + receive, + apv, + apu, + encAlg, + ephemeralKey.handle(), + recipientKey.handle(), + senderKey.handle(), + ccTag + ) + ) + } + + fun encryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + senderKey: Key, + message: String, + nonce: ByteArray? = null, + aad: String? = null, + ): askar.EncryptedBuffer { + val derived = this.deriveKey(encAlg, ephemeralKey, recipientKey, senderKey, false, ccTag = byteArrayOf()) + val encryptedBuffer = derived.aeadEncrypt(message = message, aad = aad?: "", nonce = nonce?: ByteArray(0)) + derived.handle().free() + return encryptedBuffer + } + + fun decryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + senderKey: Key, + cipherText: ByteArray, + nonce: ByteArray, + aad: String? = null, + tag: ByteArray, + ): ByteArray { + val derived = this.deriveKey(encAlg, ephemeralKey, recipientKey, senderKey, false, ccTag = byteArrayOf()) + val encryptedBuffer = derived.aeadDecrypt(cipherText, aad = aad?: "", tag = tag, nonce = nonce) + derived.handle().free() + return encryptedBuffer + } + + fun senderWrapKey( + wrapAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + senderKey: Key, + cek: Key, + ccTag: ByteArray + ): askar.EncryptedBuffer { + val derived = this.deriveKey(wrapAlg, ephemeralKey, recipientKey, senderKey, false, ccTag) + val encryptedBuffer = derived.wrapKey(cek) + derived.handle().free() + return encryptedBuffer + } + + fun receiverUnwrapKey( + wrapAlg: KeyAlgs, + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + senderKey: Key, + cipherText: ByteArray, + nonce: ByteArray = byteArrayOf(), + tag: String = "", + ccTag: ByteArray = byteArrayOf() + ): Key { + val derived = this.deriveKey(wrapAlg, ephemeralKey, recipientKey, senderKey, false, ccTag) + val encryptedBuffer = derived.unwrapKey(encAlg, tag, cipherText, nonce) + derived.handle().free() + return encryptedBuffer + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/EcdhEs.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/EcdhEs.kt new file mode 100644 index 00000000..c3ff3490 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/EcdhEs.kt @@ -0,0 +1,140 @@ +package askar.crypto + +import askar.Askar +import askar.enums.KeyAlgs + +class EcdhEs(private val algId: String, private val apu: String, private val apv: String) { + + + fun deriveKey( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + receive: Boolean, + ): Key { + return Key( + Askar.cryptoBox.keyDeriveEcdhes( + algId, + receive, + apv, + apu, + encAlg, + ephemeralKey.handle(), + recipientKey.handle(), + ) + ) + } + + fun encryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + message: String, + nonce: ByteArray? = null, + aad: String? = null, + ): askar.EncryptedBuffer { + val derived = this.deriveKey(encAlg, ephemeralKey, recipientKey, false) + val encryptedBuffer = derived.aeadEncrypt(message, aad = aad?: "", nonce = nonce?: ByteArray(0)) + derived.handle().free() + return encryptedBuffer + } + + fun encryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Jwk, + recipientKey: Jwk, + message: String, + nonce: ByteArray? = null, + aad: String? = null, + ): askar.EncryptedBuffer { + val derived = this.deriveKey(encAlg, Key.fromJwk(ephemeralKey), Key.fromJwk(recipientKey), false) + val encryptedBuffer = derived.aeadEncrypt(message, aad = aad?: "", nonce = nonce?: ByteArray(1)) + derived.handle().free() + return encryptedBuffer + } + + fun decryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + cipherText: ByteArray, + nonce: ByteArray, + aad: String? = null, + tag: ByteArray, + ): ByteArray { + val derived = this.deriveKey(encAlg, ephemeralKey, recipientKey, false) + val encryptedBuffer = derived.aeadDecrypt(cipherText, aad = aad?: "", tag = tag, nonce = nonce) + derived.handle().free() + return encryptedBuffer + } + + fun decryptDirect( + encAlg: KeyAlgs, + ephemeralKey: Jwk, + recipientKey: Jwk, + cipherText: ByteArray, + nonce: ByteArray, + aad: String? = null, + tag: ByteArray, + ): ByteArray { + val derived = this.deriveKey(encAlg, Key.fromJwk(ephemeralKey), Key.fromJwk(recipientKey), false) + val encryptedBuffer = derived.aeadDecrypt(cipherText, aad = aad?: "", tag = tag, nonce = nonce) + derived.handle().free() + return encryptedBuffer + } + + fun senderWrapKey( + wrapAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + cek: Key, + ): askar.EncryptedBuffer { + val derived = this.deriveKey(wrapAlg, ephemeralKey, recipientKey, false) + val encryptedBuffer = derived.wrapKey(cek) + derived.handle().free() + return encryptedBuffer + } + + fun senderWrapKey( + wrapAlg: KeyAlgs, + ephemeralKey: Jwk, + recipientKey: Jwk, + cek: Key, + ): askar.EncryptedBuffer { + val derived = this.deriveKey(wrapAlg, Key.fromJwk(ephemeralKey), Key.fromJwk(recipientKey), false) + val encryptedBuffer = derived.wrapKey(cek) + derived.handle().free() + return encryptedBuffer + } + + fun receiverUnwrapKey( + wrapAlg: KeyAlgs, + encAlg: KeyAlgs, + ephemeralKey: Key, + recipientKey: Key, + cipherText: ByteArray, + nonce: ByteArray = byteArrayOf(), + tag: String = "", + ): Key { + val derived = this.deriveKey(wrapAlg, ephemeralKey, recipientKey,true) + val encryptedBuffer = derived.unwrapKey(encAlg, tag, cipherText, nonce) + derived.handle().free() + return encryptedBuffer + } + + fun receiverUnwrapKey( + wrapAlg: KeyAlgs, + encAlg: KeyAlgs, + ephemeralKey: Jwk, + recipientKey: Jwk, + cipherText: ByteArray, + nonce: ByteArray = byteArrayOf(), + tag: String = "", + ): Key { + val derived = this.deriveKey(wrapAlg, Key.fromJwk(ephemeralKey), Key.fromJwk(recipientKey),true) + val encryptedBuffer = derived.unwrapKey(encAlg, tag, cipherText, nonce) + derived.handle().free() + return encryptedBuffer + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/Jwk.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/Jwk.kt new file mode 100644 index 00000000..fe46357c --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/Jwk.kt @@ -0,0 +1,25 @@ +package askar.crypto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +class Jwk( + val kty: String, + val crv: String? = null, + val x: String? = null, + val d: String? = null, + val y: String? = null, + val alg: String? = null, + val k: String? = null +) { + override fun toString(): String { + return Json.encodeToString(this) + } + + override fun equals(other: Any?): Boolean { + val o = other as Jwk + return this.toString() == o.toString() + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/Key.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/Key.kt new file mode 100644 index 00000000..36fd2595 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/Key.kt @@ -0,0 +1,138 @@ +package askar.crypto + +import askar.Askar +import askar.enums.KeyAlgs +import askar.enums.KeyMethod +import askar.enums.SigAlgs +import askar.enums.keyAlgFromString +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +class Key(private val localKeyHandle: LocalKeyHandleKot) { + + companion object { + + fun generate(algorithm: KeyAlgs, ephemeral: Boolean = false, ): Key { + return Key(Askar.key.keyGenerate(algorithm, ephemeral)) + } + + fun fromSeed(method: KeyMethod = KeyMethod.None, algorithm: KeyAlgs, seed: String): Key { + return Key(Askar.key.keyFromSeed(algorithm, seed, method)) + } + + fun fromSecretBytes(algorithm: KeyAlgs, secretKey: ByteArray): Key { + return Key(Askar.key.keyFromSecretBytes(algorithm, secretKey)) + } + + fun fromPublicBytes(algorithm: KeyAlgs, publicKey: ByteArray): Key { + return Key(Askar.key.keyFromPublicBytes(algorithm, publicKey)) + } + + fun fromJwk(jwk: Jwk): Key { + return Key(Askar.key.keyFromJwk(jwk)) + } + } + + fun handle(): LocalKeyHandleKot { + return this.localKeyHandle + } + + fun convertKey(algorithm: KeyAlgs): Key { + return Key(Askar.key.keyConvert(this.localKeyHandle.handle, algorithm)) + } + + fun fromKeyExchange(algorithm: KeyAlgs, publicKey: Key): Key { + return Key(Askar.key.keyFromKeyExchange(this.localKeyHandle.handle, publicKey.localKeyHandle.handle, algorithm)) + } + + fun algorithm(): KeyAlgs { + val alg = Askar.key.keyGetAlgorithm(this.localKeyHandle.handle) + return keyAlgFromString(alg) + } + + fun ephemeral(): Boolean { + val num = Askar.key.keyGetEphemeral(this.localKeyHandle.handle) + return num.toInt() != 0 + } + + fun publicBytes(): ByteArray { + return Askar.key.keyGetPublicBytes(this.localKeyHandle.handle) + } + + fun secretBytes(): ByteArray { + return Askar.key.keyGetSecretBytes(this.localKeyHandle.handle) + } + + fun jwkPublic(): Jwk { + return Json.decodeFromString(Askar.key.keyGetJwkPublic(this.localKeyHandle.handle, this.algorithm())) + } + + fun jwkSecret(): Jwk { + val buffer = Askar.key.keyGetJwkSecret(this.localKeyHandle.handle) + return Json.decodeFromString(buffer) + } + + fun jwkThumbprint(): String { + return Askar.key.keyGetJwkThumbprint(this.localKeyHandle.handle, this.algorithm()) + } + + fun aeadParams(): askar.AeadParams { + return Askar.key.keyGetAeadParams(this.localKeyHandle.handle) + } + + fun aeadRandomNonce(): ByteArray { + return Askar.key.keyAeadRandomNonce(this.localKeyHandle.handle) + } + + fun aeadEncrypt( + message: String, + nonce: ByteArray = ByteArray(0), + aad: String = "" + ): askar.EncryptedBuffer { + return Askar.key.keyAeadEncrypt(localKeyHandle.handle, message, nonce, aad) + } + + fun aeadEncrypt( + message: ByteArray, + nonce: ByteArray = ByteArray(0), + aad: ByteArray = byteArrayOf(0) + ): askar.EncryptedBuffer { + return Askar.key.keyAeadEncrypt(localKeyHandle.handle, message, nonce, aad) + } + + fun aeadDecrypt( + cipherText: ByteArray, + nonce: ByteArray = ByteArray(0), + tag: ByteArray = ByteArray(0), + aad: String = "" + ): ByteArray { + return Askar.key.keyAeadDecrypt(localKeyHandle.handle, cipherText, nonce, tag, aad) + } + + fun signMessage(message: String, sigType: SigAlgs? = null): ByteArray { + return Askar.key.keySignMessage(this.localKeyHandle.handle, message, sigType) + } + + fun signMessage(message: ByteArray, sigType: SigAlgs? = null): ByteArray { + return Askar.key.keySignMessage(this.localKeyHandle.handle, message, sigType) + } + + fun verifySignature(message: String, signature: ByteArray, sigType: SigAlgs? = null): Boolean { + val num = Askar.key.keyVerifySignature(this.localKeyHandle.handle, message, signature, sigType) + return num.toInt() != 0 + } + + fun verifySignature(message: ByteArray, signature: ByteArray, sigType: SigAlgs? = null): Boolean { + val num = Askar.key.keyVerifySignature(this.localKeyHandle.handle, message, signature, sigType) + return num.toInt() != 0 + } + + fun wrapKey(other: Key, nonce: String = ""): askar.EncryptedBuffer { + return Askar.key.keyWrapKey(this.localKeyHandle.handle, other.localKeyHandle.handle, nonce) + } + + fun unwrapKey(algorithm: KeyAlgs, tag: String = "", cipherText: ByteArray = ByteArray(0), nonce: ByteArray = byteArrayOf()): Key { + return Key(Askar.key.keyUnwrapKey(this.localKeyHandle.handle, algorithm, cipherText, nonce, tag)) + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/crypto/handles.kt b/wrappers/kotlin/src/commonMain/kotlin/crypto/handles.kt new file mode 100644 index 00000000..5e90851a --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/crypto/handles.kt @@ -0,0 +1,179 @@ +@file:OptIn(UnsafeNumber::class) + +package askar.crypto + +import aries_askar.* +import aries_askar.EntryListHandle +import aries_askar.KeyEntryListHandle +import aries_askar.LocalKeyHandle +import aries_askar.ScanHandle +import aries_askar.SessionHandle +import aries_askar.StoreHandle +import askar.Askar +import kotlinx.cinterop.* +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking + + +class StoreHandle(val handle: StoreHandle) { + + suspend fun close() { + Askar.store.storeClose(this.handle) + } + + companion object { + fun fromHandle(handle: StoreHandle?): askar.crypto.StoreHandle? { + if (handle != null) + return StoreHandle(handle) + return null + } + } +} + +class ScanHandle(val handle: ScanHandle) { + + fun free() { + Askar.scan.free(this.handle) + } + + companion object { + fun fromHandle(handle: ScanHandle?): askar.crypto.ScanHandle? { + if (handle != null) + return ScanHandle(handle) + return null + } + } +} + +class SessionHandle(val handle: SessionHandle) { + + suspend fun close(commit: Boolean): Long { + val h = this.handle + val code: Long + runBlocking { + val c = async {Askar.session.sessionClose(h, commit)} + code = c.await() + } + return code + } + + + companion object { + fun fromHandle(handle: SessionHandle?): askar.crypto.SessionHandle? { + if (handle != null) + return SessionHandle(handle) + return null + } + } +} + +class EntryListHandle(val handle: EntryListHandle) { + + fun getCategory(index: Int): String { + return Askar.entryList.getCategory(index, this.handle) + } + + fun getName(index: Int): String { + return Askar.entryList.getName(index, this.handle) + } + + fun getValue(index: Int): String { + return Askar.entryList.getValue(index, this.handle) + } + + fun getTags(index: Int): String? { + return Askar.entryList.getTags(index, handle) + } + + fun free() { + Askar.entryList.free(this.handle) + } + + companion object { + fun fromHandle(handle: EntryListHandle?): askar.crypto.EntryListHandle? { + if(handle?._0 == null) + return null + return EntryListHandle(handle) + } + } +} + +class KeyEntryListHandle(val handle: KeyEntryListHandle) { + + fun getAlgorithm(index: Int): String { + return Askar.keyEntryList.getAlgorithm(index, this.handle) + } + + fun getName(index: Int): String { + return Askar.keyEntryList.getName(index, this.handle) + } + + fun getTags(index: Int): String? { + return Askar.keyEntryList.getTags(index, this.handle) + } + + fun getMetadata(index: Int): String? { + return Askar.keyEntryList.getMetadata(index, this.handle) + } + + fun loadKey(index: Int): askar.crypto.LocalKeyHandleKot { + return LocalKeyHandleKot(Askar.keyEntryList.loadLocal(index, this.handle)) + } + + fun free() { + Askar.keyEntryList.free(this.handle) + } + + companion object { + fun fromHandle(handle: KeyEntryListHandle?): askar.crypto.KeyEntryListHandle? { + if(handle != null) + return KeyEntryListHandle(handle) + return null + } + } +} +class LocalKeyHandleKot(val handle: LocalKeyHandle) { + + fun free() { + Askar.key.keyFree(this.handle) + nativeHeap.free(handle) + } + + companion object { + fun fromHandle(handle: LocalKeyHandle?): askar.crypto.LocalKeyHandleKot? { + if (handle != null) + return LocalKeyHandleKot(handle) + return null + } + } + +// object LocalKeyHandleAsStringSerializer : KSerializer { +// override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("handle", PrimitiveKind.STRING) +// override fun serialize(encoder: Encoder, value: LocalKeyHandle) { +// val count = value._0!!.reinterpret().pointed.value!!.toKString().length +// println(count) +// val temp = value._0!!.pointed +// val s = value._0!!.reinterpret() +// println() +// encoder.encodeString("test") +// } +// +// override fun deserialize(decoder: Decoder): LocalKeyHandle { +// val s = decoder.decodeString() +// val h = cValue { +// //TODO: s needs to be converted back to a pointer +// _0 = null +// } +// h.useContents { +// return this +// } +// } +// } + + +} + + + + + diff --git a/wrappers/kotlin/src/commonMain/kotlin/enums/EntryOperation.kt b/wrappers/kotlin/src/commonMain/kotlin/enums/EntryOperation.kt new file mode 100644 index 00000000..cc7243ab --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/enums/EntryOperation.kt @@ -0,0 +1,7 @@ +package askar.enums + +enum class EntryOperation { + Insert, + Replace, + Remove +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/enums/KeyAlgs.kt b/wrappers/kotlin/src/commonMain/kotlin/enums/KeyAlgs.kt new file mode 100644 index 00000000..af7e1467 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/enums/KeyAlgs.kt @@ -0,0 +1,27 @@ +package askar.enums + +enum class KeyAlgs(val alg: String) { + AesA128Gcm("a128gcm"), + AesA256Gcm("a256gcm"), + AesA128CbcHs256("a128cbchs256"), + AesA256CbcHs512("a256cbchs512"), + AesA128Kw("a128kw"), + AesA256Kw("a256kw"), + Bls12381G1("bls12381g1"), + Bls12381G2("bls12381g2"), + Bls12381G1G2("bls12381g1g2"), + Chacha20C20P("c20p"), + Chacha20XC20P("xc20p"), + Ed25519("ed25519"), + X25519("x25519"), + EcSecp256k1("k256"), + EcSecp256r1("p256"), +} + +fun keyAlgFromString(algorithm: String): KeyAlgs { + for(alg in KeyAlgs.values()) + if(alg.alg == algorithm) + return alg + throw Error("Algorithm $algorithm is not a supported algorithm") +} + diff --git a/wrappers/kotlin/src/commonMain/kotlin/enums/KeyMethod.kt b/wrappers/kotlin/src/commonMain/kotlin/enums/KeyMethod.kt new file mode 100644 index 00000000..cd529f7e --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/enums/KeyMethod.kt @@ -0,0 +1,6 @@ +package askar.enums + +enum class KeyMethod (val method: String){ + None(""), + BlsKeygen("bls_keygen") +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/enums/LogLevel.kt b/wrappers/kotlin/src/commonMain/kotlin/enums/LogLevel.kt new file mode 100644 index 00000000..65248e5e --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/enums/LogLevel.kt @@ -0,0 +1,11 @@ +package askar.enums + +enum class LogLevel (val logLevel: Int) { + RUST_LOG(-1), + Off(0), + Error(1), + Warn(2), + Info(3), + Debug(4), + Trace(5) +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/enums/SigAlgs.kt b/wrappers/kotlin/src/commonMain/kotlin/enums/SigAlgs.kt new file mode 100644 index 00000000..06d2b481 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/enums/SigAlgs.kt @@ -0,0 +1,7 @@ +package askar.enums + +enum class SigAlgs (val alg: String) { + EdDSA("eddsa"), + ES256("es256"), + ES256K("es256k") +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/types.kt b/wrappers/kotlin/src/commonMain/kotlin/types.kt new file mode 100644 index 00000000..5860b044 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/types.kt @@ -0,0 +1,54 @@ +@file:OptIn(ExperimentalUnsignedTypes::class, ExperimentalUnsignedTypes::class, ExperimentalUnsignedTypes::class) + +package askar + +import askar.crypto.Jwk +import askar.crypto.Key +import askar.enums.KeyAlgs +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + + +class AeadParams(val nonceLength: Int, val tagsLength: Int) + +class EncryptedBuffer( + val buffer: ByteArray, + val tagPos: Int, + val noncePos: Int +) { + + + fun cipherTextWithTag(): ByteArray { + val p = this.noncePos + return buffer.slice(0 until p).toByteArray() + } + + fun cipherText(): ByteArray { + val p = tagPos + return buffer.slice(0 until p).toByteArray() + } + + fun nonce(): ByteArray { + val p = noncePos + return buffer.slice(p until buffer.size).toByteArray() + } + + fun tag(): ByteArray { + val p1 = tagPos + val p2 = noncePos + return buffer.slice(p1 until p2).toByteArray() + } + +} +@Serializable +class ProtectedJson(val alg: String, val enc: String, val apu: String, val apv: String, val epk: Jwk){ + + constructor(alg: String, enc: KeyAlgs, apu: String, apv: String, epk: Jwk) : this(alg, enc.alg, apu, apv, epk) + + override fun toString(): String { + return Json.encodeToString(this) + } + +} + diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/CryptoBoxWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/CryptoBoxWrapper.kt new file mode 100644 index 00000000..7b445682 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/CryptoBoxWrapper.kt @@ -0,0 +1,214 @@ +package askar.wrappers + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.byteArrayToByteBuffer +import askar.Askar.Companion.secretBufferToByteArray +import askar.Askar.Companion.secretBufferToString +import askar.Askar.Companion.stringToByteBuffer +import askar.enums.KeyAlgs +import kotlinx.cinterop.* + + +class CryptoBoxWrapper { + + fun keyCryptoBoxRandomNonce(): ByteArray { + memScoped { + val out = alloc() + val errorCode = askar_key_crypto_box_random_nonce(out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyCryptoBox( + recipientKey: askar.crypto.LocalKeyHandleKot, + senderKey: askar.crypto.LocalKeyHandleKot, + message: String, + nonce: ByteArray + ): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val sk = cValue { + _0 = senderKey.handle._0 + } + val out = alloc() + val errorCode = + askar_key_crypto_box(rk, sk, stringToByteBuffer(message, this), byteArrayToByteBuffer(nonce, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyCryptoBox( + recipientKey: askar.crypto.LocalKeyHandleKot, + senderKey: askar.crypto.LocalKeyHandleKot, + message: ByteArray, + nonce: ByteArray + ): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val sk = cValue { + _0 = senderKey.handle._0 + } + val out = alloc() + val errorCode = + askar_key_crypto_box(rk, sk, byteArrayToByteBuffer(message, this), byteArrayToByteBuffer(nonce, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun cryptoBoxOpen( + recipientKey: askar.crypto.LocalKeyHandleKot, + senderKey: askar.crypto.LocalKeyHandleKot, + message: ByteArray, + nonce: ByteArray + ): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val sk = cValue { + _0 = senderKey.handle._0 + } + val out = alloc() + + val errorCode = + askar_key_crypto_box_open(rk, sk, byteArrayToByteBuffer(message, this), byteArrayToByteBuffer(nonce, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun cryptoBoxSeal(recipientKey: askar.crypto.LocalKeyHandleKot, message: String): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val out = alloc() + val errorCode = askar_key_crypto_box_seal(rk, stringToByteBuffer(message, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun cryptoBoxSeal(recipientKey: askar.crypto.LocalKeyHandleKot, message: ByteArray): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val out = alloc() + val errorCode = askar_key_crypto_box_seal(rk, byteArrayToByteBuffer(message, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun cryptoBoxSealOpen(recipientKey: askar.crypto.LocalKeyHandleKot, cipherText: ByteArray): ByteArray { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val out = alloc() + val errorCode = askar_key_crypto_box_seal_open(rk, byteArrayToByteBuffer(cipherText, this), out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyDeriveEcdh1pu( + algId: String, + receive: Boolean, + apv: String, + apu: String, + encAlg: KeyAlgs, + ephemeralKey: askar.crypto.LocalKeyHandleKot, + recipientKey: askar.crypto.LocalKeyHandleKot, + senderKey: askar.crypto.LocalKeyHandleKot, + ccTag: String + ): askar.crypto.LocalKeyHandleKot { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val sk = cValue { + _0 = senderKey.handle._0 + } + val ek = cValue { + _0 = ephemeralKey.handle._0 + } + val bool = receive.toByte() + val out = nativeHeap.alloc() + val errorCode = askar_key_derive_ecdh_1pu( + encAlg.alg, ek, sk, rk, stringToByteBuffer(algId, this), stringToByteBuffer(apu, this), stringToByteBuffer(apv, this), + stringToByteBuffer(ccTag, this), bool, out.ptr + ) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyDeriveEcdh1pu( + algId: String, + receive: Boolean, + apv: String, + apu: String, + encAlg: KeyAlgs, + ephemeralKey: askar.crypto.LocalKeyHandleKot, + recipientKey: askar.crypto.LocalKeyHandleKot, + senderKey: askar.crypto.LocalKeyHandleKot, + ccTag: ByteArray + ): askar.crypto.LocalKeyHandleKot { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val sk = cValue { + _0 = senderKey.handle._0 + } + val ek = cValue { + _0 = ephemeralKey.handle._0 + } + val bool = receive.toByte() + val out = nativeHeap.alloc() + val errorCode = askar_key_derive_ecdh_1pu( + encAlg.alg, ek, sk, rk, stringToByteBuffer(algId, this), stringToByteBuffer(apu, this), stringToByteBuffer(apv, this), + byteArrayToByteBuffer(ccTag, this), bool, out.ptr + ) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyDeriveEcdhes( + algId: String, + receive: Boolean, + apv: String, + apu: String, + encAlg: KeyAlgs, + ephemeralKey: askar.crypto.LocalKeyHandleKot, + recipientKey: askar.crypto.LocalKeyHandleKot, + ): askar.crypto.LocalKeyHandleKot { + memScoped { + val rk = cValue { + _0 = recipientKey.handle._0 + } + val ek = cValue { + _0 = ephemeralKey.handle._0 + } + val bool = receive.toByte() + val out = nativeHeap.alloc() + val errorCode = askar_key_derive_ecdh_es( + encAlg.alg, ek, rk, stringToByteBuffer(algId, this), stringToByteBuffer(apu, this), stringToByteBuffer(apv, this), + bool, out.ptr + ) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/EntryListWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/EntryListWrapper.kt new file mode 100644 index 00000000..0a212987 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/EntryListWrapper.kt @@ -0,0 +1,84 @@ +package askar.wrappers + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.secretBufferToByteArray +import askar.Askar.Companion.secretBufferToString +import kotlinx.cinterop.* +import platform.posix.int32_t +import platform.posix.int32_tVar +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class EntryListWrapper { + + fun getCategory(index: Int, handle: EntryListHandle): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_entry_list_get_category(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun getName(index: Int, handle: EntryListHandle): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_entry_list_get_name(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun getValue(index: Int, handle: EntryListHandle): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_entry_list_get_value(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToString(out) + } + } + + fun getTags(index: Int, handle: EntryListHandle): String? { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_entry_list_get_tags(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value?.toKString() + + } + } + + fun free(handle: EntryListHandle) { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + askar_entry_list_free(cHandle) + } + } + + fun entryListCount(handle: EntryListHandle): int32_t { + memScoped { + val cHandle = cValue{ + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_entry_list_count(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return out.value + } + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyEntryListWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyEntryListWrapper.kt new file mode 100644 index 00000000..0aaa7eb5 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyEntryListWrapper.kt @@ -0,0 +1,87 @@ +package askar.wrappers + + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.getErrorCode +import kotlinx.cinterop.* +import platform.posix.int32_t +import platform.posix.int32_tVar + +class KeyEntryListWrapper { + + fun getAlgorithm(index: Int, handle: KeyEntryListHandle): String { + memScoped { + val cHandle = cValue { _0 = handle._0 } + val out = alloc>() + val errorCode = askar_key_entry_list_get_algorithm(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun getName(index: Int, handle: KeyEntryListHandle): String { + memScoped { + val cHandle = cValue { _0 = handle._0 } + val out = alloc>() + val errorCode = askar_key_entry_list_get_name(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun getTags(index: Int, handle: KeyEntryListHandle): String? { + memScoped { + val cHandle = cValue { _0 = handle._0 } + val out = alloc>() + val errorCode = askar_key_entry_list_get_tags(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value?.toKString() + } + } + + fun getMetadata(index: Int, handle: KeyEntryListHandle): String? { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_key_entry_list_get_metadata(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out.value?.toKString() + } + } + + fun loadLocal(index: Int, handle: KeyEntryListHandle): LocalKeyHandle { + val cHandle = cValue { + _0 = handle._0 + } + val out = nativeHeap.alloc() + val errorCode = askar_key_entry_list_load_local(cHandle, index, out.ptr) + Askar.assertNoError(errorCode) + return out + } + + fun free(handle: KeyEntryListHandle) { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + askar_key_entry_list_free(cHandle) + } + } + + fun count(handle: KeyEntryListHandle): int32_t { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_entry_list_count(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return out.value + } + } + + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyWrapper.kt new file mode 100644 index 00000000..796279a8 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/KeyWrapper.kt @@ -0,0 +1,370 @@ +package askar.wrappers + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.byteArrayToByteBuffer +import askar.Askar.Companion.secretBufferToByteArray +import askar.Askar.Companion.secretBufferToString +import askar.Askar.Companion.stringToByteBuffer +import askar.crypto.Jwk +import askar.enums.KeyAlgs +import askar.enums.KeyMethod +import askar.enums.SigAlgs +import kotlinx.cinterop.* +import kotlinx.serialization.json.Json +import platform.posix.uint8_tVar +import kotlinx.serialization.encodeToString +import platform.posix.int8_t +import platform.posix.int8_tVar + +class KeyWrapper { + + fun keyGenerate(algorithm: KeyAlgs, ephemeral: Boolean): askar.crypto.LocalKeyHandleKot { + val bool = if (ephemeral) 1 else 0 + val out = nativeHeap.alloc() + val errorCode = askar_key_generate(algorithm.alg, bool.toByte(), out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + + fun keyFromSeed(algorithm: KeyAlgs, seed: String, method: KeyMethod): askar.crypto.LocalKeyHandleKot { + memScoped { + val cString = stringToByteBuffer(seed, this) + val out = nativeHeap.alloc() + val errorCode = askar_key_from_seed(algorithm.alg, cString, method.method, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyFromSecretBytes(algorithm: KeyAlgs, secretString: ByteArray): askar.crypto.LocalKeyHandleKot { + memScoped { + val buffer = byteArrayToByteBuffer(secretString, this) + val out = nativeHeap.alloc() + val errorCode = askar_key_from_secret_bytes(algorithm.alg, buffer, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyFromPublicBytes(algorithm: KeyAlgs, publicBytes: ByteArray): askar.crypto.LocalKeyHandleKot { + memScoped { + val buffer = byteArrayToByteBuffer(publicBytes, this) + val out = nativeHeap.alloc() + val errorCode = askar_key_from_public_bytes(algorithm.alg, buffer, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyFromJwk(jwk: Jwk): askar.crypto.LocalKeyHandleKot { + memScoped { + val buffer = stringToByteBuffer(jwk.toString(), this) + val out = nativeHeap.alloc() + val errorCode = askar_key_from_jwk(buffer, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyConvert(handle: LocalKeyHandle, algorithm: KeyAlgs): askar.crypto.LocalKeyHandleKot { + val cHandle = cValue { + _0 = handle._0 + } + val out = nativeHeap.alloc() + val errorCode = askar_key_convert(cHandle, algorithm.alg, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + + fun keyFromKeyExchange( + sHandle: LocalKeyHandle, + pHandle: LocalKeyHandle, + algorithm: KeyAlgs + ): askar.crypto.LocalKeyHandleKot { + val scHandle = cValue { + _0 = sHandle._0 + } + val pcHandle = cValue { + _0 = pHandle._0 + } + val out = nativeHeap.alloc() + val errorCode = askar_key_from_key_exchange(algorithm.alg, scHandle, pcHandle, out.ptr) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + + fun keyGetAlgorithm(handle: LocalKeyHandle): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_key_get_algorithm(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun keyGetEphemeral(handle: LocalKeyHandle): int8_t { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_get_ephemeral(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return out.value + } + } + + fun keyGetPublicBytes(handle: LocalKeyHandle): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_get_public_bytes(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyGetSecretBytes(handle: LocalKeyHandle): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_get_secret_bytes(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + + fun keyGetJwkPublic(handle: LocalKeyHandle, algorithm: KeyAlgs): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_key_get_jwk_public(cHandle, algorithm.alg, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun keyGetJwkSecret(handle: LocalKeyHandle): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_get_jwk_secret(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToString(out) + } + } + + fun keyGetJwkThumbprint(handle: LocalKeyHandle, algorithm: KeyAlgs): String { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc>() + val errorCode = askar_key_get_jwk_thumbprint(cHandle, algorithm.alg, out.ptr) + Askar.assertNoError(errorCode) + return out.value!!.toKString() + } + } + + fun keyGetAeadParams(handle: LocalKeyHandle): askar.AeadParams { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_aead_get_params(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return askar.AeadParams(out.nonce_length, out.tag_length) + } + } + + fun keyAeadRandomNonce(handle: LocalKeyHandle): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_aead_random_nonce(cHandle, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyAeadEncrypt(handle: LocalKeyHandle, message: String, nonce: ByteArray, aad: String): askar.EncryptedBuffer { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val messageBuf = stringToByteBuffer(message, this) + val aadBuf = stringToByteBuffer(aad, this) + val nonceBuf = byteArrayToByteBuffer(nonce, this) + val out = alloc() + val errorCode = askar_key_aead_encrypt(cHandle, messageBuf, nonceBuf, aadBuf, out.ptr) + Askar.assertNoError(errorCode) + val buf = secretBufferToByteArray(out.buffer) + return askar.EncryptedBuffer(buf, out.tag_pos.toInt(), out.nonce_pos.toInt()) + } + } + + fun keyAeadEncrypt(handle: LocalKeyHandle, message: ByteArray, nonce: ByteArray, aad: ByteArray): askar.EncryptedBuffer { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val messageBuf = byteArrayToByteBuffer(message, this) + val aadBuf = byteArrayToByteBuffer(aad, this) + val nonceBuf = byteArrayToByteBuffer(nonce, this) + val out = alloc() + val errorCode = askar_key_aead_encrypt(cHandle, messageBuf, nonceBuf, aadBuf, out.ptr) + Askar.assertNoError(errorCode) + val buf = secretBufferToByteArray(out.buffer) + return askar.EncryptedBuffer(buf, out.tag_pos.toInt(), out.nonce_pos.toInt()) + } + } + + fun keyAeadDecrypt( + handle: LocalKeyHandle, + cipherText: ByteArray, + nonce: ByteArray, + tag: ByteArray, + aad: String, + ): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = this.alloc() + val cipherBuf = byteArrayToByteBuffer(cipherText, this) + val nonceBuf = byteArrayToByteBuffer(nonce, this) + val tagBuf = byteArrayToByteBuffer(tag, this) + val aadBuf = stringToByteBuffer(aad, this) + val errorCode = askar_key_aead_decrypt(cHandle, cipherBuf, nonceBuf, tagBuf, aadBuf, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keySignMessage(handle: LocalKeyHandle, message: String, sigType: SigAlgs?): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_sign_message(cHandle, stringToByteBuffer(message, this), sigType?.alg, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keySignMessage(handle: LocalKeyHandle, message: ByteArray, sigType: SigAlgs?): ByteArray { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_sign_message(cHandle, byteArrayToByteBuffer(message, this), sigType?.alg, out.ptr) + Askar.assertNoError(errorCode) + return secretBufferToByteArray(out) + } + } + + fun keyVerifySignature(handle: LocalKeyHandle, message: String, signature: ByteArray, sigType: SigAlgs?): int8_t { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_verify_signature( + cHandle, + stringToByteBuffer(message, this), + byteArrayToByteBuffer(signature, this), + sigType?.alg, + out.ptr + ) + Askar.assertNoError(errorCode) + return out.value + } + } + + fun keyVerifySignature(handle: LocalKeyHandle, message: ByteArray, signature: ByteArray, sigType: SigAlgs?): int8_t { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = alloc() + val errorCode = askar_key_verify_signature( + cHandle, + byteArrayToByteBuffer(message, this), + byteArrayToByteBuffer(signature, this), + sigType?.alg, + out.ptr + ) + Askar.assertNoError(errorCode) + return out.value + } + } + + fun keyWrapKey(handle: LocalKeyHandle, other: LocalKeyHandle, nonce: String): askar.EncryptedBuffer { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val cOther = cValue { + _0 = other._0 + } + val out = alloc() + val errorCode = askar_key_wrap_key(cHandle, cOther, stringToByteBuffer(nonce, this), out.ptr) + Askar.assertNoError(errorCode) + val buf = secretBufferToByteArray(out.buffer) + return askar.EncryptedBuffer(buf, out.tag_pos.toInt(), out.nonce_pos.toInt()) + } + } + + fun keyUnwrapKey( + handle: LocalKeyHandle, + algorithm: KeyAlgs, + cipherText: ByteArray, + nonce: ByteArray, + tag: String + ): askar.crypto.LocalKeyHandleKot { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + val out = nativeHeap.alloc() + val errorCode = askar_key_unwrap_key( + cHandle, + algorithm.alg, + byteArrayToByteBuffer(cipherText, this), + byteArrayToByteBuffer(nonce, this), + stringToByteBuffer(tag, this), + out.ptr + ) + Askar.assertNoError(errorCode) + return askar.crypto.LocalKeyHandleKot(out) + } + } + + fun keyFree(handle: LocalKeyHandle) { + memScoped { + val cHandle = cValue { + _0 = handle._0 + } + askar_key_free(cHandle) + } + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/ScanWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/ScanWrapper.kt new file mode 100644 index 00000000..0ebe0600 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/ScanWrapper.kt @@ -0,0 +1,70 @@ +package askar.wrappers + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.assertNoError +import askar.Askar.Companion.getErrorCode +import askar.crypto.EntryListHandle +import kotlinx.cinterop.* +import kotlinx.serialization.json.JsonObject +import kotlin.coroutines.Continuation +import kotlin.coroutines.suspendCoroutine + +@OptIn(UnsafeNumber::class) +class ScanWrapper { + + fun free(handle: ScanHandle) { + val errorCode = askar_scan_free(handle) + assertNoError(errorCode) + } + + suspend fun scanStart( + handle: StoreHandle, + limit: Int, + offset: Int, + tagFilter: JsonObject, + profile: String?, + category: String + ) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + + val errorCode = askar_scan_start( + handle, profile, category, tagFilter.toString(), offset.toLong(), limit.toLong(), + staticCFunction { callBackId, errorCode, scanHandle -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(askar.crypto.ScanHandle(scanHandle))) + }, contPtr.toLong() + ) + assertNoError(errorCode, continuation) + } + + class ScanNextProps(val memScope: MemScope, val cont: Continuation) + + suspend fun scanNext(handle: ScanHandle, memScope: MemScope) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(ScanNextProps(memScope, continuation)) + val contPtr = stableRef.asCPointer() + //Try writing the handle to an out variable + val errorCode = askar_scan_next(handle, staticCFunction { callBackId, errorCode, entryHandle -> + val contRef = callBackId.toCPointer()?.asStableRef() + val props = contRef?.get() + val cont = props?.cont + val scope = props?.memScope + contRef?.dispose() + if(assertNoError(errorCode, cont!!)) { + val h = entryHandle.getPointer(scope!!).pointed + if (h._0 == null) { + cont.resumeWith(Result.success(null)) + } else { + val handleCopy = EntryListHandle(h) + cont.resumeWith(Result.success(handleCopy)) + } + } + }, contPtr.toLong()) + assertNoError(errorCode, continuation) + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/SessionWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/SessionWrapper.kt new file mode 100644 index 00000000..166d82fc --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/SessionWrapper.kt @@ -0,0 +1,323 @@ +package askar.wrappers + +import aries_askar.* + +import aries_askar.LocalKeyHandle +import askar.Askar +import askar.Askar.Companion.stringToByteBuffer +import askar.crypto.EntryListHandle +import askar.crypto.Key +import askar.crypto.KeyEntryListHandle +import askar.enums.EntryOperation +import askar.enums.KeyAlgs +import kotlinx.cinterop.* +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import platform.posix.uint8_tVar +import kotlin.coroutines.Continuation + +import kotlin.coroutines.suspendCoroutine + +@OptIn(UnsafeNumber::class) +class SessionWrapper { + + suspend fun sessionClose(handle: SessionHandle, commit: Boolean) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + val bool = if (commit) 1 else 0 + + val errorCode = askar_session_close( + handle, bool.toByte(), staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionCount(handle: SessionHandle, category: String, tagFilter: String) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = + askar_session_count(handle, category, tagFilter, staticCFunction { callBackId, errorCode, count -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(count)) + + }, contPtr.toLong()) + + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionFetch( + handle: SessionHandle, + category: String, + name: String, + forUpdate: Boolean, + ) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + val bool = if (forUpdate) 1 else 0 + val errorCode = askar_session_fetch( + handle, + category, + name, + bool.toByte(), + staticCFunction { callBackId, errorCode, entryListHandle -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = nativeHeap.alloc() + val h = entryListHandle.place(temp.ptr).pointed + if (h._0 == null) + cont.resumeWith(Result.success(null)) + else { + val entryList = EntryListHandle(h) + cont.resumeWith(Result.success(entryList)) + } + } + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun fetchAll( + handle: SessionHandle, + category: String, + tagFilter: String, + limit: Long, + forUpdate: Boolean, + ) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create( continuation) + val contPtr = stableRef.asCPointer() + val bool = if (forUpdate) 1 else 0 + + val errorCode = askar_session_fetch_all( + handle, category, tagFilter, limit, bool.toByte(), + staticCFunction { callBackId, errorCode, entryListHandle -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = nativeHeap.alloc() + val h = entryListHandle.place(temp.ptr).pointed + if (h._0 == null) + cont.resumeWith(Result.success(null)) + else { + val entryList = EntryListHandle.fromHandle(h) + cont.resumeWith(Result.success(entryList)) + } + } + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionUpdate( + handle: SessionHandle, + category: String, + name: String, + expiryMs: Long, + tags: String = "{}", + value: String = "", + operation: EntryOperation + ) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + memScoped { + val buffer = stringToByteBuffer(value, this) + val errorCode = askar_session_update( + handle, + operation.ordinal.toByte(), + category, + name, + buffer, + tags, + expiryMs, + staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + } + + suspend fun sessionRemoveAll(handle: SessionHandle, category: String, tagFilter: String) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = + askar_session_remove_all(handle, category, tagFilter, staticCFunction { callBackId, errorCode, _ -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, contPtr.toLong()) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionInsertKey( + handle: SessionHandle, + name: String, + key: Key, + metadata: String? = null, + tags: String = "{}", + expiryMs: Long + ) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + val cHandle = cValue { + _0 = key.handle().handle._0 + } + + val errorCode = askar_session_insert_key( + handle, cHandle, name, metadata, tags, expiryMs, + staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionFetchKey(handle: SessionHandle, name: String, forUpdate: Boolean) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + val bool = if (forUpdate) 1 else 0 + val errorCode = askar_session_fetch_key( + handle, name, bool.toByte(), + staticCFunction { callBackId, errorCode, key -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = nativeHeap.alloc() + val h = key.place(temp.ptr).pointed + if (h._0 == null) + cont.resumeWith(Result.success(null)) + else + cont.resumeWith(Result.success(KeyEntryListHandle.fromHandle(h))) + } + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionFetchAllKeys( + handle: SessionHandle, + algorithm: KeyAlgs?, + thumbprint: String?, + tagFilter: String?, + limit: Long, + forUpdate: Boolean, + ) = suspendCoroutine { continuation -> + val stableRef = StableRef.create( continuation) + val contPtr = stableRef.asCPointer() + val bool = if (forUpdate) 1 else 0 + val errorCode = + askar_session_fetch_all_keys( + handle, algorithm?.alg, thumbprint, tagFilter, limit, bool.toByte(), + staticCFunction { callBackId, errorCode, key -> + val contRef = + callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = nativeHeap.alloc() + val h = key.place(temp.ptr).pointed + if (h._0 == null) + cont.resumeWith(Result.success(null)) + else + cont.resumeWith(Result.success(KeyEntryListHandle.fromHandle(h))) + } + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun sessionUpdateKey( + handle: SessionHandle, + name: String, + metadata: String?, + tags: String?, + expiryMs: Long + ) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + memScoped { + val errorCode = askar_session_update_key( + handle, name, metadata, tags.toString(), expiryMs, + staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + } + + suspend fun sessionRemoveKey(handle: SessionHandle, name: String) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + memScoped { + val errorCode = askar_session_remove_key(handle, name, staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, contPtr.toLong()) + Askar.assertNoError(errorCode, continuation) + } + } + + suspend fun sessionStart(handle: StoreHandle, profile: String?, isTxn: Boolean) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + val bool = if (isTxn) 1 else 0 + + val errorCode = askar_session_start( + handle, profile, bool.toByte(), + staticCFunction { callBackId, errorCode, handle -> + val contRef = + callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(askar.crypto.SessionHandle(handle))) + }, contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonMain/kotlin/wrappers/StoreWrapper.kt b/wrappers/kotlin/src/commonMain/kotlin/wrappers/StoreWrapper.kt new file mode 100644 index 00000000..774cf628 --- /dev/null +++ b/wrappers/kotlin/src/commonMain/kotlin/wrappers/StoreWrapper.kt @@ -0,0 +1,209 @@ +@file:OptIn(UnsafeNumber::class, ExperimentalUnsignedTypes::class, UnsafeNumber::class) + +package askar.wrappers + +import aries_askar.* +import askar.Askar +import askar.Askar.Companion.getErrorCode +import askar.Store.StoreKeyMethod +import kotlinx.cinterop.* +import platform.posix.uint8_tVar +import kotlin.coroutines.Continuation +import kotlin.coroutines.suspendCoroutine + +@OptIn(UnsafeNumber::class) +class StoreWrapper { + + // STORE PROVISION + class StoreProvisionResponse( + val callbackId: CallbackId, + val storeHandle: StoreHandle, + val errorCode: ErrorCode + ) + + suspend fun storeProvision( + specUri: String, + passKey: String, + profile: String?, + keyMethod: StoreKeyMethod, + recreate: Boolean + ): StoreProvisionResponse = suspendCoroutine { continuation -> + val contStableRef = StableRef.create(continuation) + val contPtr = contStableRef.asCPointer() + val bool = if (recreate) 1 else 0 + + val errorCode = askar_store_provision( + specUri, + keyMethod.toUri(), + passKey, + profile, + bool.toByte(), + staticCFunction { callbackId, errorCode, storeHandle -> + val response = StoreProvisionResponse(callbackId, storeHandle, errorCode) + val contRef = callbackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(response)) + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + // STORE CLOSE + suspend fun storeClose(storeHandle: StoreHandle) = suspendCoroutine { continuation -> + val contStableRef = StableRef.create(continuation) + val contPtr = contStableRef.asCPointer() + + val errorCode = askar_store_close( + storeHandle, + staticCFunction { callbackId, errorCode -> + val contRef = callbackId.toCPointer()?.asStableRef>() + contRef?.get()?.resumeWith(Result.success(getErrorCode(errorCode))) + contRef?.dispose() + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun storeOpen( + specUri: String, + passKey: String, + profile: String?, + keyMethod: StoreKeyMethod + ) = suspendCoroutine { continuation -> + val contStableRef = StableRef.create(continuation) + val contPtr = contStableRef.asCPointer() + + val errorCode = askar_store_open( + specUri, + keyMethod.toUri(), + passKey, + profile, + staticCFunction { callbackId, errorCode, storeHandle -> + val response = StoreProvisionResponse(callbackId, storeHandle, errorCode) + val contRef = callbackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(response)) + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun storeRemove(specUri: String) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = askar_store_remove( + specUri, + staticCFunction { callbackId, errorCode, bool -> + val contRef = callbackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(bool.toBoolean())) + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun storeCreateProfile(handle: StoreHandle, profile: String) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = askar_store_create_profile( + handle, profile, + staticCFunction { callbackId, errorCode, profile -> + val contRef = callbackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = profile?.toKString() + cont.resumeWith(Result.success(temp)) + } + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun storeGetProfileName(handle: StoreHandle) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = askar_store_get_profile_name( + handle, + staticCFunction { callbackID, errorCode, name -> + val contRef = callbackID.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) { + val temp = name?.toKString() + cont.resumeWith(Result.success(temp)) + } + }, + contPtr.toLong() + ) + Askar.assertNoError(errorCode, continuation) + } + + suspend fun storeRemoveProfile(handle: StoreHandle, name: String) = suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = askar_store_remove_profile(handle, name, staticCFunction { callBackId, errorCode, removed -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + contRef?.dispose() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(removed.toBoolean())) + }, contPtr.toLong()) + Askar.assertNoError(errorCode, continuation) + + } + + suspend fun storeRekey(handle: StoreHandle, keyMethod: StoreKeyMethod, passKey: String) = + suspendCoroutine { continuation -> + val stableRef = StableRef.create(continuation) + val contPtr = stableRef.asCPointer() + + val errorCode = + askar_store_rekey(handle, keyMethod.toUri(), passKey, staticCFunction { callBackId, errorCode -> + val contRef = callBackId.toCPointer()?.asStableRef>() + val cont = contRef?.get() + if(Askar.assertNoError(errorCode, cont!!)) + cont.resumeWith(Result.success(errorCode)) + }, contPtr.toLong()) + Askar.assertNoError(errorCode, continuation) + } + + + fun storeGenerateRawKey(key: String): String? { + var errorCode: Long = 0 + var rawKey: String? = null + memScoped { + val cString = key.cstr + val uIntBuffer = allocArray(cString.size) + for (i in 0..cString.size) { + uIntBuffer[i] = cString.ptr[i].toUByte() + } + val buffer = cValue { + data = uIntBuffer + len = cString.size.toLong() + } + val out = alloc>() + errorCode = askar_store_generate_raw_key(buffer, out.ptr) + rawKey = out.value!!.toKString() + } + Askar.assertNoError(errorCode) + return rawKey + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonTest/kotlin/AskarTest.kt b/wrappers/kotlin/src/commonTest/kotlin/AskarTest.kt new file mode 100644 index 00000000..1e9fabc5 --- /dev/null +++ b/wrappers/kotlin/src/commonTest/kotlin/AskarTest.kt @@ -0,0 +1,345 @@ +package tech.indicio.holdr + + +import askar.Askar.Companion.mapToJsonObject +import askar.Store.* +import askar.crypto.Key +import askar.enums.KeyAlgs +import kotlinx.cinterop.memScoped +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* +import tech.indicio.holdr.AskarUtils.* +import kotlin.test.* +import kotlin.test.Test + + +class AskarTest { + + private lateinit var store: Store + + + @BeforeTest + fun beforeEach() { + runBlocking { + store = setupWallet() + } + } + + @AfterTest + fun afterEach() { + runBlocking { + store.close(true) + } + } + + @Test + fun argon2imod() { + runBlocking { + val argonStore = Store.provision( + recreate = true, + passkey = "abc", + uri = testStoreUri + "1", //Cannot have duplicate URI otherwise error is thrown + keyMethod = StoreKeyMethod(KdfMethod.Argon2IMod), + profile = "test" + ) + + val session = argonStore.openSession() + assertNull(session.fetch("unknownCategory", "unknownKey")) + argonStore.close() + } + } + + @Test + fun argon2iint() { + runBlocking { + val argonStore = Store.provision( + recreate = true, + passkey = "abc", + uri = testStoreUri + "1", //Cannot have duplicate URI otherwise error is thrown + keyMethod = StoreKeyMethod(KdfMethod.Argon2IInt), + profile = "test" + ) + + val session = argonStore.openSession() + assertNull(session.fetch("unknownCategory", "unknownKey")) + argonStore.close() + } + } + + @Test + fun rekey() { + runBlocking { + val initialKey = Store.generateRawKey("1234") ?: throw Error("Key came back as null") + val storage = "./tmp" + var newStore = Store.provision( + recreate = true, + profile = "rekey", + uri = "sqlite://$storage/rekey.db", + keyMethod = StoreKeyMethod(KdfMethod.Raw), + passkey = initialKey + ) + val newKey = Store.generateRawKey("12345") ?: throw Error("Key came back as null") + newStore.rekey(StoreKeyMethod(KdfMethod.Raw), newKey) + newStore.close() + assertFails { + Store.open( + profile = "rekey", + uri = "sqlite://$storage/rekey.db", + keyMethod = StoreKeyMethod(KdfMethod.Raw), + passkey = initialKey + ) + } + newStore = Store.open( + profile = "rekey", + uri = "sqlite://$storage/rekey.db", + keyMethod = StoreKeyMethod(KdfMethod.Raw), + passkey = newKey + ) + newStore.close(true) + } + } + + @Test + fun insert() { + runBlocking { + val session = store.openSession() + session.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + assertEquals(1, session.count(firstEntry.category, firstEntry.tags)) + + session.close() + } + } + + @Test + fun replace() { + runBlocking { + val session = store.openSession() + session.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + assertEquals(1, session.count(firstEntry.category, firstEntry.tags)) + + val updatedEntry = EntryObject(firstEntry.category, firstEntry.name, value = "bar", tags = "{\"foo\": \"bar\"}" + ) + println(updatedEntry) + session.replace( + updatedEntry.category, + updatedEntry.name, + value = updatedEntry.value, + tags = updatedEntry.tags + ) + assertEquals(1, session.count(updatedEntry.category, updatedEntry.tags)) + session.close() + } + } + + @Test + fun remove() { + runBlocking { + val session = store.openSession() + session.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + + assertEquals(1, session.count(firstEntry.category, firstEntry.tags)) + + session.remove(firstEntry.category, firstEntry.name) + + assertEquals(0, session.count(firstEntry.category, firstEntry.tags)) + + session.close() + } + } + + @Test + fun removeAll() { + runBlocking { + val session = store.openSession() + session.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + session.insert( + secondEntry.category, + secondEntry.name, + value = secondEntry.value, + tags = secondEntry.tags + ) + + assertEquals(2, session.count(firstEntry.category, firstEntry.tags)) + + session.removeAll(firstEntry.category) + + assertEquals(0, session.count(firstEntry.category, firstEntry.tags)) + + session.close() + } + } + + @Test + fun scan() { + runBlocking { + val session = store.openSession() + session.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + session.insert( + category = secondEntry.category, + name = secondEntry.name, + value = secondEntry.value, + tags = secondEntry.tags + ) + + val found = store.scan(category = firstEntry.category).fetchAll() + + assertEquals(2, found.size) + + session.close() + } + } + + @Test + fun transactionBasic() { + runBlocking { + val txn = store.openSession(true) + + txn.insert( + firstEntry.category, + firstEntry.name, + value = firstEntry.value, + tags = firstEntry.tags + ) + + assertEquals(1, txn.count(firstEntry.category, firstEntry.tags)) + + val ret = txn.fetch(firstEntry.category, firstEntry.name) ?: throw Error("should not happen") + + assertEquals(ret, firstEntry) + + val found = txn.fetchAll(firstEntry.category) + + assertEquals(found[0], firstEntry) + + txn.commit() + + val session = store.openSession() + + val fetch = session.fetch(firstEntry.category, firstEntry.name) + + assertEquals(fetch, firstEntry) + + session.close() + } + } + + @Test + fun keyStore() { + runBlocking { + val session = store.openSession() + + val key = Key.generate(KeyAlgs.Ed25519) + + val keyName = "testKey" + + session.insertKey(keyName, key, metadata = "metadata", tags = mapOf(Pair("a", JsonPrimitive("b"))).mapToJsonObject().toString()) + + val fetchedKey = session.fetchKey(keyName) + + assertEquals( + fetchedKey, + KeyEntryObject(KeyAlgs.Ed25519.alg, keyName, "metadata", mapOf(Pair("a", JsonPrimitive("b"))).mapToJsonObject().toString()) + ) + + session.updateKey(keyName, "updated metadata", tags = mapOf(Pair("a", JsonPrimitive("c"))).mapToJsonObject().toString()) + + val updatedFetch = session.fetchKey(keyName) + + assertNotEquals(fetchedKey, updatedFetch) + + assertEquals(key.jwkThumbprint(), fetchedKey?.key?.jwkThumbprint()) + + val found = session.fetchAllKeys( + KeyAlgs.Ed25519, + key.jwkThumbprint(), + mapOf(Pair("a", JsonPrimitive("c"))).mapToJsonObject().toString(), + ) + + assertEquals(found[0], updatedFetch) + + session.removeKey(keyName) + + assertNull(session.fetchKey(keyName)) + + session.close() + + key.handle().free() + fetchedKey?.key!!.handle().free() + updatedFetch?.key!!.handle().free() + found.forEach { entry -> entry.key!!.handle().free() } + } + } + + + @Test + fun profile() { + runBlocking { + val session = store.openSession() + session.insert(firstEntry.category, firstEntry.name, value = firstEntry.value, tags = firstEntry.tags) + session.close() + + val profile = store.createProfile()!! + + val session2 = store.session(profile).open() + assertEquals(0, session2.count(firstEntry.category, firstEntry.tags)) + session2.insert(firstEntry.category, firstEntry.name, value = firstEntry.value, tags = firstEntry.tags) + assertEquals(1, session2.count(firstEntry.category, firstEntry.tags)) + session2.close() + + //TODO: Find out why this fails +// if(!store.uri().contains(":memory:")){ +// val key = getRawKey()!! +// val store2 = Store.open(testStoreUri, StoreKeyMethod(KdfMethod.Raw), passkey = key) +// val session3 = store2.openSession() +// assertEquals(0, session3.count(firstEntry.category, firstEntry.tags)) +// session3.close() +// store2.close() +// } + + assertFails { store.createProfile(profile) } + + val session4 = store.session(profile).open() + assertEquals(1, session4.count(firstEntry.category, firstEntry.tags)) + session4.close() + + store.removeProfile(profile) + + val session5 = store.session(profile).open() + assertEquals(0, session5.count(firstEntry.category, firstEntry.tags)) + session5.close() + + val session6 = store.session("unknown profile").open() + assertFails { session6.count(firstEntry.category, firstEntry.tags) } + session6.close() + + val session7 = store.session(profile).open() + assertEquals(0, session7.count(firstEntry.category, firstEntry.tags)) + session7.close() + } + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonTest/kotlin/AskarUtils/initialize.kt b/wrappers/kotlin/src/commonTest/kotlin/AskarUtils/initialize.kt new file mode 100644 index 00000000..b698d0fd --- /dev/null +++ b/wrappers/kotlin/src/commonTest/kotlin/AskarUtils/initialize.kt @@ -0,0 +1,52 @@ +package tech.indicio.holdr.AskarUtils + +import askar.Askar.Companion.mapToJsonObject +import askar.Store.EntryObject +import askar.Store.KdfMethod +import askar.Store.Store +import askar.Store.StoreKeyMethod +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +fun getRawKey(): String? { + return Store.generateRawKey("00000000000000000000000000000My1") +} + +val firstEntry = EntryObject("category-one", "test-entry", tags = +mapOf( + Pair("~plaintag", JsonPrimitive("a")), + Pair("enctag", JsonPrimitive("b")) +).mapToJsonObject().toString() , value = "foo" +) + + +val secondEntry = EntryObject("category-one", "secondEntry", tags = +mapOf( + Pair("~plaintag", JsonPrimitive("a")), + Pair("enctag",JsonPrimitive("b")) +).mapToJsonObject().toString(), value = buildJsonObject { + put("foo", "bar") +}.toString()) + + +// const thirdEntry = { +// category: 'category-one', +// name: 'thirdEntry', +// value: { foo: 'baz' }, +// tags: { '~plaintag': 'a', enctag: 'b' }, +// } + + +const val testStoreUri = "sqlite://local.db" + +suspend fun setupWallet(): Store { + val key = getRawKey() ?: throw Error("Key came back as null") + return Store.provision( + recreate = true, + uri = testStoreUri, + keyMethod = StoreKeyMethod(KdfMethod.Raw), + passkey = key + ) +} + diff --git a/wrappers/kotlin/src/commonTest/kotlin/CryptoBoxTest.kt b/wrappers/kotlin/src/commonTest/kotlin/CryptoBoxTest.kt new file mode 100644 index 00000000..f4395205 --- /dev/null +++ b/wrappers/kotlin/src/commonTest/kotlin/CryptoBoxTest.kt @@ -0,0 +1,24 @@ +package tech.indicio.holdr + +import askar.crypto.CryptoBox +import askar.crypto.Key +import askar.enums.KeyAlgs +import kotlinx.cinterop.toKString +import kotlin.test.Test +import kotlin.test.assertEquals + +class CryptoBoxTest { + + @Test + fun seal() { + val x25519Key = Key.generate(KeyAlgs.X25519) + + val message = "foobar" + val sealed = CryptoBox.seal(x25519Key, message) + + val opened = CryptoBox.sealOpen(x25519Key, sealed) + assertEquals(message, opened.toKString()) + + x25519Key.handle().free() + } +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonTest/kotlin/JoseEcdh.kt b/wrappers/kotlin/src/commonTest/kotlin/JoseEcdh.kt new file mode 100644 index 00000000..f3d03814 --- /dev/null +++ b/wrappers/kotlin/src/commonTest/kotlin/JoseEcdh.kt @@ -0,0 +1,327 @@ +@file:OptIn( + ExperimentalEncodingApi::class +) + +package tech.indicio.holdr + +import askar.ProtectedJson +import askar.crypto.Ecdh1PU +import askar.crypto.EcdhEs +import askar.crypto.Jwk +import askar.crypto.Key +import askar.enums.KeyAlgs +import kotlinx.cinterop.toKString +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.Test +import kotlin.test.assertEquals + +class JoseEcdh { + + fun base64Url(str: String): String { + return Base64.UrlSafe.encode(str.encodeToByteArray()) + } + + @Test + fun ecdhEsDirect() { + val bobKey = Key.generate(KeyAlgs.EcSecp256r1) + //val bobJwk = bobKey.jwkPublic() + val ephemeralKey = Key.generate(KeyAlgs.EcSecp256r1) + val ephemeralJwk = ephemeralKey.jwkPublic() + val message = "Hello there" + val alg = "ECDH-ES" + val apu = "Alice" + val apv = "Bob" + val encAlg = KeyAlgs.AesA256Gcm + val json = ProtectedJson(alg, KeyAlgs.AesA256Gcm, "Alice", "Bob", ephemeralJwk) + + val b64 = Base64.UrlSafe.encode(json.toString().encodeToByteArray()) + + val encryptedMessage = + EcdhEs(encAlg.alg, apu, apv).encryptDirect(encAlg, ephemeralKey, bobKey, message, aad = b64) + val nonce = encryptedMessage.nonce() + val tags = encryptedMessage.tag() + val cipherText = encryptedMessage.cipherText() + + val messageReceived = EcdhEs(encAlg.alg, apu, apv).decryptDirect( + encAlg, + ephemeralKey, + bobKey, + cipherText, + nonce, + b64, + tags + ) + assertEquals(message, messageReceived.toKString()) + ephemeralKey.handle().free() + bobKey.handle().free() + } + + @Test + fun ecdhEsWrapped() { + val bobKey = Key.generate(KeyAlgs.X25519) + val bobJwk = bobKey.jwkPublic() + val ephemeralKey = Key.generate(KeyAlgs.X25519) + val ephemeralJwk = ephemeralKey.jwkPublic() + val message = "Hello there" + val alg = "ECDH-ES+A128KW" + val enc = "A256GCM" + val apu = "Alice" + val apv = "bob" + + val json = ProtectedJson(alg, enc, apu, apv, ephemeralJwk) + + val b64 = Base64.UrlSafe.encode(json.toString().encodeToByteArray()) + + val cek = Key.generate(KeyAlgs.AesA256Gcm) + + val encryptedMessage = cek.aeadEncrypt(message, aad = b64) + val nonce = encryptedMessage.nonce() + val tags = encryptedMessage.tag() + val cipherText = encryptedMessage.cipherText() + + val encryptedKey = EcdhEs(alg, apu, apv).senderWrapKey( + KeyAlgs.AesA128Kw, + ephemeralKey, + Key.fromJwk(bobJwk), + cek, + ).cipherText() + + val cekReceiver = EcdhEs(alg, apu, apv).receiverUnwrapKey( + KeyAlgs.AesA128Kw, + KeyAlgs.AesA256Gcm, + Key.fromJwk(ephemeralJwk), + bobKey, + cipherText = encryptedKey + ) + + val messageReceived = cekReceiver.aeadDecrypt(cipherText, nonce, tags, b64) + + assertEquals(message, messageReceived.toKString()) + ephemeralKey.handle().free() + bobKey.handle().free() + cek.handle().free() + cekReceiver.handle().free() + } + + @OptIn(ExperimentalEncodingApi::class) + @Test + fun ecdh1puDirect() { + val aliceKey = Key.generate(KeyAlgs.EcSecp256r1) + val aliceJwk = aliceKey.jwkSecret() + val bobKey = Key.generate(KeyAlgs.EcSecp256r1) + val bobJwk = bobKey.jwkPublic() + val ephemeralKey = Key.generate(KeyAlgs.EcSecp256r1) + val ephemeralJwk = ephemeralKey.jwkPublic() + val message = "Hello there" + val alg = "ECDH-1PU" + val enc = KeyAlgs.AesA256Gcm + val apu = "Alice" + val apv = "Bob" + val protectedJson = ProtectedJson( + alg, + enc, + base64Url(apu), + base64Url(apv), + ephemeralJwk + ) + val protectedString = protectedJson.toString() + val protectedB64 = Base64.UrlSafe.encode(protectedString.encodeToByteArray()) + + val encryptedMessage = Ecdh1PU( + enc.alg, + apu, + apv + ).encryptDirect( + KeyAlgs.AesA256Gcm, + ephemeralKey, + Key.fromJwk(bobJwk), + senderKey = aliceKey, + message = message, + aad = protectedB64, + ) + + val nonce = encryptedMessage.nonce() + val tag = encryptedMessage.tag() + val ciphertext = encryptedMessage.cipherText() + + val messageReceived = Ecdh1PU( + enc.alg, + apu, + apv, + ).decryptDirect( + KeyAlgs.AesA256Gcm, + ephemeralKey, + bobKey, + Key.fromJwk(aliceJwk), + ciphertext, + nonce, + protectedB64, + tag + ) + + assertEquals(message, messageReceived.toKString()) + aliceKey.handle().free() + bobKey.handle().free() + ephemeralKey.handle().free() + } + + private fun bufferFromHex(str: String): ByteArray { + val list = str.chunked(2) + val arr = ByteArray(list.size) { + list[it].toUInt(16).toByte() + } + return arr + } + + /** + * + * These tests have been implemented as a copy from the python wapper. + * The test vectores have been taken from: + * https://www.ietf.org/archive/id/draft-madden-jose-ecdh-1pu-04.txt + */ + @Test + fun ecdh1puWrapped() { + val ephemeral = Key.fromJwk( + Jwk( + "OKP", + "X25519", + "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc", + "x8EVZH4Fwk673_mUujnliJoSrLz0zYzzCWp5GUX2fc8" + ) + ) + + val alice = Key.fromJwk( + Jwk( + "OKP", + "X25519", + "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", + "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU", + ) + ) + + val bob = Key.fromJwk( + Jwk( + "OKP", + "X25519", + "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", + "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg", + ) + ) + + val alg = "ECDH-1PU+A128KW" + val apu = "Alice" + val apv = "Bob and Charlie" + val base64urlApu = base64Url(apu) + val base64urlApv = base64Url(apv) + + assertEquals("QWxpY2U=", base64urlApu) + assertEquals("Qm9iIGFuZCBDaGFybGll", base64urlApv) + + val protectedJson = ProtectedJson( + alg, + "A256CBC-HS512", + "QWxpY2U", + "Qm9iIGFuZCBDaGFybGll", + Jwk("OKP", "X25519", "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc"), + ) + + val b64 = base64Url(protectedJson.toString()) + + val str = + "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0" + + val arr = bufferFromHex(str) + + val cek = Key.fromSecretBytes(KeyAlgs.AesA256CbcHs512, arr) + + val iv = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + val message = "Three is a magic number." + + val enc = cek.aeadEncrypt(message, iv, b64) + + val tags = enc.tag() + val ciphertext = enc.cipherText() + + assertEquals("Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw=", Base64.UrlSafe.encode(ciphertext)) + assertEquals("HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ=", Base64.UrlSafe.encode(tags)) + + val derived = Ecdh1PU( + apv = apv, + apu = apu, + algId = alg + ).deriveKey( + encAlg = KeyAlgs.AesA128Kw, + recipientKey = bob, + senderKey = alice, + ccTag = tags, + ephemeralKey = ephemeral, + receive = false + ) + + val expectedBuf = bufferFromHex("df4c37a0668306a11e3d6b0074b5d8df") + + derived.secretBytes().forEachIndexed { index, byte -> + assertEquals(expectedBuf[index], byte) + } + + val encryptedKey = derived.wrapKey(cek).cipherText() + + val expectedKey = Base64.UrlSafe.decode("pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN") + + encryptedKey.forEachIndexed { index, byte -> + assertEquals(expectedKey[index], byte) + } + + val encryptedKey2 = Ecdh1PU(apv = apv, apu = apu, algId = alg).senderWrapKey( + wrapAlg = KeyAlgs.AesA128Kw, + ephemeralKey = ephemeral, + senderKey = alice, + recipientKey = bob, + ccTag = tags, + cek = cek + ) + + encryptedKey2.cipherText().forEachIndexed { index, byte -> + assertEquals(byte, encryptedKey[index]) + } + + val derivedReciever = Ecdh1PU(apv = apv, apu = apu, algId = alg).deriveKey( + encAlg = KeyAlgs.AesA128Kw, + ephemeralKey = ephemeral, + senderKey = alice, + recipientKey = bob, + ccTag = tags, + receive = true + ) + + val cekReveiver = derivedReciever.unwrapKey(algorithm = KeyAlgs.AesA256CbcHs512, cipherText = encryptedKey) + + val messageReceived = cekReveiver.aeadDecrypt(cipherText = ciphertext, nonce = iv, aad = b64, tag = tags) + + assertEquals(message, messageReceived.toKString()) + + val cekReceiver2 = Ecdh1PU( + apv = apv, + apu = apu, + algId = alg + ).receiverUnwrapKey( + wrapAlg = KeyAlgs.AesA128Kw, + encAlg = KeyAlgs.AesA256CbcHs512, + ephemeralKey = ephemeral, + senderKey = alice, + recipientKey = bob, + cipherText = encryptedKey, + ccTag = tags + ) + + assertEquals(cekReceiver2.jwkSecret(), cek.jwkSecret()) + + cek.handle().free() + ephemeral.handle().free() + alice.handle().free() + bob.handle().free() + derived.handle().free() + } + +} \ No newline at end of file diff --git a/wrappers/kotlin/src/commonTest/kotlin/KeyTest.kt b/wrappers/kotlin/src/commonTest/kotlin/KeyTest.kt new file mode 100644 index 00000000..bb881afa --- /dev/null +++ b/wrappers/kotlin/src/commonTest/kotlin/KeyTest.kt @@ -0,0 +1,123 @@ +package tech.indicio.holdr + +import askar.crypto.Key +import askar.enums.KeyAlgs +import askar.enums.KeyMethod +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class KeyTest { + + /** + * aes cbc hmac + */ + @Test + fun aesAlgTest() { + runBlocking { + memScoped { + val key = Key.generate(KeyAlgs.AesA128CbcHs256) + assertEquals(key.algorithm(), KeyAlgs.AesA128CbcHs256) + val message = "test message" + val aeadNonce = key.aeadRandomNonce() + val params = key.aeadParams() + assertEquals(16, params.nonceLength) + assertEquals(16, params.tagsLength) + val enc = key.aeadEncrypt(message, nonce = aeadNonce) + val dec = key.aeadDecrypt(enc.cipherText(), nonce = enc.nonce(), tag = enc.tag()) + assertEquals(message, dec.toKString()) + } + } + } + + @Test + fun blsG2KeyGen() { + val seed = "testseed000000000000000000000001" + val key = Key.fromSeed(algorithm = KeyAlgs.Bls12381G2, seed = seed) + + val jwk = key.jwkPublic() + + assertEquals("BLS12381_G2", jwk.crv) + assertEquals("OKP", jwk.kty) + assertEquals( + "lH6hIRPzjlKW6LvPm0sHqyEbGqf8ag7UWpA_GFfefwq_kzDXSHmls9Yoza_be23zEw-pSOmKI_MGR1DahBa7Jbho2BGwDNV_QmyhxMYBwTH12Ltk_GLyPD4AP6pQVgge", + jwk.x + ) + + key.handle().free() + } + + @Test + fun blsG1KeyGen() { + val seed = "testseed000000000000000000000001" + val key = Key.fromSeed(algorithm = KeyAlgs.Bls12381G1, seed = seed) + + val jwk = key.jwkPublic() + + assertEquals("BLS12381_G1", jwk.crv) + assertEquals("OKP", jwk.kty) + assertEquals( + "hsjb9FSBUJXuB1fCluEcUBLeAPgIbnZGfxPKyeN3LVjQaKFWzXfNtMFAY8VL-eu-", + jwk.x + ) + + key.handle().free() + } + + @Test + fun blsG1G2KeyGen() { + val seed = "testseed000000000000000000000001" + val key = Key.fromSeed(algorithm = KeyAlgs.Bls12381G1G2, seed = seed, method = KeyMethod.BlsKeygen) + + val jwk = key.jwkPublic() + + assertEquals("BLS12381_G1G2", jwk.crv) + assertEquals("OKP", jwk.kty) + assertEquals( + "h56eYI8Qkq5hitICb-ik8wRTzcn6Fd4iY8aDNVc9q1xoPS3lh4DB_B4wNtar1HrViZIOsO6BgLV72zCrBE2ym3DEhDYcghnUMO4O8IVVD8yS-C_zu6OA3L-ny-AO4rbkAo-WuApZEjn83LY98UtoKpTufn4PCUFVQZzJNH_gXWHR3oDspJaCbOajBfm5qj6d" + ,jwk.x + ) + + key.handle().free() + } + + @Test + fun ed25519() { + val key = Key.generate(KeyAlgs.Ed25519) + assertEquals(key.algorithm(), KeyAlgs.Ed25519) + val message = "Test Message" + val signature = key.signMessage(message) + assertTrue(key.verifySignature(message, signature)) + + val messageBuffer = message.encodeToByteArray() + val byteSignature = key.signMessage(messageBuffer) + assertTrue(key.verifySignature(messageBuffer, byteSignature)) + + val x25519Key = key.convertKey(KeyAlgs.X25519) + val x25519Key2 = Key.generate(KeyAlgs.X25519) + + val kex = x25519Key.fromKeyExchange(KeyAlgs.Chacha20XC20P, x25519Key2) + assertIs(kex) + + val jwkPub = key.jwkPublic() + val jwkSec = key.jwkSecret() + + assertEquals("OKP", jwkPub.kty) + assertEquals("Ed25519", jwkPub.crv) + + assertEquals("OKP", jwkSec.kty) + assertEquals("Ed25519", jwkSec.crv) + + key.handle().free() + x25519Key.handle().free() + x25519Key2.handle().free() + kex.handle().free() + } + +} + + diff --git a/wrappers/kotlin/src/nativeInterop/cinterop/aries_askar.def b/wrappers/kotlin/src/nativeInterop/cinterop/aries_askar.def new file mode 100644 index 00000000..52b2d3b3 --- /dev/null +++ b/wrappers/kotlin/src/nativeInterop/cinterop/aries_askar.def @@ -0,0 +1,18 @@ +package = aries_askar +headers = aries_askar.h +headerFilter = ** + +staticLibraries.ios = libaries_askar.a +libraryPaths.ios_x64 = ../../target/x86_64-apple-ios/release +libraryPaths.ios_arm64 = ../../target/aarch64-apple-ios/release +libraryPaths.ios_simulator_arm64 = ../../target/aarch64-apple-ios-sim/release + +staticLibraries.osx = libaries_askar.a +libraryPaths.macos_x64 = ../../target/x86_64-apple-darwin/release ../../target/aarch64-apple-darwin/release +libraryPaths.osx = ../../target/x86_64-apple-darwin/release ../../target/aarch64-apple-darwin/release + +staticLibraries.android = libaries_askar.a +libraryPaths.android_x64 = ../../target/i686-linux-android/release +libraryPaths.android_arm64 = ../../target/aarch64-linux-android/release +libraryPaths.android_arm32 = ../../target/armv7-linux-androideabi/release +libraryPaths.android_x86 = ../../target/x86_64-linux-android/release \ No newline at end of file