From 9b66f2044a583c524f89b2602de79188f4a5cd89 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 28 Dec 2021 16:02:55 +0100 Subject: [PATCH 01/39] Update license, and readme --- native_crypto_ios/CHANGELOG.md | 4 +--- native_crypto_ios/LICENSE | 2 +- native_crypto_ios/README.md | 4 ++-- native_crypto_ios/pubspec.yaml | 2 +- native_crypto_platform_interface/CHANGELOG.md | 4 +--- native_crypto_platform_interface/LICENSE | 24 ++++++++++++++++++- native_crypto_platform_interface/README.md | 20 ++++++---------- .../lib/src/platform_interface.dart | 20 ++++++++-------- native_crypto_platform_interface/pubspec.yaml | 2 +- ...native_crypto_platform_interface_test.dart | 1 + 10 files changed, 48 insertions(+), 35 deletions(-) diff --git a/native_crypto_ios/CHANGELOG.md b/native_crypto_ios/CHANGELOG.md index 41cc7d8..403c448 100644 --- a/native_crypto_ios/CHANGELOG.md +++ b/native_crypto_ios/CHANGELOG.md @@ -1,3 +1 @@ -## 0.0.1 - -* TODO: Describe initial release. +Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_ios/LICENSE b/native_crypto_ios/LICENSE index a9f90ec..8c5c1f7 100644 --- a/native_crypto_ios/LICENSE +++ b/native_crypto_ios/LICENSE @@ -2,7 +2,7 @@ NativeCrypto - iOS Implementation MIT License -Copyright (c) 2021 Hugo Pointcheval +Copyright (c) 2019 - 2022 Hugo Pointcheval Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/native_crypto_ios/README.md b/native_crypto_ios/README.md index 302c828..a60dd49 100644 --- a/native_crypto_ios/README.md +++ b/native_crypto_ios/README.md @@ -1,6 +1,6 @@ -# native_crypto_ios +# NativeCrypto - iOS Implementation -A new flutter plugin project. +iOS Implementation of NativeCrypto Plugin. ## Getting Started diff --git a/native_crypto_ios/pubspec.yaml b/native_crypto_ios/pubspec.yaml index 211e4de..c6441c5 100644 --- a/native_crypto_ios/pubspec.yaml +++ b/native_crypto_ios/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto_ios description: iOS implementation of NativeCrypto -version: 0.0.7 +version: 0.1.0 environment: sdk: ">=2.15.0 <3.0.0" diff --git a/native_crypto_platform_interface/CHANGELOG.md b/native_crypto_platform_interface/CHANGELOG.md index 41cc7d8..403c448 100644 --- a/native_crypto_platform_interface/CHANGELOG.md +++ b/native_crypto_platform_interface/CHANGELOG.md @@ -1,3 +1 @@ -## 0.0.1 - -* TODO: Describe initial release. +Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_platform_interface/LICENSE b/native_crypto_platform_interface/LICENSE index ba75c69..68bb0c6 100644 --- a/native_crypto_platform_interface/LICENSE +++ b/native_crypto_platform_interface/LICENSE @@ -1 +1,23 @@ -TODO: Add your license here. +NativeCrypto - Platform Interface + +MIT License + +Copyright (c) 2019 - 2022 Hugo Pointcheval + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/native_crypto_platform_interface/README.md b/native_crypto_platform_interface/README.md index 7190aa2..d1cde17 100644 --- a/native_crypto_platform_interface/README.md +++ b/native_crypto_platform_interface/README.md @@ -1,18 +1,12 @@ -# native_crypto_platform_interface +# NativeCrypto - Platform Interface -A new flutter plugin project. +A common platform interface for the [`native_crypto`][1] plugin. -## Getting Started +This interface allows platform-specific implementations of the `native_crypto` plugin, as well as the plugin itself, to ensure they are supporting the same interface. -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +## Usage -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +To implement a new platform-specific implementation of `native_crypto`, extend [`NativeCryptoPlatform`][2] with an implementation that performs the platform-specific behavior, and when you register your plugin, set the default `NativeCryptoPlatform` by calling `NativeCryptoPlatform.instance = MyNativeCryptoPlatform()`. -The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. -To add platforms, run `flutter create -t plugin --platforms .` under the same -directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +[1]: ../native_crypto +[2]: lib/native_crypto_platform_interface.dart \ No newline at end of file diff --git a/native_crypto_platform_interface/lib/src/platform_interface.dart b/native_crypto_platform_interface/lib/src/platform_interface.dart index d0585c9..89267a0 100644 --- a/native_crypto_platform_interface/lib/src/platform_interface.dart +++ b/native_crypto_platform_interface/lib/src/platform_interface.dart @@ -3,7 +3,7 @@ // ----- // File: platform_interface.dart // Created Date: 25/12/2021 16:52:56 -// Last Modified: 25/12/2021 16:53:36 +// Last Modified: 27/12/2021 21:25:39 // ----- // Copyright (c) 2021 @@ -20,18 +20,18 @@ import 'package:meta/meta.dart'; /// Sample usage: /// /// ```dart -/// abstract class UrlLauncherPlatform extends PlatformInterface { -/// UrlLauncherPlatform() : super(token: _token); +/// abstract class NativeCryptoPlatform extends PlatformInterface { +/// NativeCryptoPlatform() : super(token: _token); /// -/// static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); +/// static NativeCryptoPlatform _instance = MethodChannelNativeCrypto(); /// /// static const Object _token = Object(); /// -/// static UrlLauncherPlatform get instance => _instance; +/// static NativeCryptoPlatform get instance => _instance; /// /// /// Platform-specific plugins should set this with their own platform-specific -/// /// class that extends [UrlLauncherPlatform] when they register themselves. -/// static set instance(UrlLauncherPlatform instance) { +/// /// class that extends [NativeCryptoPlatform] when they register themselves. +/// static set instance(NativeCryptoPlatform instance) { /// PlatformInterface.verifyToken(instance, _token); /// _instance = instance; /// } @@ -86,12 +86,12 @@ abstract class PlatformInterface { /// /// This class is intended for use in tests only. /// -/// Sample usage (assuming UrlLauncherPlatform extends [PlatformInterface]: +/// Sample usage (assuming NativeCryptoPlatform extends [PlatformInterface]: /// /// ```dart -/// class UrlLauncherPlatformMock extends Mock +/// class NativeCryptoPlatformMock extends Mock /// with MockPlatformInterfaceMixin -/// implements UrlLauncherPlatform {} +/// implements NativeCryptoPlatform {} /// ``` @visibleForTesting abstract class MockPlatformInterfaceMixin implements PlatformInterface {} \ No newline at end of file diff --git a/native_crypto_platform_interface/pubspec.yaml b/native_crypto_platform_interface/pubspec.yaml index 017ea35..3e90763 100644 --- a/native_crypto_platform_interface/pubspec.yaml +++ b/native_crypto_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto_platform_interface description: A common interface for NativeCrypto plugin. -version: 0.0.7 +version: 0.1.0 environment: sdk: ">=2.15.0 <3.0.0" diff --git a/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart b/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart index e69de29..0ffdd02 100644 --- a/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart +++ b/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart @@ -0,0 +1 @@ +// TODO \ No newline at end of file From c01e0a12ba40a618bd01185463aed5250f56f0a7 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 28 Dec 2021 16:03:07 +0100 Subject: [PATCH 02/39] Add android implementation --- native_crypto_android/.gitignore | 29 +++ native_crypto_android/.metadata | 10 + native_crypto_android/CHANGELOG.md | 1 + native_crypto_android/LICENSE | 23 +++ native_crypto_android/README.md | 15 ++ native_crypto_android/android/.gitignore | 8 + native_crypto_android/android/build.gradle | 50 +++++ .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + native_crypto_android/android/gradlew | 185 ++++++++++++++++++ native_crypto_android/android/gradlew.bat | 89 +++++++++ native_crypto_android/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../native_crypto_android/Cipher.kt | 6 + .../pointcheval/native_crypto_android/Hash.kt | 25 +++ .../native_crypto_android/HashAlgorithm.kt | 15 ++ .../pointcheval/native_crypto_android/Key.kt | 24 +++ .../NativeCryptoAndroidPlugin.kt | 77 ++++++++ .../ciphers/AESCipher.kt | 31 +++ native_crypto_android/example/.gitignore | 46 +++++ native_crypto_android/example/.metadata | 10 + native_crypto_android/example/README.md | 16 ++ .../example/analysis_options.yaml | 29 +++ .../example/android/.gitignore | 13 ++ .../example/android/app/build.gradle | 68 +++++++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 34 ++++ .../MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 ++ .../main/res/drawable/launch_background.xml | 12 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 ++ .../app/src/main/res/values/styles.xml | 18 ++ .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle | 31 +++ .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../example/android/settings.gradle | 11 ++ native_crypto_android/example/lib/main.dart | 75 +++++++ native_crypto_android/example/pubspec.lock | 182 +++++++++++++++++ native_crypto_android/example/pubspec.yaml | 87 ++++++++ .../example/test/widget_test.dart | 27 +++ native_crypto_android/pubspec.yaml | 23 +++ 47 files changed, 1338 insertions(+) create mode 100644 native_crypto_android/.gitignore create mode 100644 native_crypto_android/.metadata create mode 100644 native_crypto_android/CHANGELOG.md create mode 100644 native_crypto_android/LICENSE create mode 100644 native_crypto_android/README.md create mode 100644 native_crypto_android/android/.gitignore create mode 100644 native_crypto_android/android/build.gradle create mode 100644 native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 native_crypto_android/android/gradlew create mode 100644 native_crypto_android/android/gradlew.bat create mode 100644 native_crypto_android/android/settings.gradle create mode 100644 native_crypto_android/android/src/main/AndroidManifest.xml create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt create mode 100644 native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt create mode 100644 native_crypto_android/example/.gitignore create mode 100644 native_crypto_android/example/.metadata create mode 100644 native_crypto_android/example/README.md create mode 100644 native_crypto_android/example/analysis_options.yaml create mode 100644 native_crypto_android/example/android/.gitignore create mode 100644 native_crypto_android/example/android/app/build.gradle create mode 100644 native_crypto_android/example/android/app/src/debug/AndroidManifest.xml create mode 100644 native_crypto_android/example/android/app/src/main/AndroidManifest.xml create mode 100644 native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt create mode 100644 native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 native_crypto_android/example/android/app/src/main/res/values-night/styles.xml create mode 100644 native_crypto_android/example/android/app/src/main/res/values/styles.xml create mode 100644 native_crypto_android/example/android/app/src/profile/AndroidManifest.xml create mode 100644 native_crypto_android/example/android/build.gradle create mode 100644 native_crypto_android/example/android/gradle.properties create mode 100644 native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 native_crypto_android/example/android/settings.gradle create mode 100644 native_crypto_android/example/lib/main.dart create mode 100644 native_crypto_android/example/pubspec.lock create mode 100644 native_crypto_android/example/pubspec.yaml create mode 100644 native_crypto_android/example/test/widget_test.dart create mode 100644 native_crypto_android/pubspec.yaml diff --git a/native_crypto_android/.gitignore b/native_crypto_android/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/native_crypto_android/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/native_crypto_android/.metadata b/native_crypto_android/.metadata new file mode 100644 index 0000000..8c15ad7 --- /dev/null +++ b/native_crypto_android/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/native_crypto_android/CHANGELOG.md b/native_crypto_android/CHANGELOG.md new file mode 100644 index 0000000..403c448 --- /dev/null +++ b/native_crypto_android/CHANGELOG.md @@ -0,0 +1 @@ +Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_android/LICENSE b/native_crypto_android/LICENSE new file mode 100644 index 0000000..dd5d33b --- /dev/null +++ b/native_crypto_android/LICENSE @@ -0,0 +1,23 @@ +NativeCrypto - Android Implementation + +MIT License + +Copyright (c) 2019 - 2022 Hugo Pointcheval + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/native_crypto_android/README.md b/native_crypto_android/README.md new file mode 100644 index 0000000..48b93a5 --- /dev/null +++ b/native_crypto_android/README.md @@ -0,0 +1,15 @@ +# native_crypto_android + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/native_crypto_android/android/.gitignore b/native_crypto_android/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/native_crypto_android/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/native_crypto_android/android/build.gradle b/native_crypto_android/android/build.gradle new file mode 100644 index 0000000..ec447a5 --- /dev/null +++ b/native_crypto_android/android/build.gradle @@ -0,0 +1,50 @@ +group 'fr.pointcheval.native_crypto_android' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 30 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 26 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar b/native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties b/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..da9702f --- /dev/null +++ b/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/native_crypto_android/android/gradlew b/native_crypto_android/android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/native_crypto_android/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/native_crypto_android/android/gradlew.bat b/native_crypto_android/android/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/native_crypto_android/android/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/native_crypto_android/android/settings.gradle b/native_crypto_android/android/settings.gradle new file mode 100644 index 0000000..3533b9f --- /dev/null +++ b/native_crypto_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'native_crypto_android' diff --git a/native_crypto_android/android/src/main/AndroidManifest.xml b/native_crypto_android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a750e9f --- /dev/null +++ b/native_crypto_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt new file mode 100644 index 0000000..78b1a79 --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt @@ -0,0 +1,6 @@ +package fr.pointcheval.native_crypto_android + +abstract class Cipher { + abstract fun encrypt(data: ByteArray, key: ByteArray) : ByteArray?; + abstract fun decrypt(data: ByteArray, key: ByteArray) : ByteArray?; +} \ No newline at end of file diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt new file mode 100644 index 0000000..dd9c95e --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt @@ -0,0 +1,25 @@ +package fr.pointcheval.native_crypto_android + +import java.security.MessageDigest + +object Hash { + fun digest(data: ByteArray?, algorithm: HashAlgorithm): ByteArray { + val func : String = when (algorithm) { + HashAlgorithm.SHA256 -> "SHA-256" + HashAlgorithm.SHA384 -> "SHA-384" + HashAlgorithm.SHA512 -> "SHA-512" + } + val md = MessageDigest.getInstance(func) + return md.digest(data) + } + + fun digest(data: ByteArray?, algorithm: String): ByteArray { + val func : HashAlgorithm = when (algorithm) { + "sha256" -> HashAlgorithm.SHA256 + "sha384" -> HashAlgorithm.SHA384 + "sha512" -> HashAlgorithm.SHA512 + else -> HashAlgorithm.SHA256 + } + return digest(data, func) + } +} \ No newline at end of file diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt new file mode 100644 index 0000000..f820bae --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt @@ -0,0 +1,15 @@ +package fr.pointcheval.native_crypto_android + +enum class HashAlgorithm(val length : Int) { + SHA256(256), + SHA384(384), + SHA512(512); + + fun hmac(): String { + return when (this) { + HashAlgorithm.SHA256 -> "HmacSHA256" + HashAlgorithm.SHA384 -> "HmacSHA384" + HashAlgorithm.SHA512 -> "HmacSHA512" + } + } +} \ No newline at end of file diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt new file mode 100644 index 0000000..d099a89 --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt @@ -0,0 +1,24 @@ +package fr.pointcheval.native_crypto_android + +import java.security.SecureRandom +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + +object Key { + fun fromSecureRandom(bitsCount: Int) : ByteArray { + val bytes = ByteArray(bitsCount / 8) + SecureRandom.getInstanceStrong().nextBytes(bytes) + return bytes + } + + fun fromPBKDF2(password: String, salt: String, keyBytesCount: Int, iterations: Int, algorithm: String): ByteArray { + val availableHashAlgorithm: Map = mapOf( + "sha256" to "PBKDF2WithHmacSHA256", + "sha384" to "PBKDF2withHmacSHA384", + "sha512" to "PBKDF2withHmacSHA512" + ) + val spec = PBEKeySpec(password.toCharArray(), salt.toByteArray(), iterations, keyBytesCount * 8) + val skf: SecretKeyFactory = SecretKeyFactory.getInstance(availableHashAlgorithm[algorithm]) + return skf.generateSecret(spec).encoded + } +} \ No newline at end of file diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt new file mode 100644 index 0000000..04270a8 --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt @@ -0,0 +1,77 @@ +package fr.pointcheval.native_crypto_android + +import androidx.annotation.NonNull +import fr.pointcheval.native_crypto_android.ciphers.AESCipher + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** NativeCryptoAndroidPlugin */ +class NativeCryptoAndroidPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "plugins.hugop.cl/native_crypto") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "digest" -> { + val data : ByteArray? = call.argument("data") + val algorithm : String? = call.argument("algorithm") + // TODO(hpcl): check if algorithm is null + // TODO(hpcl): check if digest is null + result.success(Hash.digest(data, algorithm!!)) + } + "generateSecretKey" -> { + val bitsCount : Int? = call.argument("bitsCount") + // TODO(hpcl): check null + result.success(Key.fromSecureRandom(bitsCount!!)) + } + "generateKeyPair" -> { + result.notImplemented() + } + "pbkdf2" -> { + val password : String? = call.argument("password") + val salt : String? = call.argument("salt") + val keyBytesCount : Int? = call.argument("keyBytesCount") + val iterations : Int? = call.argument("iterations") + val algorithm : String? = call.argument("algorithm") + // TODO(hpcl): check null + result.success(Key.fromPBKDF2(password!!, salt!!, keyBytesCount!!, iterations!!, algorithm!!)) + } + "encrypt" -> { + val data : ByteArray? = call.argument("data") + val key : ByteArray? = call.argument("key") + val algorithm : String? = call.argument("algorithm") + // TODO(hpcl): check null + // TODO(hcpl): check algorithm + result.success(AESCipher().encrypt(data!!, key!!)) + } + "decrypt" -> { + val data : ByteArray? = call.argument("data") + val key : ByteArray? = call.argument("key") + val algorithm : String? = call.argument("algorithm") + // TODO(hpcl): check null + // TODO(hcpl): check algorithm + result.success(AESCipher().decrypt(data!!, key!!)) + } + "generateSharedSecretKey" -> { + result.notImplemented() + } + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt new file mode 100644 index 0000000..941cfde --- /dev/null +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt @@ -0,0 +1,31 @@ +package fr.pointcheval.native_crypto_android.ciphers + +import fr.pointcheval.native_crypto_android.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +class AESCipher : Cipher() { + override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { + val sk: SecretKey = SecretKeySpec(key, "AES") + val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) + // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] + val bytes = cipher.doFinal(data) + val iv = cipher.iv.copyOf() // 12 bytes nonce + // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] + return iv.plus(bytes) + } + + override fun decrypt(data: ByteArray, key: ByteArray): ByteArray? { + val sk: SecretKey = SecretKeySpec(key, "AES") + // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] + val iv = data.sliceArray(IntRange(0,11)) // 12 bytes nonce + // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] + val payload = data.sliceArray(IntRange(12, data.size - 1)) + val spec = GCMParameterSpec(16 * 8, iv) + val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec) + return cipher.doFinal(payload) + } +} \ No newline at end of file diff --git a/native_crypto_android/example/.gitignore b/native_crypto_android/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/native_crypto_android/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/native_crypto_android/example/.metadata b/native_crypto_android/example/.metadata new file mode 100644 index 0000000..fd70cab --- /dev/null +++ b/native_crypto_android/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: app diff --git a/native_crypto_android/example/README.md b/native_crypto_android/example/README.md new file mode 100644 index 0000000..8490cfc --- /dev/null +++ b/native_crypto_android/example/README.md @@ -0,0 +1,16 @@ +# native_crypto_android_example + +Demonstrates how to use the native_crypto_android plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/native_crypto_android/example/analysis_options.yaml b/native_crypto_android/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/native_crypto_android/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/native_crypto_android/example/android/.gitignore b/native_crypto_android/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/native_crypto_android/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/native_crypto_android/example/android/app/build.gradle b/native_crypto_android/example/android/app/build.gradle new file mode 100644 index 0000000..3ec37e2 --- /dev/null +++ b/native_crypto_android/example/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "fr.pointcheval.native_crypto_android_example" + minSdkVersion 26 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml b/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..758ae3c --- /dev/null +++ b/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_crypto_android/example/android/app/src/main/AndroidManifest.xml b/native_crypto_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d48fcc --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt b/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt new file mode 100644 index 0000000..0d084ac --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt @@ -0,0 +1,6 @@ +package fr.pointcheval.native_crypto_android_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml b/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml b/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml b/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_crypto_android/example/android/app/src/main/res/values/styles.xml b/native_crypto_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/native_crypto_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml b/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..758ae3c --- /dev/null +++ b/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/native_crypto_android/example/android/build.gradle b/native_crypto_android/example/android/build.gradle new file mode 100644 index 0000000..24047dc --- /dev/null +++ b/native_crypto_android/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/native_crypto_android/example/android/gradle.properties b/native_crypto_android/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/native_crypto_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties b/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/native_crypto_android/example/android/settings.gradle b/native_crypto_android/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/native_crypto_android/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/native_crypto_android/example/lib/main.dart b/native_crypto_android/example/lib/main.dart new file mode 100644 index 0000000..0d1e7a6 --- /dev/null +++ b/native_crypto_android/example/lib/main.dart @@ -0,0 +1,75 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + Uint8List? sk = await _nativeCryptoPlatform.generateSecretKey(256); + print(sk ?? 'null'); + + Uint8List? ciphertext = await _nativeCryptoPlatform.encrypt( + Uint8List.fromList("abc".codeUnits), sk!, "aes"); + print(ciphertext ?? 'null'); + + Uint8List? plaintext = + await _nativeCryptoPlatform.decrypt(ciphertext!, sk, "aes"); + print(plaintext ?? 'null'); + + try { + platformVersion = 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/native_crypto_android/example/pubspec.lock b/native_crypto_android/example/pubspec.lock new file mode 100644 index 0000000..ee5a879 --- /dev/null +++ b/native_crypto_android/example/pubspec.lock @@ -0,0 +1,182 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + native_crypto_android: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.0" + native_crypto_platform_interface: + dependency: "direct main" + description: + path: "../../native_crypto_platform_interface" + relative: true + source: path + version: "0.0.7" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" +sdks: + dart: ">=2.15.1 <3.0.0" + flutter: ">=2.5.0" diff --git a/native_crypto_android/example/pubspec.yaml b/native_crypto_android/example/pubspec.yaml new file mode 100644 index 0000000..3b1d332 --- /dev/null +++ b/native_crypto_android/example/pubspec.yaml @@ -0,0 +1,87 @@ +name: native_crypto_android_example +description: Demonstrates how to use the native_crypto_android plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.15.1 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + native_crypto_android: + # When depending on this package from a real application you should use: + # native_crypto_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + native_crypto_platform_interface: + path: ../../native_crypto_platform_interface + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/native_crypto_android/example/test/widget_test.dart b/native_crypto_android/example/test/widget_test.dart new file mode 100644 index 0000000..e4edd16 --- /dev/null +++ b/native_crypto_android/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:native_crypto_android_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/native_crypto_android/pubspec.yaml b/native_crypto_android/pubspec.yaml new file mode 100644 index 0000000..27bdf52 --- /dev/null +++ b/native_crypto_android/pubspec.yaml @@ -0,0 +1,23 @@ +name: native_crypto_android +description: Android implementation of NativeCrypto +version: 0.1.0 + +environment: + sdk: ">=2.15.1 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + implements: native_crypto + platforms: + android: + package: fr.pointcheval.native_crypto_android + pluginClass: NativeCryptoAndroidPlugin \ No newline at end of file From 3381ff67b91e48f399228a677f6bb9369e11c632 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 28 Dec 2021 16:03:50 +0100 Subject: [PATCH 03/39] Add new public api and example --- native_crypto/CHANGELOG.md | 34 +++- native_crypto/LICENSE | 24 ++- native_crypto/README.md | 4 +- .../example/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 1 + native_crypto/example/lib/home.dart | 68 ++++++++ native_crypto/example/lib/main.dart | 65 ++----- .../example/lib/pages/benchmark_page.dart | 137 +++++++++++++++ .../example/lib/pages/cipher_page.dart | 158 ++++++++++++++++++ native_crypto/example/lib/pages/kdf_page.dart | 130 ++++++++++++++ native_crypto/example/lib/session.dart | 24 +++ native_crypto/example/lib/utils.dart | 52 ++++++ native_crypto/example/lib/widgets/button.dart | 32 ++++ native_crypto/example/lib/widgets/output.dart | 61 +++++++ native_crypto/example/pubspec.yaml | 3 +- native_crypto/lib/native_crypto.dart | 40 +++-- native_crypto/lib/src/builder.dart | 12 ++ .../lib/src/builders/aes_builder.dart | 46 +++++ native_crypto/lib/src/byte_array.dart | 72 ++++++++ native_crypto/lib/src/cipher.dart | 37 ++++ native_crypto/lib/src/cipher_text.dart | 51 ++++++ native_crypto/lib/src/ciphers/aes.dart | 112 +++++++++++++ native_crypto/lib/src/exceptions.dart | 41 +++++ native_crypto/lib/src/hasher.dart | 27 +++ native_crypto/lib/src/hashers/sha256.dart | 15 ++ native_crypto/lib/src/hashers/sha384.dart | 15 ++ native_crypto/lib/src/hashers/sha512.dart | 15 ++ native_crypto/lib/src/kdf/pbkdf2.dart | 51 ++++++ native_crypto/lib/src/key.dart | 20 +++ native_crypto/lib/src/keyderivation.dart | 21 +++ native_crypto/lib/src/keys/secret_key.dart | 36 ++++ native_crypto/lib/src/platform.dart | 12 ++ native_crypto/lib/src/utils.dart | 83 +++++++++ native_crypto/pubspec.yaml | 12 +- native_crypto/test/native_crypto_test.dart | 24 +-- 35 files changed, 1439 insertions(+), 98 deletions(-) create mode 100644 native_crypto/example/lib/home.dart create mode 100644 native_crypto/example/lib/pages/benchmark_page.dart create mode 100644 native_crypto/example/lib/pages/cipher_page.dart create mode 100644 native_crypto/example/lib/pages/kdf_page.dart create mode 100644 native_crypto/example/lib/session.dart create mode 100644 native_crypto/example/lib/utils.dart create mode 100644 native_crypto/example/lib/widgets/button.dart create mode 100644 native_crypto/example/lib/widgets/output.dart create mode 100644 native_crypto/lib/src/builder.dart create mode 100644 native_crypto/lib/src/builders/aes_builder.dart create mode 100644 native_crypto/lib/src/byte_array.dart create mode 100644 native_crypto/lib/src/cipher.dart create mode 100644 native_crypto/lib/src/cipher_text.dart create mode 100644 native_crypto/lib/src/ciphers/aes.dart create mode 100644 native_crypto/lib/src/exceptions.dart create mode 100644 native_crypto/lib/src/hasher.dart create mode 100644 native_crypto/lib/src/hashers/sha256.dart create mode 100644 native_crypto/lib/src/hashers/sha384.dart create mode 100644 native_crypto/lib/src/hashers/sha512.dart create mode 100644 native_crypto/lib/src/kdf/pbkdf2.dart create mode 100644 native_crypto/lib/src/key.dart create mode 100644 native_crypto/lib/src/keyderivation.dart create mode 100644 native_crypto/lib/src/keys/secret_key.dart create mode 100644 native_crypto/lib/src/platform.dart create mode 100644 native_crypto/lib/src/utils.dart diff --git a/native_crypto/CHANGELOG.md b/native_crypto/CHANGELOG.md index 41cc7d8..d7c6738 100644 --- a/native_crypto/CHANGELOG.md +++ b/native_crypto/CHANGELOG.md @@ -1,3 +1,35 @@ +## 0.1.0 + +> Breaking changes ! + +* Follow **Federated Plugin** Flutter standard. + +## 0.0.6 + +* Add KeyPair generation. +* Rework exposed API. + +## 0.0.5 + +* New API. +* Add digest support. +* Clean platform specific code base. + +## 0.0.4 + +* Improve AES. + +## 0.0.3 + +* Add PBKDF2 support. +* Add exceptions. +* Improve documentation. + +## 0.0.2 + +* Add different key size support. +* Improve performances. + ## 0.0.1 -* TODO: Describe initial release. +* First AES cross-platform encryption & decryption implementation. diff --git a/native_crypto/LICENSE b/native_crypto/LICENSE index ba75c69..082d930 100644 --- a/native_crypto/LICENSE +++ b/native_crypto/LICENSE @@ -1 +1,23 @@ -TODO: Add your license here. +NativeCrypto + +MIT License + +Copyright (c) 2019 - 2022 Hugo Pointcheval + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/native_crypto/README.md b/native_crypto/README.md index 1bf39c0..c16def3 100644 --- a/native_crypto/README.md +++ b/native_crypto/README.md @@ -1,6 +1,6 @@ -# native_crypto +# NativeCrypto -A new flutter plugin project. +Fast and powerful cryptographic functions thanks to **javax.crypto** and **CryptoKit**. ## Getting Started diff --git a/native_crypto/example/android/app/build.gradle b/native_crypto/example/android/app/build.gradle index 803881a..15632c2 100644 --- a/native_crypto/example/android/app/build.gradle +++ b/native_crypto/example/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "fr.pointcheval.native_crypto_example" - minSdkVersion flutter.minSdkVersion + minSdkVersion 26 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/native_crypto/example/android/app/src/main/AndroidManifest.xml b/native_crypto/example/android/app/src/main/AndroidManifest.xml index c55f004..aa8a66a 100644 --- a/native_crypto/example/android/app/src/main/AndroidManifest.xml +++ b/native_crypto/example/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ _HomeState(); +} + +class _HomeState extends State { + int _currentIndex = 0; + final List _children = [ + KdfPage(), + CipherPage(), + BenchmarkPage() + ]; + + void onTabTapped(int index) { + setState(() { + _currentIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text('Native Crypto'), + ), + body: _children[_currentIndex], + bottomNavigationBar: BottomNavigationBar( + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.black, + showUnselectedLabels: true, + onTap: onTabTapped, // new + currentIndex: _currentIndex, // new + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.vpn_key), + label: 'Key', + ), + BottomNavigationBarItem( + icon: Icon(Icons.lock), + label: 'Encryption', + ), + BottomNavigationBarItem( + icon: Icon(Icons.timer), + label: 'Benchmark', + ), + ], + ), + ); + } +} diff --git a/native_crypto/example/lib/main.dart b/native_crypto/example/lib/main.dart index 322f70f..65b60e0 100644 --- a/native_crypto/example/lib/main.dart +++ b/native_crypto/example/lib/main.dart @@ -1,62 +1,25 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: main.dart +// Created Date: 27/12/2021 21:15:12 +// Last Modified: 28/12/2021 13:51:36 +// ----- +// Copyright (c) 2021 -import 'package:flutter/services.dart'; -import 'package:native_crypto/native_crypto.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:native_crypto_example/home.dart'; void main() { - runApp(const MyApp()); + runApp(const ProviderScope(child: MyApp())); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - String _platformVersion = 'Unknown'; - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - try { - platformVersion = - await NativeCrypto.platformVersion ?? 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); - } - @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Running on: $_platformVersion\n'), - ), - ), - ); + return const MaterialApp(home: Home()); } } diff --git a/native_crypto/example/lib/pages/benchmark_page.dart b/native_crypto/example/lib/pages/benchmark_page.dart new file mode 100644 index 0000000..bc708d9 --- /dev/null +++ b/native_crypto/example/lib/pages/benchmark_page.dart @@ -0,0 +1,137 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: benchmark_page.dart +// Created Date: 28/12/2021 15:12:39 +// Last Modified: 28/12/2021 15:21:05 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_example/widgets/button.dart'; + +import '../session.dart'; +import '../widgets/output.dart'; + +class BenchmarkPage extends ConsumerWidget { + BenchmarkPage({Key? key}) : super(key: key); + + final Output keyContent = Output(); + final Output benchmarkStatus = Output(large: true); + + Future _benchmark(WidgetRef ref, Cipher cipher) async { + Session state = ref.read(sessionProvider.state).state; + + if (state.secretKey.bytes.isEmpty) { + benchmarkStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return; + } + + benchmarkStatus.print("Benchmark 2/4/8/16/32/64/128/256MB\n"); + List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; + String csv = + "size;encryption time;encode time;decryption time;crypto time\n"; + + var beforeBench = DateTime.now(); + for (int size in testedSizes) { + var bigFile = Uint8List(size * 1000000); + csv += "${size * 1000000};"; + var cryptoTime = 0; + + // Encryption + var before = DateTime.now(); + var encryptedBigFile = await cipher.encrypt(bigFile); + var after = DateTime.now(); + + var benchmark = + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); + + csv += "$benchmark;"; + cryptoTime += benchmark; + + // Decryption + before = DateTime.now(); + await cipher.decrypt(encryptedBigFile); + after = DateTime.now(); + + benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Decryption took $benchmark ms\n'); + + csv += "$benchmark;"; + cryptoTime += benchmark; + csv += "$cryptoTime\n"; + } + var afterBench = DateTime.now(); + var benchmark = + afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch; + var sum = testedSizes.reduce((a, b) => a + b); + benchmarkStatus.append( + 'Benchmark finished.\nGenerated, encrypted and decrypted $sum MB in $benchmark ms'); + debugPrint("[Benchmark cvs]\n$csv"); + } + + void _clear() { + benchmarkStatus.clear(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + Session state = ref.read(sessionProvider.state).state; + if (state.secretKey.bytes.isEmpty) { + keyContent + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + ], + ), + ), + ); + } + keyContent.print(state.secretKey.bytes.toString()); + + AES cipher = AES(state.secretKey, AESMode.gcm); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + () => _benchmark(ref, cipher), + "Launch benchmark", + ), + Button( + _clear, + "Clear", + ), + ], + ), + benchmarkStatus, + ], + ), + ), + ); + } +} diff --git a/native_crypto/example/lib/pages/cipher_page.dart b/native_crypto/example/lib/pages/cipher_page.dart new file mode 100644 index 0000000..8a6bc7c --- /dev/null +++ b/native_crypto/example/lib/pages/cipher_page.dart @@ -0,0 +1,158 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_page.dart +// Created Date: 28/12/2021 13:33:15 +// Last Modified: 28/12/2021 15:20:43 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_example/widgets/button.dart'; + +import '../session.dart'; +import '../utils.dart'; +import '../widgets/output.dart'; + +// ignore: must_be_immutable +class CipherPage extends ConsumerWidget { + CipherPage({Key? key}) : super(key: key); + + final Output keyContent = Output(); + final Output encryptionStatus = Output(); + final Output decryptionStatus = Output(); + + final TextEditingController _plainTextController = TextEditingController(); + CipherText? cipherText; + + Future _encrypt(WidgetRef ref, Cipher cipher) async { + Session state = ref.read(sessionProvider.state).state; + final plainText = _plainTextController.text.trim(); + + if (state.secretKey.bytes.isEmpty) { + encryptionStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + } else if (plainText.isEmpty) { + encryptionStatus.print('Entry is empty'); + } else { + var stringToBytes = plainText.toBytes(); + cipherText = await cipher.encrypt(stringToBytes); + encryptionStatus.print('String successfully encrypted.\n'); + encryptionStatus.append("Nonce: " + + cipherText!.iv.toString() + + "\nData: " + + cipherText!.data.toString() + + "\nTag: " + + cipherText!.tag.toString()); + } + } + + Future _alter() async { + if (cipherText == null) { + decryptionStatus.print('Encrypt before altering CipherText!'); + } else { + // Add 1 to the first byte + Uint8List _altered = cipherText!.data; + _altered[0] += 1; + // Recreate cipher text with altered data + cipherText = CipherText(cipherText!.iv, _altered, cipherText!.tag); + encryptionStatus.print('String successfully encrypted.\n'); + encryptionStatus.append("Nonce: " + + cipherText!.iv.toString() + + "\nData: " + + cipherText!.data.toString() + + "\nTag: " + + cipherText!.tag.toString()); + decryptionStatus.print('CipherText altered!\nDecryption will fail.'); + } + } + + void _decrypt(WidgetRef ref, Cipher cipher) async { + Session state = ref.read(sessionProvider.state).state; + + if (state.secretKey.bytes.isEmpty) { + decryptionStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + } else if (cipherText == null) { + decryptionStatus.print('Encrypt before decrypting!'); + } else { + try { + Uint8List plainText = await cipher.decrypt(cipherText!); + var bytesToString = plainText.toStr(); + decryptionStatus + .print('String successfully decrypted:\n\n$bytesToString'); + } on DecryptionException catch (e) { + decryptionStatus.print(e.message); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + Session state = ref.read(sessionProvider.state).state; + if (state.secretKey.bytes.isEmpty) { + keyContent + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + ], + ), + ), + ); + } + keyContent.print(state.secretKey.bytes.toString()); + + AES cipher = AES(state.secretKey, AESMode.gcm); + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + TextField( + controller: _plainTextController, + decoration: const InputDecoration( + hintText: 'Plain text', + ), + ), + Button( + () => _encrypt(ref, cipher), + "Encrypt", + ), + encryptionStatus, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + _alter, + "Alter cipher", + ), + Button( + () => _decrypt(ref, cipher), + "Decrypt", + ), + ], + ), + decryptionStatus, + ], + ), + ), + ); + } +} diff --git a/native_crypto/example/lib/pages/kdf_page.dart b/native_crypto/example/lib/pages/kdf_page.dart new file mode 100644 index 0000000..b60a454 --- /dev/null +++ b/native_crypto/example/lib/pages/kdf_page.dart @@ -0,0 +1,130 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: kdf_page.dart +// Created Date: 28/12/2021 13:40:34 +// Last Modified: 28/12/2021 15:14:12 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_example/widgets/button.dart'; + +import '../session.dart'; +import '../utils.dart'; +import '../widgets/output.dart'; + +class KdfPage extends ConsumerWidget { + KdfPage({Key? key}) : super(key: key); + + final Output keyContent = Output(); + final Output keyStatus = Output(); + final Output pbkdf2Status = Output(); + final Output hashStatus = Output(large: true); + + final TextEditingController _pwdTextController = TextEditingController(); + final TextEditingController _messageTextController = TextEditingController(); + + Future _generate(WidgetRef ref) async { + Session state = ref.read(sessionProvider.state).state; + try { + SecretKey sk = await SecretKey.fromSecureRandom(256); + state.setKey(sk); + keyStatus.print( + "SecretKey successfully generated.\nLength: ${state.secretKey.bytes.length} bytes"); + keyContent.print(state.secretKey.bytes.toString()); + debugPrint("As hex :\n${sk.base16}"); + } catch (e) { + keyStatus.print(e.toString()); + } + } + + Future _pbkdf2(WidgetRef ref) async { + Session state = ref.read(sessionProvider.state).state; + final password = _pwdTextController.text.trim(); + + if (password.isEmpty) { + pbkdf2Status.print('Password is empty'); + } else { + PBKDF2 _pbkdf2 = PBKDF2(32, 1000, algorithm: HashAlgorithm.sha512); + SecretKey sk = await _pbkdf2.derive(password: password, salt: 'salt'); + state.setKey(sk); + pbkdf2Status.print('Key successfully derived.'); + keyContent.print(state.secretKey.bytes.toString()); + debugPrint("As hex :\n${sk.base16}"); + } + } + + Future _hash(Hasher hasher) async { + final message = _messageTextController.text.trim(); + if (message.isEmpty) { + hashStatus.print('Message is empty'); + } else { + Uint8List hash = await hasher.digest(message.toBytes()); + hashStatus.print( + 'Message successfully hashed with ${hasher.algorithm} :${hash.toStr(to: Encoding.hex)}'); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + child: Text("SecretKey"), + alignment: Alignment.centerLeft, + ), + keyContent, + Button( + () => _generate(ref), + "Generate SecretKey", + ), + keyStatus, + TextField( + controller: _pwdTextController, + decoration: const InputDecoration( + hintText: 'Password', + ), + ), + Button( + () => _pbkdf2(ref), + "Apply PBKDF2", + ), + pbkdf2Status, + TextField( + controller: _messageTextController, + decoration: const InputDecoration( + hintText: 'Message', + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + () => _hash(SHA256()), + "SHA256", + ), + Button( + () => _hash(SHA384()), + "SHA384", + ), + Button( + () => _hash(SHA512()), + "SHA512", + ), + ], + ), + hashStatus, + ], + ), + ), + ); + } +} diff --git a/native_crypto/example/lib/session.dart b/native_crypto/example/lib/session.dart new file mode 100644 index 0000000..8c25499 --- /dev/null +++ b/native_crypto/example/lib/session.dart @@ -0,0 +1,24 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: session.dart +// Created Date: 28/12/2021 13:54:29 +// Last Modified: 28/12/2021 13:58:49 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:native_crypto/native_crypto.dart'; + +class Session { + SecretKey secretKey; + Session() : secretKey = SecretKey(Uint8List(0)); + + void setKey(SecretKey sk) { secretKey = sk; } +} + +// Providers + +final sessionProvider = StateProvider((ref) => Session()); \ No newline at end of file diff --git a/native_crypto/example/lib/utils.dart b/native_crypto/example/lib/utils.dart new file mode 100644 index 0000000..874b778 --- /dev/null +++ b/native_crypto/example/lib/utils.dart @@ -0,0 +1,52 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: utils.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 28/12/2021 14:40:21 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; +import 'dart:convert'; + +enum Encoding { utf16, base64, hex } + +extension StringX on String { + Uint8List toBytes({final from = Encoding.utf16}) { + Uint8List bytes = Uint8List(0); + switch (from) { + case Encoding.utf16: + bytes = Uint8List.fromList(runes.toList()); + break; + case Encoding.base64: + bytes = base64.decode(this); + break; + case Encoding.hex: + bytes = Uint8List.fromList( + List.generate( + length ~/ 2, + (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), + ).toList(), + ); + } + return bytes; + } +} + +extension Uint8ListX on Uint8List { + String toStr({final to = Encoding.utf16}) { + String str = ""; + switch (to) { + case Encoding.utf16: + str = String.fromCharCodes(this); + break; + case Encoding.base64: + str = base64.encode(this); + break; + case Encoding.hex: + str = map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + } + return str; + } +} diff --git a/native_crypto/example/lib/widgets/button.dart b/native_crypto/example/lib/widgets/button.dart new file mode 100644 index 0000000..30c28a5 --- /dev/null +++ b/native_crypto/example/lib/widgets/button.dart @@ -0,0 +1,32 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: button.dart +// Created Date: 28/12/2021 13:31:17 +// Last Modified: 28/12/2021 13:31:34 +// ----- +// Copyright (c) 2021 + +import 'package:flutter/material.dart'; + +class Button extends StatelessWidget { + final void Function() onPressed; + final String label; + + const Button(this.onPressed, this.label, {Key? key}) : super(key: key); + + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: TextButton.styleFrom( + primary: Colors.blue, + ), + child: Text( + label, + style: const TextStyle(color: Colors.white), + ), + ); + } +} diff --git a/native_crypto/example/lib/widgets/output.dart b/native_crypto/example/lib/widgets/output.dart new file mode 100644 index 0000000..7e16b5f --- /dev/null +++ b/native_crypto/example/lib/widgets/output.dart @@ -0,0 +1,61 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: output.dart +// Created Date: 28/12/2021 13:31:39 +// Last Modified: 28/12/2021 14:12:11 +// ----- +// Copyright (c) 2021 + +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class Output extends StatelessWidget { + late TextEditingController controller; + final bool large; + final bool editable; + + Output({ + Key? key, + TextEditingController? controller, + this.large = false, + this.editable = false, + }) : super(key: key) { + this.controller = controller ?? TextEditingController(); + } + + void print(String message) { + debugPrint(message); + controller.text = message; + } + + void append(String message) { + debugPrint(message); + controller.text += message; + } + + void appendln(String message) { + debugPrint(message); + controller.text += message + "\n"; + } + + void clear() { + controller.clear(); + } + + String read() { + return controller.text; + } + + @override + Widget build(BuildContext context) { + return TextField( + enableInteractiveSelection: true, + readOnly: editable ? false : true, + minLines: large ? 3 : 1, + maxLines: large ? 500 : 5, + decoration: const InputDecoration(border: OutlineInputBorder()), + controller: controller, + ); + } +} diff --git a/native_crypto/example/pubspec.yaml b/native_crypto/example/pubspec.yaml index e6e3ec3..b8fd86e 100644 --- a/native_crypto/example/pubspec.yaml +++ b/native_crypto/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + flutter_riverpod: ^1.0.3 dev_dependencies: flutter_test: @@ -39,7 +40,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^1.0.0 + flutter_lints: ^1.0.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/native_crypto/lib/native_crypto.dart b/native_crypto/lib/native_crypto.dart index 1036e3a..015dab2 100644 --- a/native_crypto/lib/native_crypto.dart +++ b/native_crypto/lib/native_crypto.dart @@ -1,17 +1,27 @@ -// You have generated a new plugin project without -// specifying the `--platforms` flag. A plugin project supports no platforms is generated. -// To add platforms, run `flutter create -t plugin --platforms .` under the same -// directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: native_crypto.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 28/12/2021 15:06:48 +// ----- +// Copyright (c) 2021 -import 'dart:async'; +export 'src/byte_array.dart'; +export 'src/cipher.dart'; +export 'src/cipher_text.dart'; +export 'src/ciphers/aes.dart'; +export 'src/exceptions.dart'; +export 'src/hasher.dart'; +export 'src/hashers/sha256.dart'; +export 'src/hashers/sha384.dart'; +export 'src/hashers/sha512.dart'; +export 'src/kdf/pbkdf2.dart'; +export 'src/keyderivation.dart'; +export 'src/keys/secret_key.dart'; +export 'src/utils.dart'; -import 'package:flutter/services.dart'; - -class NativeCrypto { - static const MethodChannel _channel = MethodChannel('native_crypto'); - - static Future get platformVersion async { - final String? version = await _channel.invokeMethod('getPlatformVersion'); - return version; - } -} +const String version = "0.1.0"; +const String author = "Hugo Pointcheval"; +const String website = "https://hugo.pointcheval.fr/"; +const List repositories = ["https://github.com/hugo-pcl/native-crypto-flutter", "https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter"]; diff --git a/native_crypto/lib/src/builder.dart b/native_crypto/lib/src/builder.dart new file mode 100644 index 0000000..2ed8d17 --- /dev/null +++ b/native_crypto/lib/src/builder.dart @@ -0,0 +1,12 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: builder.dart +// Created Date: 28/12/2021 12:02:34 +// Last Modified: 28/12/2021 12:32:12 +// ----- +// Copyright (c) 2021 + +abstract class Builder { + Future build(); +} \ No newline at end of file diff --git a/native_crypto/lib/src/builders/aes_builder.dart b/native_crypto/lib/src/builders/aes_builder.dart new file mode 100644 index 0000000..8073490 --- /dev/null +++ b/native_crypto/lib/src/builders/aes_builder.dart @@ -0,0 +1,46 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_builder.dart +// Created Date: 28/12/2021 12:03:11 +// Last Modified: 28/12/2021 13:39:23 +// ----- +// Copyright (c) 2021 + +import '../builder.dart'; +import '../ciphers/aes.dart'; +import '../exceptions.dart'; +import '../keys/secret_key.dart'; + +class AESBuilder implements Builder { + SecretKey? _sk; + Future? _fsk; + AESMode _mode = AESMode.gcm; + + AESBuilder withGeneratedKey(int bitsCount) { + _fsk = SecretKey.fromSecureRandom(bitsCount); + return this; + } + + AESBuilder withKey(SecretKey secretKey) { + _sk = secretKey; + return this; + } + + AESBuilder using(AESMode mode) { + _mode = mode; + return this; + } + + @override + Future build() async { + if (_sk == null) { + if (_fsk == null) { + throw CipherInitException("You must specify or generate a secret key."); + } else { + _sk = await _fsk; + } + } + return AES(_sk!, _mode); + } +} \ No newline at end of file diff --git a/native_crypto/lib/src/byte_array.dart b/native_crypto/lib/src/byte_array.dart new file mode 100644 index 0000000..8343a34 --- /dev/null +++ b/native_crypto/lib/src/byte_array.dart @@ -0,0 +1,72 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: byte_array.dart +// Created Date: 16/12/2021 17:54:16 +// Last Modified: 27/12/2021 21:51:36 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'utils.dart'; +import 'dart:convert' as convert; + +class ByteArray { + Uint8List _bytes; + + ByteArray(this._bytes); + + /// Creates an ByteArray object from a hexdecimal string. + ByteArray.fromBase16(String encoded) : _bytes = Utils.decodeHexString(encoded); + + /// Creates an ByteArray object from a Base64 string. + ByteArray.fromBase64(String encoded) + : _bytes = convert.base64.decode(encoded); + + /// Creates an ByteArray object from a UTF-8 string. + ByteArray.fromUtf8(String input) + : _bytes = Uint8List.fromList(convert.utf8.encode(input)); + + /// Creates an ByteArray object from a length. + ByteArray.fromLength(int length) : _bytes = Uint8List(length); + + /// Gets the ByteArray bytes. + // ignore: unnecessary_getters_setters + Uint8List get bytes => _bytes; + + /// Sets the ByteArray bytes. + set bytes(Uint8List value) => _bytes = value; + + /// Gets the ByteArray bytes as a Hexadecimal representation. + String get base16 => + _bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + + /// Gets the ByteArray bytes as a Base64 representation. + String get base64 => convert.base64.encode(_bytes); + + @override + bool operator ==(other) { + if (other is ByteArray) { + for (int i = 0; i < _bytes.length; i++) { + if (_bytes[i] != other._bytes[i]) { + return false; + } + } + + return true; + } + + return false; + } + + @override + int get hashCode { + int hash = 0; + for (int i = 0; i < _bytes.length; i++) { + hash = _bytes[i] + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } +} \ No newline at end of file diff --git a/native_crypto/lib/src/cipher.dart b/native_crypto/lib/src/cipher.dart new file mode 100644 index 0000000..fec80ca --- /dev/null +++ b/native_crypto/lib/src/cipher.dart @@ -0,0 +1,37 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 28/12/2021 12:25:38 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'cipher_text.dart'; + +/// Represents different cipher algorithms +enum CipherAlgorithm { aes, rsa } + +/// Represents a cipher. +/// +/// In cryptography, a cipher is an algorithm for performing encryption +/// or decryption - a series of well-defined steps that can +/// be followed as a procedure. +abstract class Cipher { + /// Returns the standard algorithm name for this cipher + CipherAlgorithm get algorithm; + + /// Encrypts data. + /// + /// Takes [Uint8List] data as parameter. + /// Returns a [CipherText]. + Future encrypt(Uint8List data); + + /// Decrypts cipher text. + /// + /// Takes [CipherText] as parameter. + /// And returns plain text data as [Uint8List]. + Future decrypt(CipherText cipherText); +} \ No newline at end of file diff --git a/native_crypto/lib/src/cipher_text.dart b/native_crypto/lib/src/cipher_text.dart new file mode 100644 index 0000000..c4b53f2 --- /dev/null +++ b/native_crypto/lib/src/cipher_text.dart @@ -0,0 +1,51 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_text.dart +// Created Date: 16/12/2021 16:59:53 +// Last Modified: 27/12/2021 22:32:06 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'byte_array.dart'; + +class CipherText extends ByteArray { + final int _ivLength; + final int _dataLength; + final int _tagLength; + + CipherText(Uint8List iv, Uint8List data, Uint8List tag) : _ivLength = iv.length, _dataLength = data.length, _tagLength = tag.length, super(Uint8List.fromList(iv + data + tag)); + + /// Gets the CipherText IV. + Uint8List get iv => bytes.sublist(0, _ivLength); + + /// Gets the CipherText data. + Uint8List get data => bytes.sublist(_ivLength, _ivLength + _dataLength); + + /// Gets the CipherText tag. + Uint8List get tag => bytes.sublist(_ivLength + _dataLength, _ivLength + _dataLength + _tagLength); + + /// Gets the CipherText IV length. + int get ivLength => _ivLength; + + /// Gets the CipherText data length. + int get dataLength => _dataLength; + + /// Gets the CipherText tag length. + int get tagLength => _tagLength; +} + +class CipherTextList extends CipherText { + static const int chunkSize = 33554432; + final List _list; + + CipherTextList() : _list = [], super(Uint8List(0), Uint8List(0), Uint8List(0)); + + void add(CipherText cipherText) { + _list.add(cipherText); + } + + get list => _list; +} \ No newline at end of file diff --git a/native_crypto/lib/src/ciphers/aes.dart b/native_crypto/lib/src/ciphers/aes.dart new file mode 100644 index 0000000..592a7ce --- /dev/null +++ b/native_crypto/lib/src/ciphers/aes.dart @@ -0,0 +1,112 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 28/12/2021 13:39:00 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import '../cipher.dart'; +import '../cipher_text.dart'; +import '../exceptions.dart'; +import '../keys/secret_key.dart'; +import '../platform.dart'; +import '../utils.dart'; + +/// Defines the AES modes of operation. +enum AESMode { gcm } + +/// Defines all available key sizes. +enum AESKeySize { bits128, bits192, bits256 } + +/// Represents different paddings. +enum AESPadding { none } + +extension AESKeySizeExtension on AESKeySize { + static final Map sizes = { + AESKeySize.bits128: 128, + AESKeySize.bits192: 192, + AESKeySize.bits256: 256, + }; + static final List supportedSizes = sizes.values.toList(growable: false); + int get length { + return sizes[this]!; // this is safe because `this` is listed in the enum + } +} + +class AES implements Cipher { + final SecretKey key; + final AESMode mode; + final AESPadding padding; + + @override + CipherAlgorithm get algorithm => CipherAlgorithm.aes; + + AES(this.key, this.mode, {this.padding = AESPadding.none}) { + if (!AESKeySizeExtension.supportedSizes.contains(key.bytes.length * 8)) { + throw CipherInitException("Invalid key length!"); + } + + Map> _supported = { + AESMode.gcm: [AESPadding.none], + }; + + if (!_supported[mode]!.contains(padding)) { + throw CipherInitException("Invalid padding!"); + } + } + + @override + Future decrypt(CipherText cipherText) async { + BytesBuilder decryptedData = BytesBuilder(copy: false); + if (cipherText is CipherTextList) { + for (CipherText ct in cipherText.list) { + Uint8List d = await platform.decrypt( + ct.bytes, key.bytes, Utils.enumToStr(algorithm)) ?? + Uint8List(0); + decryptedData.add(d); + } + } else { + Uint8List d = await platform.decrypt( + cipherText.bytes, key.bytes, Utils.enumToStr(algorithm)) ?? + Uint8List(0); + decryptedData.add(d); + } + + return decryptedData.toBytes(); + } + + @override + Future encrypt(Uint8List data) async { + Uint8List dataToEncrypt; + CipherTextList cipherTextList = CipherTextList(); + // If data is bigger than 32mB -> split in chunks + if (data.length > CipherTextList.chunkSize) { + int chunkNb = (data.length / CipherTextList.chunkSize).ceil(); + for (var i = 0; i < chunkNb; i++) { + dataToEncrypt = i < (chunkNb - 1) + ? data.sublist(i * CipherTextList.chunkSize, (i + 1) * CipherTextList.chunkSize) + : data.sublist(i * CipherTextList.chunkSize); + Uint8List c = await platform.encrypt( + dataToEncrypt, + key.bytes, + Utils.enumToStr(algorithm) + ) ?? Uint8List(0); + cipherTextList.add(CipherText(c.sublist(0, 12), c.sublist(12, c.length - 16), c.sublist(c.length - 16, c.length))); // TODO: generify this + } + } else { + Uint8List c = await platform.encrypt( + data, + key.bytes, + Utils.enumToStr(algorithm) + ) ?? Uint8List(0); + + return CipherText(c.sublist(0, 12), c.sublist(12, c.length - 16), c.sublist(c.length - 16, c.length)); // TODO: generify this + } + + return cipherTextList; + } +} diff --git a/native_crypto/lib/src/exceptions.dart b/native_crypto/lib/src/exceptions.dart new file mode 100644 index 0000000..3a5d2ce --- /dev/null +++ b/native_crypto/lib/src/exceptions.dart @@ -0,0 +1,41 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: exceptions.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 27/12/2021 23:28:31 +// ----- +// Copyright (c) 2021 + +class NativeCryptoException implements Exception { + String message; + NativeCryptoException(this.message); +} + +class UtilsException extends NativeCryptoException { + UtilsException(message) : super(message); +} + +class KeyException extends NativeCryptoException { + KeyException(message) : super(message); +} + +class KeyDerivationException extends NativeCryptoException { + KeyDerivationException(message) : super(message); +} + +class CipherInitException extends NativeCryptoException { + CipherInitException(message) : super(message); +} + +class EncryptionException extends NativeCryptoException { + EncryptionException(message) : super(message); +} + +class DecryptionException extends NativeCryptoException { + DecryptionException(message) : super(message); +} + +class NotImplementedException extends NativeCryptoException { + NotImplementedException(message) : super(message); +} diff --git a/native_crypto/lib/src/hasher.dart b/native_crypto/lib/src/hasher.dart new file mode 100644 index 0000000..bacf4bd --- /dev/null +++ b/native_crypto/lib/src/hasher.dart @@ -0,0 +1,27 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: hasher.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 27/12/2021 22:06:29 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'platform.dart'; +import 'utils.dart'; + +enum HashAlgorithm { sha256, sha384, sha512 } + +abstract class Hasher { + /// Returns the standard algorithm name for this digest + HashAlgorithm get algorithm; + + /// Hashes a message + Future digest(Uint8List data) async { + Uint8List hash = (await platform.digest(data, Utils.enumToStr(algorithm))) ?? Uint8List(0); + + return hash; + } +} diff --git a/native_crypto/lib/src/hashers/sha256.dart b/native_crypto/lib/src/hashers/sha256.dart new file mode 100644 index 0000000..6c1284a --- /dev/null +++ b/native_crypto/lib/src/hashers/sha256.dart @@ -0,0 +1,15 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: sha256.dart +// Created Date: 17/12/2021 11:31:20 +// Last Modified: 18/12/2021 12:09:33 +// ----- +// Copyright (c) 2021 + +import '../hasher.dart'; + +class SHA256 extends Hasher{ + @override + HashAlgorithm get algorithm => HashAlgorithm.sha256; +} \ No newline at end of file diff --git a/native_crypto/lib/src/hashers/sha384.dart b/native_crypto/lib/src/hashers/sha384.dart new file mode 100644 index 0000000..b9f44dd --- /dev/null +++ b/native_crypto/lib/src/hashers/sha384.dart @@ -0,0 +1,15 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: sha384.dart +// Created Date: 17/12/2021 11:31:53 +// Last Modified: 18/12/2021 12:09:45 +// ----- +// Copyright (c) 2021 + +import '../hasher.dart'; + +class SHA384 extends Hasher{ + @override + HashAlgorithm get algorithm => HashAlgorithm.sha384; +} \ No newline at end of file diff --git a/native_crypto/lib/src/hashers/sha512.dart b/native_crypto/lib/src/hashers/sha512.dart new file mode 100644 index 0000000..d4f7d6b --- /dev/null +++ b/native_crypto/lib/src/hashers/sha512.dart @@ -0,0 +1,15 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: sha512.dart +// Created Date: 17/12/2021 11:32:14 +// Last Modified: 18/12/2021 12:09:58 +// ----- +// Copyright (c) 2021 + +import '../hasher.dart'; + +class SHA512 extends Hasher{ + @override + HashAlgorithm get algorithm => HashAlgorithm.sha512; +} \ No newline at end of file diff --git a/native_crypto/lib/src/kdf/pbkdf2.dart b/native_crypto/lib/src/kdf/pbkdf2.dart new file mode 100644 index 0000000..8d5a004 --- /dev/null +++ b/native_crypto/lib/src/kdf/pbkdf2.dart @@ -0,0 +1,51 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: pbkdf2.dart +// Created Date: 17/12/2021 14:50:42 +// Last Modified: 28/12/2021 13:38:50 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import '../exceptions.dart'; +import '../hasher.dart'; +import '../keyderivation.dart'; +import '../keys/secret_key.dart'; +import '../platform.dart'; +import '../utils.dart'; + +class PBKDF2 extends KeyDerivation { + final int _keyBytesCount; + final int _iterations; + final HashAlgorithm _hash; + + @override + KdfAlgorithm get algorithm => KdfAlgorithm.pbkdf2; + + PBKDF2( + int keyBytesCount, + int iterations, { + HashAlgorithm algorithm = HashAlgorithm.sha256, + }) : _keyBytesCount = keyBytesCount, + _iterations = iterations, + _hash = algorithm; + + @override + Future derive({String? password, String? salt}) async { + if (password == null || salt == null) { + throw KeyDerivationException("Password or Salt can't be null!"); + } + + Uint8List derivation = (await platform.pbkdf2( + password, + salt, + _keyBytesCount, + _iterations, + Utils.enumToStr(_hash), + )) ?? Uint8List(0); + + return SecretKey(derivation); + } +} diff --git a/native_crypto/lib/src/key.dart b/native_crypto/lib/src/key.dart new file mode 100644 index 0000000..b86c1d2 --- /dev/null +++ b/native_crypto/lib/src/key.dart @@ -0,0 +1,20 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: key.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 28/12/2021 13:37:50 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'byte_array.dart'; + +/// A class representing a key. +class Key extends ByteArray { + Key(Uint8List bytes) : super(bytes); + Key.fromBase16(String encoded) : super.fromBase16(encoded); + Key.fromBase64(String encoded) : super.fromBase64(encoded); + Key.fromUtf8(String input) : super.fromUtf8(input); +} \ No newline at end of file diff --git a/native_crypto/lib/src/keyderivation.dart b/native_crypto/lib/src/keyderivation.dart new file mode 100644 index 0000000..279346e --- /dev/null +++ b/native_crypto/lib/src/keyderivation.dart @@ -0,0 +1,21 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: kdf.dart +// Created Date: 18/12/2021 11:56:43 +// Last Modified: 28/12/2021 13:38:02 +// ----- +// Copyright (c) 2021 + +import './keys/secret_key.dart'; + +enum KdfAlgorithm { pbkdf2 } + +/// Represents a Key Derivation Function +abstract class KeyDerivation { + /// Returns the standard algorithm name for this key derivation function + KdfAlgorithm get algorithm; + + /// Derive key + Future derive(); +} \ No newline at end of file diff --git a/native_crypto/lib/src/keys/secret_key.dart b/native_crypto/lib/src/keys/secret_key.dart new file mode 100644 index 0000000..4579496 --- /dev/null +++ b/native_crypto/lib/src/keys/secret_key.dart @@ -0,0 +1,36 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: secret_key.dart +// Created Date: 28/12/2021 13:36:54 +// Last Modified: 28/12/2021 13:37:45 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +import '../exceptions.dart'; +import '../key.dart'; +import '../platform.dart'; + +/// A class representing a secret key. +/// A secret key is a key that is not accessible by anyone else. +/// It is used to encrypt and decrypt data. +class SecretKey extends Key { + SecretKey(Uint8List bytes) : super(bytes); + SecretKey.fromBase16(String encoded) : super.fromBase16(encoded); + SecretKey.fromBase64(String encoded) : super.fromBase64(encoded); + SecretKey.fromUtf8(String input) : super.fromUtf8(input); + + static Future fromSecureRandom(int bitsCount) async { + try { + Uint8List _key = (await platform.generateSecretKey(bitsCount)) ?? Uint8List(0); + + return SecretKey(_key); + } on PlatformException catch (e) { + throw KeyException(e); + } + } +} diff --git a/native_crypto/lib/src/platform.dart b/native_crypto/lib/src/platform.dart new file mode 100644 index 0000000..9834907 --- /dev/null +++ b/native_crypto/lib/src/platform.dart @@ -0,0 +1,12 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: platform.dart +// Created Date: 27/12/2021 22:03:58 +// Last Modified: 27/12/2021 22:04:30 +// ----- +// Copyright (c) 2021 + +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +NativeCryptoPlatform platform = NativeCryptoPlatform.instance; \ No newline at end of file diff --git a/native_crypto/lib/src/utils.dart b/native_crypto/lib/src/utils.dart new file mode 100644 index 0000000..df0dff2 --- /dev/null +++ b/native_crypto/lib/src/utils.dart @@ -0,0 +1,83 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: utils.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 27/12/2021 22:04:07 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'cipher.dart'; +import 'exceptions.dart'; +import 'hasher.dart'; +import 'keyderivation.dart'; + +class Utils { + /// Returns enum value to string, without the enum name + static String enumToStr(dynamic enumValue) { + return enumValue.toString().split('.').last; + } + + /// Returns enum list as string list + static List enumToList(List enumValues) { + List _res = []; + for (T enumValue in enumValues) { + _res.add(enumToStr(enumValue)); + } + + return _res; + } + + /// Returns enum from string + static T strToEnum(String str, List enumValues) { + for (T enumValue in enumValues) { + if (enumToStr(enumValue) == str) { + return enumValue; + } + } + throw UtilsException('Invalid enum value: $str'); + } + + /// Returns [HashAlgorithm] from his name. + static HashAlgorithm getHashAlgorithm(String algorithm) { + return strToEnum(algorithm.toLowerCase(), HashAlgorithm.values); + } + + /// Returns all available [HashAlgorithm] as String list + static List getAvailableHashAlgorithms() { + return enumToList(HashAlgorithm.values); + } + + /// Returns [KdfAlgorithm] from his name. + static KdfAlgorithm getKdfAlgorithm(String algorithm) { + return strToEnum(algorithm.toLowerCase(), KdfAlgorithm.values); + } + + /// Returns all available [KdfAlgorithm] as String list + static List getAvailableKdfAlgorithms() { + return enumToList(KdfAlgorithm.values); + } + + /// Returns [CipherAlgorithm] from his name. + static CipherAlgorithm getCipherAlgorithm(String algorithm) { + return strToEnum(algorithm.toLowerCase(), CipherAlgorithm.values); + } + + /// Returns all available [CipherAlgorithm] as String list + static List getAvailableCipherAlgorithms() { + return enumToList(CipherAlgorithm.values); + } + + static Uint8List decodeHexString(String input) { + assert(input.length % 2 == 0, 'Input needs to be an even length.'); + + return Uint8List.fromList( + List.generate( + input.length ~/ 2, + (i) => int.parse(input.substring(i * 2, (i * 2) + 2), radix: 16), + ).toList(), + ); + } +} diff --git a/native_crypto/pubspec.yaml b/native_crypto/pubspec.yaml index 3baf2be..5a63d48 100644 --- a/native_crypto/pubspec.yaml +++ b/native_crypto/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto description: Fast and secure cryptography for Flutter. -version: 0.0.7 +version: 0.1.0 publish_to: 'none' environment: @@ -11,9 +11,15 @@ dependencies: flutter: sdk: flutter + native_crypto_android: + path: ../native_crypto_android + native_crypto_ios: path: ../native_crypto_ios + native_crypto_platform_interface: + path: ../native_crypto_platform_interface + dev_dependencies: flutter_test: sdk: flutter @@ -22,7 +28,7 @@ dev_dependencies: flutter: plugin: platforms: - # android: - # default_package: native_crypto_android + android: + default_package: native_crypto_android ios: default_package: native_crypto_ios \ No newline at end of file diff --git a/native_crypto/test/native_crypto_test.dart b/native_crypto/test/native_crypto_test.dart index 5caceab..0ffdd02 100644 --- a/native_crypto/test/native_crypto_test.dart +++ b/native_crypto/test/native_crypto_test.dart @@ -1,23 +1 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:native_crypto/native_crypto.dart'; - -void main() { - const MethodChannel channel = MethodChannel('native_crypto'); - - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); - }); - - tearDown(() { - channel.setMockMethodCallHandler(null); - }); - - test('getPlatformVersion', () async { - expect(await NativeCrypto.platformVersion, '42'); - }); -} +// TODO \ No newline at end of file From 41b59b2b937fb5f3f6a2e04aa301e23c10244b34 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 28 Dec 2021 17:10:18 +0100 Subject: [PATCH 04/39] Investigate in slow data transferts --- .../android/app/src/main/AndroidManifest.xml | 1 - .../example/lib/pages/benchmark_page.dart | 39 +++++++- .../NativeCryptoAndroidPlugin.kt | 88 +++++++++++++------ 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/native_crypto/example/android/app/src/main/AndroidManifest.xml b/native_crypto/example/android/app/src/main/AndroidManifest.xml index aa8a66a..c55f004 100644 --- a/native_crypto/example/android/app/src/main/AndroidManifest.xml +++ b/native_crypto/example/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ _test(WidgetRef ref, Cipher cipher) async { + Session state = ref.read(sessionProvider.state).state; + + if (state.secretKey.bytes.isEmpty) { + benchmarkStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return; + } + + int size = 64; + benchmarkStatus.print("Benchmark Test\n"); + + // Encryption + var before = DateTime.now(); + var encryptedBigFile = await cipher.encrypt(Uint8List(size * 1000000)); + var after = DateTime.now(); + var benchmark = + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); + + // Decryption + var befored = DateTime.now(); + await cipher.decrypt(encryptedBigFile); + var afterd = DateTime.now(); + var benchmarkd = afterd.millisecondsSinceEpoch - befored.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Decryption took $benchmarkd ms\n'); + } + Future _benchmark(WidgetRef ref, Cipher cipher) async { Session state = ref.read(sessionProvider.state).state; @@ -39,13 +67,14 @@ class BenchmarkPage extends ConsumerWidget { var beforeBench = DateTime.now(); for (int size in testedSizes) { - var bigFile = Uint8List(size * 1000000); + var b = ByteData(size * 1000000); + //var bigFile = Uint8List.view(); csv += "${size * 1000000};"; var cryptoTime = 0; // Encryption var before = DateTime.now(); - var encryptedBigFile = await cipher.encrypt(bigFile); + var encryptedBigFile = await cipher.encrypt(b.buffer.asUint8List()); var after = DateTime.now(); var benchmark = @@ -122,6 +151,10 @@ class BenchmarkPage extends ConsumerWidget { () => _benchmark(ref, cipher), "Launch benchmark", ), + Button( + () => _test(ref, cipher), + "Test benchmark", + ), Button( _clear, "Clear", diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt index 04270a8..e9c5219 100644 --- a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt +++ b/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt @@ -18,53 +18,85 @@ class NativeCryptoAndroidPlugin: FlutterPlugin, MethodCallHandler { private lateinit var channel : MethodChannel override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "plugins.hugop.cl/native_crypto") + channel = MethodChannel(flutterPluginBinding.flutterEngine.dartExecutor, "plugins.hugop.cl/native_crypto") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "digest" -> { - val data : ByteArray? = call.argument("data") - val algorithm : String? = call.argument("algorithm") - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if digest is null - result.success(Hash.digest(data, algorithm!!)) + if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); + if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); + + val data : ByteArray = call.argument("data")!! + val algorithm : String = call.argument("algorithm")!! + + result.success(Hash.digest(data, algorithm)) } "generateSecretKey" -> { - val bitsCount : Int? = call.argument("bitsCount") - // TODO(hpcl): check null - result.success(Key.fromSecureRandom(bitsCount!!)) + if (!call.hasArgument("bitsCount")) result.error("SIZE_NULL", null, null); + + val bitsCount : Int = call.argument("bitsCount")!! + + result.success(Key.fromSecureRandom(bitsCount)) } "generateKeyPair" -> { result.notImplemented() } "pbkdf2" -> { - val password : String? = call.argument("password") - val salt : String? = call.argument("salt") - val keyBytesCount : Int? = call.argument("keyBytesCount") - val iterations : Int? = call.argument("iterations") - val algorithm : String? = call.argument("algorithm") - // TODO(hpcl): check null - result.success(Key.fromPBKDF2(password!!, salt!!, keyBytesCount!!, iterations!!, algorithm!!)) + if (!call.hasArgument("password")) result.error("PASSWORD_NULL", null, null); + if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null); + if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null); + if (!call.hasArgument("iterations")) result.error("ITERATIONS_NULL", null, null); + if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); + + val password : String = call.argument("password")!! + val salt : String = call.argument("salt")!! + val keyBytesCount : Int = call.argument("keyBytesCount")!! + val iterations : Int = call.argument("iterations")!! + val algorithm : String = call.argument("algorithm")!! + + result.success(Key.fromPBKDF2(password, salt, keyBytesCount, iterations, algorithm)) } "encrypt" -> { - val data : ByteArray? = call.argument("data") - val key : ByteArray? = call.argument("key") - val algorithm : String? = call.argument("algorithm") - // TODO(hpcl): check null - // TODO(hcpl): check algorithm - result.success(AESCipher().encrypt(data!!, key!!)) + if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); + if (!call.hasArgument("key")) result.error("KEY_NULL", null, null); + if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); + + val data : ByteArray = call.argument("data")!! + val key : ByteArray = call.argument("key")!! + val algorithm : String = call.argument("algorithm")!! + + if (algorithm == "aes") { + result.success(AESCipher().encrypt(data, key)) + } } "decrypt" -> { - val data : ByteArray? = call.argument("data") - val key : ByteArray? = call.argument("key") - val algorithm : String? = call.argument("algorithm") - // TODO(hpcl): check null - // TODO(hcpl): check algorithm - result.success(AESCipher().decrypt(data!!, key!!)) + if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); + if (!call.hasArgument("key")) result.error("KEY_NULL", null, null); + if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); + + val data : ByteArray = call.argument("data")!! + val key : ByteArray = call.argument("key")!! + val algorithm : String = call.argument("algorithm")!! + + if (algorithm == "aes") { + result.success(AESCipher().decrypt(data, key)) + } } "generateSharedSecretKey" -> { + if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null); + if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null); + if (!call.hasArgument("ephemeralPrivateKey")) result.error("PRIVATE_KEY_NULL", null, null); + if (!call.hasArgument("otherPublicKey")) result.error("PUBLIC_KEY_NULL", null, null); + if (!call.hasArgument("hkdfAlgorithm")) result.error("ALGORITHM_NULL", null, null); + + val salt : ByteArray = call.argument("salt")!! + val keyBytesCount : Int = call.argument("keyBytesCount")!! + val ephemeralPrivateKey : ByteArray = call.argument("ephemeralPrivateKey")!! + val otherPublicKey : ByteArray = call.argument("otherPublicKey")!! + val hkdfAlgorithm : String = call.argument("hkdfAlgorithm")!! + result.notImplemented() } else -> result.notImplemented() From 8cd192c6b05856bf89f132617d9d4431548177b5 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 28 Dec 2021 18:20:06 +0100 Subject: [PATCH 05/39] Add native examples --- native_crypto_android/example/lib/main.dart | 110 +++++++++--------- native_crypto_android/example/pubspec.lock | 2 +- native_crypto_ios/example/lib/main.dart | 118 ++++++++++---------- 3 files changed, 123 insertions(+), 107 deletions(-) diff --git a/native_crypto_android/example/lib/main.dart b/native_crypto_android/example/lib/main.dart index 0d1e7a6..ce84ce0 100644 --- a/native_crypto_android/example/lib/main.dart +++ b/native_crypto_android/example/lib/main.dart @@ -1,74 +1,84 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: main.dart +// Created Date: 27/12/2021 22:43:20 +// Last Modified: 28/12/2021 18:18:44 +// ----- +// Copyright (c) 2021 + import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'dart:async'; - -import 'package:flutter/services.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +void run() async { + NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; + + debugPrint("Benchmark"); - @override - State createState() => _MyAppState(); -} + int size = 25; + int iterations = 25; + int totalEnc = 0; + int totalDec = 0; -class _MyAppState extends State { - String _platformVersion = 'Unknown'; + debugPrint("Size: $size MB"); + Uint8List? secretKey = await _nativeCryptoPlatform.generateSecretKey(128); - @override - void initState() { - super.initState(); - initPlatformState(); - } + debugPrint("Generate random data..."); + var before = DateTime.now(); + Uint8List data = Uint8List(size * 1024 * 1024); + var after = DateTime.now(); + debugPrint("Generate random data: ${after.difference(before).inMilliseconds} ms"); - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - Uint8List? sk = await _nativeCryptoPlatform.generateSecretKey(256); - print(sk ?? 'null'); + for (var _ in List.generate(iterations, (index) => index)) { + debugPrint("Encrypt data..."); + before = DateTime.now(); + Uint8List? encrypted = await _nativeCryptoPlatform.encrypt(data, secretKey!, "aes"); + after = DateTime.now(); + debugPrint("Encrypt data: ${after.difference(before).inMilliseconds} ms"); + totalEnc += after.difference(before).inMilliseconds; - Uint8List? ciphertext = await _nativeCryptoPlatform.encrypt( - Uint8List.fromList("abc".codeUnits), sk!, "aes"); - print(ciphertext ?? 'null'); - - Uint8List? plaintext = - await _nativeCryptoPlatform.decrypt(ciphertext!, sk, "aes"); - print(plaintext ?? 'null'); + debugPrint("Decrypt data..."); + before = DateTime.now(); + data = (await _nativeCryptoPlatform.decrypt(encrypted!, secretKey, "aes"))!; + after = DateTime.now(); + debugPrint("Decrypt data: ${after.difference(before).inMilliseconds} ms"); + totalDec += after.difference(before).inMilliseconds; + } - try { - platformVersion = 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } + debugPrint("Average Encrypt: ${totalEnc/iterations} ms for $size MB"); + debugPrint("Average Decrypt: ${totalDec/iterations} ms for $size MB"); + +} - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); - setState(() { - _platformVersion = platformVersion; - }); + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: MyHomePage(), + ); } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Running on: $_platformVersion\n'), - ), + run(); + return Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: const Center( + child: Text("Check the console"), ), ); } diff --git a/native_crypto_android/example/pubspec.lock b/native_crypto_android/example/pubspec.lock index ee5a879..ecc71be 100644 --- a/native_crypto_android/example/pubspec.lock +++ b/native_crypto_android/example/pubspec.lock @@ -108,7 +108,7 @@ packages: path: "../../native_crypto_platform_interface" relative: true source: path - version: "0.0.7" + version: "0.1.0" path: dependency: transitive description: diff --git a/native_crypto_ios/example/lib/main.dart b/native_crypto_ios/example/lib/main.dart index 6636074..ab72cef 100644 --- a/native_crypto_ios/example/lib/main.dart +++ b/native_crypto_ios/example/lib/main.dart @@ -1,78 +1,84 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: main.dart +// Created Date: 27/12/2021 22:43:20 +// Last Modified: 28/12/2021 18:19:37 +// ----- +// Copyright (c) 2021 + import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'dart:async'; - -import 'package:flutter/services.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +void run() async { + NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; + + debugPrint("Benchmark"); + + int size = 25; + int iterations = 25; + int totalEnc = 0; + int totalDec = 0; + + debugPrint("Size: $size MB"); + Uint8List? secretKey = await _nativeCryptoPlatform.generateSecretKey(128); + + debugPrint("Generate random data..."); + var before = DateTime.now(); + Uint8List data = Uint8List(size * 1024 * 1024); + var after = DateTime.now(); + debugPrint("Generate random data: ${after.difference(before).inMilliseconds} ms"); + + for (var _ in List.generate(iterations, (index) => index)) { + debugPrint("Encrypt data..."); + before = DateTime.now(); + Uint8List? encrypted = await _nativeCryptoPlatform.encrypt(data, secretKey!, "aes"); + after = DateTime.now(); + debugPrint("Encrypt data: ${after.difference(before).inMilliseconds} ms"); + totalEnc += after.difference(before).inMilliseconds; + + debugPrint("Decrypt data..."); + before = DateTime.now(); + data = (await _nativeCryptoPlatform.decrypt(encrypted!, secretKey, "aes"))!; + after = DateTime.now(); + debugPrint("Decrypt data: ${after.difference(before).inMilliseconds} ms"); + totalDec += after.difference(before).inMilliseconds; + } - @override - State createState() => _MyAppState(); + debugPrint("Average Encrypt: ${totalEnc/iterations} ms for $size MB"); + debugPrint("Average Decrypt: ${totalDec/iterations} ms for $size MB"); + } -class _MyAppState extends State { - String _platformVersion = 'Unknown'; +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); @override - void initState() { - super.initState(); - initPlatformState(); + Widget build(BuildContext context) { + return const MaterialApp( + home: MyHomePage(), + ); } +} - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - Uint8List? sk = await _nativeCryptoPlatform.generateSecretKey(256); - print(sk ?? 'null'); - - Uint8List? ciphertext = await _nativeCryptoPlatform.encrypt(Uint8List.fromList("abc".codeUnits), sk!, "aes"); - print(ciphertext ?? 'null'); - - Uint8List? plaintext = await _nativeCryptoPlatform.decrypt(ciphertext!, sk, "aes"); - print(plaintext ?? 'null'); - - Uint8List? kp = await _nativeCryptoPlatform.generateKeyPair(); - print(kp!.sublist(0, 31)); - print(kp.sublist(32).length); - - Uint8List? sharedSecret = await _nativeCryptoPlatform.generateSharedSecretKey(Uint8List.fromList("salt".codeUnits), 32, kp.sublist(0, 31), kp.sublist(32), "sha256"); - - try { - platformVersion = 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); - } +class MyHomePage extends StatelessWidget { + const MyHomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Running on: $_platformVersion\n'), - ), + run(); + return Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: const Center( + child: Text("Check the console"), ), ); } From 7fc1ef59680db3cf81e18f42e3defede7c8f426a Mon Sep 17 00:00:00 2001 From: Pointcheval Hugo Date: Fri, 14 Jan 2022 19:12:42 +0100 Subject: [PATCH 06/39] Add all --- native_crypto/.vscode/settings.json | 3 + native_crypto/example/ios/Podfile | 2 +- native_crypto/example/ios/Podfile.lock | 22 ++++++ .../ios/Runner.xcodeproj/project.pbxproj | 68 +++++++++++++++++++ .../contents.xcworkspacedata | 3 + native_crypto_ios/.vscode/settings.json | 3 + native_crypto_ios/example/ios/Podfile | 2 +- native_crypto_ios/example/ios/Podfile.lock | 4 +- native_crypto_ios/example/lib/main.dart | 4 +- 9 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 native_crypto/.vscode/settings.json create mode 100644 native_crypto/example/ios/Podfile.lock create mode 100644 native_crypto_ios/.vscode/settings.json diff --git a/native_crypto/.vscode/settings.json b/native_crypto/.vscode/settings.json new file mode 100644 index 0000000..909bed1 --- /dev/null +++ b/native_crypto/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": "/Users/hpcl/.flutter/2.8.1" +} \ No newline at end of file diff --git a/native_crypto/example/ios/Podfile b/native_crypto/example/ios/Podfile index 1e8c3c9..10f3c9b 100644 --- a/native_crypto/example/ios/Podfile +++ b/native_crypto/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/native_crypto/example/ios/Podfile.lock b/native_crypto/example/ios/Podfile.lock new file mode 100644 index 0000000..e1bca6a --- /dev/null +++ b/native_crypto/example/ios/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - Flutter (1.0.0) + - native_crypto_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - native_crypto_ios (from `.symlinks/plugins/native_crypto_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + native_crypto_ios: + :path: ".symlinks/plugins/native_crypto_ios/ios" + +SPEC CHECKSUMS: + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + native_crypto_ios: de03ec2f594e8d41bcba2341b7ad57fd926ada5d + +PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b + +COCOAPODS: 1.10.1 diff --git a/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj b/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj index 6571d99..8c503eb 100644 --- a/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj +++ b/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 57C8B66CEF3FCADD27359CF2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B565CC9BF59F330E881E6A5 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -31,7 +32,10 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2ABDB9EE0C984D50A4E8A819 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4276EDF2B0F07350DCE3D5A1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5CD4F461EBFD40A270B90468 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9B565CC9BF59F330E881E6A5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 57C8B66CEF3FCADD27359CF2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 56AFD4323C9A594E66DE3CA2 /* Pods */ = { + isa = PBXGroup; + children = ( + 4276EDF2B0F07350DCE3D5A1 /* Pods-Runner.debug.xcconfig */, + 2ABDB9EE0C984D50A4E8A819 /* Pods-Runner.release.xcconfig */, + 5CD4F461EBFD40A270B90468 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 56AFD4323C9A594E66DE3CA2 /* Pods */, + CECC25F8D34DF7912A93B0E6 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +117,14 @@ path = Runner; sourceTree = ""; }; + CECC25F8D34DF7912A93B0E6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9B565CC9BF59F330E881E6A5 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C54B0935BA386F7769969821 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + D9E654BBB4893905628A201A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -197,6 +226,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C54B0935BA386F7769969821 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D9E654BBB4893905628A201A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/native_crypto_ios/.vscode/settings.json b/native_crypto_ios/.vscode/settings.json new file mode 100644 index 0000000..909bed1 --- /dev/null +++ b/native_crypto_ios/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": "/Users/hpcl/.flutter/2.8.1" +} \ No newline at end of file diff --git a/native_crypto_ios/example/ios/Podfile b/native_crypto_ios/example/ios/Podfile index 1e8c3c9..10f3c9b 100644 --- a/native_crypto_ios/example/ios/Podfile +++ b/native_crypto_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/native_crypto_ios/example/ios/Podfile.lock b/native_crypto_ios/example/ios/Podfile.lock index d6ba796..e1bca6a 100644 --- a/native_crypto_ios/example/ios/Podfile.lock +++ b/native_crypto_ios/example/ios/Podfile.lock @@ -15,8 +15,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - native_crypto_ios: 01f5aa926eb715d08259fd20bb951ba0f69c4e74 + native_crypto_ios: de03ec2f594e8d41bcba2341b7ad57fd926ada5d -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b COCOAPODS: 1.10.1 diff --git a/native_crypto_ios/example/lib/main.dart b/native_crypto_ios/example/lib/main.dart index ab72cef..f11a109 100644 --- a/native_crypto_ios/example/lib/main.dart +++ b/native_crypto_ios/example/lib/main.dart @@ -3,7 +3,7 @@ // ----- // File: main.dart // Created Date: 27/12/2021 22:43:20 -// Last Modified: 28/12/2021 18:19:37 +// Last Modified: 28/12/2021 18:32:25 // ----- // Copyright (c) 2021 @@ -21,7 +21,7 @@ void run() async { debugPrint("Benchmark"); - int size = 25; + int size = 256; int iterations = 25; int totalEnc = 0; int totalDec = 0; From 7c1208156e29447352e2126bebd45023112bb989 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 21:24:27 +0200 Subject: [PATCH 07/39] chore: update entire repo --- AUTHORS | 6 + .../ios/Assets/.gitkeep => CHANGELOG.md | 0 LICENSE | 21 + README.md | 0 melos.yaml | 39 ++ native_crypto/.gitignore | 389 ------------ native_crypto/.vscode/settings.json | 3 - native_crypto/example/test/widget_test.dart | 27 - native_crypto/test/native_crypto_test.dart | 1 - native_crypto_android/CHANGELOG.md | 1 - native_crypto_android/example/.gitignore | 46 -- native_crypto_android/example/.metadata | 10 - native_crypto_android/example/README.md | 16 - .../example/analysis_options.yaml | 29 - .../example/android/.gitignore | 13 - .../example/android/app/build.gradle | 68 --- .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 34 -- .../MainActivity.kt | 6 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - .../example/android/build.gradle | 31 - .../example/android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.properties | 6 - .../example/android/settings.gradle | 11 - native_crypto_android/example/lib/main.dart | 85 --- native_crypto_android/example/pubspec.lock | 182 ------ native_crypto_android/example/pubspec.yaml | 87 --- .../example/test/widget_test.dart | 27 - native_crypto_ios/.vscode/settings.json | 3 - native_crypto_ios/CHANGELOG.md | 1 - native_crypto_ios/example/.gitignore | 46 -- native_crypto_ios/example/.metadata | 10 - native_crypto_ios/example/README.md | 16 - .../example/analysis_options.yaml | 29 - native_crypto_ios/example/ios/.gitignore | 34 -- .../ios/Flutter/AppFrameworkInfo.plist | 26 - .../example/ios/Flutter/Debug.xcconfig | 2 - .../example/ios/Flutter/Release.xcconfig | 2 - native_crypto_ios/example/ios/Podfile | 41 -- native_crypto_ios/example/ios/Podfile.lock | 22 - .../ios/Runner.xcodeproj/project.pbxproj | 552 ------------------ .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 --- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../example/ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 122 ---- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 -- .../ios/Runner/Base.lproj/Main.storyboard | 26 - .../example/ios/Runner/Info.plist | 47 -- .../ios/Runner/Runner-Bridging-Header.h | 1 - native_crypto_ios/example/lib/main.dart | 85 --- native_crypto_ios/example/pubspec.yaml | 87 --- .../example/test/widget_test.dart | 27 - native_crypto_platform_interface/CHANGELOG.md | 1 - ...native_crypto_platform_interface_test.dart | 1 - .../native_crypto}/.metadata | 0 .../native_crypto}/CHANGELOG.md | 0 .../native_crypto}/LICENSE | 0 .../native_crypto}/README.md | 0 .../native_crypto}/analysis_options.yaml | 0 .../native_crypto}/example/.gitignore | 0 .../native_crypto}/example/.metadata | 0 .../native_crypto}/example/README.md | 0 .../example/analysis_options.yaml | 0 .../native_crypto}/example/android/.gitignore | 0 .../example/android/app/build.gradle | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../native_crypto_example/MainActivity.kt | 0 .../res/drawable-v21/launch_background.xml | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values-night/styles.xml | 0 .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../example/android/build.gradle | 0 .../example/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example/android/settings.gradle | 0 .../native_crypto}/example/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../example/ios/Flutter/Debug.xcconfig | 0 .../example/ios/Flutter/Release.xcconfig | 0 .../native_crypto}/example/ios/Podfile | 0 .../native_crypto}/example/ios/Podfile.lock | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../example/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../native_crypto}/example/lib/home.dart | 0 .../native_crypto}/example/lib/main.dart | 0 .../example/lib/pages/benchmark_page.dart | 0 .../example/lib/pages/cipher_page.dart | 0 .../example/lib/pages/kdf_page.dart | 0 .../native_crypto}/example/lib/session.dart | 0 .../native_crypto}/example/lib/utils.dart | 0 .../example/lib/widgets/button.dart | 0 .../example/lib/widgets/output.dart | 0 .../native_crypto}/example/pubspec.yaml | 0 .../native_crypto}/lib/native_crypto.dart | 0 .../native_crypto}/lib/src/builder.dart | 0 .../lib/src/builders/aes_builder.dart | 0 .../native_crypto}/lib/src/byte_array.dart | 0 .../native_crypto}/lib/src/cipher.dart | 0 .../native_crypto}/lib/src/cipher_text.dart | 0 .../native_crypto}/lib/src/ciphers/aes.dart | 0 .../native_crypto}/lib/src/exceptions.dart | 0 .../native_crypto}/lib/src/hasher.dart | 0 .../lib/src/hashers/sha256.dart | 0 .../lib/src/hashers/sha384.dart | 0 .../lib/src/hashers/sha512.dart | 0 .../native_crypto}/lib/src/kdf/pbkdf2.dart | 0 .../native_crypto}/lib/src/key.dart | 0 .../native_crypto}/lib/src/keyderivation.dart | 0 .../lib/src/keys/secret_key.dart | 0 .../native_crypto}/lib/src/platform.dart | 0 .../native_crypto}/lib/src/utils.dart | 0 .../native_crypto}/pubspec.yaml | 0 .../native_crypto_android}/.gitignore | 0 .../native_crypto_android}/.metadata | 0 packages/native_crypto_android/CHANGELOG.md | 35 ++ .../native_crypto_android}/LICENSE | 0 .../native_crypto_android}/README.md | 0 .../native_crypto_android}/android/.gitignore | 0 .../android/build.gradle | 0 .../android/gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../native_crypto_android}/android/gradlew | 0 .../android/gradlew.bat | 0 .../android/settings.gradle | 0 .../android/src/main/AndroidManifest.xml | 0 .../native_crypto_android/Cipher.kt | 0 .../pointcheval/native_crypto_android/Hash.kt | 0 .../native_crypto_android/HashAlgorithm.kt | 0 .../pointcheval/native_crypto_android/Key.kt | 0 .../NativeCryptoAndroidPlugin.kt | 0 .../ciphers/AESCipher.kt | 0 .../native_crypto_android}/pubspec.yaml | 0 .../native_crypto_ios}/.gitignore | 0 .../native_crypto_ios}/.metadata | 0 packages/native_crypto_ios/CHANGELOG.md | 35 ++ .../native_crypto_ios}/LICENSE | 0 .../native_crypto_ios}/README.md | 0 .../native_crypto_ios}/ios/.gitignore | 0 .../native_crypto_ios/ios/Assets/.gitkeep | 0 .../ios/Classes/Cipher.swift | 0 .../native_crypto_ios}/ios/Classes/Hash.swift | 0 .../native_crypto_ios}/ios/Classes/KEM.swift | 0 .../native_crypto_ios}/ios/Classes/Key.swift | 0 .../ios/Classes/NativeCryptoIosPlugin.h | 0 .../ios/Classes/NativeCryptoIosPlugin.m | 0 .../Classes/SwiftNativeCryptoIosPlugin.swift | 0 .../ios/native_crypto_ios.podspec | 0 .../native_crypto_ios}/pubspec.yaml | 0 .../.gitignore | 0 .../.metadata | 0 .../CHANGELOG.md | 35 ++ .../native_crypto_platform_interface}/LICENSE | 0 .../README.md | 0 .../analysis_options.yaml | 0 .../lib/native_crypto_platform_interface.dart | 0 .../lib/src/method_channel_native_crypto.dart | 0 .../lib/src/platform_interface.dart | 0 .../pubspec.yaml | 0 231 files changed, 171 insertions(+), 2544 deletions(-) create mode 100644 AUTHORS rename native_crypto_ios/ios/Assets/.gitkeep => CHANGELOG.md (100%) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 melos.yaml delete mode 100644 native_crypto/.gitignore delete mode 100644 native_crypto/.vscode/settings.json delete mode 100644 native_crypto/example/test/widget_test.dart delete mode 100644 native_crypto/test/native_crypto_test.dart delete mode 100644 native_crypto_android/CHANGELOG.md delete mode 100644 native_crypto_android/example/.gitignore delete mode 100644 native_crypto_android/example/.metadata delete mode 100644 native_crypto_android/example/README.md delete mode 100644 native_crypto_android/example/analysis_options.yaml delete mode 100644 native_crypto_android/example/android/.gitignore delete mode 100644 native_crypto_android/example/android/app/build.gradle delete mode 100644 native_crypto_android/example/android/app/src/debug/AndroidManifest.xml delete mode 100644 native_crypto_android/example/android/app/src/main/AndroidManifest.xml delete mode 100644 native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt delete mode 100644 native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 native_crypto_android/example/android/app/src/main/res/values-night/styles.xml delete mode 100644 native_crypto_android/example/android/app/src/main/res/values/styles.xml delete mode 100644 native_crypto_android/example/android/app/src/profile/AndroidManifest.xml delete mode 100644 native_crypto_android/example/android/build.gradle delete mode 100644 native_crypto_android/example/android/gradle.properties delete mode 100644 native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 native_crypto_android/example/android/settings.gradle delete mode 100644 native_crypto_android/example/lib/main.dart delete mode 100644 native_crypto_android/example/pubspec.lock delete mode 100644 native_crypto_android/example/pubspec.yaml delete mode 100644 native_crypto_android/example/test/widget_test.dart delete mode 100644 native_crypto_ios/.vscode/settings.json delete mode 100644 native_crypto_ios/CHANGELOG.md delete mode 100644 native_crypto_ios/example/.gitignore delete mode 100644 native_crypto_ios/example/.metadata delete mode 100644 native_crypto_ios/example/README.md delete mode 100644 native_crypto_ios/example/analysis_options.yaml delete mode 100644 native_crypto_ios/example/ios/.gitignore delete mode 100644 native_crypto_ios/example/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 native_crypto_ios/example/ios/Flutter/Debug.xcconfig delete mode 100644 native_crypto_ios/example/ios/Flutter/Release.xcconfig delete mode 100644 native_crypto_ios/example/ios/Podfile delete mode 100644 native_crypto_ios/example/ios/Podfile.lock delete mode 100644 native_crypto_ios/example/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 native_crypto_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 native_crypto_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 native_crypto_ios/example/ios/Runner/AppDelegate.swift delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 native_crypto_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 native_crypto_ios/example/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 native_crypto_ios/example/ios/Runner/Info.plist delete mode 100644 native_crypto_ios/example/ios/Runner/Runner-Bridging-Header.h delete mode 100644 native_crypto_ios/example/lib/main.dart delete mode 100644 native_crypto_ios/example/pubspec.yaml delete mode 100644 native_crypto_ios/example/test/widget_test.dart delete mode 100644 native_crypto_platform_interface/CHANGELOG.md delete mode 100644 native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart rename {native_crypto => packages/native_crypto}/.metadata (100%) rename {native_crypto => packages/native_crypto}/CHANGELOG.md (100%) rename {native_crypto => packages/native_crypto}/LICENSE (100%) rename {native_crypto => packages/native_crypto}/README.md (100%) rename {native_crypto => packages/native_crypto}/analysis_options.yaml (100%) rename {native_crypto => packages/native_crypto}/example/.gitignore (100%) rename {native_crypto => packages/native_crypto}/example/.metadata (100%) rename {native_crypto => packages/native_crypto}/example/README.md (100%) rename {native_crypto => packages/native_crypto}/example/analysis_options.yaml (100%) rename {native_crypto => packages/native_crypto}/example/android/.gitignore (100%) rename {native_crypto => packages/native_crypto}/example/android/app/build.gradle (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/debug/AndroidManifest.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/AndroidManifest.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_example/MainActivity.kt (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/drawable-v21/launch_background.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/drawable/launch_background.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/values-night/styles.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/main/res/values/styles.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/app/src/profile/AndroidManifest.xml (100%) rename {native_crypto => packages/native_crypto}/example/android/build.gradle (100%) rename {native_crypto => packages/native_crypto}/example/android/gradle.properties (100%) rename {native_crypto => packages/native_crypto}/example/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {native_crypto => packages/native_crypto}/example/android/settings.gradle (100%) rename {native_crypto => packages/native_crypto}/example/ios/.gitignore (100%) rename {native_crypto => packages/native_crypto}/example/ios/Flutter/AppFrameworkInfo.plist (100%) rename {native_crypto => packages/native_crypto}/example/ios/Flutter/Debug.xcconfig (100%) rename {native_crypto => packages/native_crypto}/example/ios/Flutter/Release.xcconfig (100%) rename {native_crypto => packages/native_crypto}/example/ios/Podfile (100%) rename {native_crypto => packages/native_crypto}/example/ios/Podfile.lock (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcodeproj/project.pbxproj (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/AppDelegate.swift (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Base.lproj/Main.storyboard (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Info.plist (100%) rename {native_crypto => packages/native_crypto}/example/ios/Runner/Runner-Bridging-Header.h (100%) rename {native_crypto => packages/native_crypto}/example/lib/home.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/main.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/pages/benchmark_page.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/pages/cipher_page.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/pages/kdf_page.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/session.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/utils.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/widgets/button.dart (100%) rename {native_crypto => packages/native_crypto}/example/lib/widgets/output.dart (100%) rename {native_crypto => packages/native_crypto}/example/pubspec.yaml (100%) rename {native_crypto => packages/native_crypto}/lib/native_crypto.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/builder.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/builders/aes_builder.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/byte_array.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/cipher.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/cipher_text.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/ciphers/aes.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/exceptions.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/hasher.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/hashers/sha256.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/hashers/sha384.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/hashers/sha512.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/kdf/pbkdf2.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/key.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/keyderivation.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/keys/secret_key.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/platform.dart (100%) rename {native_crypto => packages/native_crypto}/lib/src/utils.dart (100%) rename {native_crypto => packages/native_crypto}/pubspec.yaml (100%) rename {native_crypto_android => packages/native_crypto_android}/.gitignore (100%) rename {native_crypto_android => packages/native_crypto_android}/.metadata (100%) create mode 100644 packages/native_crypto_android/CHANGELOG.md rename {native_crypto_android => packages/native_crypto_android}/LICENSE (100%) rename {native_crypto_android => packages/native_crypto_android}/README.md (100%) rename {native_crypto_android => packages/native_crypto_android}/android/.gitignore (100%) rename {native_crypto_android => packages/native_crypto_android}/android/build.gradle (100%) rename {native_crypto_android => packages/native_crypto_android}/android/gradle/wrapper/gradle-wrapper.jar (100%) rename {native_crypto_android => packages/native_crypto_android}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {native_crypto_android => packages/native_crypto_android}/android/gradlew (100%) rename {native_crypto_android => packages/native_crypto_android}/android/gradlew.bat (100%) rename {native_crypto_android => packages/native_crypto_android}/android/settings.gradle (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/AndroidManifest.xml (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt (100%) rename {native_crypto_android => packages/native_crypto_android}/pubspec.yaml (100%) rename {native_crypto_ios => packages/native_crypto_ios}/.gitignore (100%) rename {native_crypto_ios => packages/native_crypto_ios}/.metadata (100%) create mode 100644 packages/native_crypto_ios/CHANGELOG.md rename {native_crypto_ios => packages/native_crypto_ios}/LICENSE (100%) rename {native_crypto_ios => packages/native_crypto_ios}/README.md (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/.gitignore (100%) create mode 100644 packages/native_crypto_ios/ios/Assets/.gitkeep rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/Cipher.swift (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/Hash.swift (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/KEM.swift (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/Key.swift (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/NativeCryptoIosPlugin.h (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/NativeCryptoIosPlugin.m (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/Classes/SwiftNativeCryptoIosPlugin.swift (100%) rename {native_crypto_ios => packages/native_crypto_ios}/ios/native_crypto_ios.podspec (100%) rename {native_crypto_ios => packages/native_crypto_ios}/pubspec.yaml (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/.gitignore (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/.metadata (100%) create mode 100644 packages/native_crypto_platform_interface/CHANGELOG.md rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/LICENSE (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/README.md (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/analysis_options.yaml (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/lib/native_crypto_platform_interface.dart (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/lib/src/method_channel_native_crypto.dart (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/lib/src/platform_interface.dart (100%) rename {native_crypto_platform_interface => packages/native_crypto_platform_interface}/pubspec.yaml (100%) diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d9808a7 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to this project. Names should be added to the list like so: +# +# Name/Organization + +Hugo Pointcheval \ No newline at end of file diff --git a/native_crypto_ios/ios/Assets/.gitkeep b/CHANGELOG.md similarity index 100% rename from native_crypto_ios/ios/Assets/.gitkeep rename to CHANGELOG.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2f00d5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Hugo Pointcheval + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..906aa1b --- /dev/null +++ b/melos.yaml @@ -0,0 +1,39 @@ +name: NativeCrypto +repository: https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter + +packages: + - packages/** + +command: + version: + updateGitTagRefs: true + linkToCommits: false # Gitea not support this + workspaceChangelog: true + branch: master + +scripts: + lint:all: + run: melos run analyze && melos run format + description: Run all static analysis checks. + + analyze: + run: | + melos exec -c 10 -- \ + flutter analyze --fatal-infos + description: Run `flutter analyze` for all packages. + + format: + run: melos exec flutter format . --fix + description: Run `flutter format` for all packages. + + format-check: + run: melos exec flutter format . --set-exit-if-changed + description: Run `flutter format` checks for all packages. + + clean:deep: + run: git clean -x -d -f -q + description: Clean things very deeply with `git clean`. + + # Additional cleanup lifecycle script, executed when `melos clean` is run. + postclean: > + melos exec -c 6 -- "flutter clean" \ No newline at end of file diff --git a/native_crypto/.gitignore b/native_crypto/.gitignore deleted file mode 100644 index 9a04984..0000000 --- a/native_crypto/.gitignore +++ /dev/null @@ -1,389 +0,0 @@ -# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig - -# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,dart,flutter,intellij+all,kotlin,linux,swift,windows -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,dart,flutter,intellij+all,kotlin,linux,swift,windows - -### Dart ### -# See https://www.dartlang.org/guides/libraries/private-files - -# Files and directories created by pub -.dart_tool/ -.packages -build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ - -# dotenv environment variables file -.env* - -# Avoid committing generated Javascript files: -*.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ -*.js.deps -*.js.map - -.flutter-plugins -.flutter-plugins-dependencies - -### Dart Patch ### -# dotenv environment variables file -.env - -### Flutter ### -# Flutter/Dart/Pub related -**/doc/api/ -.fvm/ -.pub-cache/ -.pub/ -coverage/ -lib/generated_plugin_registrant.dart -# For library packages, don’t commit the pubspec.lock file. -# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. -# See https://dart.dev/guides/libraries/private-files#pubspeclock -#pubspec.lock - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/key.properties -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages - -### Intellij+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij+all Patch ### -# Ignores the whole .idea folder and all .iml files -# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 - -.idea/ - -# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 - -*.iml -modules.xml -.idea/misc.xml -*.ipr - -# Sonarlint plugin -.idea/sonarlint - -### Kotlin ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Swift ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# Pods/ -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# Support for Project snippet scope -!.vscode/*.code-snippets - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,dart,flutter,intellij+all,kotlin,linux,swift,windows - -# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) - diff --git a/native_crypto/.vscode/settings.json b/native_crypto/.vscode/settings.json deleted file mode 100644 index 909bed1..0000000 --- a/native_crypto/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dart.flutterSdkPath": "/Users/hpcl/.flutter/2.8.1" -} \ No newline at end of file diff --git a/native_crypto/example/test/widget_test.dart b/native_crypto/example/test/widget_test.dart deleted file mode 100644 index ffdf51c..0000000 --- a/native_crypto/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:native_crypto_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/native_crypto/test/native_crypto_test.dart b/native_crypto/test/native_crypto_test.dart deleted file mode 100644 index 0ffdd02..0000000 --- a/native_crypto/test/native_crypto_test.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/native_crypto_android/CHANGELOG.md b/native_crypto_android/CHANGELOG.md deleted file mode 100644 index 403c448..0000000 --- a/native_crypto_android/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_android/example/.gitignore b/native_crypto_android/example/.gitignore deleted file mode 100644 index 0fa6b67..0000000 --- a/native_crypto_android/example/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/native_crypto_android/example/.metadata b/native_crypto_android/example/.metadata deleted file mode 100644 index fd70cab..0000000 --- a/native_crypto_android/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b - channel: stable - -project_type: app diff --git a/native_crypto_android/example/README.md b/native_crypto_android/example/README.md deleted file mode 100644 index 8490cfc..0000000 --- a/native_crypto_android/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# native_crypto_android_example - -Demonstrates how to use the native_crypto_android plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/native_crypto_android/example/analysis_options.yaml b/native_crypto_android/example/analysis_options.yaml deleted file mode 100644 index 61b6c4d..0000000 --- a/native_crypto_android/example/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/native_crypto_android/example/android/.gitignore b/native_crypto_android/example/android/.gitignore deleted file mode 100644 index 6f56801..0000000 --- a/native_crypto_android/example/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/native_crypto_android/example/android/app/build.gradle b/native_crypto_android/example/android/app/build.gradle deleted file mode 100644 index 3ec37e2..0000000 --- a/native_crypto_android/example/android/app/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion flutter.compileSdkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "fr.pointcheval.native_crypto_android_example" - minSdkVersion 26 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml b/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 758ae3c..0000000 --- a/native_crypto_android/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/native_crypto_android/example/android/app/src/main/AndroidManifest.xml b/native_crypto_android/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 4d48fcc..0000000 --- a/native_crypto_android/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt b/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt deleted file mode 100644 index 0d084ac..0000000 --- a/native_crypto_android/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_android_example/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package fr.pointcheval.native_crypto_android_example - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml b/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f..0000000 --- a/native_crypto_android/example/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml b/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/native_crypto_android/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/native_crypto_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml b/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 3db14bb..0000000 --- a/native_crypto_android/example/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/native_crypto_android/example/android/app/src/main/res/values/styles.xml b/native_crypto_android/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index d460d1e..0000000 --- a/native_crypto_android/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml b/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 758ae3c..0000000 --- a/native_crypto_android/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/native_crypto_android/example/android/build.gradle b/native_crypto_android/example/android/build.gradle deleted file mode 100644 index 24047dc..0000000 --- a/native_crypto_android/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.3.50' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/native_crypto_android/example/android/gradle.properties b/native_crypto_android/example/android/gradle.properties deleted file mode 100644 index 94adc3a..0000000 --- a/native_crypto_android/example/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties b/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index bc6a58a..0000000 --- a/native_crypto_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/native_crypto_android/example/android/settings.gradle b/native_crypto_android/example/android/settings.gradle deleted file mode 100644 index 44e62bc..0000000 --- a/native_crypto_android/example/android/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/native_crypto_android/example/lib/main.dart b/native_crypto_android/example/lib/main.dart deleted file mode 100644 index ce84ce0..0000000 --- a/native_crypto_android/example/lib/main.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: main.dart -// Created Date: 27/12/2021 22:43:20 -// Last Modified: 28/12/2021 18:18:44 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -void main() { - runApp(const MyApp()); -} - -void run() async { - NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; - - debugPrint("Benchmark"); - - int size = 25; - int iterations = 25; - int totalEnc = 0; - int totalDec = 0; - - debugPrint("Size: $size MB"); - Uint8List? secretKey = await _nativeCryptoPlatform.generateSecretKey(128); - - debugPrint("Generate random data..."); - var before = DateTime.now(); - Uint8List data = Uint8List(size * 1024 * 1024); - var after = DateTime.now(); - debugPrint("Generate random data: ${after.difference(before).inMilliseconds} ms"); - - for (var _ in List.generate(iterations, (index) => index)) { - debugPrint("Encrypt data..."); - before = DateTime.now(); - Uint8List? encrypted = await _nativeCryptoPlatform.encrypt(data, secretKey!, "aes"); - after = DateTime.now(); - debugPrint("Encrypt data: ${after.difference(before).inMilliseconds} ms"); - totalEnc += after.difference(before).inMilliseconds; - - debugPrint("Decrypt data..."); - before = DateTime.now(); - data = (await _nativeCryptoPlatform.decrypt(encrypted!, secretKey, "aes"))!; - after = DateTime.now(); - debugPrint("Decrypt data: ${after.difference(before).inMilliseconds} ms"); - totalDec += after.difference(before).inMilliseconds; - } - - debugPrint("Average Encrypt: ${totalEnc/iterations} ms for $size MB"); - debugPrint("Average Decrypt: ${totalDec/iterations} ms for $size MB"); - -} - -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: MyHomePage(), - ); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - run(); - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: const Center( - child: Text("Check the console"), - ), - ); - } -} diff --git a/native_crypto_android/example/pubspec.lock b/native_crypto_android/example/pubspec.lock deleted file mode 100644 index ecc71be..0000000 --- a/native_crypto_android/example/pubspec.lock +++ /dev/null @@ -1,182 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - native_crypto_android: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.1.0" - native_crypto_platform_interface: - dependency: "direct main" - description: - path: "../../native_crypto_platform_interface" - relative: true - source: path - version: "0.1.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.3" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" -sdks: - dart: ">=2.15.1 <3.0.0" - flutter: ">=2.5.0" diff --git a/native_crypto_android/example/pubspec.yaml b/native_crypto_android/example/pubspec.yaml deleted file mode 100644 index 3b1d332..0000000 --- a/native_crypto_android/example/pubspec.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: native_crypto_android_example -description: Demonstrates how to use the native_crypto_android plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -environment: - sdk: ">=2.15.1 <3.0.0" - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - native_crypto_android: - # When depending on this package from a real application you should use: - # native_crypto_android: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - native_crypto_platform_interface: - path: ../../native_crypto_platform_interface - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^1.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/native_crypto_android/example/test/widget_test.dart b/native_crypto_android/example/test/widget_test.dart deleted file mode 100644 index e4edd16..0000000 --- a/native_crypto_android/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:native_crypto_android_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/native_crypto_ios/.vscode/settings.json b/native_crypto_ios/.vscode/settings.json deleted file mode 100644 index 909bed1..0000000 --- a/native_crypto_ios/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dart.flutterSdkPath": "/Users/hpcl/.flutter/2.8.1" -} \ No newline at end of file diff --git a/native_crypto_ios/CHANGELOG.md b/native_crypto_ios/CHANGELOG.md deleted file mode 100644 index 403c448..0000000 --- a/native_crypto_ios/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_ios/example/.gitignore b/native_crypto_ios/example/.gitignore deleted file mode 100644 index 0fa6b67..0000000 --- a/native_crypto_ios/example/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/native_crypto_ios/example/.metadata b/native_crypto_ios/example/.metadata deleted file mode 100644 index ee7f61d..0000000 --- a/native_crypto_ios/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: cf4400006550b70f28e4b4af815151d1e74846c6 - channel: stable - -project_type: app diff --git a/native_crypto_ios/example/README.md b/native_crypto_ios/example/README.md deleted file mode 100644 index 340d8c1..0000000 --- a/native_crypto_ios/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# native_crypto_ios_example - -Demonstrates how to use the native_crypto_ios plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/native_crypto_ios/example/analysis_options.yaml b/native_crypto_ios/example/analysis_options.yaml deleted file mode 100644 index 61b6c4d..0000000 --- a/native_crypto_ios/example/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/native_crypto_ios/example/ios/.gitignore b/native_crypto_ios/example/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/native_crypto_ios/example/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/native_crypto_ios/example/ios/Flutter/AppFrameworkInfo.plist b/native_crypto_ios/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 8d4492f..0000000 --- a/native_crypto_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 9.0 - - diff --git a/native_crypto_ios/example/ios/Flutter/Debug.xcconfig b/native_crypto_ios/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6..0000000 --- a/native_crypto_ios/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/native_crypto_ios/example/ios/Flutter/Release.xcconfig b/native_crypto_ios/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bf..0000000 --- a/native_crypto_ios/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/native_crypto_ios/example/ios/Podfile b/native_crypto_ios/example/ios/Podfile deleted file mode 100644 index 10f3c9b..0000000 --- a/native_crypto_ios/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/native_crypto_ios/example/ios/Podfile.lock b/native_crypto_ios/example/ios/Podfile.lock deleted file mode 100644 index e1bca6a..0000000 --- a/native_crypto_ios/example/ios/Podfile.lock +++ /dev/null @@ -1,22 +0,0 @@ -PODS: - - Flutter (1.0.0) - - native_crypto_ios (0.0.1): - - Flutter - -DEPENDENCIES: - - Flutter (from `Flutter`) - - native_crypto_ios (from `.symlinks/plugins/native_crypto_ios/ios`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - native_crypto_ios: - :path: ".symlinks/plugins/native_crypto_ios/ios" - -SPEC CHECKSUMS: - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - native_crypto_ios: de03ec2f594e8d41bcba2341b7ad57fd926ada5d - -PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b - -COCOAPODS: 1.10.1 diff --git a/native_crypto_ios/example/ios/Runner.xcodeproj/project.pbxproj b/native_crypto_ios/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 7156cef..0000000 --- a/native_crypto_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,552 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 20AC864B3037BB896BD381B1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 056766980834B5FAC1C4D331 /* Pods_Runner.framework */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 056766980834B5FAC1C4D331 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 1180301722C337B6C625E46F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2A0F0FD6D80A80663D317892 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EC40AF9C4AC2A38A0B8CC0E6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 20AC864B3037BB896BD381B1 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 3024D600AF0E0DED3BB44E03 /* Pods */ = { - isa = PBXGroup; - children = ( - EC40AF9C4AC2A38A0B8CC0E6 /* Pods-Runner.debug.xcconfig */, - 2A0F0FD6D80A80663D317892 /* Pods-Runner.release.xcconfig */, - 1180301722C337B6C625E46F /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 3024D600AF0E0DED3BB44E03 /* Pods */, - F589250F1A900F4BC6E4BB56 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - F589250F1A900F4BC6E4BB56 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 056766980834B5FAC1C4D331 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - CE2C3E978245C5FF80E431AC /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ED05A5A0A8AF0FA8F71C2C2B /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - CE2C3E978245C5FF80E431AC /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - ED05A5A0A8AF0FA8F71C2C2B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6Z5P8GG96U; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = fr.pointcheval.nativeCryptoIosExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6Z5P8GG96U; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = fr.pointcheval.nativeCryptoIosExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6Z5P8GG96U; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = fr.pointcheval.nativeCryptoIosExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/native_crypto_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/native_crypto_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/native_crypto_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index c87d15a..0000000 --- a/native_crypto_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/native_crypto_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/native_crypto_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc1..0000000 --- a/native_crypto_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/native_crypto_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/native_crypto_ios/example/ios/Runner/AppDelegate.swift b/native_crypto_ios/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4..0000000 --- a/native_crypto_ios/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/native_crypto_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/native_crypto_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/native_crypto_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/native_crypto_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/native_crypto_ios/example/ios/Runner/Base.lproj/Main.storyboard b/native_crypto_ios/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/native_crypto_ios/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/native_crypto_ios/example/ios/Runner/Info.plist b/native_crypto_ios/example/ios/Runner/Info.plist deleted file mode 100644 index 292a791..0000000 --- a/native_crypto_ios/example/ios/Runner/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Native Crypto Ios - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - native_crypto_ios_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/native_crypto_ios/example/ios/Runner/Runner-Bridging-Header.h b/native_crypto_ios/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/native_crypto_ios/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/native_crypto_ios/example/lib/main.dart b/native_crypto_ios/example/lib/main.dart deleted file mode 100644 index f11a109..0000000 --- a/native_crypto_ios/example/lib/main.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: main.dart -// Created Date: 27/12/2021 22:43:20 -// Last Modified: 28/12/2021 18:32:25 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -void main() { - runApp(const MyApp()); -} - -void run() async { - NativeCryptoPlatform _nativeCryptoPlatform = NativeCryptoPlatform.instance; - - debugPrint("Benchmark"); - - int size = 256; - int iterations = 25; - int totalEnc = 0; - int totalDec = 0; - - debugPrint("Size: $size MB"); - Uint8List? secretKey = await _nativeCryptoPlatform.generateSecretKey(128); - - debugPrint("Generate random data..."); - var before = DateTime.now(); - Uint8List data = Uint8List(size * 1024 * 1024); - var after = DateTime.now(); - debugPrint("Generate random data: ${after.difference(before).inMilliseconds} ms"); - - for (var _ in List.generate(iterations, (index) => index)) { - debugPrint("Encrypt data..."); - before = DateTime.now(); - Uint8List? encrypted = await _nativeCryptoPlatform.encrypt(data, secretKey!, "aes"); - after = DateTime.now(); - debugPrint("Encrypt data: ${after.difference(before).inMilliseconds} ms"); - totalEnc += after.difference(before).inMilliseconds; - - debugPrint("Decrypt data..."); - before = DateTime.now(); - data = (await _nativeCryptoPlatform.decrypt(encrypted!, secretKey, "aes"))!; - after = DateTime.now(); - debugPrint("Decrypt data: ${after.difference(before).inMilliseconds} ms"); - totalDec += after.difference(before).inMilliseconds; - } - - debugPrint("Average Encrypt: ${totalEnc/iterations} ms for $size MB"); - debugPrint("Average Decrypt: ${totalDec/iterations} ms for $size MB"); - -} - -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: MyHomePage(), - ); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - run(); - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: const Center( - child: Text("Check the console"), - ), - ); - } -} diff --git a/native_crypto_ios/example/pubspec.yaml b/native_crypto_ios/example/pubspec.yaml deleted file mode 100644 index ab0098f..0000000 --- a/native_crypto_ios/example/pubspec.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: native_crypto_ios_example -description: Demonstrates how to use the native_crypto_ios plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -environment: - sdk: ">=2.15.0 <3.0.0" - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - native_crypto_ios: - # When depending on this package from a real application you should use: - # native_crypto_ios: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - native_crypto_platform_interface: - path: ../../native_crypto_platform_interface - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^1.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/native_crypto_ios/example/test/widget_test.dart b/native_crypto_ios/example/test/widget_test.dart deleted file mode 100644 index 75f369c..0000000 --- a/native_crypto_ios/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:native_crypto_ios_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/native_crypto_platform_interface/CHANGELOG.md b/native_crypto_platform_interface/CHANGELOG.md deleted file mode 100644 index 403c448..0000000 --- a/native_crypto_platform_interface/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -Check [`native_crypto`](../native_crypto/CHANGELOG.md) plugin's changelog. \ No newline at end of file diff --git a/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart b/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart deleted file mode 100644 index 0ffdd02..0000000 --- a/native_crypto_platform_interface/test/native_crypto_platform_interface_test.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/native_crypto/.metadata b/packages/native_crypto/.metadata similarity index 100% rename from native_crypto/.metadata rename to packages/native_crypto/.metadata diff --git a/native_crypto/CHANGELOG.md b/packages/native_crypto/CHANGELOG.md similarity index 100% rename from native_crypto/CHANGELOG.md rename to packages/native_crypto/CHANGELOG.md diff --git a/native_crypto/LICENSE b/packages/native_crypto/LICENSE similarity index 100% rename from native_crypto/LICENSE rename to packages/native_crypto/LICENSE diff --git a/native_crypto/README.md b/packages/native_crypto/README.md similarity index 100% rename from native_crypto/README.md rename to packages/native_crypto/README.md diff --git a/native_crypto/analysis_options.yaml b/packages/native_crypto/analysis_options.yaml similarity index 100% rename from native_crypto/analysis_options.yaml rename to packages/native_crypto/analysis_options.yaml diff --git a/native_crypto/example/.gitignore b/packages/native_crypto/example/.gitignore similarity index 100% rename from native_crypto/example/.gitignore rename to packages/native_crypto/example/.gitignore diff --git a/native_crypto/example/.metadata b/packages/native_crypto/example/.metadata similarity index 100% rename from native_crypto/example/.metadata rename to packages/native_crypto/example/.metadata diff --git a/native_crypto/example/README.md b/packages/native_crypto/example/README.md similarity index 100% rename from native_crypto/example/README.md rename to packages/native_crypto/example/README.md diff --git a/native_crypto/example/analysis_options.yaml b/packages/native_crypto/example/analysis_options.yaml similarity index 100% rename from native_crypto/example/analysis_options.yaml rename to packages/native_crypto/example/analysis_options.yaml diff --git a/native_crypto/example/android/.gitignore b/packages/native_crypto/example/android/.gitignore similarity index 100% rename from native_crypto/example/android/.gitignore rename to packages/native_crypto/example/android/.gitignore diff --git a/native_crypto/example/android/app/build.gradle b/packages/native_crypto/example/android/app/build.gradle similarity index 100% rename from native_crypto/example/android/app/build.gradle rename to packages/native_crypto/example/android/app/build.gradle diff --git a/native_crypto/example/android/app/src/debug/AndroidManifest.xml b/packages/native_crypto/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from native_crypto/example/android/app/src/debug/AndroidManifest.xml rename to packages/native_crypto/example/android/app/src/debug/AndroidManifest.xml diff --git a/native_crypto/example/android/app/src/main/AndroidManifest.xml b/packages/native_crypto/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from native_crypto/example/android/app/src/main/AndroidManifest.xml rename to packages/native_crypto/example/android/app/src/main/AndroidManifest.xml diff --git a/native_crypto/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_example/MainActivity.kt b/packages/native_crypto/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_example/MainActivity.kt similarity index 100% rename from native_crypto/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_example/MainActivity.kt rename to packages/native_crypto/example/android/app/src/main/kotlin/fr/pointcheval/native_crypto_example/MainActivity.kt diff --git a/native_crypto/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/native_crypto/example/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from native_crypto/example/android/app/src/main/res/drawable-v21/launch_background.xml rename to packages/native_crypto/example/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/native_crypto/example/android/app/src/main/res/drawable/launch_background.xml b/packages/native_crypto/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from native_crypto/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/native_crypto/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/native_crypto/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/native_crypto/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from native_crypto/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/native_crypto/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/native_crypto/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/native_crypto/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from native_crypto/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/native_crypto/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/native_crypto/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/native_crypto/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from native_crypto/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/native_crypto/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/native_crypto/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/native_crypto/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from native_crypto/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/native_crypto/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/native_crypto/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/native_crypto/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from native_crypto/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/native_crypto/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/native_crypto/example/android/app/src/main/res/values-night/styles.xml b/packages/native_crypto/example/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from native_crypto/example/android/app/src/main/res/values-night/styles.xml rename to packages/native_crypto/example/android/app/src/main/res/values-night/styles.xml diff --git a/native_crypto/example/android/app/src/main/res/values/styles.xml b/packages/native_crypto/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from native_crypto/example/android/app/src/main/res/values/styles.xml rename to packages/native_crypto/example/android/app/src/main/res/values/styles.xml diff --git a/native_crypto/example/android/app/src/profile/AndroidManifest.xml b/packages/native_crypto/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from native_crypto/example/android/app/src/profile/AndroidManifest.xml rename to packages/native_crypto/example/android/app/src/profile/AndroidManifest.xml diff --git a/native_crypto/example/android/build.gradle b/packages/native_crypto/example/android/build.gradle similarity index 100% rename from native_crypto/example/android/build.gradle rename to packages/native_crypto/example/android/build.gradle diff --git a/native_crypto/example/android/gradle.properties b/packages/native_crypto/example/android/gradle.properties similarity index 100% rename from native_crypto/example/android/gradle.properties rename to packages/native_crypto/example/android/gradle.properties diff --git a/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/native_crypto/example/android/settings.gradle b/packages/native_crypto/example/android/settings.gradle similarity index 100% rename from native_crypto/example/android/settings.gradle rename to packages/native_crypto/example/android/settings.gradle diff --git a/native_crypto/example/ios/.gitignore b/packages/native_crypto/example/ios/.gitignore similarity index 100% rename from native_crypto/example/ios/.gitignore rename to packages/native_crypto/example/ios/.gitignore diff --git a/native_crypto/example/ios/Flutter/AppFrameworkInfo.plist b/packages/native_crypto/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from native_crypto/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/native_crypto/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/native_crypto/example/ios/Flutter/Debug.xcconfig b/packages/native_crypto/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from native_crypto/example/ios/Flutter/Debug.xcconfig rename to packages/native_crypto/example/ios/Flutter/Debug.xcconfig diff --git a/native_crypto/example/ios/Flutter/Release.xcconfig b/packages/native_crypto/example/ios/Flutter/Release.xcconfig similarity index 100% rename from native_crypto/example/ios/Flutter/Release.xcconfig rename to packages/native_crypto/example/ios/Flutter/Release.xcconfig diff --git a/native_crypto/example/ios/Podfile b/packages/native_crypto/example/ios/Podfile similarity index 100% rename from native_crypto/example/ios/Podfile rename to packages/native_crypto/example/ios/Podfile diff --git a/native_crypto/example/ios/Podfile.lock b/packages/native_crypto/example/ios/Podfile.lock similarity index 100% rename from native_crypto/example/ios/Podfile.lock rename to packages/native_crypto/example/ios/Podfile.lock diff --git a/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj b/packages/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from native_crypto/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/native_crypto/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/native_crypto/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/native_crypto/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/native_crypto/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from native_crypto/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/native_crypto/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/native_crypto/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from native_crypto/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from native_crypto/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/native_crypto/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/native_crypto/example/ios/Runner/AppDelegate.swift b/packages/native_crypto/example/ios/Runner/AppDelegate.swift similarity index 100% rename from native_crypto/example/ios/Runner/AppDelegate.swift rename to packages/native_crypto/example/ios/Runner/AppDelegate.swift diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/native_crypto/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/native_crypto/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/native_crypto/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from native_crypto/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/native_crypto/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/native_crypto/example/ios/Runner/Base.lproj/Main.storyboard b/packages/native_crypto/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from native_crypto/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/native_crypto/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/native_crypto/example/ios/Runner/Info.plist b/packages/native_crypto/example/ios/Runner/Info.plist similarity index 100% rename from native_crypto/example/ios/Runner/Info.plist rename to packages/native_crypto/example/ios/Runner/Info.plist diff --git a/native_crypto/example/ios/Runner/Runner-Bridging-Header.h b/packages/native_crypto/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from native_crypto/example/ios/Runner/Runner-Bridging-Header.h rename to packages/native_crypto/example/ios/Runner/Runner-Bridging-Header.h diff --git a/native_crypto/example/lib/home.dart b/packages/native_crypto/example/lib/home.dart similarity index 100% rename from native_crypto/example/lib/home.dart rename to packages/native_crypto/example/lib/home.dart diff --git a/native_crypto/example/lib/main.dart b/packages/native_crypto/example/lib/main.dart similarity index 100% rename from native_crypto/example/lib/main.dart rename to packages/native_crypto/example/lib/main.dart diff --git a/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart similarity index 100% rename from native_crypto/example/lib/pages/benchmark_page.dart rename to packages/native_crypto/example/lib/pages/benchmark_page.dart diff --git a/native_crypto/example/lib/pages/cipher_page.dart b/packages/native_crypto/example/lib/pages/cipher_page.dart similarity index 100% rename from native_crypto/example/lib/pages/cipher_page.dart rename to packages/native_crypto/example/lib/pages/cipher_page.dart diff --git a/native_crypto/example/lib/pages/kdf_page.dart b/packages/native_crypto/example/lib/pages/kdf_page.dart similarity index 100% rename from native_crypto/example/lib/pages/kdf_page.dart rename to packages/native_crypto/example/lib/pages/kdf_page.dart diff --git a/native_crypto/example/lib/session.dart b/packages/native_crypto/example/lib/session.dart similarity index 100% rename from native_crypto/example/lib/session.dart rename to packages/native_crypto/example/lib/session.dart diff --git a/native_crypto/example/lib/utils.dart b/packages/native_crypto/example/lib/utils.dart similarity index 100% rename from native_crypto/example/lib/utils.dart rename to packages/native_crypto/example/lib/utils.dart diff --git a/native_crypto/example/lib/widgets/button.dart b/packages/native_crypto/example/lib/widgets/button.dart similarity index 100% rename from native_crypto/example/lib/widgets/button.dart rename to packages/native_crypto/example/lib/widgets/button.dart diff --git a/native_crypto/example/lib/widgets/output.dart b/packages/native_crypto/example/lib/widgets/output.dart similarity index 100% rename from native_crypto/example/lib/widgets/output.dart rename to packages/native_crypto/example/lib/widgets/output.dart diff --git a/native_crypto/example/pubspec.yaml b/packages/native_crypto/example/pubspec.yaml similarity index 100% rename from native_crypto/example/pubspec.yaml rename to packages/native_crypto/example/pubspec.yaml diff --git a/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart similarity index 100% rename from native_crypto/lib/native_crypto.dart rename to packages/native_crypto/lib/native_crypto.dart diff --git a/native_crypto/lib/src/builder.dart b/packages/native_crypto/lib/src/builder.dart similarity index 100% rename from native_crypto/lib/src/builder.dart rename to packages/native_crypto/lib/src/builder.dart diff --git a/native_crypto/lib/src/builders/aes_builder.dart b/packages/native_crypto/lib/src/builders/aes_builder.dart similarity index 100% rename from native_crypto/lib/src/builders/aes_builder.dart rename to packages/native_crypto/lib/src/builders/aes_builder.dart diff --git a/native_crypto/lib/src/byte_array.dart b/packages/native_crypto/lib/src/byte_array.dart similarity index 100% rename from native_crypto/lib/src/byte_array.dart rename to packages/native_crypto/lib/src/byte_array.dart diff --git a/native_crypto/lib/src/cipher.dart b/packages/native_crypto/lib/src/cipher.dart similarity index 100% rename from native_crypto/lib/src/cipher.dart rename to packages/native_crypto/lib/src/cipher.dart diff --git a/native_crypto/lib/src/cipher_text.dart b/packages/native_crypto/lib/src/cipher_text.dart similarity index 100% rename from native_crypto/lib/src/cipher_text.dart rename to packages/native_crypto/lib/src/cipher_text.dart diff --git a/native_crypto/lib/src/ciphers/aes.dart b/packages/native_crypto/lib/src/ciphers/aes.dart similarity index 100% rename from native_crypto/lib/src/ciphers/aes.dart rename to packages/native_crypto/lib/src/ciphers/aes.dart diff --git a/native_crypto/lib/src/exceptions.dart b/packages/native_crypto/lib/src/exceptions.dart similarity index 100% rename from native_crypto/lib/src/exceptions.dart rename to packages/native_crypto/lib/src/exceptions.dart diff --git a/native_crypto/lib/src/hasher.dart b/packages/native_crypto/lib/src/hasher.dart similarity index 100% rename from native_crypto/lib/src/hasher.dart rename to packages/native_crypto/lib/src/hasher.dart diff --git a/native_crypto/lib/src/hashers/sha256.dart b/packages/native_crypto/lib/src/hashers/sha256.dart similarity index 100% rename from native_crypto/lib/src/hashers/sha256.dart rename to packages/native_crypto/lib/src/hashers/sha256.dart diff --git a/native_crypto/lib/src/hashers/sha384.dart b/packages/native_crypto/lib/src/hashers/sha384.dart similarity index 100% rename from native_crypto/lib/src/hashers/sha384.dart rename to packages/native_crypto/lib/src/hashers/sha384.dart diff --git a/native_crypto/lib/src/hashers/sha512.dart b/packages/native_crypto/lib/src/hashers/sha512.dart similarity index 100% rename from native_crypto/lib/src/hashers/sha512.dart rename to packages/native_crypto/lib/src/hashers/sha512.dart diff --git a/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart similarity index 100% rename from native_crypto/lib/src/kdf/pbkdf2.dart rename to packages/native_crypto/lib/src/kdf/pbkdf2.dart diff --git a/native_crypto/lib/src/key.dart b/packages/native_crypto/lib/src/key.dart similarity index 100% rename from native_crypto/lib/src/key.dart rename to packages/native_crypto/lib/src/key.dart diff --git a/native_crypto/lib/src/keyderivation.dart b/packages/native_crypto/lib/src/keyderivation.dart similarity index 100% rename from native_crypto/lib/src/keyderivation.dart rename to packages/native_crypto/lib/src/keyderivation.dart diff --git a/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart similarity index 100% rename from native_crypto/lib/src/keys/secret_key.dart rename to packages/native_crypto/lib/src/keys/secret_key.dart diff --git a/native_crypto/lib/src/platform.dart b/packages/native_crypto/lib/src/platform.dart similarity index 100% rename from native_crypto/lib/src/platform.dart rename to packages/native_crypto/lib/src/platform.dart diff --git a/native_crypto/lib/src/utils.dart b/packages/native_crypto/lib/src/utils.dart similarity index 100% rename from native_crypto/lib/src/utils.dart rename to packages/native_crypto/lib/src/utils.dart diff --git a/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml similarity index 100% rename from native_crypto/pubspec.yaml rename to packages/native_crypto/pubspec.yaml diff --git a/native_crypto_android/.gitignore b/packages/native_crypto_android/.gitignore similarity index 100% rename from native_crypto_android/.gitignore rename to packages/native_crypto_android/.gitignore diff --git a/native_crypto_android/.metadata b/packages/native_crypto_android/.metadata similarity index 100% rename from native_crypto_android/.metadata rename to packages/native_crypto_android/.metadata diff --git a/packages/native_crypto_android/CHANGELOG.md b/packages/native_crypto_android/CHANGELOG.md new file mode 100644 index 0000000..d7c6738 --- /dev/null +++ b/packages/native_crypto_android/CHANGELOG.md @@ -0,0 +1,35 @@ +## 0.1.0 + +> Breaking changes ! + +* Follow **Federated Plugin** Flutter standard. + +## 0.0.6 + +* Add KeyPair generation. +* Rework exposed API. + +## 0.0.5 + +* New API. +* Add digest support. +* Clean platform specific code base. + +## 0.0.4 + +* Improve AES. + +## 0.0.3 + +* Add PBKDF2 support. +* Add exceptions. +* Improve documentation. + +## 0.0.2 + +* Add different key size support. +* Improve performances. + +## 0.0.1 + +* First AES cross-platform encryption & decryption implementation. diff --git a/native_crypto_android/LICENSE b/packages/native_crypto_android/LICENSE similarity index 100% rename from native_crypto_android/LICENSE rename to packages/native_crypto_android/LICENSE diff --git a/native_crypto_android/README.md b/packages/native_crypto_android/README.md similarity index 100% rename from native_crypto_android/README.md rename to packages/native_crypto_android/README.md diff --git a/native_crypto_android/android/.gitignore b/packages/native_crypto_android/android/.gitignore similarity index 100% rename from native_crypto_android/android/.gitignore rename to packages/native_crypto_android/android/.gitignore diff --git a/native_crypto_android/android/build.gradle b/packages/native_crypto_android/android/build.gradle similarity index 100% rename from native_crypto_android/android/build.gradle rename to packages/native_crypto_android/android/build.gradle diff --git a/native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar b/packages/native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar rename to packages/native_crypto_android/android/gradle/wrapper/gradle-wrapper.jar diff --git a/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties rename to packages/native_crypto_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/native_crypto_android/android/gradlew b/packages/native_crypto_android/android/gradlew similarity index 100% rename from native_crypto_android/android/gradlew rename to packages/native_crypto_android/android/gradlew diff --git a/native_crypto_android/android/gradlew.bat b/packages/native_crypto_android/android/gradlew.bat similarity index 100% rename from native_crypto_android/android/gradlew.bat rename to packages/native_crypto_android/android/gradlew.bat diff --git a/native_crypto_android/android/settings.gradle b/packages/native_crypto_android/android/settings.gradle similarity index 100% rename from native_crypto_android/android/settings.gradle rename to packages/native_crypto_android/android/settings.gradle diff --git a/native_crypto_android/android/src/main/AndroidManifest.xml b/packages/native_crypto_android/android/src/main/AndroidManifest.xml similarity index 100% rename from native_crypto_android/android/src/main/AndroidManifest.xml rename to packages/native_crypto_android/android/src/main/AndroidManifest.xml diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt diff --git a/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt similarity index 100% rename from native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt rename to packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt diff --git a/native_crypto_android/pubspec.yaml b/packages/native_crypto_android/pubspec.yaml similarity index 100% rename from native_crypto_android/pubspec.yaml rename to packages/native_crypto_android/pubspec.yaml diff --git a/native_crypto_ios/.gitignore b/packages/native_crypto_ios/.gitignore similarity index 100% rename from native_crypto_ios/.gitignore rename to packages/native_crypto_ios/.gitignore diff --git a/native_crypto_ios/.metadata b/packages/native_crypto_ios/.metadata similarity index 100% rename from native_crypto_ios/.metadata rename to packages/native_crypto_ios/.metadata diff --git a/packages/native_crypto_ios/CHANGELOG.md b/packages/native_crypto_ios/CHANGELOG.md new file mode 100644 index 0000000..d7c6738 --- /dev/null +++ b/packages/native_crypto_ios/CHANGELOG.md @@ -0,0 +1,35 @@ +## 0.1.0 + +> Breaking changes ! + +* Follow **Federated Plugin** Flutter standard. + +## 0.0.6 + +* Add KeyPair generation. +* Rework exposed API. + +## 0.0.5 + +* New API. +* Add digest support. +* Clean platform specific code base. + +## 0.0.4 + +* Improve AES. + +## 0.0.3 + +* Add PBKDF2 support. +* Add exceptions. +* Improve documentation. + +## 0.0.2 + +* Add different key size support. +* Improve performances. + +## 0.0.1 + +* First AES cross-platform encryption & decryption implementation. diff --git a/native_crypto_ios/LICENSE b/packages/native_crypto_ios/LICENSE similarity index 100% rename from native_crypto_ios/LICENSE rename to packages/native_crypto_ios/LICENSE diff --git a/native_crypto_ios/README.md b/packages/native_crypto_ios/README.md similarity index 100% rename from native_crypto_ios/README.md rename to packages/native_crypto_ios/README.md diff --git a/native_crypto_ios/ios/.gitignore b/packages/native_crypto_ios/ios/.gitignore similarity index 100% rename from native_crypto_ios/ios/.gitignore rename to packages/native_crypto_ios/ios/.gitignore diff --git a/packages/native_crypto_ios/ios/Assets/.gitkeep b/packages/native_crypto_ios/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/native_crypto_ios/ios/Classes/Cipher.swift b/packages/native_crypto_ios/ios/Classes/Cipher.swift similarity index 100% rename from native_crypto_ios/ios/Classes/Cipher.swift rename to packages/native_crypto_ios/ios/Classes/Cipher.swift diff --git a/native_crypto_ios/ios/Classes/Hash.swift b/packages/native_crypto_ios/ios/Classes/Hash.swift similarity index 100% rename from native_crypto_ios/ios/Classes/Hash.swift rename to packages/native_crypto_ios/ios/Classes/Hash.swift diff --git a/native_crypto_ios/ios/Classes/KEM.swift b/packages/native_crypto_ios/ios/Classes/KEM.swift similarity index 100% rename from native_crypto_ios/ios/Classes/KEM.swift rename to packages/native_crypto_ios/ios/Classes/KEM.swift diff --git a/native_crypto_ios/ios/Classes/Key.swift b/packages/native_crypto_ios/ios/Classes/Key.swift similarity index 100% rename from native_crypto_ios/ios/Classes/Key.swift rename to packages/native_crypto_ios/ios/Classes/Key.swift diff --git a/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.h b/packages/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.h similarity index 100% rename from native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.h rename to packages/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.h diff --git a/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.m b/packages/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.m similarity index 100% rename from native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.m rename to packages/native_crypto_ios/ios/Classes/NativeCryptoIosPlugin.m diff --git a/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift b/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift similarity index 100% rename from native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift rename to packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift diff --git a/native_crypto_ios/ios/native_crypto_ios.podspec b/packages/native_crypto_ios/ios/native_crypto_ios.podspec similarity index 100% rename from native_crypto_ios/ios/native_crypto_ios.podspec rename to packages/native_crypto_ios/ios/native_crypto_ios.podspec diff --git a/native_crypto_ios/pubspec.yaml b/packages/native_crypto_ios/pubspec.yaml similarity index 100% rename from native_crypto_ios/pubspec.yaml rename to packages/native_crypto_ios/pubspec.yaml diff --git a/native_crypto_platform_interface/.gitignore b/packages/native_crypto_platform_interface/.gitignore similarity index 100% rename from native_crypto_platform_interface/.gitignore rename to packages/native_crypto_platform_interface/.gitignore diff --git a/native_crypto_platform_interface/.metadata b/packages/native_crypto_platform_interface/.metadata similarity index 100% rename from native_crypto_platform_interface/.metadata rename to packages/native_crypto_platform_interface/.metadata diff --git a/packages/native_crypto_platform_interface/CHANGELOG.md b/packages/native_crypto_platform_interface/CHANGELOG.md new file mode 100644 index 0000000..d7c6738 --- /dev/null +++ b/packages/native_crypto_platform_interface/CHANGELOG.md @@ -0,0 +1,35 @@ +## 0.1.0 + +> Breaking changes ! + +* Follow **Federated Plugin** Flutter standard. + +## 0.0.6 + +* Add KeyPair generation. +* Rework exposed API. + +## 0.0.5 + +* New API. +* Add digest support. +* Clean platform specific code base. + +## 0.0.4 + +* Improve AES. + +## 0.0.3 + +* Add PBKDF2 support. +* Add exceptions. +* Improve documentation. + +## 0.0.2 + +* Add different key size support. +* Improve performances. + +## 0.0.1 + +* First AES cross-platform encryption & decryption implementation. diff --git a/native_crypto_platform_interface/LICENSE b/packages/native_crypto_platform_interface/LICENSE similarity index 100% rename from native_crypto_platform_interface/LICENSE rename to packages/native_crypto_platform_interface/LICENSE diff --git a/native_crypto_platform_interface/README.md b/packages/native_crypto_platform_interface/README.md similarity index 100% rename from native_crypto_platform_interface/README.md rename to packages/native_crypto_platform_interface/README.md diff --git a/native_crypto_platform_interface/analysis_options.yaml b/packages/native_crypto_platform_interface/analysis_options.yaml similarity index 100% rename from native_crypto_platform_interface/analysis_options.yaml rename to packages/native_crypto_platform_interface/analysis_options.yaml diff --git a/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart b/packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart similarity index 100% rename from native_crypto_platform_interface/lib/native_crypto_platform_interface.dart rename to packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart diff --git a/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart b/packages/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart similarity index 100% rename from native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart rename to packages/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart diff --git a/native_crypto_platform_interface/lib/src/platform_interface.dart b/packages/native_crypto_platform_interface/lib/src/platform_interface.dart similarity index 100% rename from native_crypto_platform_interface/lib/src/platform_interface.dart rename to packages/native_crypto_platform_interface/lib/src/platform_interface.dart diff --git a/native_crypto_platform_interface/pubspec.yaml b/packages/native_crypto_platform_interface/pubspec.yaml similarity index 100% rename from native_crypto_platform_interface/pubspec.yaml rename to packages/native_crypto_platform_interface/pubspec.yaml From 2559112bebcb2d7ccea62665e55845151c9bfc5e Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 21:53:08 +0200 Subject: [PATCH 08/39] docs: add some resources --- .gitignore | 482 +++++++++++++++++++++++++++ melos.yaml | 4 +- resources/benchmark_pointycastle.png | Bin 0 -> 123423 bytes resources/native_crypto.png | Bin 0 -> 30364 bytes 4 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 resources/benchmark_pointycastle.png create mode 100644 resources/native_crypto.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c39101 --- /dev/null +++ b/.gitignore @@ -0,0 +1,482 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,dart,flutter,java,jetbrains+all,kotlin,linux,objective-c,rust,swift,windows,xcode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,dart,flutter,java,jetbrains+all,kotlin,linux,objective-c,rust,swift,windows,xcode + +### Dart ### +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# dotenv environment variables file +.env* + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +.flutter-plugins +.flutter-plugins-dependencies + +### Dart Patch ### +# dotenv environment variables file +.env + +### Flutter ### +# Flutter/Dart/Pub related +**/doc/api/ +.fvm/ +.pub-cache/ +.pub/ +coverage/ +lib/generated_plugin_registrant.dart +# For library packages, don’t commit the pubspec.lock file. +# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. +# See https://dart.dev/guides/libraries/private-files#pubspeclock +#pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/key.properties +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Kotlin ### +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Objective-C ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Objective-C Patch ### + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### Swift ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + + + + + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Xcode ### + +## Xcode 8 and earlier + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,dart,flutter,java,jetbrains+all,kotlin,linux,objective-c,rust,swift,windows,xcode + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ + +# Mac +.DS_Store \ No newline at end of file diff --git a/melos.yaml b/melos.yaml index 906aa1b..f034109 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,5 +1,5 @@ name: NativeCrypto -repository: https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter +# repository: https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter packages: - packages/** @@ -9,7 +9,7 @@ command: updateGitTagRefs: true linkToCommits: false # Gitea not support this workspaceChangelog: true - branch: master + # branch: master scripts: lint:all: diff --git a/resources/benchmark_pointycastle.png b/resources/benchmark_pointycastle.png new file mode 100644 index 0000000000000000000000000000000000000000..0f357353cae33376c1ee99e346f06bd57e1a07d9 GIT binary patch literal 123423 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%0AEm!wrv$ZlK@zT#e)HU`0CSuGUrPE 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 S<0|XmAN?c7+Jr;4*Z&VLp)522 literal 0 HcmV?d00001 diff --git a/resources/native_crypto.png b/resources/native_crypto.png new file mode 100644 index 0000000000000000000000000000000000000000..e3fdadf556b1027af53e8ef8dbb391490a7bf9ac GIT binary patch literal 30364 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�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 Ws#CNmE8y|i|2|47NtTHl2mC))zcQcz literal 0 HcmV?d00001 From 51f6e6aa241c7186a902c95c03e6c1609e4e1820 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 21:54:16 +0200 Subject: [PATCH 09/39] style(platform): reformat code --- .../lib/src/platform_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/native_crypto_platform_interface/lib/src/platform_interface.dart b/packages/native_crypto_platform_interface/lib/src/platform_interface.dart index 89267a0..31f8037 100644 --- a/packages/native_crypto_platform_interface/lib/src/platform_interface.dart +++ b/packages/native_crypto_platform_interface/lib/src/platform_interface.dart @@ -94,4 +94,4 @@ abstract class PlatformInterface { /// implements NativeCryptoPlatform {} /// ``` @visibleForTesting -abstract class MockPlatformInterfaceMixin implements PlatformInterface {} \ No newline at end of file +abstract class MockPlatformInterfaceMixin implements PlatformInterface {} From 32106f549f39336fb32e257b9e75cbc4873c4d70 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 21:54:48 +0200 Subject: [PATCH 10/39] fix: update and fix code --- packages/native_crypto/analysis_options.yaml | 5 +- packages/native_crypto/example/lib/home.dart | 6 +- .../example/lib/pages/benchmark_page.dart | 13 +-- .../native_crypto/example/lib/session.dart | 6 +- packages/native_crypto/example/lib/utils.dart | 12 +-- .../example/lib/widgets/button.dart | 1 - packages/native_crypto/lib/native_crypto.dart | 13 +-- packages/native_crypto/lib/src/builder.dart | 2 +- .../lib/src/builders/aes_builder.dart | 14 ++-- .../native_crypto/lib/src/byte_array.dart | 19 +++-- packages/native_crypto/lib/src/cipher.dart | 2 +- .../native_crypto/lib/src/cipher_text.dart | 27 ++++-- .../native_crypto/lib/src/ciphers/aes.dart | 82 +++++++++++-------- .../native_crypto/lib/src/exceptions.dart | 20 ++--- packages/native_crypto/lib/src/hasher.dart | 6 +- .../native_crypto/lib/src/hashers/sha256.dart | 8 +- .../native_crypto/lib/src/hashers/sha384.dart | 8 +- .../native_crypto/lib/src/hashers/sha512.dart | 8 +- .../native_crypto/lib/src/kdf/pbkdf2.dart | 29 +++---- packages/native_crypto/lib/src/key.dart | 2 +- .../native_crypto/lib/src/keyderivation.dart | 2 +- .../lib/src/keys/secret_key.dart | 15 ++-- packages/native_crypto/lib/src/platform.dart | 2 +- packages/native_crypto/lib/src/utils.dart | 37 +++++---- packages/native_crypto/pubspec.yaml | 25 ++++-- 25 files changed, 209 insertions(+), 155 deletions(-) diff --git a/packages/native_crypto/analysis_options.yaml b/packages/native_crypto/analysis_options.yaml index a5744c1..db48808 100644 --- a/packages/native_crypto/analysis_options.yaml +++ b/packages/native_crypto/analysis_options.yaml @@ -1,4 +1 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +include: package:wyatt_analysis/analysis_options.flutter.experimental.yaml \ No newline at end of file diff --git a/packages/native_crypto/example/lib/home.dart b/packages/native_crypto/example/lib/home.dart index 50e65e8..8357b7e 100644 --- a/packages/native_crypto/example/lib/home.dart +++ b/packages/native_crypto/example/lib/home.dart @@ -22,11 +22,7 @@ class Home extends StatefulWidget { class _HomeState extends State { int _currentIndex = 0; - final List _children = [ - KdfPage(), - CipherPage(), - BenchmarkPage() - ]; + final List _children = [KdfPage(), CipherPage(), BenchmarkPage()]; void onTabTapped(int index) { setState(() { diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index 27552ec..71c4bea 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -34,21 +34,22 @@ class BenchmarkPage extends ConsumerWidget { int size = 64; benchmarkStatus.print("Benchmark Test\n"); - + // Encryption var before = DateTime.now(); var encryptedBigFile = await cipher.encrypt(Uint8List(size * 1000000)); var after = DateTime.now(); var benchmark = - after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); + // Decryption var befored = DateTime.now(); await cipher.decrypt(encryptedBigFile); var afterd = DateTime.now(); - var benchmarkd = afterd.millisecondsSinceEpoch - befored.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Decryption took $benchmarkd ms\n'); + var benchmarkd = + afterd.millisecondsSinceEpoch - befored.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Decryption took $benchmarkd ms\n'); } Future _benchmark(WidgetRef ref, Cipher cipher) async { diff --git a/packages/native_crypto/example/lib/session.dart b/packages/native_crypto/example/lib/session.dart index 8c25499..7636866 100644 --- a/packages/native_crypto/example/lib/session.dart +++ b/packages/native_crypto/example/lib/session.dart @@ -16,9 +16,11 @@ class Session { SecretKey secretKey; Session() : secretKey = SecretKey(Uint8List(0)); - void setKey(SecretKey sk) { secretKey = sk; } + void setKey(SecretKey sk) { + secretKey = sk; + } } // Providers -final sessionProvider = StateProvider((ref) => Session()); \ No newline at end of file +final sessionProvider = StateProvider((ref) => Session()); diff --git a/packages/native_crypto/example/lib/utils.dart b/packages/native_crypto/example/lib/utils.dart index 874b778..d93a923 100644 --- a/packages/native_crypto/example/lib/utils.dart +++ b/packages/native_crypto/example/lib/utils.dart @@ -23,12 +23,12 @@ extension StringX on String { bytes = base64.decode(this); break; case Encoding.hex: - bytes = Uint8List.fromList( - List.generate( - length ~/ 2, - (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), - ).toList(), - ); + bytes = Uint8List.fromList( + List.generate( + length ~/ 2, + (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), + ).toList(), + ); } return bytes; } diff --git a/packages/native_crypto/example/lib/widgets/button.dart b/packages/native_crypto/example/lib/widgets/button.dart index 30c28a5..2244bb4 100644 --- a/packages/native_crypto/example/lib/widgets/button.dart +++ b/packages/native_crypto/example/lib/widgets/button.dart @@ -15,7 +15,6 @@ class Button extends StatelessWidget { const Button(this.onPressed, this.label, {Key? key}) : super(key: key); - @override Widget build(BuildContext context) { return ElevatedButton( diff --git a/packages/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart index 015dab2..12076c7 100644 --- a/packages/native_crypto/lib/native_crypto.dart +++ b/packages/native_crypto/lib/native_crypto.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 28/12/2021 15:06:48 +// Last Modified: 23/05/2022 21:43:54 // ----- // Copyright (c) 2021 @@ -21,7 +21,10 @@ export 'src/keyderivation.dart'; export 'src/keys/secret_key.dart'; export 'src/utils.dart'; -const String version = "0.1.0"; -const String author = "Hugo Pointcheval"; -const String website = "https://hugo.pointcheval.fr/"; -const List repositories = ["https://github.com/hugo-pcl/native-crypto-flutter", "https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter"]; +const String version = '0.1.0'; +const String author = 'Hugo Pointcheval'; +const String website = 'https://hugo.pointcheval.fr/'; +const List repositories = [ + 'https://github.com/hugo-pcl/native-crypto-flutter', + 'https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter' +]; diff --git a/packages/native_crypto/lib/src/builder.dart b/packages/native_crypto/lib/src/builder.dart index 2ed8d17..4817768 100644 --- a/packages/native_crypto/lib/src/builder.dart +++ b/packages/native_crypto/lib/src/builder.dart @@ -9,4 +9,4 @@ abstract class Builder { Future build(); -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/builders/aes_builder.dart b/packages/native_crypto/lib/src/builders/aes_builder.dart index 8073490..898d79a 100644 --- a/packages/native_crypto/lib/src/builders/aes_builder.dart +++ b/packages/native_crypto/lib/src/builders/aes_builder.dart @@ -3,14 +3,14 @@ // ----- // File: aes_builder.dart // Created Date: 28/12/2021 12:03:11 -// Last Modified: 28/12/2021 13:39:23 +// Last Modified: 23/05/2022 21:46:33 // ----- // Copyright (c) 2021 -import '../builder.dart'; -import '../ciphers/aes.dart'; -import '../exceptions.dart'; -import '../keys/secret_key.dart'; +import 'package:native_crypto/src/builder.dart'; +import 'package:native_crypto/src/ciphers/aes.dart'; +import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; class AESBuilder implements Builder { SecretKey? _sk; @@ -36,11 +36,11 @@ class AESBuilder implements Builder { Future build() async { if (_sk == null) { if (_fsk == null) { - throw CipherInitException("You must specify or generate a secret key."); + throw CipherInitException('You must specify or generate a secret key.'); } else { _sk = await _fsk; } } return AES(_sk!, _mode); } -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/byte_array.dart b/packages/native_crypto/lib/src/byte_array.dart index 8343a34..18c2fcf 100644 --- a/packages/native_crypto/lib/src/byte_array.dart +++ b/packages/native_crypto/lib/src/byte_array.dart @@ -3,14 +3,14 @@ // ----- // File: byte_array.dart // Created Date: 16/12/2021 17:54:16 -// Last Modified: 27/12/2021 21:51:36 +// Last Modified: 23/05/2022 21:44:38 // ----- // Copyright (c) 2021 +import 'dart:convert' as convert; import 'dart:typed_data'; -import 'utils.dart'; -import 'dart:convert' as convert; +import 'package:native_crypto/src/utils.dart'; class ByteArray { Uint8List _bytes; @@ -18,7 +18,8 @@ class ByteArray { ByteArray(this._bytes); /// Creates an ByteArray object from a hexdecimal string. - ByteArray.fromBase16(String encoded) : _bytes = Utils.decodeHexString(encoded); + ByteArray.fromBase16(String encoded) + : _bytes = Utils.decodeHexString(encoded); /// Creates an ByteArray object from a Base64 string. ByteArray.fromBase64(String encoded) @@ -46,17 +47,17 @@ class ByteArray { String get base64 => convert.base64.encode(_bytes); @override - bool operator ==(other) { + bool operator ==(Object other) { if (other is ByteArray) { for (int i = 0; i < _bytes.length; i++) { if (_bytes[i] != other._bytes[i]) { return false; } } - + return true; } - + return false; } @@ -66,7 +67,7 @@ class ByteArray { for (int i = 0; i < _bytes.length; i++) { hash = _bytes[i] + (hash << 6) + (hash << 16) - hash; } - + return hash; } -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/cipher.dart b/packages/native_crypto/lib/src/cipher.dart index fec80ca..9d5dee8 100644 --- a/packages/native_crypto/lib/src/cipher.dart +++ b/packages/native_crypto/lib/src/cipher.dart @@ -34,4 +34,4 @@ abstract class Cipher { /// Takes [CipherText] as parameter. /// And returns plain text data as [Uint8List]. Future decrypt(CipherText cipherText); -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/cipher_text.dart b/packages/native_crypto/lib/src/cipher_text.dart index c4b53f2..bf79c27 100644 --- a/packages/native_crypto/lib/src/cipher_text.dart +++ b/packages/native_crypto/lib/src/cipher_text.dart @@ -3,21 +3,25 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 27/12/2021 22:32:06 +// Last Modified: 23/05/2022 21:48:27 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'byte_array.dart'; +import 'package:native_crypto/src/byte_array.dart'; class CipherText extends ByteArray { final int _ivLength; final int _dataLength; final int _tagLength; - - CipherText(Uint8List iv, Uint8List data, Uint8List tag) : _ivLength = iv.length, _dataLength = data.length, _tagLength = tag.length, super(Uint8List.fromList(iv + data + tag)); - + + CipherText(Uint8List iv, Uint8List data, Uint8List tag) + : _ivLength = iv.length, + _dataLength = data.length, + _tagLength = tag.length, + super(Uint8List.fromList(iv + data + tag)); + /// Gets the CipherText IV. Uint8List get iv => bytes.sublist(0, _ivLength); @@ -25,7 +29,10 @@ class CipherText extends ByteArray { Uint8List get data => bytes.sublist(_ivLength, _ivLength + _dataLength); /// Gets the CipherText tag. - Uint8List get tag => bytes.sublist(_ivLength + _dataLength, _ivLength + _dataLength + _tagLength); + Uint8List get tag => bytes.sublist( + _ivLength + _dataLength, + _ivLength + _dataLength + _tagLength, + ); /// Gets the CipherText IV length. int get ivLength => _ivLength; @@ -41,11 +48,13 @@ class CipherTextList extends CipherText { static const int chunkSize = 33554432; final List _list; - CipherTextList() : _list = [], super(Uint8List(0), Uint8List(0), Uint8List(0)); + CipherTextList() + : _list = [], + super(Uint8List(0), Uint8List(0), Uint8List(0)); void add(CipherText cipherText) { _list.add(cipherText); } - get list => _list; -} \ No newline at end of file + List get list => _list; +} diff --git a/packages/native_crypto/lib/src/ciphers/aes.dart b/packages/native_crypto/lib/src/ciphers/aes.dart index 592a7ce..56c19a8 100644 --- a/packages/native_crypto/lib/src/ciphers/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes.dart @@ -3,18 +3,18 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 28/12/2021 13:39:00 +// Last Modified: 23/05/2022 21:47:08 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import '../cipher.dart'; -import '../cipher_text.dart'; -import '../exceptions.dart'; -import '../keys/secret_key.dart'; -import '../platform.dart'; -import '../utils.dart'; +import 'package:native_crypto/src/cipher.dart'; +import 'package:native_crypto/src/cipher_text.dart'; +import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto/src/utils.dart'; /// Defines the AES modes of operation. enum AESMode { gcm } @@ -47,31 +47,37 @@ class AES implements Cipher { AES(this.key, this.mode, {this.padding = AESPadding.none}) { if (!AESKeySizeExtension.supportedSizes.contains(key.bytes.length * 8)) { - throw CipherInitException("Invalid key length!"); + throw CipherInitException('Invalid key length!'); } - Map> _supported = { + final Map> _supported = { AESMode.gcm: [AESPadding.none], }; if (!_supported[mode]!.contains(padding)) { - throw CipherInitException("Invalid padding!"); + throw CipherInitException('Invalid padding!'); } } @override Future decrypt(CipherText cipherText) async { - BytesBuilder decryptedData = BytesBuilder(copy: false); + final BytesBuilder decryptedData = BytesBuilder(copy: false); if (cipherText is CipherTextList) { - for (CipherText ct in cipherText.list) { - Uint8List d = await platform.decrypt( - ct.bytes, key.bytes, Utils.enumToStr(algorithm)) ?? + for (final CipherText ct in cipherText.list) { + final Uint8List d = await platform.decrypt( + ct.bytes, + key.bytes, + Utils.enumToStr(algorithm), + ) ?? Uint8List(0); decryptedData.add(d); } } else { - Uint8List d = await platform.decrypt( - cipherText.bytes, key.bytes, Utils.enumToStr(algorithm)) ?? + final Uint8List d = await platform.decrypt( + cipherText.bytes, + key.bytes, + Utils.enumToStr(algorithm), + ) ?? Uint8List(0); decryptedData.add(d); } @@ -82,29 +88,41 @@ class AES implements Cipher { @override Future encrypt(Uint8List data) async { Uint8List dataToEncrypt; - CipherTextList cipherTextList = CipherTextList(); + final CipherTextList cipherTextList = CipherTextList(); // If data is bigger than 32mB -> split in chunks if (data.length > CipherTextList.chunkSize) { - int chunkNb = (data.length / CipherTextList.chunkSize).ceil(); + final int chunkNb = (data.length / CipherTextList.chunkSize).ceil(); for (var i = 0; i < chunkNb; i++) { dataToEncrypt = i < (chunkNb - 1) - ? data.sublist(i * CipherTextList.chunkSize, (i + 1) * CipherTextList.chunkSize) + ? data.sublist( + i * CipherTextList.chunkSize, + (i + 1) * CipherTextList.chunkSize, + ) : data.sublist(i * CipherTextList.chunkSize); - Uint8List c = await platform.encrypt( - dataToEncrypt, - key.bytes, - Utils.enumToStr(algorithm) - ) ?? Uint8List(0); - cipherTextList.add(CipherText(c.sublist(0, 12), c.sublist(12, c.length - 16), c.sublist(c.length - 16, c.length))); // TODO: generify this + final Uint8List c = await platform.encrypt( + dataToEncrypt, + key.bytes, + Utils.enumToStr(algorithm), + ) ?? + Uint8List(0); + cipherTextList.add( + CipherText( + c.sublist(0, 12), + c.sublist(12, c.length - 16), + c.sublist(c.length - 16, c.length), + ), + ); // TODO(hpcl): generify this } } else { - Uint8List c = await platform.encrypt( - data, - key.bytes, - Utils.enumToStr(algorithm) - ) ?? Uint8List(0); - - return CipherText(c.sublist(0, 12), c.sublist(12, c.length - 16), c.sublist(c.length - 16, c.length)); // TODO: generify this + final Uint8List c = + await platform.encrypt(data, key.bytes, Utils.enumToStr(algorithm)) ?? + Uint8List(0); + + return CipherText( + c.sublist(0, 12), + c.sublist(12, c.length - 16), + c.sublist(c.length - 16, c.length), + ); // TODO(hpcl): generify this } return cipherTextList; diff --git a/packages/native_crypto/lib/src/exceptions.dart b/packages/native_crypto/lib/src/exceptions.dart index 3a5d2ce..10af511 100644 --- a/packages/native_crypto/lib/src/exceptions.dart +++ b/packages/native_crypto/lib/src/exceptions.dart @@ -3,39 +3,39 @@ // ----- // File: exceptions.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 27/12/2021 23:28:31 +// Last Modified: 23/05/2022 21:51:55 // ----- // Copyright (c) 2021 class NativeCryptoException implements Exception { - String message; - NativeCryptoException(this.message); + final String message; + const NativeCryptoException(this.message); } class UtilsException extends NativeCryptoException { - UtilsException(message) : super(message); + UtilsException(String message) : super(message); } class KeyException extends NativeCryptoException { - KeyException(message) : super(message); + KeyException(String message) : super(message); } class KeyDerivationException extends NativeCryptoException { - KeyDerivationException(message) : super(message); + KeyDerivationException(String message) : super(message); } class CipherInitException extends NativeCryptoException { - CipherInitException(message) : super(message); + CipherInitException(String message) : super(message); } class EncryptionException extends NativeCryptoException { - EncryptionException(message) : super(message); + EncryptionException(String message) : super(message); } class DecryptionException extends NativeCryptoException { - DecryptionException(message) : super(message); + DecryptionException(String message) : super(message); } class NotImplementedException extends NativeCryptoException { - NotImplementedException(message) : super(message); + NotImplementedException(String message) : super(message); } diff --git a/packages/native_crypto/lib/src/hasher.dart b/packages/native_crypto/lib/src/hasher.dart index bacf4bd..d81b7cc 100644 --- a/packages/native_crypto/lib/src/hasher.dart +++ b/packages/native_crypto/lib/src/hasher.dart @@ -20,8 +20,10 @@ abstract class Hasher { /// Hashes a message Future digest(Uint8List data) async { - Uint8List hash = (await platform.digest(data, Utils.enumToStr(algorithm))) ?? Uint8List(0); - + Uint8List hash = + (await platform.digest(data, Utils.enumToStr(algorithm))) ?? + Uint8List(0); + return hash; } } diff --git a/packages/native_crypto/lib/src/hashers/sha256.dart b/packages/native_crypto/lib/src/hashers/sha256.dart index 6c1284a..8f88189 100644 --- a/packages/native_crypto/lib/src/hashers/sha256.dart +++ b/packages/native_crypto/lib/src/hashers/sha256.dart @@ -3,13 +3,13 @@ // ----- // File: sha256.dart // Created Date: 17/12/2021 11:31:20 -// Last Modified: 18/12/2021 12:09:33 +// Last Modified: 23/05/2022 21:47:23 // ----- // Copyright (c) 2021 -import '../hasher.dart'; +import 'package:native_crypto/src/hasher.dart'; -class SHA256 extends Hasher{ +class SHA256 extends Hasher { @override HashAlgorithm get algorithm => HashAlgorithm.sha256; -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/hashers/sha384.dart b/packages/native_crypto/lib/src/hashers/sha384.dart index b9f44dd..f7630eb 100644 --- a/packages/native_crypto/lib/src/hashers/sha384.dart +++ b/packages/native_crypto/lib/src/hashers/sha384.dart @@ -3,13 +3,13 @@ // ----- // File: sha384.dart // Created Date: 17/12/2021 11:31:53 -// Last Modified: 18/12/2021 12:09:45 +// Last Modified: 23/05/2022 21:47:28 // ----- // Copyright (c) 2021 -import '../hasher.dart'; +import 'package:native_crypto/src/hasher.dart'; -class SHA384 extends Hasher{ +class SHA384 extends Hasher { @override HashAlgorithm get algorithm => HashAlgorithm.sha384; -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/hashers/sha512.dart b/packages/native_crypto/lib/src/hashers/sha512.dart index d4f7d6b..881887c 100644 --- a/packages/native_crypto/lib/src/hashers/sha512.dart +++ b/packages/native_crypto/lib/src/hashers/sha512.dart @@ -3,13 +3,13 @@ // ----- // File: sha512.dart // Created Date: 17/12/2021 11:32:14 -// Last Modified: 18/12/2021 12:09:58 +// Last Modified: 23/05/2022 21:47:35 // ----- // Copyright (c) 2021 -import '../hasher.dart'; +import 'package:native_crypto/src/hasher.dart'; -class SHA512 extends Hasher{ +class SHA512 extends Hasher { @override HashAlgorithm get algorithm => HashAlgorithm.sha512; -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index 8d5a004..8d8aa55 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -3,18 +3,18 @@ // ----- // File: pbkdf2.dart // Created Date: 17/12/2021 14:50:42 -// Last Modified: 28/12/2021 13:38:50 +// Last Modified: 23/05/2022 21:47:43 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import '../exceptions.dart'; -import '../hasher.dart'; -import '../keyderivation.dart'; -import '../keys/secret_key.dart'; -import '../platform.dart'; -import '../utils.dart'; +import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/hasher.dart'; +import 'package:native_crypto/src/keyderivation.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto/src/utils.dart'; class PBKDF2 extends KeyDerivation { final int _keyBytesCount; @@ -38,13 +38,14 @@ class PBKDF2 extends KeyDerivation { throw KeyDerivationException("Password or Salt can't be null!"); } - Uint8List derivation = (await platform.pbkdf2( - password, - salt, - _keyBytesCount, - _iterations, - Utils.enumToStr(_hash), - )) ?? Uint8List(0); + final Uint8List derivation = (await platform.pbkdf2( + password, + salt, + _keyBytesCount, + _iterations, + Utils.enumToStr(_hash), + )) ?? + Uint8List(0); return SecretKey(derivation); } diff --git a/packages/native_crypto/lib/src/key.dart b/packages/native_crypto/lib/src/key.dart index b86c1d2..ed7d98c 100644 --- a/packages/native_crypto/lib/src/key.dart +++ b/packages/native_crypto/lib/src/key.dart @@ -17,4 +17,4 @@ class Key extends ByteArray { Key.fromBase16(String encoded) : super.fromBase16(encoded); Key.fromBase64(String encoded) : super.fromBase64(encoded); Key.fromUtf8(String input) : super.fromUtf8(input); -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/keyderivation.dart b/packages/native_crypto/lib/src/keyderivation.dart index 279346e..b3cba30 100644 --- a/packages/native_crypto/lib/src/keyderivation.dart +++ b/packages/native_crypto/lib/src/keyderivation.dart @@ -18,4 +18,4 @@ abstract class KeyDerivation { /// Derive key Future derive(); -} \ No newline at end of file +} diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index 4579496..7fbdd5f 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,7 +3,7 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 28/12/2021 13:37:45 +// Last Modified: 23/05/2022 21:52:05 // ----- // Copyright (c) 2021 @@ -11,9 +11,9 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import '../exceptions.dart'; -import '../key.dart'; -import '../platform.dart'; +import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/key.dart'; +import 'package:native_crypto/src/platform.dart'; /// A class representing a secret key. /// A secret key is a key that is not accessible by anyone else. @@ -26,11 +26,12 @@ class SecretKey extends Key { static Future fromSecureRandom(int bitsCount) async { try { - Uint8List _key = (await platform.generateSecretKey(bitsCount)) ?? Uint8List(0); - + final Uint8List _key = + (await platform.generateSecretKey(bitsCount)) ?? Uint8List(0); + return SecretKey(_key); } on PlatformException catch (e) { - throw KeyException(e); + throw KeyException(e.toString()); } } } diff --git a/packages/native_crypto/lib/src/platform.dart b/packages/native_crypto/lib/src/platform.dart index 9834907..c4c13c1 100644 --- a/packages/native_crypto/lib/src/platform.dart +++ b/packages/native_crypto/lib/src/platform.dart @@ -9,4 +9,4 @@ import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -NativeCryptoPlatform platform = NativeCryptoPlatform.instance; \ No newline at end of file +NativeCryptoPlatform platform = NativeCryptoPlatform.instance; diff --git a/packages/native_crypto/lib/src/utils.dart b/packages/native_crypto/lib/src/utils.dart index df0dff2..67a6411 100644 --- a/packages/native_crypto/lib/src/utils.dart +++ b/packages/native_crypto/lib/src/utils.dart @@ -3,16 +3,16 @@ // ----- // File: utils.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 27/12/2021 22:04:07 +// Last Modified: 23/05/2022 21:45:56 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'cipher.dart'; -import 'exceptions.dart'; -import 'hasher.dart'; -import 'keyderivation.dart'; +import 'package:native_crypto/src/cipher.dart'; +import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/hasher.dart'; +import 'package:native_crypto/src/keyderivation.dart'; class Utils { /// Returns enum value to string, without the enum name @@ -22,17 +22,17 @@ class Utils { /// Returns enum list as string list static List enumToList(List enumValues) { - List _res = []; - for (T enumValue in enumValues) { + final List _res = []; + for (final T enumValue in enumValues) { _res.add(enumToStr(enumValue)); } - + return _res; } /// Returns enum from string static T strToEnum(String str, List enumValues) { - for (T enumValue in enumValues) { + for (final T enumValue in enumValues) { if (enumToStr(enumValue) == str) { return enumValue; } @@ -42,7 +42,10 @@ class Utils { /// Returns [HashAlgorithm] from his name. static HashAlgorithm getHashAlgorithm(String algorithm) { - return strToEnum(algorithm.toLowerCase(), HashAlgorithm.values); + return strToEnum( + algorithm.toLowerCase(), + HashAlgorithm.values, + ); } /// Returns all available [HashAlgorithm] as String list @@ -52,7 +55,10 @@ class Utils { /// Returns [KdfAlgorithm] from his name. static KdfAlgorithm getKdfAlgorithm(String algorithm) { - return strToEnum(algorithm.toLowerCase(), KdfAlgorithm.values); + return strToEnum( + algorithm.toLowerCase(), + KdfAlgorithm.values, + ); } /// Returns all available [KdfAlgorithm] as String list @@ -62,7 +68,10 @@ class Utils { /// Returns [CipherAlgorithm] from his name. static CipherAlgorithm getCipherAlgorithm(String algorithm) { - return strToEnum(algorithm.toLowerCase(), CipherAlgorithm.values); + return strToEnum( + algorithm.toLowerCase(), + CipherAlgorithm.values, + ); } /// Returns all available [CipherAlgorithm] as String list @@ -70,8 +79,8 @@ class Utils { return enumToList(CipherAlgorithm.values); } - static Uint8List decodeHexString(String input) { - assert(input.length % 2 == 0, 'Input needs to be an even length.'); + static Uint8List decodeHexString(String input) { + assert(input.length.isEven, 'Input needs to be an even length.'); return Uint8List.fromList( List.generate( diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index 5a63d48..2f2648e 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -1,6 +1,7 @@ name: native_crypto description: Fast and secure cryptography for Flutter. version: 0.1.0 + publish_to: 'none' environment: @@ -12,18 +13,32 @@ dependencies: sdk: flutter native_crypto_android: - path: ../native_crypto_android - + git: + url: https://github.com/hugo-pcl/native-crypto-flutter.git + ref: native_crypto_android-v0.1.0 + path: packages/native_crypto_android + native_crypto_ios: - path: ../native_crypto_ios + git: + url: https://github.com/hugo-pcl/native-crypto-flutter.git + ref: native_crypto_ios-v0.1.0 + path: packages/native_crypto_ios native_crypto_platform_interface: - path: ../native_crypto_platform_interface + git: + url: https://github.com/hugo-pcl/native-crypto-flutter.git + ref: native_crypto_platform_interface-v0.1.0 + path: packages/native_crypto_platform_interface dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.4 + + wyatt_analysis: + git: + url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages + ref: wyatt_analysis-v2.1.0 + path: packages/wyatt_analysis flutter: plugin: From 70a6fda3edb3229370a2a523a575eb57c6ef76e7 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 23:11:04 +0200 Subject: [PATCH 11/39] refactor: change file organization --- .../example/lib/pages/kdf_page.dart | 14 +-- packages/native_crypto/lib/native_crypto.dart | 41 ++++----- .../lib/src/builders/aes_builder.dart | 8 +- .../lib/src/builders/builders.dart | 10 ++ .../lib/src/ciphers/{ => aes}/aes.dart | 49 ++++------ .../lib/src/ciphers/aes/aes_key_size.dart | 22 +++++ .../lib/src/ciphers/aes/aes_mode.dart | 11 +++ .../lib/src/ciphers/aes/aes_padding.dart | 11 +++ .../lib/src/ciphers/ciphers.dart | 10 ++ .../lib/src/{ => core}/cipher_text.dart | 19 +--- .../lib/src/core/cipher_text_list.dart | 27 ++++++ packages/native_crypto/lib/src/core/core.dart | 12 +++ .../lib/src/{ => core}/exceptions.dart | 16 ++-- packages/native_crypto/lib/src/hasher.dart | 29 ------ .../native_crypto/lib/src/hashers/sha256.dart | 15 --- .../native_crypto/lib/src/hashers/sha384.dart | 15 --- .../native_crypto/lib/src/hashers/sha512.dart | 15 --- .../lib/src/{ => interfaces}/builder.dart | 4 +- .../lib/src/{ => interfaces}/byte_array.dart | 17 ++-- .../lib/src/{ => interfaces}/cipher.dart | 8 +- .../lib/src/interfaces/interfaces.dart | 14 +++ .../native_crypto/lib/src/interfaces/key.dart | 18 ++++ .../src/{ => interfaces}/keyderivation.dart | 7 +- packages/native_crypto/lib/src/kdf/kdf.dart | 10 ++ .../native_crypto/lib/src/kdf/pbkdf2.dart | 16 ++-- packages/native_crypto/lib/src/key.dart | 20 ---- packages/native_crypto/lib/src/keys/keys.dart | 10 ++ .../lib/src/keys/secret_key.dart | 15 ++- packages/native_crypto/lib/src/utils.dart | 92 ------------------- .../lib/src/utils/cipher_algorithm.dart | 11 +++ .../native_crypto/lib/src/utils/convert.dart | 23 +++++ .../lib/src/utils/hash_algorithm.dart | 25 +++++ .../lib/src/utils/kdf_algorithm.dart | 10 ++ packages/native_crypto/pubspec.yaml | 2 +- 34 files changed, 316 insertions(+), 310 deletions(-) create mode 100644 packages/native_crypto/lib/src/builders/builders.dart rename packages/native_crypto/lib/src/ciphers/{ => aes}/aes.dart (71%) create mode 100644 packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart create mode 100644 packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart create mode 100644 packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart create mode 100644 packages/native_crypto/lib/src/ciphers/ciphers.dart rename packages/native_crypto/lib/src/{ => core}/cipher_text.dart (72%) create mode 100644 packages/native_crypto/lib/src/core/cipher_text_list.dart create mode 100644 packages/native_crypto/lib/src/core/core.dart rename packages/native_crypto/lib/src/{ => core}/exceptions.dart (62%) delete mode 100644 packages/native_crypto/lib/src/hasher.dart delete mode 100644 packages/native_crypto/lib/src/hashers/sha256.dart delete mode 100644 packages/native_crypto/lib/src/hashers/sha384.dart delete mode 100644 packages/native_crypto/lib/src/hashers/sha512.dart rename packages/native_crypto/lib/src/{ => interfaces}/builder.dart (71%) rename packages/native_crypto/lib/src/{ => interfaces}/byte_array.dart (84%) rename packages/native_crypto/lib/src/{ => interfaces}/cipher.dart (83%) create mode 100644 packages/native_crypto/lib/src/interfaces/interfaces.dart create mode 100644 packages/native_crypto/lib/src/interfaces/key.dart rename packages/native_crypto/lib/src/{ => interfaces}/keyderivation.dart (70%) create mode 100644 packages/native_crypto/lib/src/kdf/kdf.dart delete mode 100644 packages/native_crypto/lib/src/key.dart create mode 100644 packages/native_crypto/lib/src/keys/keys.dart delete mode 100644 packages/native_crypto/lib/src/utils.dart create mode 100644 packages/native_crypto/lib/src/utils/cipher_algorithm.dart create mode 100644 packages/native_crypto/lib/src/utils/convert.dart create mode 100644 packages/native_crypto/lib/src/utils/hash_algorithm.dart create mode 100644 packages/native_crypto/lib/src/utils/kdf_algorithm.dart diff --git a/packages/native_crypto/example/lib/pages/kdf_page.dart b/packages/native_crypto/example/lib/pages/kdf_page.dart index b60a454..6eb9011 100644 --- a/packages/native_crypto/example/lib/pages/kdf_page.dart +++ b/packages/native_crypto/example/lib/pages/kdf_page.dart @@ -3,7 +3,7 @@ // ----- // File: kdf_page.dart // Created Date: 28/12/2021 13:40:34 -// Last Modified: 28/12/2021 15:14:12 +// Last Modified: 23/05/2022 22:49:06 // ----- // Copyright (c) 2021 @@ -50,7 +50,7 @@ class KdfPage extends ConsumerWidget { if (password.isEmpty) { pbkdf2Status.print('Password is empty'); } else { - PBKDF2 _pbkdf2 = PBKDF2(32, 1000, algorithm: HashAlgorithm.sha512); + Pbkdf2 _pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); SecretKey sk = await _pbkdf2.derive(password: password, salt: 'salt'); state.setKey(sk); pbkdf2Status.print('Key successfully derived.'); @@ -59,14 +59,14 @@ class KdfPage extends ConsumerWidget { } } - Future _hash(Hasher hasher) async { + Future _hash(HashAlgorithm hasher) async { final message = _messageTextController.text.trim(); if (message.isEmpty) { hashStatus.print('Message is empty'); } else { Uint8List hash = await hasher.digest(message.toBytes()); hashStatus.print( - 'Message successfully hashed with ${hasher.algorithm} :${hash.toStr(to: Encoding.hex)}'); + 'Message successfully hashed with $hasher :${hash.toStr(to: Encoding.hex)}'); } } @@ -108,15 +108,15 @@ class KdfPage extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Button( - () => _hash(SHA256()), + () => _hash(HashAlgorithm.sha256), "SHA256", ), Button( - () => _hash(SHA384()), + () => _hash(HashAlgorithm.sha384), "SHA384", ), Button( - () => _hash(SHA512()), + () => _hash(HashAlgorithm.sha512), "SHA512", ), ], diff --git a/packages/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart index 12076c7..226381f 100644 --- a/packages/native_crypto/lib/native_crypto.dart +++ b/packages/native_crypto/lib/native_crypto.dart @@ -3,28 +3,27 @@ // ----- // File: native_crypto.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 21:43:54 +// Last Modified: 23/05/2022 23:09:10 // ----- // Copyright (c) 2021 -export 'src/byte_array.dart'; -export 'src/cipher.dart'; -export 'src/cipher_text.dart'; -export 'src/ciphers/aes.dart'; -export 'src/exceptions.dart'; -export 'src/hasher.dart'; -export 'src/hashers/sha256.dart'; -export 'src/hashers/sha384.dart'; -export 'src/hashers/sha512.dart'; -export 'src/kdf/pbkdf2.dart'; -export 'src/keyderivation.dart'; -export 'src/keys/secret_key.dart'; -export 'src/utils.dart'; +/// Fast and powerful cryptographic functions +/// thanks to javax.crypto, CommonCrypto and CryptoKit. +/// +/// Author: Hugo Pointcheval +library native_crypto; -const String version = '0.1.0'; -const String author = 'Hugo Pointcheval'; -const String website = 'https://hugo.pointcheval.fr/'; -const List repositories = [ - 'https://github.com/hugo-pcl/native-crypto-flutter', - 'https://git.pointcheval.fr/NativeCrypto/native-crypto-flutter' -]; +export 'src/builders/builders.dart'; +export 'src/ciphers/ciphers.dart'; +export 'src/core/core.dart'; +export 'src/interfaces/interfaces.dart'; +export 'src/kdf/kdf.dart'; +export 'src/keys/keys.dart'; +// Utils +export 'src/utils/cipher_algorithm.dart'; +export 'src/utils/convert.dart'; +export 'src/utils/hash_algorithm.dart'; +export 'src/utils/kdf_algorithm.dart'; + +// ignore: constant_identifier_names +const String AUTHOR = 'Hugo Pointcheval'; diff --git a/packages/native_crypto/lib/src/builders/aes_builder.dart b/packages/native_crypto/lib/src/builders/aes_builder.dart index 898d79a..955849c 100644 --- a/packages/native_crypto/lib/src/builders/aes_builder.dart +++ b/packages/native_crypto/lib/src/builders/aes_builder.dart @@ -3,13 +3,13 @@ // ----- // File: aes_builder.dart // Created Date: 28/12/2021 12:03:11 -// Last Modified: 23/05/2022 21:46:33 +// Last Modified: 23/05/2022 23:05:19 // ----- // Copyright (c) 2021 -import 'package:native_crypto/src/builder.dart'; -import 'package:native_crypto/src/ciphers/aes.dart'; -import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/ciphers/aes/aes.dart'; +import 'package:native_crypto/src/core/exceptions.dart'; +import 'package:native_crypto/src/interfaces/builder.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; class AESBuilder implements Builder { diff --git a/packages/native_crypto/lib/src/builders/builders.dart b/packages/native_crypto/lib/src/builders/builders.dart new file mode 100644 index 0000000..e20a4a6 --- /dev/null +++ b/packages/native_crypto/lib/src/builders/builders.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: builders.dart +// Created Date: 23/05/2022 22:56:03 +// Last Modified: 23/05/2022 22:56:12 +// ----- +// Copyright (c) 2022 + +export 'aes_builder.dart'; diff --git a/packages/native_crypto/lib/src/ciphers/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart similarity index 71% rename from packages/native_crypto/lib/src/ciphers/aes.dart rename to packages/native_crypto/lib/src/ciphers/aes/aes.dart index 56c19a8..a1ae944 100644 --- a/packages/native_crypto/lib/src/ciphers/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,39 +3,26 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 21:47:08 +// Last Modified: 23/05/2022 23:06:05 // ----- -// Copyright (c) 2021 +// Copyright (c) 2022 import 'dart:typed_data'; -import 'package:native_crypto/src/cipher.dart'; -import 'package:native_crypto/src/cipher_text.dart'; -import 'package:native_crypto/src/exceptions.dart'; +import 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; +import 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; +import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; +import 'package:native_crypto/src/core/cipher_text.dart'; +import 'package:native_crypto/src/core/cipher_text_list.dart'; +import 'package:native_crypto/src/core/exceptions.dart'; +import 'package:native_crypto/src/interfaces/cipher.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils.dart'; +import 'package:native_crypto/src/utils/cipher_algorithm.dart'; -/// Defines the AES modes of operation. -enum AESMode { gcm } - -/// Defines all available key sizes. -enum AESKeySize { bits128, bits192, bits256 } - -/// Represents different paddings. -enum AESPadding { none } - -extension AESKeySizeExtension on AESKeySize { - static final Map sizes = { - AESKeySize.bits128: 128, - AESKeySize.bits192: 192, - AESKeySize.bits256: 256, - }; - static final List supportedSizes = sizes.values.toList(growable: false); - int get length { - return sizes[this]!; // this is safe because `this` is listed in the enum - } -} +export 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; +export 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; +export 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; class AES implements Cipher { final SecretKey key; @@ -46,7 +33,7 @@ class AES implements Cipher { CipherAlgorithm get algorithm => CipherAlgorithm.aes; AES(this.key, this.mode, {this.padding = AESPadding.none}) { - if (!AESKeySizeExtension.supportedSizes.contains(key.bytes.length * 8)) { + if (!AESKeySize.supportedSizes.contains(key.bytes.length * 8)) { throw CipherInitException('Invalid key length!'); } @@ -67,7 +54,7 @@ class AES implements Cipher { final Uint8List d = await platform.decrypt( ct.bytes, key.bytes, - Utils.enumToStr(algorithm), + algorithm.name, ) ?? Uint8List(0); decryptedData.add(d); @@ -76,7 +63,7 @@ class AES implements Cipher { final Uint8List d = await platform.decrypt( cipherText.bytes, key.bytes, - Utils.enumToStr(algorithm), + algorithm.name, ) ?? Uint8List(0); decryptedData.add(d); @@ -102,7 +89,7 @@ class AES implements Cipher { final Uint8List c = await platform.encrypt( dataToEncrypt, key.bytes, - Utils.enumToStr(algorithm), + algorithm.name, ) ?? Uint8List(0); cipherTextList.add( @@ -115,7 +102,7 @@ class AES implements Cipher { } } else { final Uint8List c = - await platform.encrypt(data, key.bytes, Utils.enumToStr(algorithm)) ?? + await platform.encrypt(data, key.bytes, algorithm.name) ?? Uint8List(0); return CipherText( diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart new file mode 100644 index 0000000..8018f0b --- /dev/null +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart @@ -0,0 +1,22 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_key_size.dart +// Created Date: 23/05/2022 22:10:07 +// Last Modified: 23/05/2022 22:33:32 +// ----- +// Copyright (c) 2022 + +/// Defines all available key sizes. +enum AESKeySize { + bits128(128), + bits192(192), + bits256(256); + + static final List supportedSizes = [128, 192, 256]; + + final int bits; + int get bytes => bits ~/ 8; + + const AESKeySize(this.bits); +} diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart new file mode 100644 index 0000000..554cdde --- /dev/null +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -0,0 +1,11 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_mode.dart +// Created Date: 23/05/2022 22:09:16 +// Last Modified: 23/05/2022 22:10:31 +// ----- +// Copyright (c) 2022 + +/// Defines the AES modes of operation. +enum AESMode { gcm } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart new file mode 100644 index 0000000..06e323b --- /dev/null +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart @@ -0,0 +1,11 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_padding.dart +// Created Date: 23/05/2022 22:10:17 +// Last Modified: 23/05/2022 22:13:28 +// ----- +// Copyright (c) 2022 + +/// Represents different paddings. +enum AESPadding { none } diff --git a/packages/native_crypto/lib/src/ciphers/ciphers.dart b/packages/native_crypto/lib/src/ciphers/ciphers.dart new file mode 100644 index 0000000..edae6a4 --- /dev/null +++ b/packages/native_crypto/lib/src/ciphers/ciphers.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: ciphers.dart +// Created Date: 23/05/2022 22:56:30 +// Last Modified: 23/05/2022 22:56:47 +// ----- +// Copyright (c) 2022 + +export 'aes/aes.dart'; diff --git a/packages/native_crypto/lib/src/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart similarity index 72% rename from packages/native_crypto/lib/src/cipher_text.dart rename to packages/native_crypto/lib/src/core/cipher_text.dart index bf79c27..cd840a3 100644 --- a/packages/native_crypto/lib/src/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,13 +3,13 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 23/05/2022 21:48:27 +// Last Modified: 23/05/2022 23:02:10 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/byte_array.dart'; +import 'package:native_crypto/src/interfaces/byte_array.dart'; class CipherText extends ByteArray { final int _ivLength; @@ -43,18 +43,3 @@ class CipherText extends ByteArray { /// Gets the CipherText tag length. int get tagLength => _tagLength; } - -class CipherTextList extends CipherText { - static const int chunkSize = 33554432; - final List _list; - - CipherTextList() - : _list = [], - super(Uint8List(0), Uint8List(0), Uint8List(0)); - - void add(CipherText cipherText) { - _list.add(cipherText); - } - - List get list => _list; -} diff --git a/packages/native_crypto/lib/src/core/cipher_text_list.dart b/packages/native_crypto/lib/src/core/cipher_text_list.dart new file mode 100644 index 0000000..9d16211 --- /dev/null +++ b/packages/native_crypto/lib/src/core/cipher_text_list.dart @@ -0,0 +1,27 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_text_list.dart +// Created Date: 23/05/2022 22:59:02 +// Last Modified: 23/05/2022 23:05:02 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:native_crypto/src/core/cipher_text.dart'; + +class CipherTextList extends CipherText { + static const int chunkSize = 33554432; + final List _list; + + CipherTextList() + : _list = [], + super(Uint8List(0), Uint8List(0), Uint8List(0)); + + void add(CipherText cipherText) { + _list.add(cipherText); + } + + List get list => _list; +} diff --git a/packages/native_crypto/lib/src/core/core.dart b/packages/native_crypto/lib/src/core/core.dart new file mode 100644 index 0000000..97597ac --- /dev/null +++ b/packages/native_crypto/lib/src/core/core.dart @@ -0,0 +1,12 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: core.dart +// Created Date: 23/05/2022 23:05:26 +// Last Modified: 23/05/2022 23:05:30 +// ----- +// Copyright (c) 2022 + +export 'cipher_text.dart'; +export 'cipher_text_list.dart'; +export 'exceptions.dart'; diff --git a/packages/native_crypto/lib/src/exceptions.dart b/packages/native_crypto/lib/src/core/exceptions.dart similarity index 62% rename from packages/native_crypto/lib/src/exceptions.dart rename to packages/native_crypto/lib/src/core/exceptions.dart index 10af511..96c21d0 100644 --- a/packages/native_crypto/lib/src/exceptions.dart +++ b/packages/native_crypto/lib/src/core/exceptions.dart @@ -3,7 +3,7 @@ // ----- // File: exceptions.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 21:51:55 +// Last Modified: 23/05/2022 22:30:27 // ----- // Copyright (c) 2021 @@ -13,29 +13,29 @@ class NativeCryptoException implements Exception { } class UtilsException extends NativeCryptoException { - UtilsException(String message) : super(message); + UtilsException(super.message); } class KeyException extends NativeCryptoException { - KeyException(String message) : super(message); + KeyException(super.message); } class KeyDerivationException extends NativeCryptoException { - KeyDerivationException(String message) : super(message); + KeyDerivationException(super.message); } class CipherInitException extends NativeCryptoException { - CipherInitException(String message) : super(message); + CipherInitException(super.message); } class EncryptionException extends NativeCryptoException { - EncryptionException(String message) : super(message); + EncryptionException(super.message); } class DecryptionException extends NativeCryptoException { - DecryptionException(String message) : super(message); + DecryptionException(super.message); } class NotImplementedException extends NativeCryptoException { - NotImplementedException(String message) : super(message); + NotImplementedException(super.message); } diff --git a/packages/native_crypto/lib/src/hasher.dart b/packages/native_crypto/lib/src/hasher.dart deleted file mode 100644 index d81b7cc..0000000 --- a/packages/native_crypto/lib/src/hasher.dart +++ /dev/null @@ -1,29 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: hasher.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 27/12/2021 22:06:29 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'platform.dart'; -import 'utils.dart'; - -enum HashAlgorithm { sha256, sha384, sha512 } - -abstract class Hasher { - /// Returns the standard algorithm name for this digest - HashAlgorithm get algorithm; - - /// Hashes a message - Future digest(Uint8List data) async { - Uint8List hash = - (await platform.digest(data, Utils.enumToStr(algorithm))) ?? - Uint8List(0); - - return hash; - } -} diff --git a/packages/native_crypto/lib/src/hashers/sha256.dart b/packages/native_crypto/lib/src/hashers/sha256.dart deleted file mode 100644 index 8f88189..0000000 --- a/packages/native_crypto/lib/src/hashers/sha256.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: sha256.dart -// Created Date: 17/12/2021 11:31:20 -// Last Modified: 23/05/2022 21:47:23 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/hasher.dart'; - -class SHA256 extends Hasher { - @override - HashAlgorithm get algorithm => HashAlgorithm.sha256; -} diff --git a/packages/native_crypto/lib/src/hashers/sha384.dart b/packages/native_crypto/lib/src/hashers/sha384.dart deleted file mode 100644 index f7630eb..0000000 --- a/packages/native_crypto/lib/src/hashers/sha384.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: sha384.dart -// Created Date: 17/12/2021 11:31:53 -// Last Modified: 23/05/2022 21:47:28 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/hasher.dart'; - -class SHA384 extends Hasher { - @override - HashAlgorithm get algorithm => HashAlgorithm.sha384; -} diff --git a/packages/native_crypto/lib/src/hashers/sha512.dart b/packages/native_crypto/lib/src/hashers/sha512.dart deleted file mode 100644 index 881887c..0000000 --- a/packages/native_crypto/lib/src/hashers/sha512.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: sha512.dart -// Created Date: 17/12/2021 11:32:14 -// Last Modified: 23/05/2022 21:47:35 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/hasher.dart'; - -class SHA512 extends Hasher { - @override - HashAlgorithm get algorithm => HashAlgorithm.sha512; -} diff --git a/packages/native_crypto/lib/src/builder.dart b/packages/native_crypto/lib/src/interfaces/builder.dart similarity index 71% rename from packages/native_crypto/lib/src/builder.dart rename to packages/native_crypto/lib/src/interfaces/builder.dart index 4817768..a1b39aa 100644 --- a/packages/native_crypto/lib/src/builder.dart +++ b/packages/native_crypto/lib/src/interfaces/builder.dart @@ -3,10 +3,12 @@ // ----- // File: builder.dart // Created Date: 28/12/2021 12:02:34 -// Last Modified: 28/12/2021 12:32:12 +// Last Modified: 23/05/2022 22:38:44 // ----- // Copyright (c) 2021 +// ignore_for_file: one_member_abstracts + abstract class Builder { Future build(); } diff --git a/packages/native_crypto/lib/src/byte_array.dart b/packages/native_crypto/lib/src/interfaces/byte_array.dart similarity index 84% rename from packages/native_crypto/lib/src/byte_array.dart rename to packages/native_crypto/lib/src/interfaces/byte_array.dart index 18c2fcf..23d3ba4 100644 --- a/packages/native_crypto/lib/src/byte_array.dart +++ b/packages/native_crypto/lib/src/interfaces/byte_array.dart @@ -3,23 +3,25 @@ // ----- // File: byte_array.dart // Created Date: 16/12/2021 17:54:16 -// Last Modified: 23/05/2022 21:44:38 +// Last Modified: 23/05/2022 23:07:03 // ----- // Copyright (c) 2021 import 'dart:convert' as convert; import 'dart:typed_data'; -import 'package:native_crypto/src/utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:native_crypto/src/utils/convert.dart'; -class ByteArray { - Uint8List _bytes; +@immutable +abstract class ByteArray { + final Uint8List _bytes; - ByteArray(this._bytes); + const ByteArray(this._bytes); /// Creates an ByteArray object from a hexdecimal string. ByteArray.fromBase16(String encoded) - : _bytes = Utils.decodeHexString(encoded); + : _bytes = Convert.decodeHexString(encoded); /// Creates an ByteArray object from a Base64 string. ByteArray.fromBase64(String encoded) @@ -36,9 +38,6 @@ class ByteArray { // ignore: unnecessary_getters_setters Uint8List get bytes => _bytes; - /// Sets the ByteArray bytes. - set bytes(Uint8List value) => _bytes = value; - /// Gets the ByteArray bytes as a Hexadecimal representation. String get base16 => _bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); diff --git a/packages/native_crypto/lib/src/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart similarity index 83% rename from packages/native_crypto/lib/src/cipher.dart rename to packages/native_crypto/lib/src/interfaces/cipher.dart index 9d5dee8..df8296a 100644 --- a/packages/native_crypto/lib/src/cipher.dart +++ b/packages/native_crypto/lib/src/interfaces/cipher.dart @@ -3,16 +3,14 @@ // ----- // File: cipher.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 28/12/2021 12:25:38 +// Last Modified: 23/05/2022 23:06:20 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'cipher_text.dart'; - -/// Represents different cipher algorithms -enum CipherAlgorithm { aes, rsa } +import 'package:native_crypto/src/core/cipher_text.dart'; +import 'package:native_crypto/src/utils/cipher_algorithm.dart'; /// Represents a cipher. /// diff --git a/packages/native_crypto/lib/src/interfaces/interfaces.dart b/packages/native_crypto/lib/src/interfaces/interfaces.dart new file mode 100644 index 0000000..3ff3855 --- /dev/null +++ b/packages/native_crypto/lib/src/interfaces/interfaces.dart @@ -0,0 +1,14 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: interfaces.dart +// Created Date: 23/05/2022 23:03:47 +// Last Modified: 23/05/2022 23:10:15 +// ----- +// Copyright (c) 2022 + +export 'builder.dart'; +export 'byte_array.dart'; +export 'cipher.dart'; +// export 'key.dart'; +export 'keyderivation.dart'; diff --git a/packages/native_crypto/lib/src/interfaces/key.dart b/packages/native_crypto/lib/src/interfaces/key.dart new file mode 100644 index 0000000..3c99f0c --- /dev/null +++ b/packages/native_crypto/lib/src/interfaces/key.dart @@ -0,0 +1,18 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: key.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 23/05/2022 23:02:10 +// ----- +// Copyright (c) 2021 + +import 'package:native_crypto/src/interfaces/byte_array.dart'; + +/// A class representing a key. +abstract class Key extends ByteArray { + const Key(super.bytes); + Key.fromBase16(super.encoded) : super.fromBase16(); + Key.fromBase64(super.encoded) : super.fromBase64(); + Key.fromUtf8(super.input) : super.fromUtf8(); +} diff --git a/packages/native_crypto/lib/src/keyderivation.dart b/packages/native_crypto/lib/src/interfaces/keyderivation.dart similarity index 70% rename from packages/native_crypto/lib/src/keyderivation.dart rename to packages/native_crypto/lib/src/interfaces/keyderivation.dart index b3cba30..cddb1bc 100644 --- a/packages/native_crypto/lib/src/keyderivation.dart +++ b/packages/native_crypto/lib/src/interfaces/keyderivation.dart @@ -3,13 +3,12 @@ // ----- // File: kdf.dart // Created Date: 18/12/2021 11:56:43 -// Last Modified: 28/12/2021 13:38:02 +// Last Modified: 23/05/2022 22:37:04 // ----- // Copyright (c) 2021 -import './keys/secret_key.dart'; - -enum KdfAlgorithm { pbkdf2 } +import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto/src/utils/kdf_algorithm.dart'; /// Represents a Key Derivation Function abstract class KeyDerivation { diff --git a/packages/native_crypto/lib/src/kdf/kdf.dart b/packages/native_crypto/lib/src/kdf/kdf.dart new file mode 100644 index 0000000..cb7d609 --- /dev/null +++ b/packages/native_crypto/lib/src/kdf/kdf.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: kdf.dart +// Created Date: 23/05/2022 22:57:11 +// Last Modified: 23/05/2022 23:04:15 +// ----- +// Copyright (c) 2022 + +export 'pbkdf2.dart'; diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index 8d8aa55..04672bd 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -3,20 +3,20 @@ // ----- // File: pbkdf2.dart // Created Date: 17/12/2021 14:50:42 -// Last Modified: 23/05/2022 21:47:43 +// Last Modified: 23/05/2022 23:07:19 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/exceptions.dart'; -import 'package:native_crypto/src/hasher.dart'; -import 'package:native_crypto/src/keyderivation.dart'; +import 'package:native_crypto/src/core/exceptions.dart'; +import 'package:native_crypto/src/interfaces/keyderivation.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; -import 'package:native_crypto/src/utils.dart'; +import 'package:native_crypto/src/utils/hash_algorithm.dart'; +import 'package:native_crypto/src/utils/kdf_algorithm.dart'; -class PBKDF2 extends KeyDerivation { +class Pbkdf2 extends KeyDerivation { final int _keyBytesCount; final int _iterations; final HashAlgorithm _hash; @@ -24,7 +24,7 @@ class PBKDF2 extends KeyDerivation { @override KdfAlgorithm get algorithm => KdfAlgorithm.pbkdf2; - PBKDF2( + Pbkdf2( int keyBytesCount, int iterations, { HashAlgorithm algorithm = HashAlgorithm.sha256, @@ -43,7 +43,7 @@ class PBKDF2 extends KeyDerivation { salt, _keyBytesCount, _iterations, - Utils.enumToStr(_hash), + _hash.name, )) ?? Uint8List(0); diff --git a/packages/native_crypto/lib/src/key.dart b/packages/native_crypto/lib/src/key.dart deleted file mode 100644 index ed7d98c..0000000 --- a/packages/native_crypto/lib/src/key.dart +++ /dev/null @@ -1,20 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: key.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 28/12/2021 13:37:50 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'byte_array.dart'; - -/// A class representing a key. -class Key extends ByteArray { - Key(Uint8List bytes) : super(bytes); - Key.fromBase16(String encoded) : super.fromBase16(encoded); - Key.fromBase64(String encoded) : super.fromBase64(encoded); - Key.fromUtf8(String input) : super.fromUtf8(input); -} diff --git a/packages/native_crypto/lib/src/keys/keys.dart b/packages/native_crypto/lib/src/keys/keys.dart new file mode 100644 index 0000000..912bb39 --- /dev/null +++ b/packages/native_crypto/lib/src/keys/keys.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: keys.dart +// Created Date: 23/05/2022 23:04:04 +// Last Modified: 23/05/2022 23:04:07 +// ----- +// Copyright (c) 2022 + +export 'secret_key.dart'; diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index 7fbdd5f..98a1097 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,26 +3,25 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 23/05/2022 21:52:05 +// Last Modified: 23/05/2022 23:07:28 // ----- // Copyright (c) 2021 import 'dart:typed_data'; import 'package:flutter/services.dart'; - -import 'package:native_crypto/src/exceptions.dart'; -import 'package:native_crypto/src/key.dart'; +import 'package:native_crypto/src/core/exceptions.dart'; +import 'package:native_crypto/src/interfaces/key.dart'; import 'package:native_crypto/src/platform.dart'; /// A class representing a secret key. /// A secret key is a key that is not accessible by anyone else. /// It is used to encrypt and decrypt data. class SecretKey extends Key { - SecretKey(Uint8List bytes) : super(bytes); - SecretKey.fromBase16(String encoded) : super.fromBase16(encoded); - SecretKey.fromBase64(String encoded) : super.fromBase64(encoded); - SecretKey.fromUtf8(String input) : super.fromUtf8(input); + const SecretKey(super.bytes); + SecretKey.fromBase16(super.encoded) : super.fromBase16(); + SecretKey.fromBase64(super.encoded) : super.fromBase64(); + SecretKey.fromUtf8(super.input) : super.fromUtf8(); static Future fromSecureRandom(int bitsCount) async { try { diff --git a/packages/native_crypto/lib/src/utils.dart b/packages/native_crypto/lib/src/utils.dart deleted file mode 100644 index 67a6411..0000000 --- a/packages/native_crypto/lib/src/utils.dart +++ /dev/null @@ -1,92 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: utils.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 21:45:56 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -import 'package:native_crypto/src/cipher.dart'; -import 'package:native_crypto/src/exceptions.dart'; -import 'package:native_crypto/src/hasher.dart'; -import 'package:native_crypto/src/keyderivation.dart'; - -class Utils { - /// Returns enum value to string, without the enum name - static String enumToStr(dynamic enumValue) { - return enumValue.toString().split('.').last; - } - - /// Returns enum list as string list - static List enumToList(List enumValues) { - final List _res = []; - for (final T enumValue in enumValues) { - _res.add(enumToStr(enumValue)); - } - - return _res; - } - - /// Returns enum from string - static T strToEnum(String str, List enumValues) { - for (final T enumValue in enumValues) { - if (enumToStr(enumValue) == str) { - return enumValue; - } - } - throw UtilsException('Invalid enum value: $str'); - } - - /// Returns [HashAlgorithm] from his name. - static HashAlgorithm getHashAlgorithm(String algorithm) { - return strToEnum( - algorithm.toLowerCase(), - HashAlgorithm.values, - ); - } - - /// Returns all available [HashAlgorithm] as String list - static List getAvailableHashAlgorithms() { - return enumToList(HashAlgorithm.values); - } - - /// Returns [KdfAlgorithm] from his name. - static KdfAlgorithm getKdfAlgorithm(String algorithm) { - return strToEnum( - algorithm.toLowerCase(), - KdfAlgorithm.values, - ); - } - - /// Returns all available [KdfAlgorithm] as String list - static List getAvailableKdfAlgorithms() { - return enumToList(KdfAlgorithm.values); - } - - /// Returns [CipherAlgorithm] from his name. - static CipherAlgorithm getCipherAlgorithm(String algorithm) { - return strToEnum( - algorithm.toLowerCase(), - CipherAlgorithm.values, - ); - } - - /// Returns all available [CipherAlgorithm] as String list - static List getAvailableCipherAlgorithms() { - return enumToList(CipherAlgorithm.values); - } - - static Uint8List decodeHexString(String input) { - assert(input.length.isEven, 'Input needs to be an even length.'); - - return Uint8List.fromList( - List.generate( - input.length ~/ 2, - (i) => int.parse(input.substring(i * 2, (i * 2) + 2), radix: 16), - ).toList(), - ); - } -} diff --git a/packages/native_crypto/lib/src/utils/cipher_algorithm.dart b/packages/native_crypto/lib/src/utils/cipher_algorithm.dart new file mode 100644 index 0000000..5498bbc --- /dev/null +++ b/packages/native_crypto/lib/src/utils/cipher_algorithm.dart @@ -0,0 +1,11 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_algorithm.dart +// Created Date: 23/05/2022 22:07:54 +// Last Modified: 23/05/2022 22:33:56 +// ----- +// Copyright (c) 2022 + +/// Represents different cipher algorithms +enum CipherAlgorithm { aes, rsa } diff --git a/packages/native_crypto/lib/src/utils/convert.dart b/packages/native_crypto/lib/src/utils/convert.dart new file mode 100644 index 0000000..f68bb78 --- /dev/null +++ b/packages/native_crypto/lib/src/utils/convert.dart @@ -0,0 +1,23 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: convert.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 23/05/2022 22:39:19 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +abstract class Convert { + static Uint8List decodeHexString(String input) { + assert(input.length.isEven, 'Input needs to be an even length.'); + + return Uint8List.fromList( + List.generate( + input.length ~/ 2, + (i) => int.parse(input.substring(i * 2, (i * 2) + 2), radix: 16), + ).toList(), + ); + } +} diff --git a/packages/native_crypto/lib/src/utils/hash_algorithm.dart b/packages/native_crypto/lib/src/utils/hash_algorithm.dart new file mode 100644 index 0000000..4e914cd --- /dev/null +++ b/packages/native_crypto/lib/src/utils/hash_algorithm.dart @@ -0,0 +1,25 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: hash_algorithm.dart +// Created Date: 23/05/2022 22:01:59 +// Last Modified: 23/05/2022 22:47:08 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:native_crypto/src/platform.dart'; + +enum HashAlgorithm { + sha256, + sha384, + sha512; + + /// Hashes a message + Future digest(Uint8List data) async { + final Uint8List hash = (await platform.digest(data, name)) ?? Uint8List(0); + + return hash; + } +} diff --git a/packages/native_crypto/lib/src/utils/kdf_algorithm.dart b/packages/native_crypto/lib/src/utils/kdf_algorithm.dart new file mode 100644 index 0000000..58ff68a --- /dev/null +++ b/packages/native_crypto/lib/src/utils/kdf_algorithm.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: kdf_algorithm.dart +// Created Date: 23/05/2022 22:36:24 +// Last Modified: 23/05/2022 22:36:36 +// ----- +// Copyright (c) 2022 + +enum KdfAlgorithm { pbkdf2 } diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index 2f2648e..39e6c48 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.1.0 publish_to: 'none' environment: - sdk: ">=2.15.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.5.0" dependencies: From 2fe41721314ffa142ee1d0fc56424ee6e4b2e205 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 23 May 2022 23:15:00 +0200 Subject: [PATCH 12/39] docs: add link to readme file --- README.md | 3 +++ packages/native_crypto/README.md | 17 +---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e69de29..e7423e8 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +# NativeCrypto + +Fast and powerful cryptographic functions thanks to **javax.crypto** , **CommonCrypto** and **CryptoKit**. \ No newline at end of file diff --git a/packages/native_crypto/README.md b/packages/native_crypto/README.md index c16def3..e7423e8 100644 --- a/packages/native_crypto/README.md +++ b/packages/native_crypto/README.md @@ -1,18 +1,3 @@ # NativeCrypto -Fast and powerful cryptographic functions thanks to **javax.crypto** and **CryptoKit**. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - -The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. -To add platforms, run `flutter create -t plugin --platforms .` under the same -directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +Fast and powerful cryptographic functions thanks to **javax.crypto** , **CommonCrypto** and **CryptoKit**. \ No newline at end of file From 41354e3dc4f68e1ac77ce95f06824d79d7e24b8d Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 24 May 2022 18:43:14 +0200 Subject: [PATCH 13/39] refactor(android): clean and modernize kotlin code --- .../android/build.gradle | 4 +- .../native_crypto_android/Cipher.kt | 6 - .../pointcheval/native_crypto_android/Hash.kt | 25 --- .../native_crypto_android/HashAlgorithm.kt | 15 -- .../pointcheval/native_crypto_android/Key.kt | 24 -- .../NativeCryptoAndroidPlugin.kt | 206 ++++++++++-------- .../native_crypto_android/ciphers/AES.kt | 59 +++++ .../ciphers/AESCipher.kt | 31 --- .../interfaces/Cipher.kt | 10 + .../native_crypto_android/interfaces/Key.kt | 5 + .../interfaces/KeyDerivation.kt | 10 + .../native_crypto_android/kdf/Pbkdf2.kt | 39 ++++ .../native_crypto_android/keys/SecretKey.kt | 14 ++ .../utils/CipherAlgorithm.kt | 14 ++ .../native_crypto_android/utils/Constants.kt | 14 ++ .../utils/HashAlgorithm.kt | 50 +++++ .../utils/KdfAlgorithm.kt | 5 + .../native_crypto_android/utils/Task.kt | 44 ++++ 18 files changed, 378 insertions(+), 197 deletions(-) delete mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt delete mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt delete mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt delete mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt delete mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Key.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/KeyDerivation.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/kdf/Pbkdf2.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/keys/SecretKey.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/CipherAlgorithm.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Constants.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/HashAlgorithm.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/KdfAlgorithm.kt create mode 100644 packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Task.kt diff --git a/packages/native_crypto_android/android/build.gradle b/packages/native_crypto_android/android/build.gradle index ec447a5..301735f 100644 --- a/packages/native_crypto_android/android/build.gradle +++ b/packages/native_crypto_android/android/build.gradle @@ -2,14 +2,14 @@ group 'fr.pointcheval.native_crypto_android' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt deleted file mode 100644 index 78b1a79..0000000 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Cipher.kt +++ /dev/null @@ -1,6 +0,0 @@ -package fr.pointcheval.native_crypto_android - -abstract class Cipher { - abstract fun encrypt(data: ByteArray, key: ByteArray) : ByteArray?; - abstract fun decrypt(data: ByteArray, key: ByteArray) : ByteArray?; -} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt deleted file mode 100644 index dd9c95e..0000000 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Hash.kt +++ /dev/null @@ -1,25 +0,0 @@ -package fr.pointcheval.native_crypto_android - -import java.security.MessageDigest - -object Hash { - fun digest(data: ByteArray?, algorithm: HashAlgorithm): ByteArray { - val func : String = when (algorithm) { - HashAlgorithm.SHA256 -> "SHA-256" - HashAlgorithm.SHA384 -> "SHA-384" - HashAlgorithm.SHA512 -> "SHA-512" - } - val md = MessageDigest.getInstance(func) - return md.digest(data) - } - - fun digest(data: ByteArray?, algorithm: String): ByteArray { - val func : HashAlgorithm = when (algorithm) { - "sha256" -> HashAlgorithm.SHA256 - "sha384" -> HashAlgorithm.SHA384 - "sha512" -> HashAlgorithm.SHA512 - else -> HashAlgorithm.SHA256 - } - return digest(data, func) - } -} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt deleted file mode 100644 index f820bae..0000000 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/HashAlgorithm.kt +++ /dev/null @@ -1,15 +0,0 @@ -package fr.pointcheval.native_crypto_android - -enum class HashAlgorithm(val length : Int) { - SHA256(256), - SHA384(384), - SHA512(512); - - fun hmac(): String { - return when (this) { - HashAlgorithm.SHA256 -> "HmacSHA256" - HashAlgorithm.SHA384 -> "HmacSHA384" - HashAlgorithm.SHA512 -> "HmacSHA512" - } - } -} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt deleted file mode 100644 index d099a89..0000000 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/Key.kt +++ /dev/null @@ -1,24 +0,0 @@ -package fr.pointcheval.native_crypto_android - -import java.security.SecureRandom -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.PBEKeySpec - -object Key { - fun fromSecureRandom(bitsCount: Int) : ByteArray { - val bytes = ByteArray(bitsCount / 8) - SecureRandom.getInstanceStrong().nextBytes(bytes) - return bytes - } - - fun fromPBKDF2(password: String, salt: String, keyBytesCount: Int, iterations: Int, algorithm: String): ByteArray { - val availableHashAlgorithm: Map = mapOf( - "sha256" to "PBKDF2WithHmacSHA256", - "sha384" to "PBKDF2withHmacSHA384", - "sha512" to "PBKDF2withHmacSHA512" - ) - val spec = PBEKeySpec(password.toCharArray(), salt.toByteArray(), iterations, keyBytesCount * 8) - val skf: SecretKeyFactory = SecretKeyFactory.getInstance(availableHashAlgorithm[algorithm]) - return skf.generateSecret(spec).encoded - } -} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt index e9c5219..2e1b5e3 100644 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt @@ -1,109 +1,127 @@ package fr.pointcheval.native_crypto_android import androidx.annotation.NonNull -import fr.pointcheval.native_crypto_android.ciphers.AESCipher - +import fr.pointcheval.native_crypto_android.kdf.Pbkdf2 +import fr.pointcheval.native_crypto_android.keys.SecretKey +import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm +import fr.pointcheval.native_crypto_android.utils.Constants +import fr.pointcheval.native_crypto_android.utils.HashAlgorithm +import fr.pointcheval.native_crypto_android.utils.Task import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result +import java.util.* /** NativeCryptoAndroidPlugin */ -class NativeCryptoAndroidPlugin: FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.flutterEngine.dartExecutor, "plugins.hugop.cl/native_crypto") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "digest" -> { - if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); - if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); - - val data : ByteArray = call.argument("data")!! - val algorithm : String = call.argument("algorithm")!! - - result.success(Hash.digest(data, algorithm)) - } - "generateSecretKey" -> { - if (!call.hasArgument("bitsCount")) result.error("SIZE_NULL", null, null); - - val bitsCount : Int = call.argument("bitsCount")!! - - result.success(Key.fromSecureRandom(bitsCount)) - } - "generateKeyPair" -> { - result.notImplemented() - } - "pbkdf2" -> { - if (!call.hasArgument("password")) result.error("PASSWORD_NULL", null, null); - if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null); - if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null); - if (!call.hasArgument("iterations")) result.error("ITERATIONS_NULL", null, null); - if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); - - val password : String = call.argument("password")!! - val salt : String = call.argument("salt")!! - val keyBytesCount : Int = call.argument("keyBytesCount")!! - val iterations : Int = call.argument("iterations")!! - val algorithm : String = call.argument("algorithm")!! - - result.success(Key.fromPBKDF2(password, salt, keyBytesCount, iterations, algorithm)) - } - "encrypt" -> { - if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); - if (!call.hasArgument("key")) result.error("KEY_NULL", null, null); - if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); - - val data : ByteArray = call.argument("data")!! - val key : ByteArray = call.argument("key")!! - val algorithm : String = call.argument("algorithm")!! - - if (algorithm == "aes") { - result.success(AESCipher().encrypt(data, key)) +class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private val name = "plugins.hugop.cl/native_crypto" + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, name) + channel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + lateinit var methodCallTask: Task<*> + + when (call.method) { + "digest" -> methodCallTask = handleDigest(call.arguments()) + "generateSecretKey" -> methodCallTask = handleGenerateSecretKey(call.arguments()) + "generateKeyPair" -> result.notImplemented() + "pbkdf2" -> methodCallTask = handlePbkdf2(call.arguments()) + "encrypt" -> methodCallTask = handleEncrypt(call.arguments()) + "decrypt" -> methodCallTask = handleDecrypt(call.arguments()) + "generateSharedSecretKey" -> result.notImplemented() + else -> result.notImplemented() } - } - "decrypt" -> { - if (!call.hasArgument("data")) result.error("DATA_NULL", null, null); - if (!call.hasArgument("key")) result.error("KEY_NULL", null, null); - if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null); - - val data : ByteArray = call.argument("data")!! - val key : ByteArray = call.argument("key")!! - val algorithm : String = call.argument("algorithm")!! - - if (algorithm == "aes") { - result.success(AESCipher().decrypt(data, key)) + + methodCallTask.call() + + methodCallTask.finalize { task -> + if (task.isSuccessful()) { + result.success(task.getResult()) + } else { + val exception: Exception = task.getException() + val message = exception.message + result.error("native_crypto", message, null) + } + } + } + + private fun handleDigest(arguments: Map?): Task { + return Task { + val data: ByteArray = + Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray + val algorithm: String = + Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String + HashAlgorithm.digest(data, algorithm) + } + } + + private fun handleGenerateSecretKey(arguments: Map?): Task { + return Task { + val bitsCount: Int = Objects.requireNonNull(arguments?.get(Constants.BITS_COUNT)) as Int + SecretKey.fromSecureRandom(bitsCount).bytes } - } - "generateSharedSecretKey" -> { - if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null); - if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null); - if (!call.hasArgument("ephemeralPrivateKey")) result.error("PRIVATE_KEY_NULL", null, null); - if (!call.hasArgument("otherPublicKey")) result.error("PUBLIC_KEY_NULL", null, null); - if (!call.hasArgument("hkdfAlgorithm")) result.error("ALGORITHM_NULL", null, null); - - val salt : ByteArray = call.argument("salt")!! - val keyBytesCount : Int = call.argument("keyBytesCount")!! - val ephemeralPrivateKey : ByteArray = call.argument("ephemeralPrivateKey")!! - val otherPublicKey : ByteArray = call.argument("otherPublicKey")!! - val hkdfAlgorithm : String = call.argument("hkdfAlgorithm")!! - - result.notImplemented() - } - else -> result.notImplemented() } - } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } + private fun handlePbkdf2(arguments: Map?): Task { + return Task { + val password: String = + Objects.requireNonNull(arguments?.get(Constants.PASSWORD)) as String + val salt: String = Objects.requireNonNull(arguments?.get(Constants.SALT)) as String + val keyBytesCount: Int = + Objects.requireNonNull(arguments?.get(Constants.KEY_BYTES_COUNT)) as Int + val iterations: Int = + Objects.requireNonNull(arguments?.get(Constants.ITERATIONS)) as Int + val algorithm: String = + Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String + + val pbkdf2: Pbkdf2 = Pbkdf2(keyBytesCount, iterations, HashAlgorithm.valueOf(algorithm)) + pbkdf2.init(password, salt) + + pbkdf2.derive().bytes + } + } + + private fun handleEncrypt(arguments: Map?): Task { + return Task { + val data: ByteArray = + Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray + val key: ByteArray = Objects.requireNonNull(arguments?.get(Constants.KEY)) as ByteArray + val algorithm: String = + Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String + + val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm) + val cipher = cipherAlgorithm.getCipher() + + cipher.encrypt(data, key) + } + } + + private fun handleDecrypt(arguments: Map?): Task { + return Task { + val data: ByteArray = + Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray + val key: ByteArray = Objects.requireNonNull(arguments?.get(Constants.KEY)) as ByteArray + val algorithm: String = + Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String + + val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm) + val cipher = cipherAlgorithm.getCipher() + + cipher.decrypt(data, key) + } + } } diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt new file mode 100644 index 0000000..cbb9706 --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt @@ -0,0 +1,59 @@ +package fr.pointcheval.native_crypto_android.ciphers + +import fr.pointcheval.native_crypto_android.interfaces.Cipher +import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class AES : Cipher { + override val algorithm: CipherAlgorithm + get() = CipherAlgorithm.aes + +/* override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { + val sk: SecretKey = SecretKeySpec(key, "AES") + val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) + // javax.crypto representation = [CIPHERTEXT(n-16) || TAG(16)] + val bytes = cipher.doFinal(data) + val iv = cipher.iv.copyOf() // 12 bytes nonce + // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] + return iv.plus(bytes) + } + + override fun decrypt(data: ByteArray, key: ByteArray): ByteArray { + val sk: SecretKey = SecretKeySpec(key, "AES") + // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-16) || TAG(16)] + val iv: ByteArray = data.take(12).toByteArray() + // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] + val payload: ByteArray = data.drop(12).toByteArray() + val spec = GCMParameterSpec(16 * 8, iv) + val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec) + return cipher.doFinal(payload) + }*/ + + override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { + val sk: SecretKey = SecretKeySpec(key, "AES") + val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) + // javax.crypto representation = [CIPHERTEXT(n-16)] + val bytes = cipher.doFinal(data) + val iv = cipher.iv + // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] + return iv.plus(bytes) + } + + override fun decrypt(data: ByteArray, key: ByteArray): ByteArray { + val sk: SecretKey = SecretKeySpec(key, "AES") + // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] + val iv: ByteArray = data.take(16).toByteArray() + // javax.crypto representation = [CIPHERTEXT(n-16)] + val payload: ByteArray = data.drop(16).toByteArray() + val ivSpec = IvParameterSpec(iv) + val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, ivSpec) + return cipher.doFinal(payload) + } +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt deleted file mode 100644 index 941cfde..0000000 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AESCipher.kt +++ /dev/null @@ -1,31 +0,0 @@ -package fr.pointcheval.native_crypto_android.ciphers - -import fr.pointcheval.native_crypto_android.Cipher -import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec - -class AESCipher : Cipher() { - override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { - val sk: SecretKey = SecretKeySpec(key, "AES") - val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) - // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] - val bytes = cipher.doFinal(data) - val iv = cipher.iv.copyOf() // 12 bytes nonce - // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] - return iv.plus(bytes) - } - - override fun decrypt(data: ByteArray, key: ByteArray): ByteArray? { - val sk: SecretKey = SecretKeySpec(key, "AES") - // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] - val iv = data.sliceArray(IntRange(0,11)) // 12 bytes nonce - // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] - val payload = data.sliceArray(IntRange(12, data.size - 1)) - val spec = GCMParameterSpec(16 * 8, iv) - val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec) - return cipher.doFinal(payload) - } -} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt new file mode 100644 index 0000000..1667507 --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt @@ -0,0 +1,10 @@ +package fr.pointcheval.native_crypto_android.interfaces + +import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm + +interface Cipher { + val algorithm: CipherAlgorithm + + fun encrypt(data: ByteArray, key: ByteArray): ByteArray + fun decrypt(data: ByteArray, key: ByteArray): ByteArray +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Key.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Key.kt new file mode 100644 index 0000000..4df212b --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Key.kt @@ -0,0 +1,5 @@ +package fr.pointcheval.native_crypto_android.interfaces + +interface Key { + val bytes: ByteArray +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/KeyDerivation.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/KeyDerivation.kt new file mode 100644 index 0000000..6bd9216 --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/KeyDerivation.kt @@ -0,0 +1,10 @@ +package fr.pointcheval.native_crypto_android.interfaces + +import fr.pointcheval.native_crypto_android.keys.SecretKey +import fr.pointcheval.native_crypto_android.utils.KdfAlgorithm + +interface KeyDerivation { + val algorithm: KdfAlgorithm + + fun derive(): SecretKey +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/kdf/Pbkdf2.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/kdf/Pbkdf2.kt new file mode 100644 index 0000000..17d8c5e --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/kdf/Pbkdf2.kt @@ -0,0 +1,39 @@ +package fr.pointcheval.native_crypto_android.kdf + +import fr.pointcheval.native_crypto_android.interfaces.KeyDerivation +import fr.pointcheval.native_crypto_android.keys.SecretKey +import fr.pointcheval.native_crypto_android.utils.HashAlgorithm +import fr.pointcheval.native_crypto_android.utils.KdfAlgorithm +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + +class Pbkdf2( + private val keyBytesCount: Int, private val iterations: Int, + private val hash: HashAlgorithm = HashAlgorithm.sha256 +) : KeyDerivation { + + private var password: String? = null + private var salt: String? = null + + fun init(password: String, salt: String) { + this.password = password + this.salt = salt + } + + override val algorithm: KdfAlgorithm + get() = KdfAlgorithm.pbkdf2 + + override fun derive(): SecretKey { + if (password == null || salt == null) { + throw Exception("Password and Salt must be initialized.") + } + val spec = PBEKeySpec( + password!!.toCharArray(), + salt!!.toByteArray(), + iterations, + keyBytesCount * 8 + ) + val skf: SecretKeyFactory = SecretKeyFactory.getInstance(hash.pbkdf2String()) + return SecretKey(skf.generateSecret(spec).encoded) + } +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/keys/SecretKey.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/keys/SecretKey.kt new file mode 100644 index 0000000..07e9833 --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/keys/SecretKey.kt @@ -0,0 +1,14 @@ +package fr.pointcheval.native_crypto_android.keys + +import fr.pointcheval.native_crypto_android.interfaces.Key +import java.security.SecureRandom + +class SecretKey(override val bytes: ByteArray) : Key { + companion object { + fun fromSecureRandom(bitsCount: Int): SecretKey { + val bytes = ByteArray(bitsCount / 8) + SecureRandom.getInstanceStrong().nextBytes(bytes) + return SecretKey(bytes) + } + } +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/CipherAlgorithm.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/CipherAlgorithm.kt new file mode 100644 index 0000000..a95185f --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/CipherAlgorithm.kt @@ -0,0 +1,14 @@ +package fr.pointcheval.native_crypto_android.utils + +import fr.pointcheval.native_crypto_android.ciphers.AES +import fr.pointcheval.native_crypto_android.interfaces.Cipher + +enum class CipherAlgorithm { + aes; + + fun getCipher(): Cipher { + return when (this) { + aes -> AES() + } + } +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Constants.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Constants.kt new file mode 100644 index 0000000..1cb359a --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Constants.kt @@ -0,0 +1,14 @@ +package fr.pointcheval.native_crypto_android.utils + +object Constants { + const val ALGORITHM = "algorithm" + const val BITS_COUNT = "bitsCount" + const val DATA = "data" + const val PASSWORD = "password" + const val SALT = "salt" + const val KEY = "key" + const val KEY_BYTES_COUNT = "keyBytesCount" + const val ITERATIONS = "iterations" + const val EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey" + const val OTHER_PUBLIC_KEY = "otherPublicKey" +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/HashAlgorithm.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/HashAlgorithm.kt new file mode 100644 index 0000000..904f7ce --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/HashAlgorithm.kt @@ -0,0 +1,50 @@ +package fr.pointcheval.native_crypto_android.utils + +import java.security.MessageDigest + +@Suppress("EnumEntryName") +enum class HashAlgorithm(val bitsCount: Int) { + sha256(256), + sha384(384), + sha512(512); + + fun messageDigestString(): String { + return when (this) { + sha256 -> "SHA-256" + sha384 -> "SHA-384" + sha512 -> "SHA-512" + } + } + + fun hmacString(): String { + return when (this) { + sha256 -> "HmacSHA256" + sha384 -> "HmacSHA384" + sha512 -> "HmacSHA512" + } + } + + fun pbkdf2String(): String { + return when (this) { + sha256 -> "PBKDF2WithHmacSHA256" + sha384 -> "PBKDF2WithHmacSHA384" + sha512 -> "PBKDF2WithHmacSHA512" + } + } + + fun digest(data: ByteArray): ByteArray { + val md = MessageDigest.getInstance(messageDigestString()) + return md.digest(data) + } + + companion object { + fun digest(data: ByteArray, algorithm: String): ByteArray { + for (h in values()) { + if (h.name == algorithm) { + return h.digest(data) + } + } + throw Exception("Unknown HashAlgorithm: $algorithm") + } + } +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/KdfAlgorithm.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/KdfAlgorithm.kt new file mode 100644 index 0000000..4ef9f69 --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/KdfAlgorithm.kt @@ -0,0 +1,5 @@ +package fr.pointcheval.native_crypto_android.utils + +enum class KdfAlgorithm { + pbkdf2 +} \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Task.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Task.kt new file mode 100644 index 0000000..d4832de --- /dev/null +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/utils/Task.kt @@ -0,0 +1,44 @@ +package fr.pointcheval.native_crypto_android.utils + +class Task(private var task: () -> T) { + + private var successful = false + private var result: T? = null + private var exception: Exception? = null + + fun isSuccessful(): Boolean { + return successful + } + + fun getResult(): T { + if (successful && result != null) { + return result!! + } else { + throw Exception("No result found!") + } + } + + fun getException(): Exception { + if (exception != null) { + return exception!! + } else { + throw Exception("No exception found!") + } + } + + fun call() { + try { + result = task() + exception = null + successful = true + } catch (e: Exception) { + exception = e + result = null + successful = false + } + } + + fun finalize(callback: (task: Task) -> Unit) { + callback(this) + } +} \ No newline at end of file From 6397e10c0521213d0ddecbc494bf1385ec848f04 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 24 May 2022 18:43:46 +0200 Subject: [PATCH 14/39] feat(example): add PointyCastle benchmark --- .../example/android/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/lib/pages/benchmark_page.dart | 56 +++++-------- .../example/lib/pointycastle/aes_gcm.dart | 78 +++++++++++++++++++ packages/native_crypto/example/pubspec.yaml | 1 + 5 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 packages/native_crypto/example/lib/pointycastle/aes_gcm.dart diff --git a/packages/native_crypto/example/android/build.gradle b/packages/native_crypto/example/android/build.gradle index 24047dc..3245887 100644 --- a/packages/native_crypto/example/android/build.gradle +++ b/packages/native_crypto/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = "1.6.21" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..562c5e4 100644 --- a/packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/native_crypto/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index 71c4bea..34e8ba0 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -3,7 +3,7 @@ // ----- // File: benchmark_page.dart // Created Date: 28/12/2021 15:12:39 -// Last Modified: 28/12/2021 17:01:41 +// Last Modified: 24/05/2022 17:23:33 // ----- // Copyright (c) 2021 @@ -12,6 +12,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_example/pointycastle/aes_gcm.dart'; import 'package:native_crypto_example/widgets/button.dart'; import '../session.dart'; @@ -23,37 +24,10 @@ class BenchmarkPage extends ConsumerWidget { final Output keyContent = Output(); final Output benchmarkStatus = Output(large: true); - Future _test(WidgetRef ref, Cipher cipher) async { - Session state = ref.read(sessionProvider.state).state; - - if (state.secretKey.bytes.isEmpty) { - benchmarkStatus - .print('No SecretKey!\nGo in Key tab and generate or derive one.'); - return; - } - - int size = 64; - benchmarkStatus.print("Benchmark Test\n"); - - // Encryption - var before = DateTime.now(); - var encryptedBigFile = await cipher.encrypt(Uint8List(size * 1000000)); - var after = DateTime.now(); - var benchmark = - after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - - // Decryption - var befored = DateTime.now(); - await cipher.decrypt(encryptedBigFile); - var afterd = DateTime.now(); - var benchmarkd = - afterd.millisecondsSinceEpoch - befored.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Decryption took $benchmarkd ms\n'); - } - - Future _benchmark(WidgetRef ref, Cipher cipher) async { + Future _benchmark(WidgetRef ref, Cipher cipher, + {bool usePc = false}) async { Session state = ref.read(sessionProvider.state).state; + AesGcm pc = AesGcm(); if (state.secretKey.bytes.isEmpty) { benchmarkStatus @@ -75,7 +49,13 @@ class BenchmarkPage extends ConsumerWidget { // Encryption var before = DateTime.now(); - var encryptedBigFile = await cipher.encrypt(b.buffer.asUint8List()); + Object encryptedBigFile; + if (usePc) { + encryptedBigFile = + pc.encrypt(b.buffer.asUint8List(), state.secretKey.bytes); + } else { + encryptedBigFile = await cipher.encrypt(b.buffer.asUint8List()); + } var after = DateTime.now(); var benchmark = @@ -87,7 +67,11 @@ class BenchmarkPage extends ConsumerWidget { // Decryption before = DateTime.now(); - await cipher.decrypt(encryptedBigFile); + if (usePc) { + pc.decrypt(encryptedBigFile as Uint8List, state.secretKey.bytes); + } else { + await cipher.decrypt(encryptedBigFile as CipherText); + } after = DateTime.now(); benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; @@ -150,11 +134,11 @@ class BenchmarkPage extends ConsumerWidget { children: [ Button( () => _benchmark(ref, cipher), - "Launch benchmark", + "NativeCrypto", ), Button( - () => _test(ref, cipher), - "Test benchmark", + () => _benchmark(ref, cipher, usePc: true), + "PointyCastle", ), Button( _clear, diff --git a/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart b/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart new file mode 100644 index 0000000..a0aa0be --- /dev/null +++ b/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart @@ -0,0 +1,78 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_gcm.dart +// Created Date: 24/05/2022 16:34:54 +// Last Modified: 24/05/2022 17:15:22 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/src/platform_check/platform_check.dart'; + +class AesGcm { + FortunaRandom? _secureRandom; + + Uint8List encrypt(Uint8List data, Uint8List key) { + final iv = generateRandomBytes(12); + + final gcm = GCMBlockCipher(AESEngine()) + ..init( + true, + AEADParameters( + KeyParameter(key), + 16 * 8, + iv!, + Uint8List(0), + ), + ); // true=encrypt + + final cipherText = gcm.process(data); + + return Uint8List.fromList( + iv + cipherText, + ); + } + + Uint8List decrypt(Uint8List cipherText, Uint8List key) { + final iv = Uint8List.fromList(cipherText.sublist(0, 12)); + final cipherTextWithoutIv = Uint8List.fromList( + cipherText.sublist(12), + ); + + final gcm = GCMBlockCipher(AESEngine()) + ..init( + false, + AEADParameters( + KeyParameter(key), + 16 * 8, + iv, + Uint8List(0), + ), + ); // false=decrypt + + // Decrypt the cipherText block-by-block + + final paddedPlainText = gcm.process(cipherTextWithoutIv); + + return paddedPlainText; + } + + /// Generate random bytes to use as the Initialization Vector (IV). + Uint8List? generateRandomBytes(int numBytes) { + if (_secureRandom == null) { + // First invocation: create _secureRandom and seed it + + _secureRandom = FortunaRandom(); + _secureRandom!.seed( + KeyParameter(Platform.instance.platformEntropySource().getBytes(32))); + } + + // Use it to generate the random bytes + + final iv = _secureRandom!.nextBytes(numBytes); + return iv; + } +} diff --git a/packages/native_crypto/example/pubspec.yaml b/packages/native_crypto/example/pubspec.yaml index b8fd86e..7fb09c7 100644 --- a/packages/native_crypto/example/pubspec.yaml +++ b/packages/native_crypto/example/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 flutter_riverpod: ^1.0.3 + pointycastle: ^3.6.0 dev_dependencies: flutter_test: From a7affea1e14192b870306aa9c17270d688ed8fab Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Tue, 24 May 2022 23:59:10 +0200 Subject: [PATCH 15/39] perf: x10 perfomance improvement on android with better list management --- .../example/lib/pages/benchmark_page.dart | 101 ++++++++++++++---- .../lib/src/ciphers/aes/aes.dart | 77 ++++++------- .../lib/src/ciphers/aes/aes_mode.dart | 4 +- .../lib/src/ciphers/aes/aes_padding.dart | 4 +- .../lib/src/core/cipher_text.dart | 58 ++++++++-- .../lib/src/core/cipher_text_list.dart | 3 +- .../lib/src/interfaces/cipher.dart | 13 ++- .../NativeCryptoAndroidPlugin.kt | 53 +++++++-- .../native_crypto_android/ciphers/AES.kt | 49 ++++++--- .../interfaces/Cipher.kt | 2 + .../analysis_options.yaml | 5 +- .../lib/native_crypto_platform_interface.dart | 75 ++----------- .../method_channel_native_crypto.dart | 87 +++++++++------ .../lib/src/platform_interface.dart | 97 ----------------- .../native_crypto_platform.dart | 92 ++++++++++++++++ .../lib/src/utils/exception.dart | 53 +++++++++ .../pubspec.yaml | 10 +- 17 files changed, 474 insertions(+), 309 deletions(-) rename packages/native_crypto_platform_interface/lib/src/{ => method_channel}/method_channel_native_crypto.dart (53%) delete mode 100644 packages/native_crypto_platform_interface/lib/src/platform_interface.dart create mode 100644 packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart create mode 100644 packages/native_crypto_platform_interface/lib/src/utils/exception.dart diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index 34e8ba0..e444eac 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -3,7 +3,7 @@ // ----- // File: benchmark_page.dart // Created Date: 28/12/2021 15:12:39 -// Last Modified: 24/05/2022 17:23:33 +// Last Modified: 24/05/2022 23:43:59 // ----- // Copyright (c) 2021 @@ -24,6 +24,51 @@ class BenchmarkPage extends ConsumerWidget { final Output keyContent = Output(); final Output benchmarkStatus = Output(large: true); + Future _benchmarkEncryptionOnly( + WidgetRef ref, + Cipher cipher, + ) async { + Session state = ref.read(sessionProvider.state).state; + AesGcm pc = AesGcm(); + + if (state.secretKey.bytes.isEmpty) { + benchmarkStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return; + } + + benchmarkStatus.print("Benchmark 2/4/8/16/32/64/128/256MB\n"); + List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; + String csv = "size;encryption time\n"; + + var beforeBench = DateTime.now(); + Cipher.bytesCountPerChunk = Cipher.bytesCountPerChunk; + benchmarkStatus + .append('[Benchmark] ${Cipher.bytesCountPerChunk} bytes/chunk \n'); + for (int size in testedSizes) { + var b = Uint8List(size * 1000000); + csv += "${size * 1000000};"; + + // Encryption + var before = DateTime.now(); + var encryptedBigFile = await cipher.encrypt(b); + var after = DateTime.now(); + + var benchmark = + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); + + csv += "$benchmark\n"; + } + var afterBench = DateTime.now(); + var benchmark = + afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch; + var sum = testedSizes.reduce((a, b) => a + b); + benchmarkStatus.append( + 'Benchmark finished.\nGenerated, and encrypted $sum MB in $benchmark ms'); + debugPrint("[Benchmark cvs]\n$csv"); + } + Future _benchmark(WidgetRef ref, Cipher cipher, {bool usePc = false}) async { Session state = ref.read(sessionProvider.state).state; @@ -37,13 +82,14 @@ class BenchmarkPage extends ConsumerWidget { benchmarkStatus.print("Benchmark 2/4/8/16/32/64/128/256MB\n"); List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; - String csv = - "size;encryption time;encode time;decryption time;crypto time\n"; + String csv = "size;encryption time;decryption time;crypto time\n"; var beforeBench = DateTime.now(); + Cipher.bytesCountPerChunk = Cipher.bytesCountPerChunk; + benchmarkStatus + .append('[Benchmark] ${Cipher.bytesCountPerChunk} bytes/chunk \n'); for (int size in testedSizes) { - var b = ByteData(size * 1000000); - //var bigFile = Uint8List.view(); + var b = Uint8List(size * 1000000); csv += "${size * 1000000};"; var cryptoTime = 0; @@ -51,10 +97,9 @@ class BenchmarkPage extends ConsumerWidget { var before = DateTime.now(); Object encryptedBigFile; if (usePc) { - encryptedBigFile = - pc.encrypt(b.buffer.asUint8List(), state.secretKey.bytes); + encryptedBigFile = pc.encrypt(b, state.secretKey.bytes); } else { - encryptedBigFile = await cipher.encrypt(b.buffer.asUint8List()); + encryptedBigFile = await cipher.encrypt(b); } var after = DateTime.now(); @@ -62,7 +107,7 @@ class BenchmarkPage extends ConsumerWidget { after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - csv += "$benchmark;"; + csv += "$benchmark"; cryptoTime += benchmark; // Decryption @@ -129,20 +174,34 @@ class BenchmarkPage extends ConsumerWidget { alignment: Alignment.centerLeft, ), keyContent, - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Button( - () => _benchmark(ref, cipher), - "NativeCrypto", - ), - Button( - () => _benchmark(ref, cipher, usePc: true), - "PointyCastle", + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + () => _benchmark(ref, cipher), + "NativeCrypto", + ), + Button( + () => _benchmark(ref, cipher, usePc: true), + "PointyCastle", + ), + Button( + _clear, + "Clear", + ), + ], ), - Button( - _clear, - "Clear", + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + () => _benchmarkEncryptionOnly(ref, cipher), + "NC Persistence", + ), + ], ), ], ), diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index a1ae944..d740757 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 23:06:05 +// Last Modified: 24/05/2022 23:29:42 // ----- // Copyright (c) 2022 @@ -39,6 +39,7 @@ class AES implements Cipher { final Map> _supported = { AESMode.gcm: [AESPadding.none], + AESMode.cbc: [AESPadding.pkcs5], }; if (!_supported[mode]!.contains(padding)) { @@ -46,27 +47,35 @@ class AES implements Cipher { } } + Future _decrypt(CipherText cipherText) async { + return await platform.decryptAsList( + [cipherText.iv, cipherText.payload], + key.bytes, + algorithm.name, + ) ?? + Uint8List(0); + } + + Future _encrypt(Uint8List data) async { + final List cipherText = + await platform.encryptAsList(data, key.bytes, algorithm.name) ?? + List.empty(); + return CipherText.fromPairIvAndBytes( + cipherText, + dataLength: cipherText.last.length, + ); + } + @override Future decrypt(CipherText cipherText) async { final BytesBuilder decryptedData = BytesBuilder(copy: false); + if (cipherText is CipherTextList) { for (final CipherText ct in cipherText.list) { - final Uint8List d = await platform.decrypt( - ct.bytes, - key.bytes, - algorithm.name, - ) ?? - Uint8List(0); - decryptedData.add(d); + decryptedData.add(await _decrypt(ct)); } } else { - final Uint8List d = await platform.decrypt( - cipherText.bytes, - key.bytes, - algorithm.name, - ) ?? - Uint8List(0); - decryptedData.add(d); + decryptedData.add(await _decrypt(cipherText)); } return decryptedData.toBytes(); @@ -75,43 +84,23 @@ class AES implements Cipher { @override Future encrypt(Uint8List data) async { Uint8List dataToEncrypt; + final CipherTextList cipherTextList = CipherTextList(); - // If data is bigger than 32mB -> split in chunks - if (data.length > CipherTextList.chunkSize) { - final int chunkNb = (data.length / CipherTextList.chunkSize).ceil(); + + if (data.length > Cipher.bytesCountPerChunk) { + final int chunkNb = (data.length / Cipher.bytesCountPerChunk).ceil(); for (var i = 0; i < chunkNb; i++) { dataToEncrypt = i < (chunkNb - 1) ? data.sublist( - i * CipherTextList.chunkSize, - (i + 1) * CipherTextList.chunkSize, + i * Cipher.bytesCountPerChunk, + (i + 1) * Cipher.bytesCountPerChunk, ) - : data.sublist(i * CipherTextList.chunkSize); - final Uint8List c = await platform.encrypt( - dataToEncrypt, - key.bytes, - algorithm.name, - ) ?? - Uint8List(0); - cipherTextList.add( - CipherText( - c.sublist(0, 12), - c.sublist(12, c.length - 16), - c.sublist(c.length - 16, c.length), - ), - ); // TODO(hpcl): generify this + : data.sublist(i * Cipher.bytesCountPerChunk); + cipherTextList.add(await _encrypt(dataToEncrypt)); } } else { - final Uint8List c = - await platform.encrypt(data, key.bytes, algorithm.name) ?? - Uint8List(0); - - return CipherText( - c.sublist(0, 12), - c.sublist(12, c.length - 16), - c.sublist(c.length - 16, c.length), - ); // TODO(hpcl): generify this + return _encrypt(data); } - return cipherTextList; } } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart index 554cdde..1ff7b5d 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -3,9 +3,9 @@ // ----- // File: aes_mode.dart // Created Date: 23/05/2022 22:09:16 -// Last Modified: 23/05/2022 22:10:31 +// Last Modified: 24/05/2022 23:17:01 // ----- // Copyright (c) 2022 /// Defines the AES modes of operation. -enum AESMode { gcm } +enum AESMode { gcm, cbc } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart index 06e323b..676f526 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart @@ -3,9 +3,9 @@ // ----- // File: aes_padding.dart // Created Date: 23/05/2022 22:10:17 -// Last Modified: 23/05/2022 22:13:28 +// Last Modified: 24/05/2022 23:17:25 // ----- // Copyright (c) 2022 /// Represents different paddings. -enum AESPadding { none } +enum AESPadding { none, pkcs5 } diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index cd840a3..fcf09aa 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 23/05/2022 23:02:10 +// Last Modified: 24/05/2022 21:27:44 // ----- // Copyright (c) 2021 @@ -16,23 +16,61 @@ class CipherText extends ByteArray { final int _dataLength; final int _tagLength; - CipherText(Uint8List iv, Uint8List data, Uint8List tag) + final Uint8List _iv; + + CipherText(Uint8List iv, Uint8List data, Uint8List? tag) : _ivLength = iv.length, _dataLength = data.length, - _tagLength = tag.length, - super(Uint8List.fromList(iv + data + tag)); + _tagLength = tag?.length ?? 0, + _iv = iv, + super((tag != null) ? Uint8List.fromList(data + tag) : data); + + CipherText.fromBytes( + Uint8List bytes, { + required int ivLength, + required int dataLength, + int tagLength = 0, + }) : _ivLength = ivLength, + _dataLength = dataLength, + _tagLength = tagLength, + _iv = bytes.sublist(0, ivLength), + super(bytes.sublist(ivLength, bytes.length - tagLength)); + + const CipherText.fromIvAndBytes( + Uint8List iv, + super.data, { + required int dataLength, + int tagLength = 0, + }) : _ivLength = iv.length, + _dataLength = dataLength, + _tagLength = tagLength, + _iv = iv; + + CipherText.fromPairIvAndBytes( + List pair, { + required int dataLength, + int tagLength = 0, + }) : _ivLength = pair.first.length, + _dataLength = dataLength, + _tagLength = tagLength, + _iv = pair.first, + super(pair.last); /// Gets the CipherText IV. - Uint8List get iv => bytes.sublist(0, _ivLength); + Uint8List get iv => _iv; /// Gets the CipherText data. - Uint8List get data => bytes.sublist(_ivLength, _ivLength + _dataLength); + Uint8List get data => _tagLength > 0 + ? bytes.sublist(0, _dataLength - _tagLength) + : bytes; /// Gets the CipherText tag. - Uint8List get tag => bytes.sublist( - _ivLength + _dataLength, - _ivLength + _dataLength + _tagLength, - ); + Uint8List get tag => _tagLength > 0 + ? bytes.sublist(_dataLength - _tagLength, _dataLength) + : Uint8List(0); + + /// Gets the CipherText data and tag. + Uint8List get payload => bytes; /// Gets the CipherText IV length. int get ivLength => _ivLength; diff --git a/packages/native_crypto/lib/src/core/cipher_text_list.dart b/packages/native_crypto/lib/src/core/cipher_text_list.dart index 9d16211..dab2709 100644 --- a/packages/native_crypto/lib/src/core/cipher_text_list.dart +++ b/packages/native_crypto/lib/src/core/cipher_text_list.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text_list.dart // Created Date: 23/05/2022 22:59:02 -// Last Modified: 23/05/2022 23:05:02 +// Last Modified: 24/05/2022 20:18:26 // ----- // Copyright (c) 2022 @@ -12,7 +12,6 @@ import 'dart:typed_data'; import 'package:native_crypto/src/core/cipher_text.dart'; class CipherTextList extends CipherText { - static const int chunkSize = 33554432; final List _list; CipherTextList() diff --git a/packages/native_crypto/lib/src/interfaces/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart index df8296a..8941ee8 100644 --- a/packages/native_crypto/lib/src/interfaces/cipher.dart +++ b/packages/native_crypto/lib/src/interfaces/cipher.dart @@ -3,7 +3,7 @@ // ----- // File: cipher.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 23:06:20 +// Last Modified: 24/05/2022 19:55:38 // ----- // Copyright (c) 2021 @@ -18,9 +18,20 @@ import 'package:native_crypto/src/utils/cipher_algorithm.dart'; /// or decryption - a series of well-defined steps that can /// be followed as a procedure. abstract class Cipher { + /// Returns the size of a chunk of data + /// that can be processed by the cipher. + static int _bytesCountPerChunk = 33554432; + + static int get bytesCountPerChunk => Cipher._bytesCountPerChunk; + + static set bytesCountPerChunk(int bytesCount) { + _bytesCountPerChunk = bytesCount; + } + /// Returns the standard algorithm name for this cipher CipherAlgorithm get algorithm; + /// Encrypts data. /// /// Takes [Uint8List] data as parameter. diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt index 2e1b5e3..36cd412 100644 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/NativeCryptoAndroidPlugin.kt @@ -1,6 +1,7 @@ package fr.pointcheval.native_crypto_android import androidx.annotation.NonNull +import fr.pointcheval.native_crypto_android.interfaces.Cipher import fr.pointcheval.native_crypto_android.kdf.Pbkdf2 import fr.pointcheval.native_crypto_android.keys.SecretKey import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm @@ -23,6 +24,8 @@ class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { private lateinit var channel: MethodChannel private val name = "plugins.hugop.cl/native_crypto" + private var cipherInstance: Cipher? = null + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, name) channel.setMethodCallHandler(this) @@ -38,11 +41,11 @@ class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { when (call.method) { "digest" -> methodCallTask = handleDigest(call.arguments()) "generateSecretKey" -> methodCallTask = handleGenerateSecretKey(call.arguments()) - "generateKeyPair" -> result.notImplemented() "pbkdf2" -> methodCallTask = handlePbkdf2(call.arguments()) - "encrypt" -> methodCallTask = handleEncrypt(call.arguments()) - "decrypt" -> methodCallTask = handleDecrypt(call.arguments()) - "generateSharedSecretKey" -> result.notImplemented() + "encryptAsList" -> methodCallTask = handleEncryptAsList(call.arguments()) + "decryptAsList" -> methodCallTask = handleDecryptAsList(call.arguments()) + "encrypt" -> methodCallTask = handleCrypt(call.arguments(), true) + "decrypt" -> methodCallTask = handleCrypt(call.arguments(), false) else -> result.notImplemented() } @@ -95,7 +98,17 @@ class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { } } - private fun handleEncrypt(arguments: Map?): Task { + private fun lazyLoadCipher(cipherAlgorithm: CipherAlgorithm) { + if (cipherInstance == null) { + cipherInstance = cipherAlgorithm.getCipher() + } else { + if (cipherInstance!!.algorithm != cipherAlgorithm) { + cipherInstance = cipherAlgorithm.getCipher() + } + } + } + + private fun handleEncryptAsList(arguments: Map?): Task> { return Task { val data: ByteArray = Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray @@ -104,13 +117,29 @@ class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm) - val cipher = cipherAlgorithm.getCipher() + lazyLoadCipher(cipherAlgorithm) - cipher.encrypt(data, key) + cipherInstance!!.encryptAsList(data, key) } } - private fun handleDecrypt(arguments: Map?): Task { + private fun handleDecryptAsList(arguments: Map?): Task { + return Task { + val data: List = + Objects.requireNonNull(arguments?.get(Constants.DATA)) as List + val key: ByteArray = Objects.requireNonNull(arguments?.get(Constants.KEY)) as ByteArray + val algorithm: String = + Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String + + val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm) + lazyLoadCipher(cipherAlgorithm) + + cipherInstance!!.decryptAsList(data, key) + } + } + + // **EN**Crypt and **DE**Crypt + private fun handleCrypt(arguments: Map?, forEncryption: Boolean): Task { return Task { val data: ByteArray = Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray @@ -119,9 +148,13 @@ class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler { Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm) - val cipher = cipherAlgorithm.getCipher() + lazyLoadCipher(cipherAlgorithm) - cipher.decrypt(data, key) + if (forEncryption) { + cipherInstance!!.encrypt(data, key) + } else { + cipherInstance!!.decrypt(data, key) + } } } } diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt index cbb9706..1bb2c3b 100644 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt @@ -11,6 +11,10 @@ class AES : Cipher { override val algorithm: CipherAlgorithm get() = CipherAlgorithm.aes + var forEncryption: Boolean = true + var cipherInstance: javax.crypto.Cipher? = null; + var secretKey: SecretKeySpec? = null; + /* override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { val sk: SecretKey = SecretKeySpec(key, "AES") val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") @@ -35,25 +39,46 @@ class AES : Cipher { }*/ override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { - val sk: SecretKey = SecretKeySpec(key, "AES") - val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) + val list : List = encryptAsList(data, key) + return list.first().plus(list.last()) + } + + override fun encryptAsList(data: ByteArray, key: ByteArray): List { + val sk = SecretKeySpec(key, "AES") + if (cipherInstance == null || !forEncryption || secretKey != sk) { + secretKey = sk + forEncryption = true + // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] + // javax.crypto representation = [CIPHERTEXT(n-16)] + cipherInstance = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") + cipherInstance!!.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) + } // javax.crypto representation = [CIPHERTEXT(n-16)] - val bytes = cipher.doFinal(data) - val iv = cipher.iv + val bytes: ByteArray = cipherInstance!!.doFinal(data) + val iv: ByteArray = cipherInstance!!.iv // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] - return iv.plus(bytes) + return listOf(iv, bytes) } override fun decrypt(data: ByteArray, key: ByteArray): ByteArray { - val sk: SecretKey = SecretKeySpec(key, "AES") - // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] - val iv: ByteArray = data.take(16).toByteArray() // javax.crypto representation = [CIPHERTEXT(n-16)] + val iv: ByteArray = data.take(16).toByteArray() val payload: ByteArray = data.drop(16).toByteArray() + return decryptAsList(listOf(iv, payload), key) + } + + override fun decryptAsList(data: List, key: ByteArray): ByteArray { + if (cipherInstance == null) { + // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] + // javax.crypto representation = [CIPHERTEXT(n-16)] + cipherInstance = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") + } + val sk = SecretKeySpec(key, "AES") + val iv: ByteArray = data.first() val ivSpec = IvParameterSpec(iv) - val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, ivSpec) - return cipher.doFinal(payload) + cipherInstance!!.init(javax.crypto.Cipher.DECRYPT_MODE, sk, ivSpec) + forEncryption = false + val payload: ByteArray = data.last() + return cipherInstance!!.doFinal(payload) } } \ No newline at end of file diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt index 1667507..5893d25 100644 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/interfaces/Cipher.kt @@ -7,4 +7,6 @@ interface Cipher { fun encrypt(data: ByteArray, key: ByteArray): ByteArray fun decrypt(data: ByteArray, key: ByteArray): ByteArray + fun encryptAsList(data: ByteArray, key: ByteArray): List + fun decryptAsList(data: List, key: ByteArray): ByteArray } \ No newline at end of file diff --git a/packages/native_crypto_platform_interface/analysis_options.yaml b/packages/native_crypto_platform_interface/analysis_options.yaml index a5744c1..db48808 100644 --- a/packages/native_crypto_platform_interface/analysis_options.yaml +++ b/packages/native_crypto_platform_interface/analysis_options.yaml @@ -1,4 +1 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +include: package:wyatt_analysis/analysis_options.flutter.experimental.yaml \ No newline at end of file diff --git a/packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart b/packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart index 1d6fbd5..c85bda1 100644 --- a/packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart +++ b/packages/native_crypto_platform_interface/lib/native_crypto_platform_interface.dart @@ -2,74 +2,13 @@ // Email: git@pcl.ovh // ----- // File: native_crypto_platform_interface.dart -// Created Date: 25/12/2021 16:43:49 -// Last Modified: 25/12/2021 17:39:39 +// Created Date: 24/05/2022 19:39:11 +// Last Modified: 24/05/2022 19:39:58 // ----- -// Copyright (c) 2021 +// Copyright (c) 2022 -import 'dart:typed_data'; +library native_crypto_platform_interface; -import './src/method_channel_native_crypto.dart'; -import './src/platform_interface.dart'; - -/// The interface that implementations of path_provider must implement. -/// -/// Platform implementations should extend this class rather than implement it as `NativeCrypto` -/// does not consider newly added methods to be breaking changes. Extending this class -/// (using `extends`) ensures that the subclass will get the default implementation, while -/// platform implementations that `implements` this interface will be broken by newly added -/// [NativeCryptoPlatform] methods. -abstract class NativeCryptoPlatform extends PlatformInterface { - /// Constructs a NativeCryptoPlatform. - NativeCryptoPlatform() : super(token: _token); - - static final Object _token = Object(); - - static NativeCryptoPlatform _instance = MethodChannelNativeCrypto(); - - /// The default instance of [NativeCryptoPlatform] to use. - /// - /// Defaults to [MethodChannelPathProvider]. - static NativeCryptoPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [NativeCryptoPlatform] when they register themselves. - static set instance(NativeCryptoPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future digest(Uint8List data, String algorithm) { - throw UnimplementedError('digest is not implemented'); - } - - Future generateSecretKey(int bitsCount) { - throw UnimplementedError('generateSecretKey is not implemented'); - } - - Future generateKeyPair() { - throw UnimplementedError('generateKeyPair is not implemented'); - } - - Future pbkdf2(String password, String salt, int keyBytesCount, - int iterations, String algorithm) { - throw UnimplementedError('pbkdf2 is not implemented'); - } - - Future encrypt(Uint8List data, Uint8List key, String algorithm) { - throw UnimplementedError('encrypt is not implemented'); - } - - Future decrypt(Uint8List data, Uint8List key, String algorithm) { - throw UnimplementedError('decrypt is not implemented'); - } - - Future generateSharedSecretKey( - Uint8List salt, - int keyBytesCount, - Uint8List ephemeralPrivateKey, - Uint8List otherPublicKey, - String hkdfAlgorithm) { - throw UnimplementedError('generateSharedSecretKey is not implemented'); - } -} +export 'src/method_channel/method_channel_native_crypto.dart'; +export 'src/platform_interface/native_crypto_platform.dart'; +export 'src/utils/exception.dart'; diff --git a/packages/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart b/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart similarity index 53% rename from packages/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart rename to packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart index 3cab922..7205096 100644 --- a/packages/native_crypto_platform_interface/lib/src/method_channel_native_crypto.dart +++ b/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto_method_channel.dart // Created Date: 25/12/2021 16:58:04 -// Last Modified: 25/12/2021 18:58:53 +// Last Modified: 24/05/2022 22:59:32 // ----- // Copyright (c) 2021 @@ -11,19 +11,17 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; - -import '../native_crypto_platform_interface.dart'; +import 'package:native_crypto_platform_interface/src/platform_interface/native_crypto_platform.dart'; /// An implementation of [NativeCryptoPlatform] that uses method channels. class MethodChannelNativeCrypto extends NativeCryptoPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - MethodChannel methodChannel = - const MethodChannel('plugins.hugop.cl/native_crypto'); + MethodChannel channel = const MethodChannel('plugins.hugop.cl/native_crypto'); @override Future digest(Uint8List data, String algorithm) { - return methodChannel.invokeMethod( + return channel.invokeMethod( 'digest', { 'data': data, @@ -34,7 +32,7 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { @override Future generateSecretKey(int bitsCount) { - return methodChannel.invokeMethod( + return channel.invokeMethod( 'generateSecretKey', { 'bitsCount': bitsCount, @@ -43,14 +41,14 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { } @override - Future generateKeyPair() { - return methodChannel.invokeMethod('generateKeyPair'); - } - - @override - Future pbkdf2(String password, String salt, int keyBytesCount, - int iterations, String algorithm) { - return methodChannel.invokeMethod( + Future pbkdf2( + String password, + String salt, + int keyBytesCount, + int iterations, + String algorithm, + ) { + return channel.invokeMethod( 'pbkdf2', { 'password': password, @@ -63,9 +61,13 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { } @override - Future encrypt(Uint8List data, Uint8List key, String algorithm) { - return methodChannel.invokeMethod( - 'encrypt', + Future?> encryptAsList( + Uint8List data, + Uint8List key, + String algorithm, + ) { + return channel.invokeListMethod( + 'encryptAsList', { 'data': data, 'key': key, @@ -75,9 +77,13 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { } @override - Future decrypt(Uint8List data, Uint8List key, String algorithm) { - return methodChannel.invokeMethod( - 'decrypt', + Future decryptAsList( + List data, + Uint8List key, + String algorithm, + ) { + return channel.invokeMethod( + 'decryptAsList', { 'data': data, 'key': key, @@ -87,20 +93,33 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { } @override - Future generateSharedSecretKey( - Uint8List salt, - int keyBytesCount, - Uint8List ephemeralPrivateKey, - Uint8List otherPublicKey, - String hkdfAlgorithm) { - return methodChannel.invokeMethod( - 'generateSharedSecretKey', + Future encrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) { + return channel.invokeMethod( + 'encrypt', { - 'salt': salt, - 'keyBytesCount': keyBytesCount, - 'ephemeralPrivateKey': ephemeralPrivateKey, - 'otherPublicKey': otherPublicKey, - 'hkdfAlgorithm': hkdfAlgorithm, + 'data': data, + 'key': key, + 'algorithm': algorithm, + }, + ); + } + + @override + Future decrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) { + return channel.invokeMethod( + 'decrypt', + { + 'data': data, + 'key': key, + 'algorithm': algorithm, }, ); } diff --git a/packages/native_crypto_platform_interface/lib/src/platform_interface.dart b/packages/native_crypto_platform_interface/lib/src/platform_interface.dart deleted file mode 100644 index 31f8037..0000000 --- a/packages/native_crypto_platform_interface/lib/src/platform_interface.dart +++ /dev/null @@ -1,97 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: platform_interface.dart -// Created Date: 25/12/2021 16:52:56 -// Last Modified: 27/12/2021 21:25:39 -// ----- -// Copyright (c) 2021 - -import 'package:meta/meta.dart'; - -/// Base class for platform interfaces. -/// -/// Provides a static helper method for ensuring that platform interfaces are -/// implemented using `extends` instead of `implements`. -/// -/// Platform interface classes are expected to have a private static token object which will be -/// be passed to [verifyToken] along with a platform interface object for verification. -/// -/// Sample usage: -/// -/// ```dart -/// abstract class NativeCryptoPlatform extends PlatformInterface { -/// NativeCryptoPlatform() : super(token: _token); -/// -/// static NativeCryptoPlatform _instance = MethodChannelNativeCrypto(); -/// -/// static const Object _token = Object(); -/// -/// static NativeCryptoPlatform get instance => _instance; -/// -/// /// Platform-specific plugins should set this with their own platform-specific -/// /// class that extends [NativeCryptoPlatform] when they register themselves. -/// static set instance(NativeCryptoPlatform instance) { -/// PlatformInterface.verifyToken(instance, _token); -/// _instance = instance; -/// } -/// -/// } -/// ``` -/// -/// Mockito mocks of platform interfaces will fail the verification, in test code only it is possible -/// to include the [MockPlatformInterfaceMixin] for the verification to be temporarily disabled. See -/// [MockPlatformInterfaceMixin] for a sample of using Mockito to mock a platform interface. -abstract class PlatformInterface { - /// Pass a private, class-specific `const Object()` as the `token`. - PlatformInterface({required Object token}) : _instanceToken = token; - - final Object? _instanceToken; - - /// Ensures that the platform instance has a token that matches the - /// provided token and throws [AssertionError] if not. - /// - /// This is used to ensure that implementers are using `extends` rather than - /// `implements`. - /// - /// Subclasses of [MockPlatformInterfaceMixin] are assumed to be valid in debug - /// builds. - /// - /// This is implemented as a static method so that it cannot be overridden - /// with `noSuchMethod`. - static void verifyToken(PlatformInterface instance, Object token) { - if (instance is MockPlatformInterfaceMixin) { - bool assertionsEnabled = false; - assert(() { - assertionsEnabled = true; - return true; - }()); - if (!assertionsEnabled) { - throw AssertionError( - '`MockPlatformInterfaceMixin` is not intended for use in release builds.'); - } - return; - } - if (!identical(token, instance._instanceToken)) { - throw AssertionError( - 'Platform interfaces must not be implemented with `implements`'); - } - } -} - -/// A [PlatformInterface] mixin that can be combined with mockito's `Mock`. -/// -/// It passes the [PlatformInterface.verifyToken] check even though it isn't -/// using `extends`. -/// -/// This class is intended for use in tests only. -/// -/// Sample usage (assuming NativeCryptoPlatform extends [PlatformInterface]: -/// -/// ```dart -/// class NativeCryptoPlatformMock extends Mock -/// with MockPlatformInterfaceMixin -/// implements NativeCryptoPlatform {} -/// ``` -@visibleForTesting -abstract class MockPlatformInterfaceMixin implements PlatformInterface {} diff --git a/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart b/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart new file mode 100644 index 0000000..b5caf6b --- /dev/null +++ b/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart @@ -0,0 +1,92 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: native_crypto_platform_interface.dart +// Created Date: 25/12/2021 16:43:49 +// Last Modified: 24/05/2022 22:58:31 +// ----- +// Copyright (c) 2021 + +import 'dart:typed_data'; + +import 'package:native_crypto_platform_interface/src/method_channel/method_channel_native_crypto.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of path_provider must implement. +/// +/// Platform implementations should extend this class rather than implement +/// it as `NativeCrypto` does not consider newly added methods to be +/// breaking changes. Extending this class (using `extends`) ensures +/// that the subclass will get the default implementation, while platform +/// implementations that `implements` this interface will be +/// broken by newly added [NativeCryptoPlatform] methods. +abstract class NativeCryptoPlatform extends PlatformInterface { + /// Constructs a NativeCryptoPlatform. + NativeCryptoPlatform() : super(token: _token); + + static final Object _token = Object(); + + static NativeCryptoPlatform _instance = MethodChannelNativeCrypto(); + + /// The default instance of [NativeCryptoPlatform] to use. + /// + /// Defaults to [MethodChannelNativeCrypto]. + static NativeCryptoPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [NativeCryptoPlatform] when they register themselves. + static set instance(NativeCryptoPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future digest(Uint8List data, String algorithm) { + throw UnimplementedError('digest is not implemented'); + } + + Future generateSecretKey(int bitsCount) { + throw UnimplementedError('generateSecretKey is not implemented'); + } + + Future pbkdf2( + String password, + String salt, + int keyBytesCount, + int iterations, + String algorithm, + ) { + throw UnimplementedError('pbkdf2 is not implemented'); + } + + Future?> encryptAsList( + Uint8List data, + Uint8List key, + String algorithm, + ) { + throw UnimplementedError('encryptAsList is not implemented'); + } + + Future decryptAsList( + List data, + Uint8List key, + String algorithm, + ) { + throw UnimplementedError('decryptAsList is not implemented'); + } + + Future encrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) { + throw UnimplementedError('encrypt is not implemented'); + } + + Future decrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) { + throw UnimplementedError('decrypt is not implemented'); + } +} diff --git a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart new file mode 100644 index 0000000..24af864 --- /dev/null +++ b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart @@ -0,0 +1,53 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: exception.dart +// Created Date: 24/05/2022 18:54:48 +// Last Modified: 24/05/2022 18:58:39 +// ----- +// Copyright (c) 2022 + +import 'package:flutter/services.dart'; + +class NativeCryptoException implements Exception { + final String message; + const NativeCryptoException(this.message); +} + +/// Catches a [PlatformException] and returns an [Exception]. +/// +/// If the [Exception] is a [PlatformException], +/// a [NativeCryptoException] is returned. +Never convertPlatformException(Object exception, StackTrace stackTrace) { + if (exception is! Exception || exception is! PlatformException) { + Error.throwWithStackTrace(exception, stackTrace); + } + + Error.throwWithStackTrace( + platformExceptionToNativeCryptoException(exception, stackTrace), + stackTrace, + ); +} + +/// Converts a [PlatformException] into a [NativeCryptoException]. +/// +/// A [PlatformException] can only be converted to a [NativeCryptoException] +/// if the `details` of the exception exist. +NativeCryptoException platformExceptionToNativeCryptoException( + PlatformException platformException, + StackTrace stackTrace, +) { + final Map? details = platformException.details != null + ? Map.from( + platformException.details as Map, + ) + : null; + + String message = platformException.message ?? ''; + + if (details != null) { + message = details['message'] ?? message; + } + + return NativeCryptoException(message); +} diff --git a/packages/native_crypto_platform_interface/pubspec.yaml b/packages/native_crypto_platform_interface/pubspec.yaml index 3e90763..fd0ceb5 100644 --- a/packages/native_crypto_platform_interface/pubspec.yaml +++ b/packages/native_crypto_platform_interface/pubspec.yaml @@ -3,15 +3,21 @@ description: A common interface for NativeCrypto plugin. version: 0.1.0 environment: - sdk: ">=2.15.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.5.0" dependencies: flutter: sdk: flutter + + plugin_platform_interface: ^2.1.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.4 \ No newline at end of file + wyatt_analysis: + git: + url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages + ref: wyatt_analysis-v2.1.0 + path: packages/wyatt_analysis \ No newline at end of file From a1112b5c80ac0c702efe93db4e60390ada0a1603 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 10:51:20 +0200 Subject: [PATCH 16/39] feat: export new exceptions --- .vscode/launch.json | 28 +++ .../example/lib/pages/cipher_page.dart | 6 +- packages/native_crypto/lib/native_crypto.dart | 4 +- .../lib/src/builders/aes_builder.dart | 9 +- .../lib/src/ciphers/aes/aes.dart | 15 +- .../lib/src/ciphers/aes/aes_mode.dart | 4 +- .../lib/src/ciphers/aes/aes_padding.dart | 4 +- packages/native_crypto/lib/src/core/core.dart | 3 +- .../lib/src/core/exceptions.dart | 41 ----- .../native_crypto/lib/src/kdf/pbkdf2.dart | 9 +- .../lib/src/keys/secret_key.dart | 13 +- packages/native_crypto/lib/src/platform.dart | 2 +- .../native_crypto_android/ciphers/AES.kt | 61 ++----- .../method_channel_native_crypto.dart | 156 +++++++++------- .../lib/src/utils/exception.dart | 170 ++++++++++++++---- 15 files changed, 313 insertions(+), 212 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 packages/native_crypto/lib/src/core/exceptions.dart diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d9becfd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "native_crypto", + "cwd": "packages/native_crypto/example", + "request": "launch", + "type": "dart" + }, + { + "name": "native_crypto (profile mode)", + "cwd": "packages/native_crypto/example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "native_crypto (release mode)", + "cwd": "packages/native_crypto/example", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + ] +} \ No newline at end of file diff --git a/packages/native_crypto/example/lib/pages/cipher_page.dart b/packages/native_crypto/example/lib/pages/cipher_page.dart index 8a6bc7c..e953a87 100644 --- a/packages/native_crypto/example/lib/pages/cipher_page.dart +++ b/packages/native_crypto/example/lib/pages/cipher_page.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_page.dart // Created Date: 28/12/2021 13:33:15 -// Last Modified: 28/12/2021 15:20:43 +// Last Modified: 25/05/2022 10:49:30 // ----- // Copyright (c) 2021 @@ -85,8 +85,8 @@ class CipherPage extends ConsumerWidget { var bytesToString = plainText.toStr(); decryptionStatus .print('String successfully decrypted:\n\n$bytesToString'); - } on DecryptionException catch (e) { - decryptionStatus.print(e.message); + } on NativeCryptoException catch (e) { + decryptionStatus.print(e.message ?? 'Decryption failed!'); } } } diff --git a/packages/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart index 226381f..c42ea91 100644 --- a/packages/native_crypto/lib/native_crypto.dart +++ b/packages/native_crypto/lib/native_crypto.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 23:09:10 +// Last Modified: 25/05/2022 10:48:20 // ----- // Copyright (c) 2021 @@ -13,6 +13,8 @@ /// Author: Hugo Pointcheval library native_crypto; +export 'package:native_crypto_platform_interface/src/utils/exception.dart'; + export 'src/builders/builders.dart'; export 'src/ciphers/ciphers.dart'; export 'src/core/core.dart'; diff --git a/packages/native_crypto/lib/src/builders/aes_builder.dart b/packages/native_crypto/lib/src/builders/aes_builder.dart index 955849c..e256d29 100644 --- a/packages/native_crypto/lib/src/builders/aes_builder.dart +++ b/packages/native_crypto/lib/src/builders/aes_builder.dart @@ -3,14 +3,14 @@ // ----- // File: aes_builder.dart // Created Date: 28/12/2021 12:03:11 -// Last Modified: 23/05/2022 23:05:19 +// Last Modified: 25/05/2022 10:47:11 // ----- // Copyright (c) 2021 import 'package:native_crypto/src/ciphers/aes/aes.dart'; -import 'package:native_crypto/src/core/exceptions.dart'; import 'package:native_crypto/src/interfaces/builder.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; class AESBuilder implements Builder { SecretKey? _sk; @@ -36,7 +36,10 @@ class AESBuilder implements Builder { Future build() async { if (_sk == null) { if (_fsk == null) { - throw CipherInitException('You must specify or generate a secret key.'); + throw const CipherInitException( + message: 'You must specify or generate a secret key.', + code: 'missing_key', + ); } else { _sk = await _fsk; } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index d740757..1f175ab 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 24/05/2022 23:29:42 +// Last Modified: 25/05/2022 10:44:25 // ----- // Copyright (c) 2022 @@ -14,11 +14,11 @@ import 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; import 'package:native_crypto/src/core/cipher_text.dart'; import 'package:native_crypto/src/core/cipher_text_list.dart'; -import 'package:native_crypto/src/core/exceptions.dart'; import 'package:native_crypto/src/interfaces/cipher.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; import 'package:native_crypto/src/utils/cipher_algorithm.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; export 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; export 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; @@ -34,16 +34,21 @@ class AES implements Cipher { AES(this.key, this.mode, {this.padding = AESPadding.none}) { if (!AESKeySize.supportedSizes.contains(key.bytes.length * 8)) { - throw CipherInitException('Invalid key length!'); + throw const CipherInitException( + message: 'Invalid key length!', + code: 'invalid_key_length', + ); } final Map> _supported = { AESMode.gcm: [AESPadding.none], - AESMode.cbc: [AESPadding.pkcs5], }; if (!_supported[mode]!.contains(padding)) { - throw CipherInitException('Invalid padding!'); + throw const CipherInitException( + message: 'Invalid padding!', + code: 'invalid_padding', + ); } } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart index 1ff7b5d..c1d98cb 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -3,9 +3,9 @@ // ----- // File: aes_mode.dart // Created Date: 23/05/2022 22:09:16 -// Last Modified: 24/05/2022 23:17:01 +// Last Modified: 25/05/2022 09:23:54 // ----- // Copyright (c) 2022 /// Defines the AES modes of operation. -enum AESMode { gcm, cbc } +enum AESMode { gcm } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart index 676f526..343ae03 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_padding.dart @@ -3,9 +3,9 @@ // ----- // File: aes_padding.dart // Created Date: 23/05/2022 22:10:17 -// Last Modified: 24/05/2022 23:17:25 +// Last Modified: 25/05/2022 09:23:49 // ----- // Copyright (c) 2022 /// Represents different paddings. -enum AESPadding { none, pkcs5 } +enum AESPadding { none } diff --git a/packages/native_crypto/lib/src/core/core.dart b/packages/native_crypto/lib/src/core/core.dart index 97597ac..5ab875c 100644 --- a/packages/native_crypto/lib/src/core/core.dart +++ b/packages/native_crypto/lib/src/core/core.dart @@ -3,10 +3,9 @@ // ----- // File: core.dart // Created Date: 23/05/2022 23:05:26 -// Last Modified: 23/05/2022 23:05:30 +// Last Modified: 25/05/2022 10:44:32 // ----- // Copyright (c) 2022 export 'cipher_text.dart'; export 'cipher_text_list.dart'; -export 'exceptions.dart'; diff --git a/packages/native_crypto/lib/src/core/exceptions.dart b/packages/native_crypto/lib/src/core/exceptions.dart deleted file mode 100644 index 96c21d0..0000000 --- a/packages/native_crypto/lib/src/core/exceptions.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: exceptions.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 22:30:27 -// ----- -// Copyright (c) 2021 - -class NativeCryptoException implements Exception { - final String message; - const NativeCryptoException(this.message); -} - -class UtilsException extends NativeCryptoException { - UtilsException(super.message); -} - -class KeyException extends NativeCryptoException { - KeyException(super.message); -} - -class KeyDerivationException extends NativeCryptoException { - KeyDerivationException(super.message); -} - -class CipherInitException extends NativeCryptoException { - CipherInitException(super.message); -} - -class EncryptionException extends NativeCryptoException { - EncryptionException(super.message); -} - -class DecryptionException extends NativeCryptoException { - DecryptionException(super.message); -} - -class NotImplementedException extends NativeCryptoException { - NotImplementedException(super.message); -} diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index 04672bd..7d8acc2 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -3,18 +3,18 @@ // ----- // File: pbkdf2.dart // Created Date: 17/12/2021 14:50:42 -// Last Modified: 23/05/2022 23:07:19 +// Last Modified: 25/05/2022 10:45:00 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/core/exceptions.dart'; import 'package:native_crypto/src/interfaces/keyderivation.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; import 'package:native_crypto/src/utils/hash_algorithm.dart'; import 'package:native_crypto/src/utils/kdf_algorithm.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; class Pbkdf2 extends KeyDerivation { final int _keyBytesCount; @@ -35,7 +35,10 @@ class Pbkdf2 extends KeyDerivation { @override Future derive({String? password, String? salt}) async { if (password == null || salt == null) { - throw KeyDerivationException("Password or Salt can't be null!"); + throw const KeyDerivationException( + message: "Password or Salt can't be null!", + code: 'invalid_password_or_salt', + ); } final Uint8List derivation = (await platform.pbkdf2( diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index 98a1097..1aa9031 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,16 +3,15 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 23/05/2022 23:07:28 +// Last Modified: 25/05/2022 10:45:55 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:flutter/services.dart'; -import 'package:native_crypto/src/core/exceptions.dart'; import 'package:native_crypto/src/interfaces/key.dart'; import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; /// A class representing a secret key. /// A secret key is a key that is not accessible by anyone else. @@ -29,8 +28,12 @@ class SecretKey extends Key { (await platform.generateSecretKey(bitsCount)) ?? Uint8List(0); return SecretKey(_key); - } on PlatformException catch (e) { - throw KeyException(e.toString()); + } catch (e, s) { + throw KeyException( + message: 'Failed to generate a secret key!', + code: 'failed_to_generate_secret_key', + stackTrace: s, + ); } } } diff --git a/packages/native_crypto/lib/src/platform.dart b/packages/native_crypto/lib/src/platform.dart index c4c13c1..5d62b5e 100644 --- a/packages/native_crypto/lib/src/platform.dart +++ b/packages/native_crypto/lib/src/platform.dart @@ -3,7 +3,7 @@ // ----- // File: platform.dart // Created Date: 27/12/2021 22:03:58 -// Last Modified: 27/12/2021 22:04:30 +// Last Modified: 25/05/2022 10:09:18 // ----- // Copyright (c) 2021 diff --git a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt index 1bb2c3b..0b4c86c 100644 --- a/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt +++ b/packages/native_crypto_android/android/src/main/kotlin/fr/pointcheval/native_crypto_android/ciphers/AES.kt @@ -11,74 +11,45 @@ class AES : Cipher { override val algorithm: CipherAlgorithm get() = CipherAlgorithm.aes - var forEncryption: Boolean = true var cipherInstance: javax.crypto.Cipher? = null; - var secretKey: SecretKeySpec? = null; -/* override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { - val sk: SecretKey = SecretKeySpec(key, "AES") - val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) - // javax.crypto representation = [CIPHERTEXT(n-16) || TAG(16)] - val bytes = cipher.doFinal(data) - val iv = cipher.iv.copyOf() // 12 bytes nonce - // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] - return iv.plus(bytes) + fun lazyLoadCipher() { + if (cipherInstance == null) { + cipherInstance = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") + } } - override fun decrypt(data: ByteArray, key: ByteArray): ByteArray { - val sk: SecretKey = SecretKeySpec(key, "AES") - // native.crypto representation = [NONCE(12) || CIPHERTEXT(n-16) || TAG(16)] - val iv: ByteArray = data.take(12).toByteArray() - // javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)] - val payload: ByteArray = data.drop(12).toByteArray() - val spec = GCMParameterSpec(16 * 8, iv) - val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec) - return cipher.doFinal(payload) - }*/ - + // native.crypto cipherText representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)] + // javax.crypto cipherText representation = [NONCE(12)] + [CIPHERTEXT(n-16) || TAG(16)] override fun encrypt(data: ByteArray, key: ByteArray): ByteArray { val list : List = encryptAsList(data, key) return list.first().plus(list.last()) } + // native.crypto cipherText representation = [NONCE(12)] + [CIPHERTEXT(n-16) || TAG(16)] + // javax.crypto cipherText representation = [NONCE(12)] + [CIPHERTEXT(n-16) || TAG(16)] override fun encryptAsList(data: ByteArray, key: ByteArray): List { val sk = SecretKeySpec(key, "AES") - if (cipherInstance == null || !forEncryption || secretKey != sk) { - secretKey = sk - forEncryption = true - // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] - // javax.crypto representation = [CIPHERTEXT(n-16)] - cipherInstance = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") - cipherInstance!!.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) - } - // javax.crypto representation = [CIPHERTEXT(n-16)] + lazyLoadCipher() + cipherInstance!!.init(javax.crypto.Cipher.ENCRYPT_MODE, sk) val bytes: ByteArray = cipherInstance!!.doFinal(data) val iv: ByteArray = cipherInstance!!.iv - // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] return listOf(iv, bytes) } override fun decrypt(data: ByteArray, key: ByteArray): ByteArray { - // javax.crypto representation = [CIPHERTEXT(n-16)] - val iv: ByteArray = data.take(16).toByteArray() - val payload: ByteArray = data.drop(16).toByteArray() + val iv: ByteArray = data.take(12).toByteArray() + val payload: ByteArray = data.drop(12).toByteArray() return decryptAsList(listOf(iv, payload), key) } override fun decryptAsList(data: List, key: ByteArray): ByteArray { - if (cipherInstance == null) { - // native.crypto representation = [IV(16) || CIPHERTEXT(n-16)] - // javax.crypto representation = [CIPHERTEXT(n-16)] - cipherInstance = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding") - } val sk = SecretKeySpec(key, "AES") - val iv: ByteArray = data.first() - val ivSpec = IvParameterSpec(iv) - cipherInstance!!.init(javax.crypto.Cipher.DECRYPT_MODE, sk, ivSpec) - forEncryption = false val payload: ByteArray = data.last() + val iv: ByteArray = data.first() + val gcmSpec = GCMParameterSpec(16 * 8, iv) + lazyLoadCipher() + cipherInstance!!.init(javax.crypto.Cipher.DECRYPT_MODE, sk, gcmSpec) return cipherInstance!!.doFinal(payload) } } \ No newline at end of file diff --git a/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart b/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart index 7205096..0559ed4 100644 --- a/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart +++ b/packages/native_crypto_platform_interface/lib/src/method_channel/method_channel_native_crypto.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto_method_channel.dart // Created Date: 25/12/2021 16:58:04 -// Last Modified: 24/05/2022 22:59:32 +// Last Modified: 25/05/2022 10:40:29 // ----- // Copyright (c) 2021 @@ -11,7 +11,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:native_crypto_platform_interface/src/platform_interface/native_crypto_platform.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; /// An implementation of [NativeCryptoPlatform] that uses method channels. class MethodChannelNativeCrypto extends NativeCryptoPlatform { @@ -20,24 +20,32 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { MethodChannel channel = const MethodChannel('plugins.hugop.cl/native_crypto'); @override - Future digest(Uint8List data, String algorithm) { - return channel.invokeMethod( - 'digest', - { - 'data': data, - 'algorithm': algorithm, - }, - ); + Future digest(Uint8List data, String algorithm) async { + try { + return await channel.invokeMethod( + 'digest', + { + 'data': data, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override - Future generateSecretKey(int bitsCount) { - return channel.invokeMethod( - 'generateSecretKey', - { - 'bitsCount': bitsCount, - }, - ); + Future generateSecretKey(int bitsCount) async { + try { + return await channel.invokeMethod( + 'generateSecretKey', + { + 'bitsCount': bitsCount, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override @@ -47,17 +55,21 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { int keyBytesCount, int iterations, String algorithm, - ) { - return channel.invokeMethod( - 'pbkdf2', - { - 'password': password, - 'salt': salt, - 'keyBytesCount': keyBytesCount, - 'iterations': iterations, - 'algorithm': algorithm, - }, - ); + ) async { + try { + return await channel.invokeMethod( + 'pbkdf2', + { + 'password': password, + 'salt': salt, + 'keyBytesCount': keyBytesCount, + 'iterations': iterations, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override @@ -65,15 +77,19 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { Uint8List data, Uint8List key, String algorithm, - ) { - return channel.invokeListMethod( - 'encryptAsList', - { - 'data': data, - 'key': key, - 'algorithm': algorithm, - }, - ); + ) async { + try { + return await channel.invokeListMethod( + 'encryptAsList', + { + 'data': data, + 'key': key, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override @@ -81,15 +97,19 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { List data, Uint8List key, String algorithm, - ) { - return channel.invokeMethod( - 'decryptAsList', - { - 'data': data, - 'key': key, - 'algorithm': algorithm, - }, - ); + ) async { + try { + return await channel.invokeMethod( + 'decryptAsList', + { + 'data': data, + 'key': key, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override @@ -97,15 +117,19 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { Uint8List data, Uint8List key, String algorithm, - ) { - return channel.invokeMethod( - 'encrypt', - { - 'data': data, - 'key': key, - 'algorithm': algorithm, - }, - ); + ) async { + try { + return await channel.invokeMethod( + 'encrypt', + { + 'data': data, + 'key': key, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } @override @@ -113,14 +137,18 @@ class MethodChannelNativeCrypto extends NativeCryptoPlatform { Uint8List data, Uint8List key, String algorithm, - ) { - return channel.invokeMethod( - 'decrypt', - { - 'data': data, - 'key': key, - 'algorithm': algorithm, - }, - ); + ) async { + try { + return await channel.invokeMethod( + 'decrypt', + { + 'data': data, + 'key': key, + 'algorithm': algorithm, + }, + ); + } catch (e, s) { + NativeCryptoException.convertPlatformException(e, s); + } } } diff --git a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart index 24af864..196a2bd 100644 --- a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart +++ b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart @@ -3,51 +3,151 @@ // ----- // File: exception.dart // Created Date: 24/05/2022 18:54:48 -// Last Modified: 24/05/2022 18:58:39 +// Last Modified: 25/05/2022 10:43:29 // ----- // Copyright (c) 2022 +import 'dart:developer'; + import 'package:flutter/services.dart'; class NativeCryptoException implements Exception { - final String message; - const NativeCryptoException(this.message); -} + const NativeCryptoException({ + this.message, + String? code, + this.stackTrace, + // ignore: unnecessary_this + }) : this.code = code ?? 'unknown'; + + /// The long form message of the exception. + final String? message; + + /// The optional code to accommodate the message. + final String code; + + /// The stack trace which provides information to the user about the call + /// sequence that triggered an exception + final StackTrace? stackTrace; + + @override + String toString() { + String output = '[NativeException/$code] $message'; + + if (stackTrace != null) { + output += '\n\n${stackTrace.toString()}'; + } -/// Catches a [PlatformException] and returns an [Exception]. -/// -/// If the [Exception] is a [PlatformException], -/// a [NativeCryptoException] is returned. -Never convertPlatformException(Object exception, StackTrace stackTrace) { - if (exception is! Exception || exception is! PlatformException) { - Error.throwWithStackTrace(exception, stackTrace); + return output; } - Error.throwWithStackTrace( - platformExceptionToNativeCryptoException(exception, stackTrace), - stackTrace, - ); -} + /// Catches a [PlatformException] and returns an [Exception]. + /// + /// If the [Exception] is a [PlatformException], + /// a [NativeCryptoException] is returned. + static Never convertPlatformException( + Object exception, + StackTrace stackTrace, + ) { + log(exception.toString()); + if (exception is! Exception || exception is! PlatformException) { + Error.throwWithStackTrace(exception, stackTrace); + } + + Error.throwWithStackTrace( + NativeCryptoException.fromPlatformException(exception, stackTrace), + stackTrace, + ); + } + + /// Converts a [PlatformException] into a [NativeCryptoException]. + /// + /// A [PlatformException] can only be converted to a [NativeCryptoException] + /// if the `details` of the exception exist. + factory NativeCryptoException.fromPlatformException( + PlatformException platformException, + StackTrace stackTrace, + ) { + final Map? details = platformException.details != null + ? Map.from( + platformException.details as Map, + ) + : null; + + String code = 'unknown'; + String message = platformException.message ?? ''; + + if (details != null) { + code = details['code'] ?? code; + message = details['message'] ?? message; + } + + return NativeCryptoException( + message: message, + code: code, + stackTrace: stackTrace, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (identical(this, other)) return true; -/// Converts a [PlatformException] into a [NativeCryptoException]. -/// -/// A [PlatformException] can only be converted to a [NativeCryptoException] -/// if the `details` of the exception exist. -NativeCryptoException platformExceptionToNativeCryptoException( - PlatformException platformException, - StackTrace stackTrace, -) { - final Map? details = platformException.details != null - ? Map.from( - platformException.details as Map, - ) - : null; - - String message = platformException.message ?? ''; - - if (details != null) { - message = details['message'] ?? message; + return other is NativeCryptoException && + other.message == message && + other.code == code && + other.stackTrace == stackTrace; } - return NativeCryptoException(message); + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => message.hashCode ^ code.hashCode ^ stackTrace.hashCode; +} + +class KeyException extends NativeCryptoException { + const KeyException({ + super.message, + super.code, + super.stackTrace, + }); +} + +class KeyDerivationException extends NativeCryptoException { + const KeyDerivationException({ + super.message, + super.code, + super.stackTrace, + }); +} + +class CipherInitException extends NativeCryptoException { + const CipherInitException({ + super.message, + super.code, + super.stackTrace, + }); +} + +class EncryptionException extends NativeCryptoException { + const EncryptionException({ + super.message, + super.code, + super.stackTrace, + }); +} + +class DecryptionException extends NativeCryptoException { + const DecryptionException({ + super.message, + super.code, + super.stackTrace, + }); +} + +class NotImplementedException extends NativeCryptoException { + const NotImplementedException({ + super.message, + super.code, + super.stackTrace, + }); } From 142dd17ad2c612f0448912a1777ea0277464b340 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 15:42:56 +0200 Subject: [PATCH 17/39] refactor(ios): rework swift part --- .../ios/Classes/Cipher.swift | 53 ------------- .../native_crypto_ios/ios/Classes/Hash.swift | 43 ---------- .../native_crypto_ios/ios/Classes/KEM.swift | 78 ------------------- .../native_crypto_ios/ios/Classes/Key.swift | 62 --------------- .../ios/Classes/ciphers/AESCipher.swift | 28 +++++++ .../ios/Classes/kdf/Pbkdf2.swift | 8 ++ .../ios/Classes/keys/SecretKey.swift | 8 ++ .../ios/Classes/protocols/Cipher.swift | 8 ++ .../ios/Classes/protocols/Key.swift | 8 ++ .../ios/Classes/protocols/KeyDerivation.swift | 8 ++ .../ios/Classes/utils/CipherAlgorithm.swift | 8 ++ .../ios/Classes/utils/HashAlgorithm.swift | 8 ++ .../ios/Classes/utils/KdfAlgorithm.swift | 8 ++ .../ios/Classes/utils/NativeCryptoError.swift | 8 ++ .../ios/Classes/utils/Task.swift | 8 ++ 15 files changed, 108 insertions(+), 236 deletions(-) delete mode 100644 packages/native_crypto_ios/ios/Classes/Cipher.swift delete mode 100644 packages/native_crypto_ios/ios/Classes/Hash.swift delete mode 100644 packages/native_crypto_ios/ios/Classes/KEM.swift delete mode 100644 packages/native_crypto_ios/ios/Classes/Key.swift create mode 100644 packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift create mode 100644 packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift create mode 100644 packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift create mode 100644 packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift create mode 100644 packages/native_crypto_ios/ios/Classes/protocols/Key.swift create mode 100644 packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift create mode 100644 packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift create mode 100644 packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift create mode 100644 packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift create mode 100644 packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift create mode 100644 packages/native_crypto_ios/ios/Classes/utils/Task.swift diff --git a/packages/native_crypto_ios/ios/Classes/Cipher.swift b/packages/native_crypto_ios/ios/Classes/Cipher.swift deleted file mode 100644 index 7204680..0000000 --- a/packages/native_crypto_ios/ios/Classes/Cipher.swift +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Author: Hugo Pointcheval - * Email: git@pcl.ovh - * ----- - * File: Cipher.swift - * Created Date: 25/12/2021 18:31:28 - * Last Modified: 25/12/2021 18:38:53 - * ----- - * Copyright (c) 2021 - */ - -import Foundation -import CryptoKit - -class AESCipher { - /// Encrypts plaintext with key using AES GCM - @available(iOS 13.0, *) - static func encrypt(plaintext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let encrypted = try? AES.GCM.seal(plaintext, using: symmetricKey) - return encrypted?.combined - } - - /// Decrypts ciphertext with key using AES GCM - @available(iOS 13.0, *) - static func decrypt(ciphertext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let sealedBox = try? AES.GCM.SealedBox(combined: ciphertext) - if (sealedBox == nil) { return nil } - let decryptedData = try? AES.GCM.open(sealedBox!, using: symmetricKey) - return decryptedData - } -} - -class CHACHACipher { - /// Encrypts plaintext with key using CHACHAPOLY - @available(iOS 13.0, *) - static func encrypt(plaintext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let encrypted = try? ChaChaPoly.seal(plaintext, using: symmetricKey) - return encrypted?.combined - } - - /// Decrypts ciphertext with key using CHACHAPOLY - @available(iOS 13.0, *) - static func decrypt(ciphertext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let sealedBox = try? ChaChaPoly.SealedBox(combined: ciphertext) - if (sealedBox == nil) { return nil } - let decryptedData = try? ChaChaPoly.open(sealedBox!, using: symmetricKey) - return decryptedData - } -} diff --git a/packages/native_crypto_ios/ios/Classes/Hash.swift b/packages/native_crypto_ios/ios/Classes/Hash.swift deleted file mode 100644 index 94990ec..0000000 --- a/packages/native_crypto_ios/ios/Classes/Hash.swift +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Author: Hugo Pointcheval - * Email: git@pcl.ovh - * ----- - * File: Hash.swift - * Created Date: 25/12/2021 18:31:11 - * Last Modified: 25/12/2021 18:38:20 - * ----- - * Copyright (c) 2021 - */ - -import Foundation -import CommonCrypto -import CryptoKit - -enum HashAlgorithm: String { - case HashSHA256 = "sha256" - case HashSHA384 = "sha384" - case HashSHA512 = "sha512" - - var commonCrypto: UInt32 { - switch self { - case .HashSHA256: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256) - case .HashSHA384: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA384) - case .HashSHA512: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512) - } - } -} - -@available(iOS 13.0, *) -class Hash { - /// Hash a message with a specified HashAlgorithm - static func digest(data: Data, algorithm: HashAlgorithm) -> Data { - switch algorithm { - case .HashSHA256: - return Data(SHA256.hash(data: data)) - case .HashSHA384: - return Data(SHA384.hash(data: data)) - case .HashSHA512: - return Data(SHA512.hash(data: data)) - } - } -} diff --git a/packages/native_crypto_ios/ios/Classes/KEM.swift b/packages/native_crypto_ios/ios/Classes/KEM.swift deleted file mode 100644 index cf3a27b..0000000 --- a/packages/native_crypto_ios/ios/Classes/KEM.swift +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Author: Hugo Pointcheval - * Email: git@pcl.ovh - * ----- - * File: KEM.swift - * Created Date: 25/12/2021 18:31:48 - * Last Modified: 25/12/2021 18:40:00 - * ----- - * Copyright (c) 2021 - */ - -import Foundation -import CryptoKit - -class KeyPair { - /// Generate a keypair. - @available(iOS 13.0, *) - static func fromCurve() -> Data { - let sk = P256.KeyAgreement.PrivateKey() - var kp = sk.rawRepresentation - kp.append(contentsOf: sk.publicKey.rawRepresentation) - return kp; - } - - /// Import private key from Data - @available(iOS 13.0, *) - static func importPrivateKey(privateKey: Data) throws -> P256.KeyAgreement.PrivateKey { - let sk = try P256.KeyAgreement.PrivateKey(rawRepresentation: privateKey) - - return sk; - } - - /// Import public key from Data - @available(iOS 13.0, *) - static func importPublicKey(publicKey: Data) throws -> P256.KeyAgreement.PublicKey { - let pk = try P256.KeyAgreement.PublicKey(rawRepresentation: publicKey) - - return pk; - } -} - -class ECDH { - /// Generate a shared secret with your private key and other party public key. - @available(iOS 13.0, *) - static func generateSharedSecretKey(salt: Data, hash: HashAlgorithm, keyBytesCount: Int ,privateKey: Data, publicKey: Data) -> Data? { - let sk = try? KeyPair.importPrivateKey(privateKey: privateKey) - if (sk == nil) {return nil} - - let pk = try? KeyPair.importPublicKey(publicKey: publicKey) - if (pk == nil) {return nil} - - let secret = try? sk!.sharedSecretFromKeyAgreement(with: pk!) - - switch hash { - case .HashSHA256: - let key = secret?.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(), outputByteCount: keyBytesCount) - if (key == nil) { - return nil - } else { - return Key.toBytes(key: key!) - } - case .HashSHA384: - let key = secret?.hkdfDerivedSymmetricKey(using: SHA384.self, salt: salt, sharedInfo: Data(), outputByteCount: keyBytesCount) - if (key == nil) { - return nil - } else { - return Key.toBytes(key: key!) - } - case .HashSHA512: - let key = secret?.hkdfDerivedSymmetricKey(using: SHA512.self, salt: salt, sharedInfo: Data(), outputByteCount: keyBytesCount) - if (key == nil) { - return nil - } else { - return Key.toBytes(key: key!) - } - } - } -} diff --git a/packages/native_crypto_ios/ios/Classes/Key.swift b/packages/native_crypto_ios/ios/Classes/Key.swift deleted file mode 100644 index b113df5..0000000 --- a/packages/native_crypto_ios/ios/Classes/Key.swift +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Author: Hugo Pointcheval - * Email: git@pcl.ovh - * ----- - * File: KDF.swift - * Created Date: 25/12/2021 17:45:28 - * Last Modified: 25/12/2021 17:45:38 - * ----- - * Copyright (c) 2021 - */ - -import Foundation -import CryptoKit -import CommonCrypto - -class Key { - /// Generate secret key of a specified length - @available(iOS 13.0, *) - static func fromSecureRandom(bitsCount : Int) -> Data { - let symmetricKey = SymmetricKey.init(size: SymmetricKeySize(bitCount: bitsCount)) - return toBytes(key: symmetricKey) - } - - /// Encode key as Data - @available(iOS 13.0, *) - static func toBytes(key: SymmetricKey) -> Data { - let keyBytes = key.withUnsafeBytes - { - return Data(Array($0)) - } - return keyBytes - } - - /// Derive a new secret key with PBKDF2 algorithm - static func fromPBKDF2(password: String, salt: String, keyBytesCount: Int, iterations: Int, algorithm: HashAlgorithm) -> Data? { - let passwordData = password.data(using: .utf8)! - let saltData = salt.data(using: .utf8)! - - var derivedKeyData = Data(repeating: 0, count: keyBytesCount) - let localDerivedKeyData = derivedKeyData - - let status = derivedKeyData.withUnsafeMutableBytes { (derivedKeyBytes: UnsafeMutableRawBufferPointer) in - saltData.withUnsafeBytes { (saltBytes: UnsafeRawBufferPointer) in - CCKeyDerivationPBKDF( - CCPBKDFAlgorithm(kCCPBKDF2), - password, - passwordData.count, - saltBytes.bindMemory(to: UInt8.self).baseAddress, - saltData.count, - algorithm.commonCrypto, - UInt32(iterations), - derivedKeyBytes.bindMemory(to: UInt8.self).baseAddress, - localDerivedKeyData.count) - } - } - if (status != kCCSuccess) { - return nil; - } - - return derivedKeyData - } -} diff --git a/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift new file mode 100644 index 0000000..a52a925 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift @@ -0,0 +1,28 @@ +// +// AES.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation + +class AES : Cipher { + /// Encrypts plaintext with key using AES GCM + @available(iOS 13.0, *) + static func encrypt(plaintext: Data, key: Data) -> Data? { + let symmetricKey = SymmetricKey.init(data: key) + let encrypted = try? AES.GCM.seal(plaintext, using: symmetricKey) + return encrypted?.combined + } + + /// Decrypts ciphertext with key using AES GCM + @available(iOS 13.0, *) + static func decrypt(ciphertext: Data, key: Data) -> Data? { + let symmetricKey = SymmetricKey.init(data: key) + let sealedBox = try? AES.GCM.SealedBox(combined: ciphertext) + if (sealedBox == nil) { return nil } + let decryptedData = try? AES.GCM.open(sealedBox!, using: symmetricKey) + return decryptedData + } +} diff --git a/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift new file mode 100644 index 0000000..1c52aa9 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift @@ -0,0 +1,8 @@ +// +// Pbkdf2.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift new file mode 100644 index 0000000..df97a3d --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift @@ -0,0 +1,8 @@ +// +// SecretKey.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift new file mode 100644 index 0000000..9c2cc81 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift @@ -0,0 +1,8 @@ +// +// Cipher.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Key.swift b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift new file mode 100644 index 0000000..19bea38 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift @@ -0,0 +1,8 @@ +// +// Key.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift new file mode 100644 index 0000000..3313121 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift @@ -0,0 +1,8 @@ +// +// KeyDerivation.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift new file mode 100644 index 0000000..0617eb0 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift @@ -0,0 +1,8 @@ +// +// CipherAlgorithm.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift new file mode 100644 index 0000000..1dbe5f7 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift @@ -0,0 +1,8 @@ +// +// HashAlgorithm.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift new file mode 100644 index 0000000..7629866 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift @@ -0,0 +1,8 @@ +// +// KdfAlgorithm.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift new file mode 100644 index 0000000..540909d --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift @@ -0,0 +1,8 @@ +// +// NativeCryptoError.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation diff --git a/packages/native_crypto_ios/ios/Classes/utils/Task.swift b/packages/native_crypto_ios/ios/Classes/utils/Task.swift new file mode 100644 index 0000000..db3fa79 --- /dev/null +++ b/packages/native_crypto_ios/ios/Classes/utils/Task.swift @@ -0,0 +1,8 @@ +// +// Task.swift +// native_crypto_ios +// +// Created by Hugo Pointcheval on 25/05/2022. +// + +import Foundation From 2ed8aab69fe74e64496c0b5902aca60b24043dcb Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 15:43:12 +0200 Subject: [PATCH 18/39] perf(ios): optimize swift code --- .../Classes/SwiftNativeCryptoIosPlugin.swift | 223 +++++++++++------- .../ios/Classes/ciphers/AESCipher.swift | 47 +++- .../ios/Classes/kdf/Pbkdf2.swift | 55 +++++ .../ios/Classes/keys/SecretKey.swift | 17 ++ .../ios/Classes/protocols/Cipher.swift | 8 + .../ios/Classes/protocols/Key.swift | 4 + .../ios/Classes/protocols/KeyDerivation.swift | 5 + .../ios/Classes/utils/CipherAlgorithm.swift | 10 + .../ios/Classes/utils/HashAlgorithm.swift | 37 +++ .../ios/Classes/utils/KdfAlgorithm.swift | 4 + .../ios/Classes/utils/NativeCryptoError.swift | 10 + .../ios/Classes/utils/Task.swift | 44 ++++ 12 files changed, 369 insertions(+), 95 deletions(-) diff --git a/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift b/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift index 00fdbb7..c9b1a84 100644 --- a/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift +++ b/packages/native_crypto_ios/ios/Classes/SwiftNativeCryptoIosPlugin.swift @@ -3,93 +3,144 @@ import UIKit @available(iOS 13.0, *) public class SwiftNativeCryptoIosPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "plugins.hugop.cl/native_crypto", binaryMessenger: registrar.messenger()) - let instance = SwiftNativeCryptoIosPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "digest": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: algo) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if digest is null - result(FlutterStandardTypedData.init(bytes: Hash.digest(data: data, algorithm: algorithm!))) - case "generateSecretKey": - let args : NSDictionary = call.arguments as! NSDictionary - - let bitsCount : NSNumber = args["bitsCount"] as! NSNumber - // TODO(hpcl): check if secure random is null - result(FlutterStandardTypedData.init(bytes: Key.fromSecureRandom(bitsCount: bitsCount.intValue))) - case "generateKeyPair": - result(FlutterStandardTypedData.init(bytes: KeyPair.fromCurve())) - case "pbkdf2": - let args : NSDictionary = call.arguments as! NSDictionary - - let password : String = args["password"] as! String - let salt : String = args["salt"] as! String - let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber - let iterations : NSNumber = args["iterations"] as! NSNumber - let algo : String = args["algorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: algo) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if derivation is null - result(FlutterStandardTypedData.init(bytes: Key.fromPBKDF2(password: password, salt: salt, keyBytesCount: keyBytesCount.intValue, iterations: iterations.intValue, algorithm: algorithm!)!)) - case "encrypt": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let key : Data = (args["key"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if ciphertext is null - var ciphertext : Data - switch algo { - case "aes": - ciphertext = AESCipher.encrypt(plaintext: data, key: key)! - case "chachapoly": - ciphertext = CHACHACipher.encrypt(plaintext: data, key: key)! - default: - ciphertext = Data.init(); - } - result(FlutterStandardTypedData.init(bytes: ciphertext)) - case "decrypt": - let args : NSDictionary = call.arguments as! NSDictionary - - let data : Data = (args["data"] as! FlutterStandardTypedData).data - let key : Data = (args["key"] as! FlutterStandardTypedData).data - let algo : String = args["algorithm"] as! String - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if ciphertext is null - var ciphertext : Data - switch algo { - case "aes": - ciphertext = AESCipher.decrypt(ciphertext: data, key: key)! - case "chachapoly": - ciphertext = CHACHACipher.decrypt(ciphertext: data, key: key)! - default: - ciphertext = Data.init(); + static let name: String = "plugins.hugop.cl/native_crypto" + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: name, binaryMessenger: registrar.messenger()) + let instance = SwiftNativeCryptoIosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "digest": _call(task: handleDigest(call: call), result: result) + case "generateSecretKey": _call(task: handleGenerateSecretKey(call: call), result: result) + case "pbkdf2": _call(task: handlePbkdf2(call: call), result: result) + case "encryptAsList": _call(task: handleEncryptAsList(call: call), result: result) + case "decryptAsList": _call(task: handleDecryptAsList(call: call), result: result) + case "encrypt": _call(task: handleCrypt(call: call, forEncryption: true), result: result) + case "decrypt": _call(task: handleCrypt(call: call, forEncryption: false), result: result) + default: result(FlutterMethodNotImplemented) } - result(FlutterStandardTypedData.init(bytes: ciphertext)) - case "generateSharedSecretKey": - let args : NSDictionary = call.arguments as! NSDictionary - - let salt : Data = (args["salt"] as! FlutterStandardTypedData).data - let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber - let ephemeralPrivateKey : Data = (args["ephemeralPrivateKey"] as! FlutterStandardTypedData).data - let otherPublicKey : Data = (args["otherPublicKey"] as! FlutterStandardTypedData).data - let hkdfAlgorithm : String = args["hkdfAlgorithm"] as! String - let algorithm : HashAlgorithm? = HashAlgorithm.init(rawValue: hkdfAlgorithm) - // TODO(hpcl): check if algorithm is null - // TODO(hpcl): check if generated key is null - result(FlutterStandardTypedData.init(bytes: ECDH.generateSharedSecretKey(salt: salt, hash: algorithm!, keyBytesCount: keyBytesCount.intValue, privateKey: ephemeralPrivateKey, publicKey: otherPublicKey)!)) + } + + private func _call(task: Task, result: @escaping FlutterResult) { + task.call() + task.finalize(callback: {(task: Task) in + if (task.isSuccessful()) { + result(task.getResult()!) + } else { + let exception: Error = task.getException() + let message = exception.localizedDescription + result(FlutterError(code: "native_crypto", message: message, details: nil)) + } + }) + } + + private func handleDigest(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + return FlutterStandardTypedData.init(bytes: try HashAlgorithm.digest(data: data, algorithm: algorithm)) + }) + } - default: result(FlutterMethodNotImplemented) + private func handleGenerateSecretKey(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let bitsCount : NSNumber = args["bitsCount"] as! NSNumber + + return FlutterStandardTypedData.init(bytes: SecretKey(fromSecureRandom: bitsCount.intValue).bytes) + }) } - } + + private func handlePbkdf2(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let password : String = args["password"] as! String + let salt : String = args["salt"] as! String + let keyBytesCount : NSNumber = args["keyBytesCount"] as! NSNumber + let iterations : NSNumber = args["iterations"] as! NSNumber + let algorithm : String = args["algorithm"] as! String + + let pbkdf2 : Pbkdf2 = Pbkdf2(keyBytesCount: keyBytesCount.intValue, iterations: iterations.intValue) + pbkdf2.hash = HashAlgorithm.init(rawValue: algorithm) ?? pbkdf2.hash + pbkdf2.initialize(password: password, salt: salt) + + return FlutterStandardTypedData.init(bytes: try pbkdf2.derive().bytes) + }) + } + + private func handleEncryptAsList(call: FlutterMethodCall) -> Task> { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + return try cipher.encryptAsList(data: data, key: key) + }) + } + + private func handleDecryptAsList(call: FlutterMethodCall) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data = args["data"] as! NSArray + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let iv = (data[0] as! FlutterStandardTypedData).data + let encrypted = (data[1] as! FlutterStandardTypedData).data + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + return FlutterStandardTypedData.init(bytes: try cipher.decryptAsList(data: [iv, encrypted], key: key)) + }) + } + + private func handleCrypt(call: FlutterMethodCall, forEncryption: Bool) -> Task { + return Task(task: { + let args : NSDictionary = call.arguments as! NSDictionary + + let data : Data = (args["data"] as! FlutterStandardTypedData).data + let key : Data = (args["key"] as! FlutterStandardTypedData).data + let algorithm : String = args["algorithm"] as! String + + let cipherAlgorithm : CipherAlgorithm? = CipherAlgorithm.init(rawValue: algorithm) + var cipher : Cipher + if (cipherAlgorithm != nil) { + cipher = cipherAlgorithm!.getCipher + } else { + throw NativeCryptoError.cipherError + } + + if (forEncryption) { + return FlutterStandardTypedData.init(bytes: try cipher.encrypt(data: data, key: key)) + } else { + return FlutterStandardTypedData.init(bytes: try cipher.decrypt(data: data, key: key)) + } + }) + } + } diff --git a/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift index a52a925..2110ff5 100644 --- a/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift +++ b/packages/native_crypto_ios/ios/Classes/ciphers/AESCipher.swift @@ -1,28 +1,57 @@ // -// AES.swift +// AESCipher.swift // native_crypto_ios // // Created by Hugo Pointcheval on 25/05/2022. // import Foundation +import CryptoKit -class AES : Cipher { +class AESCipher : Cipher { + var algorithm: CipherAlgorithm = CipherAlgorithm.aes + /// Encrypts plaintext with key using AES GCM @available(iOS 13.0, *) - static func encrypt(plaintext: Data, key: Data) -> Data? { - let symmetricKey = SymmetricKey.init(data: key) - let encrypted = try? AES.GCM.seal(plaintext, using: symmetricKey) - return encrypted?.combined + func encrypt(data: Data, key: Data) throws -> Data { + let symmetricKey : SymmetricKey = SymmetricKey.init(data: key) + let encrypted : AES.GCM.SealedBox? = try? AES.GCM.seal(data, using: symmetricKey) + + let encryptedData : Data? = encrypted?.combined + if (encryptedData == nil) { + throw NativeCryptoError.encryptionError + } + return encryptedData! } /// Decrypts ciphertext with key using AES GCM @available(iOS 13.0, *) - static func decrypt(ciphertext: Data, key: Data) -> Data? { + func decrypt(data: Data, key: Data) throws -> Data { let symmetricKey = SymmetricKey.init(data: key) - let sealedBox = try? AES.GCM.SealedBox(combined: ciphertext) - if (sealedBox == nil) { return nil } + let sealedBox = try? AES.GCM.SealedBox(combined: data) + if (sealedBox == nil) { return Data.init() } let decryptedData = try? AES.GCM.open(sealedBox!, using: symmetricKey) + if (decryptedData == nil) { + throw NativeCryptoError.decryptionError + } + return decryptedData! + } + + func encryptAsList(data: Data, key: Data) throws -> [Data] { + let encryptedData = try encrypt(data: data, key: key) + + let iv = encryptedData.prefix(12) + let data = encryptedData.suffix(from: 12) + + return [iv, data] + } + + func decryptAsList(data: [Data], key: Data) throws -> Data { + var encryptedData = data.first! + let data = data.last! + encryptedData.append(data) + + let decryptedData = try decrypt(data: encryptedData, key: key) return decryptedData } } diff --git a/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift index 1c52aa9..8a34a1e 100644 --- a/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift +++ b/packages/native_crypto_ios/ios/Classes/kdf/Pbkdf2.swift @@ -6,3 +6,58 @@ // import Foundation +import CommonCrypto + +class Pbkdf2 : KeyDerivation { + var algorithm: KdfAlgorithm = KdfAlgorithm.pbkdf2 + + var keyBytesCount: Int + var iterations: Int + var hash: HashAlgorithm = HashAlgorithm.HashSHA256 + + var password: String? = nil + var salt: String? = nil + + init(keyBytesCount: Int, iterations: Int) { + self.keyBytesCount = keyBytesCount + self.iterations = iterations + } + + func initialize(password: String, salt: String) { + self.password = password + self.salt = salt + } + + func derive() throws -> SecretKey { + if (password == nil || salt == nil) { + throw NativeCryptoError.pbkdf2Error + } + + let passwordData = password!.data(using: .utf8)! + let saltData = salt!.data(using: .utf8)! + + var derivedKeyData = Data(repeating: 0, count: keyBytesCount) + let localDerivedKeyData = derivedKeyData + + let status = derivedKeyData.withUnsafeMutableBytes { (derivedKeyBytes: UnsafeMutableRawBufferPointer) in + saltData.withUnsafeBytes { (saltBytes: UnsafeRawBufferPointer) in + CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + password, + passwordData.count, + saltBytes.bindMemory(to: UInt8.self).baseAddress, + saltData.count, + hash.pbkdf2identifier, + UInt32(iterations), + derivedKeyBytes.bindMemory(to: UInt8.self).baseAddress, + localDerivedKeyData.count) + } + } + + if (status != kCCSuccess) { + throw NativeCryptoError.pbkdf2Error + } + + return SecretKey(derivedKeyData) + } +} diff --git a/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift index df97a3d..6325143 100644 --- a/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift +++ b/packages/native_crypto_ios/ios/Classes/keys/SecretKey.swift @@ -6,3 +6,20 @@ // import Foundation +import CryptoKit + +class SecretKey : Key { + var bytes: Data + + init(_ bytes: Data) { + self.bytes = bytes + } + + init(fromSecureRandom bitsCount: Int) { + let symmetricKey = SymmetricKey.init(size: SymmetricKeySize(bitCount: bitsCount)) + bytes = symmetricKey.withUnsafeBytes + { + return Data(Array($0)) + } + } +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift index 9c2cc81..f1291f9 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/Cipher.swift @@ -6,3 +6,11 @@ // import Foundation + +protocol Cipher { + var algorithm: CipherAlgorithm { get } + func encrypt(data: Data, key: Data) throws -> Data + func decrypt(data: Data, key: Data) throws -> Data + func encryptAsList(data: Data, key: Data) throws -> [Data] + func decryptAsList(data: [Data], key: Data) throws-> Data +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/Key.swift b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift index 19bea38..9fc9199 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/Key.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/Key.swift @@ -6,3 +6,7 @@ // import Foundation + +protocol Key { + var bytes: Data { get set } +} diff --git a/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift index 3313121..df3a0d5 100644 --- a/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift +++ b/packages/native_crypto_ios/ios/Classes/protocols/KeyDerivation.swift @@ -6,3 +6,8 @@ // import Foundation + +protocol KeyDerivation { + var algorithm : KdfAlgorithm { get } + func derive() throws -> SecretKey +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift index 0617eb0..afb9094 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/CipherAlgorithm.swift @@ -6,3 +6,13 @@ // import Foundation + +enum CipherAlgorithm : String { + case aes = "aes" + + var getCipher: Cipher { + switch self { + case .aes: return AESCipher() + } + } +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift index 1dbe5f7..38b7740 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/HashAlgorithm.swift @@ -6,3 +6,40 @@ // import Foundation +import CommonCrypto +import CryptoKit + +enum HashAlgorithm: String { + case HashSHA256 = "sha256" + case HashSHA384 = "sha384" + case HashSHA512 = "sha512" + + var pbkdf2identifier: UInt32 { + switch self { + case .HashSHA256: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256) + case .HashSHA384: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA384) + case .HashSHA512: return CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512) + } + } + + @available(iOS 13.0, *) + func digest(data: Data) -> Data { + switch self { + case .HashSHA256: + return Data(SHA256.hash(data: data)) + case .HashSHA384: + return Data(SHA384.hash(data: data)) + case .HashSHA512: + return Data(SHA512.hash(data: data)) + } + } + + @available(iOS 13.0, *) + static func digest(data: Data, algorithm: String) throws -> Data { + let algo = HashAlgorithm.init(rawValue: algorithm) + if (algo == nil) { + throw NativeCryptoError.messageDigestError + } + return algo!.digest(data: data) + } +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift index 7629866..d6af3c1 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/KdfAlgorithm.swift @@ -6,3 +6,7 @@ // import Foundation + +enum KdfAlgorithm { + case pbkdf2 +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift index 540909d..a2ac159 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/NativeCryptoError.swift @@ -6,3 +6,13 @@ // import Foundation + +enum NativeCryptoError : Error { +case decryptionError +case encryptionError +case messageDigestError +case pbkdf2Error +case cipherError +case resultError +case exceptionError +} diff --git a/packages/native_crypto_ios/ios/Classes/utils/Task.swift b/packages/native_crypto_ios/ios/Classes/utils/Task.swift index db3fa79..4d11070 100644 --- a/packages/native_crypto_ios/ios/Classes/utils/Task.swift +++ b/packages/native_crypto_ios/ios/Classes/utils/Task.swift @@ -6,3 +6,47 @@ // import Foundation + +class Task { + + var task: () throws -> T + private var successful: Bool = false + private var result: T? = nil + private var exception: Error? = nil + + init(task: @escaping () throws -> T) { + self.task = task + } + + func isSuccessful() -> Bool { + return successful + } + + func getResult() -> T? { + return result + } + + func getException() -> Error { + if (exception != nil) { + return exception! + } else { + return NativeCryptoError.exceptionError + } + } + + func call() { + do { + result = try task() + exception = nil + successful = true + } catch { + exception = error + result = nil + successful = false + } + } + + func finalize(callback: (_ task: Task) -> Void) { + callback(self) + } +} From 0bb6aa4b787246cdcb626b17b8a8a0fa8746faf3 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 15:43:51 +0200 Subject: [PATCH 19/39] fix: benchmark output --- packages/native_crypto/example/ios/Podfile.lock | 2 +- packages/native_crypto/example/ios/Runner/Info.plist | 2 ++ .../example/lib/pages/benchmark_page.dart | 12 ++++++------ packages/native_crypto/lib/src/ciphers/aes/aes.dart | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/native_crypto/example/ios/Podfile.lock b/packages/native_crypto/example/ios/Podfile.lock index e1bca6a..3257d42 100644 --- a/packages/native_crypto/example/ios/Podfile.lock +++ b/packages/native_crypto/example/ios/Podfile.lock @@ -19,4 +19,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.3 diff --git a/packages/native_crypto/example/ios/Runner/Info.plist b/packages/native_crypto/example/ios/Runner/Info.plist index 58d9657..8293c48 100644 --- a/packages/native_crypto/example/ios/Runner/Info.plist +++ b/packages/native_crypto/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index e444eac..bee79bc 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -3,7 +3,7 @@ // ----- // File: benchmark_page.dart // Created Date: 28/12/2021 15:12:39 -// Last Modified: 24/05/2022 23:43:59 +// Last Modified: 25/05/2022 15:26:42 // ----- // Copyright (c) 2021 @@ -107,7 +107,7 @@ class BenchmarkPage extends ConsumerWidget { after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - csv += "$benchmark"; + csv += "$benchmark;"; cryptoTime += benchmark; // Decryption @@ -188,10 +188,6 @@ class BenchmarkPage extends ConsumerWidget { () => _benchmark(ref, cipher, usePc: true), "PointyCastle", ), - Button( - _clear, - "Clear", - ), ], ), Row( @@ -201,6 +197,10 @@ class BenchmarkPage extends ConsumerWidget { () => _benchmarkEncryptionOnly(ref, cipher), "NC Persistence", ), + Button( + _clear, + "Clear", + ), ], ), ], diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index 1f175ab..f899c5c 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 25/05/2022 10:44:25 +// Last Modified: 25/05/2022 15:40:07 // ----- // Copyright (c) 2022 From ee79b8d20fa418ca5b8fe02d7128cc16801c35bc Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 16:23:41 +0200 Subject: [PATCH 20/39] chore(release): publish packages - native_crypto_ios@0.1.1 - native_crypto_android@0.1.1 - native_crypto_platform_interface@0.1.1 --- CHANGELOG.md | 39 +++++++++++++++++++ packages/native_crypto/pubspec.yaml | 6 +-- packages/native_crypto_android/CHANGELOG.md | 6 +++ packages/native_crypto_android/pubspec.yaml | 2 +- packages/native_crypto_ios/CHANGELOG.md | 5 +++ packages/native_crypto_ios/pubspec.yaml | 2 +- .../CHANGELOG.md | 5 +++ .../pubspec.yaml | 2 +- 8 files changed, 61 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..16cf9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 2022-05-25 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`native_crypto_ios` - `v0.1.1`](#native_crypto_ios---v011) + - [`native_crypto_android` - `v0.1.1`](#native_crypto_android---v011) + - [`native_crypto_platform_interface` - `v0.1.1`](#native_crypto_platform_interface---v011) + +--- + +#### `native_crypto_ios` - `v0.1.1` + + - **REFACTOR**: rework swift part. + - **PERF**: optimize swift code. + +#### `native_crypto_android` - `v0.1.1` + + - **REFACTOR**: clean and modernize kotlin code. + - **PERF**: x10 perfomance improvement on android with better list management. + - **FEAT**: export new exceptions. + +#### `native_crypto_platform_interface` - `v0.1.1` + + - **PERF**: x10 perfomance improvement on android with better list management. + - **FEAT**: export new exceptions. + diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index 39e6c48..b8abaa2 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -15,19 +15,19 @@ dependencies: native_crypto_android: git: url: https://github.com/hugo-pcl/native-crypto-flutter.git - ref: native_crypto_android-v0.1.0 + ref: native_crypto_android-v0.1.1 path: packages/native_crypto_android native_crypto_ios: git: url: https://github.com/hugo-pcl/native-crypto-flutter.git - ref: native_crypto_ios-v0.1.0 + ref: native_crypto_ios-v0.1.1 path: packages/native_crypto_ios native_crypto_platform_interface: git: url: https://github.com/hugo-pcl/native-crypto-flutter.git - ref: native_crypto_platform_interface-v0.1.0 + ref: native_crypto_platform_interface-v0.1.1 path: packages/native_crypto_platform_interface dev_dependencies: diff --git a/packages/native_crypto_android/CHANGELOG.md b/packages/native_crypto_android/CHANGELOG.md index d7c6738..ce1055b 100644 --- a/packages/native_crypto_android/CHANGELOG.md +++ b/packages/native_crypto_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.1 + + - **REFACTOR**: clean and modernize kotlin code. + - **PERF**: x10 perfomance improvement on android with better list management. + - **FEAT**: export new exceptions. + ## 0.1.0 > Breaking changes ! diff --git a/packages/native_crypto_android/pubspec.yaml b/packages/native_crypto_android/pubspec.yaml index 27bdf52..8b40a04 100644 --- a/packages/native_crypto_android/pubspec.yaml +++ b/packages/native_crypto_android/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto_android description: Android implementation of NativeCrypto -version: 0.1.0 +version: 0.1.1 environment: sdk: ">=2.15.1 <3.0.0" diff --git a/packages/native_crypto_ios/CHANGELOG.md b/packages/native_crypto_ios/CHANGELOG.md index d7c6738..1511feb 100644 --- a/packages/native_crypto_ios/CHANGELOG.md +++ b/packages/native_crypto_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.1 + + - **REFACTOR**: rework swift part. + - **PERF**: optimize swift code. + ## 0.1.0 > Breaking changes ! diff --git a/packages/native_crypto_ios/pubspec.yaml b/packages/native_crypto_ios/pubspec.yaml index c6441c5..f4de824 100644 --- a/packages/native_crypto_ios/pubspec.yaml +++ b/packages/native_crypto_ios/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto_ios description: iOS implementation of NativeCrypto -version: 0.1.0 +version: 0.1.1 environment: sdk: ">=2.15.0 <3.0.0" diff --git a/packages/native_crypto_platform_interface/CHANGELOG.md b/packages/native_crypto_platform_interface/CHANGELOG.md index d7c6738..8b44888 100644 --- a/packages/native_crypto_platform_interface/CHANGELOG.md +++ b/packages/native_crypto_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.1 + + - **PERF**: x10 perfomance improvement on android with better list management. + - **FEAT**: export new exceptions. + ## 0.1.0 > Breaking changes ! diff --git a/packages/native_crypto_platform_interface/pubspec.yaml b/packages/native_crypto_platform_interface/pubspec.yaml index fd0ceb5..8bd0ca3 100644 --- a/packages/native_crypto_platform_interface/pubspec.yaml +++ b/packages/native_crypto_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: native_crypto_platform_interface description: A common interface for NativeCrypto plugin. -version: 0.1.0 +version: 0.1.1 environment: sdk: ">=2.17.0 <3.0.0" From ff6af2491ab93ecc35ee4b7b0e2a944587715fac Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 16:26:10 +0200 Subject: [PATCH 21/39] chore(release): publish packages - native_crypto@0.1.1 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ packages/native_crypto/CHANGELOG.md | 10 ++++++++++ packages/native_crypto/pubspec.yaml | 4 ++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16cf9f8..e4d77c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,33 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2022-05-25 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`native_crypto` - `v0.1.1`](#native_crypto---v011) + +--- + +#### `native_crypto` - `v0.1.1` + + - **REFACTOR**: change file organization. + - **PERF**: x10 perfomance improvement on android with better list management. + - **FIX**: benchmark output. + - **FIX**: update and fix code. + - **FEAT**: export new exceptions. + - **FEAT**: add PointyCastle benchmark. + - **DOCS**: add link to readme file. + + ## 2022-05-25 ### Changes diff --git a/packages/native_crypto/CHANGELOG.md b/packages/native_crypto/CHANGELOG.md index d7c6738..55a24cb 100644 --- a/packages/native_crypto/CHANGELOG.md +++ b/packages/native_crypto/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.1.1 + + - **REFACTOR**: change file organization. + - **PERF**: x10 perfomance improvement on android with better list management. + - **FIX**: benchmark output. + - **FIX**: update and fix code. + - **FEAT**: export new exceptions. + - **FEAT**: add PointyCastle benchmark. + - **DOCS**: add link to readme file. + ## 0.1.0 > Breaking changes ! diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index b8abaa2..a613749 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -1,8 +1,8 @@ name: native_crypto description: Fast and secure cryptography for Flutter. -version: 0.1.0 +version: 0.1.1 -publish_to: 'none' +# publish_to: 'none' environment: sdk: ">=2.17.0 <3.0.0" From f59279997063b3ce4751c5f3ef26f98a0c501378 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 21:33:51 +0200 Subject: [PATCH 22/39] docs: update readme --- README.md | 110 ++++++++++++++++++- resources/bench_nc_android_oneplus_A5010.csv | 14 +++ resources/bench_nc_ios_iphone13.csv | 14 +++ resources/bench_pc_android_oneplus_A5010.csv | 9 ++ resources/bench_pc_ios_iphone13.csv | 14 +++ resources/benchmark_pointycastle.png | Bin 123423 -> 0 bytes resources/benchmarks.png | Bin 0 -> 115262 bytes 7 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 resources/bench_nc_android_oneplus_A5010.csv create mode 100644 resources/bench_nc_ios_iphone13.csv create mode 100644 resources/bench_pc_android_oneplus_A5010.csv create mode 100644 resources/bench_pc_ios_iphone13.csv delete mode 100644 resources/benchmark_pointycastle.png create mode 100644 resources/benchmarks.png diff --git a/README.md b/README.md index e7423e8..e9b0b7c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ -# NativeCrypto +

+ +

Fast and powerful cryptographic functions for Flutter.
+

-Fast and powerful cryptographic functions thanks to **javax.crypto** , **CommonCrypto** and **CryptoKit**. \ No newline at end of file +

+ +Style: Wyatt Analysis + + + +Maintained with Melos + +

+ +--- + +[[Changelog]](./CHANGELOG.md) | [[License]](./LICENSE) + +--- + +## About + +The goal of this plugin is to provide a fast and powerful cryptographic functions by calling native libraries. On Android, it uses [javax.cypto](https://developer.android.com/reference/javax/crypto/package-summary), and on iOS, it uses [CommonCrypto](https://opensource.apple.com/source/CommonCrypto/) and [CryptoKit](https://developer.apple.com/documentation/cryptokit/) + +I started this projet because I wanted to add cryptographic functions on a Flutter app. But I faced a problem with the well-known [Pointy Castle](https://pub.dev/packages/pointycastle) library: the performance was very poor. Here some benchmarks and comparison: + +![](resources/benchmarks.png) + +For comparison, on a *iPhone 13*, you can encrypt/decrypt a message of **2MiB** in **~5.6s** with PointyCastle and in **~40ms** with NativeCrypto. And on an *OnePlus 5*, you can encrypt/decrypt a message of **50MiB** in **~6min30** with PointyCastle and in less than **~1s** with NativeCrypto. + +In short, NativeCrypto is incomparable with PointyCastle. + +## Usage + +First, check compatibility with your targets. + +| iOS | Android | MacOS | Linux | Windows | Web | +| --- | ------- | ----- | ----- | ------- | --- | +| ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | + +#### Hash + +To digest a message, you can use the following function: + +```dart +Uint8List hash = await HashAlgorithm.sha256.digest(message); +``` + +Note that you can find a `toBytes()` method in the example app, to convert a `String` to a `Uint8List`. + +> In NativeCrypto, you can use the following hash functions: SHA-256, SHA-384, SHA-512 + +#### Keys + +You can build a `SecretKey` from a utf8, base64, base16 (hex) strings or raw bytes. You can also generate a SecretKey from secure random. + +```dart +SecretKey secretKey = SecretKey(Uint8List.fromList([0x73, 0x65, 0x63, 0x72, 0x65, 0x74])); +SecretKey secretKey = SecretKey.fromUtf8('secret'); +SecretKey secretKey = SecretKey.fromBase64('c2VjcmV0'); +SecretKey secretKey = SecretKey.fromBase16('63657274'); +SecretKey secretKey = await SecretKey.fromSecureRandom(256); +``` + +#### Key derivation + +You can derive a `SecretKey` using **PBKDF2**. + +First, you need to initialize a `Pbkdf2` object. + +```dart +Pbkdf2 pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); +``` + +Then, you can derive a `SecretKey` from a password and salt. + +```dart +SecretKey secretKey = await pbkdf2.derive(password: password, salt: 'salt'); +``` + +> In NativeCrypto, you can use the following key derivation function: PBKDF2 + +#### Cipher + +And now, you can use the `SecretKey` to encrypt/decrypt a message. + +First, you need to initialize a `Cipher` object. + +```dart +AES cipher = AES(secretKey, AESMode.gcm); +``` + +Then, you can encrypt/decrypt your message. + +```dart +CipherText encrypted = await cipher.encrypt(message); +Uint8List decrypted = await cipher.decrypt(encrypted); +``` + +After an encryption, you can use the `CipherText` object to access underlying data. + +```dart +Uint8List iv = encrypted.iv; +Uint8List data = encrypted.data; +Uint8List tag = encrypted.tag; +``` + +Note that data and tag are costly to access, so you should only use them if you need to ! \ No newline at end of file diff --git a/resources/bench_nc_android_oneplus_A5010.csv b/resources/bench_nc_android_oneplus_A5010.csv new file mode 100644 index 0000000..5789c98 --- /dev/null +++ b/resources/bench_nc_android_oneplus_A5010.csv @@ -0,0 +1,14 @@ +Run;Size (B);Encryption Time (ms);Decryption Time (ms) +1;2097152;48;64 +2;6291456;52;52 +3;10485760;105;83 +4;14680064;96;198 +5;18874368;116;143 +6;23068672;253;257 +7;27262976;164;213 +8;31457280;271;270 +9;35651584;339;356 +10;39845888;316;320 +11;44040192;370;412 +12;48234496;425;368 +13;52428800;430;402 \ No newline at end of file diff --git a/resources/bench_nc_ios_iphone13.csv b/resources/bench_nc_ios_iphone13.csv new file mode 100644 index 0000000..47ef2ec --- /dev/null +++ b/resources/bench_nc_ios_iphone13.csv @@ -0,0 +1,14 @@ +Run;Size (B);Encryption Time (ms);Decryption Time (ms) +1;2097152;20;16 +2;6291456;13;13 +3;10485760;16;22 +4;14680064;23;37 +5;18874368;21;48 +6;23068672;34;45 +7;27262976;38;72 +8;31457280;59;72 +9;35651584;70;74 +10;39845888;61;85 +11;44040192;80;92 +12;48234496;90;113 +13;52428800;79;111 \ No newline at end of file diff --git a/resources/bench_pc_android_oneplus_A5010.csv b/resources/bench_pc_android_oneplus_A5010.csv new file mode 100644 index 0000000..48346c9 --- /dev/null +++ b/resources/bench_pc_android_oneplus_A5010.csv @@ -0,0 +1,9 @@ +size;encryption time;encode time;decryption time;crypto time +2000000;7764;7302;0 +4000000;14720;14580;0 +8000000;29421;29037;0 +16000000;58874;58304;0 +32000000;117964;116570;0 +64000000;235122;232923;0 +128000000;469697;466278;0 +256000000;0;0;0 \ No newline at end of file diff --git a/resources/bench_pc_ios_iphone13.csv b/resources/bench_pc_ios_iphone13.csv new file mode 100644 index 0000000..3f01464 --- /dev/null +++ b/resources/bench_pc_ios_iphone13.csv @@ -0,0 +1,14 @@ +Run;Size (B);Encryption Time (ms);Decryption Time (ms) +1;2097152;2420;2289 +2;6291456;7118;7369 +3;10485760;12766;13051 +4;14680064;18594;19029 +5;18874368;27449;27927 +6;23068672;34093;33863 +7;27262976;39855;39981 +8;31457280;46359;46089 +9;35651584;52596;52375 +10;39845888;59151;58881 +11;44040192;65914;65590 +12;48234496;71908;72099 +13;52428800;79202;78571 \ No newline at end of file diff --git a/resources/benchmark_pointycastle.png b/resources/benchmark_pointycastle.png deleted file mode 100644 index 0f357353cae33376c1ee99e346f06bd57e1a07d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123423 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%0AEm!wrv$ZlK@zT#e)HU`0CSuGUrPE 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 S<0|XmAN?c7+Jr;4*Z&VLp)522 diff --git a/resources/benchmarks.png b/resources/benchmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..62dac68a5bb5e44da6162d8a61a404a871a25043 GIT binary patch literal 115262 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 b$aaq$bBOEbitR*C!GFqE)Grq*ng#qHxakja literal 0 HcmV?d00001 From 5729fff09b8e499d3d1ba5fc5c129b5be8a05205 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 23:27:22 +0200 Subject: [PATCH 23/39] refactor(example): update benchmark page --- .../example/lib/pages/benchmark_page.dart | 185 ++++++++---------- .../example/lib/widgets/output.dart | 2 +- 2 files changed, 81 insertions(+), 106 deletions(-) diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index bee79bc..6cf3e63 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -3,10 +3,11 @@ // ----- // File: benchmark_page.dart // Created Date: 28/12/2021 15:12:39 -// Last Modified: 25/05/2022 15:26:42 +// Last Modified: 25/05/2022 17:16:12 // ----- // Copyright (c) 2021 +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -24,10 +25,12 @@ class BenchmarkPage extends ConsumerWidget { final Output keyContent = Output(); final Output benchmarkStatus = Output(large: true); - Future _benchmarkEncryptionOnly( + Future _benchmark( WidgetRef ref, - Cipher cipher, - ) async { + Cipher cipher, { + bool usePc = false, + bool encryptionOnly = false, + }) async { Session state = ref.read(sessionProvider.state).state; AesGcm pc = AesGcm(); @@ -37,102 +40,72 @@ class BenchmarkPage extends ConsumerWidget { return; } - benchmarkStatus.print("Benchmark 2/4/8/16/32/64/128/256MB\n"); - List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; - String csv = "size;encryption time\n"; + List testedSizes = [2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50]; + int multiplier = pow(2, 20).toInt(); // MiB - var beforeBench = DateTime.now(); - Cipher.bytesCountPerChunk = Cipher.bytesCountPerChunk; - benchmarkStatus - .append('[Benchmark] ${Cipher.bytesCountPerChunk} bytes/chunk \n'); - for (int size in testedSizes) { - var b = Uint8List(size * 1000000); - csv += "${size * 1000000};"; + benchmarkStatus.print("[Benchmark] Sizes: ${testedSizes.join('/')}MiB\n"); + benchmarkStatus.appendln( + "[Benchmark] Engine: " + (usePc ? " PointyCastle" : " NativeCrypto")); + benchmarkStatus.appendln("[Benchmark] Test: " + + (encryptionOnly ? " Encryption Only" : " Encryption & Decryption")); + benchmarkStatus.appendln( + '[Benchmark] bytesCountPerChunk: ${Cipher.bytesCountPerChunk} bytes/chunk'); - // Encryption - var before = DateTime.now(); - var encryptedBigFile = await cipher.encrypt(b); - var after = DateTime.now(); - - var benchmark = - after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - - csv += "$benchmark\n"; - } - var afterBench = DateTime.now(); - var benchmark = - afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch; - var sum = testedSizes.reduce((a, b) => a + b); - benchmarkStatus.append( - 'Benchmark finished.\nGenerated, and encrypted $sum MB in $benchmark ms'); - debugPrint("[Benchmark cvs]\n$csv"); - } - - Future _benchmark(WidgetRef ref, Cipher cipher, - {bool usePc = false}) async { - Session state = ref.read(sessionProvider.state).state; - AesGcm pc = AesGcm(); - - if (state.secretKey.bytes.isEmpty) { - benchmarkStatus - .print('No SecretKey!\nGo in Key tab and generate or derive one.'); - return; - } - - benchmarkStatus.print("Benchmark 2/4/8/16/32/64/128/256MB\n"); - List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; - String csv = "size;encryption time;decryption time;crypto time\n"; + String csv = encryptionOnly + ? "Run;Size (B);Encryption Time (ms)\n" + : "Run;Size (B);Encryption Time (ms);Decryption Time (ms)\n"; + int run = 0; var beforeBench = DateTime.now(); - Cipher.bytesCountPerChunk = Cipher.bytesCountPerChunk; - benchmarkStatus - .append('[Benchmark] ${Cipher.bytesCountPerChunk} bytes/chunk \n'); + for (int size in testedSizes) { - var b = Uint8List(size * 1000000); - csv += "${size * 1000000};"; - var cryptoTime = 0; + run++; + final StringBuffer csvLine = StringBuffer(); + final dummyBytes = Uint8List(size * multiplier); + csvLine.write('$run;${size * multiplier};'); // Encryption - var before = DateTime.now(); Object encryptedBigFile; + var before = DateTime.now(); if (usePc) { - encryptedBigFile = pc.encrypt(b, state.secretKey.bytes); + encryptedBigFile = pc.encrypt(dummyBytes, state.secretKey.bytes); } else { - encryptedBigFile = await cipher.encrypt(b); + encryptedBigFile = await cipher.encrypt(dummyBytes); } var after = DateTime.now(); var benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); - - csv += "$benchmark;"; - cryptoTime += benchmark; - - // Decryption - before = DateTime.now(); - if (usePc) { - pc.decrypt(encryptedBigFile as Uint8List, state.secretKey.bytes); - } else { - await cipher.decrypt(encryptedBigFile as CipherText); + benchmarkStatus + .appendln('[Benchmark] ${size}MiB => Encryption took $benchmark ms'); + csvLine.write('$benchmark'); + + if (!encryptionOnly) { + // Decryption + before = DateTime.now(); + if (usePc) { + pc.decrypt(encryptedBigFile as Uint8List, state.secretKey.bytes); + } else { + await cipher.decrypt(encryptedBigFile as CipherText); + } + after = DateTime.now(); + benchmark = + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus + .appendln('[Benchmark] ${size}MiB => Decryption took $benchmark ms'); + csvLine.write(';$benchmark'); } - after = DateTime.now(); - - benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus.append('[$size MB] Decryption took $benchmark ms\n'); - - csv += "$benchmark;"; - cryptoTime += benchmark; - csv += "$cryptoTime\n"; + csv += csvLine.toString() + '\n'; } var afterBench = DateTime.now(); var benchmark = afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch; var sum = testedSizes.reduce((a, b) => a + b); - benchmarkStatus.append( - 'Benchmark finished.\nGenerated, encrypted and decrypted $sum MB in $benchmark ms'); - debugPrint("[Benchmark cvs]\n$csv"); + benchmarkStatus + .appendln('[Benchmark] Finished: ${sum}MiB in $benchmark ms'); + benchmarkStatus.appendln('[Benchmark] Check the console for csv data'); + benchmarkStatus.appendln(csv); + print(csv); } void _clear() { @@ -174,34 +147,36 @@ class BenchmarkPage extends ConsumerWidget { alignment: Alignment.centerLeft, ), keyContent, - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Wrap( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Button( - () => _benchmark(ref, cipher), - "NativeCrypto", - ), - Button( - () => _benchmark(ref, cipher, usePc: true), - "PointyCastle", - ), - ], + Button( + () => _benchmark(ref, cipher), + "NativeCrypto", + ), + const SizedBox(width: 8), + Button( + () => _benchmark(ref, cipher, usePc: true), + "PointyCastle", + ), + const SizedBox(width: 8), + Button( + () => _benchmark(ref, cipher, encryptionOnly: true), + "NC Encryption Only", + ), + const SizedBox(width: 8), + Button( + () => _benchmark( + ref, + cipher, + usePc: true, + encryptionOnly: true, + ), + "PC Encryption Only", ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Button( - () => _benchmarkEncryptionOnly(ref, cipher), - "NC Persistence", - ), - Button( - _clear, - "Clear", - ), - ], + const SizedBox(width: 8), + Button( + _clear, + "Clear", ), ], ), diff --git a/packages/native_crypto/example/lib/widgets/output.dart b/packages/native_crypto/example/lib/widgets/output.dart index 7e16b5f..1dc9b70 100644 --- a/packages/native_crypto/example/lib/widgets/output.dart +++ b/packages/native_crypto/example/lib/widgets/output.dart @@ -3,7 +3,7 @@ // ----- // File: output.dart // Created Date: 28/12/2021 13:31:39 -// Last Modified: 28/12/2021 14:12:11 +// Last Modified: 25/05/2022 16:39:39 // ----- // Copyright (c) 2021 From 39a0a44730a012663dd3088684c15005d93d9875 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 23:29:02 +0200 Subject: [PATCH 24/39] fix: change tag length in aes gcm cipher --- packages/native_crypto/lib/src/ciphers/aes/aes.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index f899c5c..267c91b 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 25/05/2022 15:40:07 +// Last Modified: 25/05/2022 21:17:10 // ----- // Copyright (c) 2022 @@ -67,7 +67,8 @@ class AES implements Cipher { List.empty(); return CipherText.fromPairIvAndBytes( cipherText, - dataLength: cipherText.last.length, + dataLength: cipherText.last.length - 16, + tagLength: 16, ); } From e016d640c40b659b1f1e5a2008076de797e1ed8b Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 23:29:13 +0200 Subject: [PATCH 25/39] doc: copy readme --- packages/native_crypto/README.md | 110 ++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/packages/native_crypto/README.md b/packages/native_crypto/README.md index e7423e8..e9b0b7c 100644 --- a/packages/native_crypto/README.md +++ b/packages/native_crypto/README.md @@ -1,3 +1,109 @@ -# NativeCrypto +

+ +

Fast and powerful cryptographic functions for Flutter.
+

-Fast and powerful cryptographic functions thanks to **javax.crypto** , **CommonCrypto** and **CryptoKit**. \ No newline at end of file +

+ +Style: Wyatt Analysis + + + +Maintained with Melos + +

+ +--- + +[[Changelog]](./CHANGELOG.md) | [[License]](./LICENSE) + +--- + +## About + +The goal of this plugin is to provide a fast and powerful cryptographic functions by calling native libraries. On Android, it uses [javax.cypto](https://developer.android.com/reference/javax/crypto/package-summary), and on iOS, it uses [CommonCrypto](https://opensource.apple.com/source/CommonCrypto/) and [CryptoKit](https://developer.apple.com/documentation/cryptokit/) + +I started this projet because I wanted to add cryptographic functions on a Flutter app. But I faced a problem with the well-known [Pointy Castle](https://pub.dev/packages/pointycastle) library: the performance was very poor. Here some benchmarks and comparison: + +![](resources/benchmarks.png) + +For comparison, on a *iPhone 13*, you can encrypt/decrypt a message of **2MiB** in **~5.6s** with PointyCastle and in **~40ms** with NativeCrypto. And on an *OnePlus 5*, you can encrypt/decrypt a message of **50MiB** in **~6min30** with PointyCastle and in less than **~1s** with NativeCrypto. + +In short, NativeCrypto is incomparable with PointyCastle. + +## Usage + +First, check compatibility with your targets. + +| iOS | Android | MacOS | Linux | Windows | Web | +| --- | ------- | ----- | ----- | ------- | --- | +| ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | + +#### Hash + +To digest a message, you can use the following function: + +```dart +Uint8List hash = await HashAlgorithm.sha256.digest(message); +``` + +Note that you can find a `toBytes()` method in the example app, to convert a `String` to a `Uint8List`. + +> In NativeCrypto, you can use the following hash functions: SHA-256, SHA-384, SHA-512 + +#### Keys + +You can build a `SecretKey` from a utf8, base64, base16 (hex) strings or raw bytes. You can also generate a SecretKey from secure random. + +```dart +SecretKey secretKey = SecretKey(Uint8List.fromList([0x73, 0x65, 0x63, 0x72, 0x65, 0x74])); +SecretKey secretKey = SecretKey.fromUtf8('secret'); +SecretKey secretKey = SecretKey.fromBase64('c2VjcmV0'); +SecretKey secretKey = SecretKey.fromBase16('63657274'); +SecretKey secretKey = await SecretKey.fromSecureRandom(256); +``` + +#### Key derivation + +You can derive a `SecretKey` using **PBKDF2**. + +First, you need to initialize a `Pbkdf2` object. + +```dart +Pbkdf2 pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); +``` + +Then, you can derive a `SecretKey` from a password and salt. + +```dart +SecretKey secretKey = await pbkdf2.derive(password: password, salt: 'salt'); +``` + +> In NativeCrypto, you can use the following key derivation function: PBKDF2 + +#### Cipher + +And now, you can use the `SecretKey` to encrypt/decrypt a message. + +First, you need to initialize a `Cipher` object. + +```dart +AES cipher = AES(secretKey, AESMode.gcm); +``` + +Then, you can encrypt/decrypt your message. + +```dart +CipherText encrypted = await cipher.encrypt(message); +Uint8List decrypted = await cipher.decrypt(encrypted); +``` + +After an encryption, you can use the `CipherText` object to access underlying data. + +```dart +Uint8List iv = encrypted.iv; +Uint8List data = encrypted.data; +Uint8List tag = encrypted.tag; +``` + +Note that data and tag are costly to access, so you should only use them if you need to ! \ No newline at end of file From 9aa4eeb567eafb4fb830a9e4e1b47293604b7e3e Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 23:30:13 +0200 Subject: [PATCH 26/39] fix: update verify function --- .../lib/src/platform_interface/native_crypto_platform.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart b/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart index b5caf6b..b09c0d3 100644 --- a/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart +++ b/packages/native_crypto_platform_interface/lib/src/platform_interface/native_crypto_platform.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto_platform_interface.dart // Created Date: 25/12/2021 16:43:49 -// Last Modified: 24/05/2022 22:58:31 +// Last Modified: 25/05/2022 22:11:02 // ----- // Copyright (c) 2021 @@ -36,7 +36,7 @@ abstract class NativeCryptoPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [NativeCryptoPlatform] when they register themselves. static set instance(NativeCryptoPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } From 81335dc3505d6c07daab5875d826bfd80741ff17 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 25 May 2022 23:31:01 +0200 Subject: [PATCH 27/39] test(platform): add tests for platform and method channel --- .../pubspec.yaml | 6 +- .../method_channel_native_crypto_test.dart | 176 ++++++++++++++++++ .../native_crypto_platform_test.dart | 166 +++++++++++++++++ 3 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart create mode 100644 packages/native_crypto_platform_interface/test/platform_interface/native_crypto_platform_test.dart diff --git a/packages/native_crypto_platform_interface/pubspec.yaml b/packages/native_crypto_platform_interface/pubspec.yaml index 8bd0ca3..57593de 100644 --- a/packages/native_crypto_platform_interface/pubspec.yaml +++ b/packages/native_crypto_platform_interface/pubspec.yaml @@ -9,15 +9,17 @@ environment: dependencies: flutter: sdk: flutter - + plugin_platform_interface: ^2.1.2 dev_dependencies: flutter_test: sdk: flutter + mockito: ^5.2.0 + wyatt_analysis: git: url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages ref: wyatt_analysis-v2.1.0 - path: packages/wyatt_analysis \ No newline at end of file + path: packages/wyatt_analysis diff --git a/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart b/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart new file mode 100644 index 0000000..5f59f97 --- /dev/null +++ b/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart @@ -0,0 +1,176 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: method_channel_native_crypto_test.dart +// Created Date: 25/05/2022 22:47:41 +// Last Modified: 25/05/2022 23:22:44 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto_platform_interface/src/method_channel/method_channel_native_crypto.dart'; + +void main() { + TestWidgetsFlutterBinding + .ensureInitialized(); // Required for setMockMethodCallHandler + + group('$MethodChannelNativeCrypto', () { + const MethodChannel channel = + MethodChannel('plugins.hugop.cl/native_crypto'); + final List log = []; + final MethodChannelNativeCrypto nativeCrypto = MethodChannelNativeCrypto(); + + TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + log.add(call); + return null; + }); + + // Run after each test. + tearDown(log.clear); + + test('digest', () async { + await nativeCrypto.digest(Uint8List(0), 'sha256'); + expect( + log, + [ + isMethodCall( + 'digest', + arguments: { + 'data': Uint8List(0), + 'algorithm': 'sha256', + }, + ), + ], + ); + }); + + test('generateSecretKey', () async { + await nativeCrypto.generateSecretKey(256); + expect( + log, + [ + isMethodCall( + 'generateSecretKey', + arguments: { + 'bitsCount': 256, + }, + ), + ], + ); + }); + + test('pbkdf2', () async { + await nativeCrypto.pbkdf2( + 'password', + 'salt', + 32, + 10000, + 'sha256', + ); + expect( + log, + [ + isMethodCall( + 'pbkdf2', + arguments: { + 'password': 'password', + 'salt': 'salt', + 'keyBytesCount': 32, + 'iterations': 10000, + 'algorithm': 'sha256', + }, + ), + ], + ); + }); + + test('encryptAsList', () async { + await nativeCrypto.encryptAsList( + Uint8List(0), + Uint8List(0), + 'aes', + ); + expect( + log, + [ + isMethodCall( + 'encryptAsList', + arguments: { + 'data': Uint8List(0), + 'key': Uint8List(0), + 'algorithm': 'aes', + }, + ), + ], + ); + }); + + test('decryptAsList', () async { + await nativeCrypto.decryptAsList( + [Uint8List(0)], + Uint8List(0), + 'aes', + ); + expect( + log, + [ + isMethodCall( + 'decryptAsList', + arguments: { + 'data': [Uint8List(0)], + 'key': Uint8List(0), + 'algorithm': 'aes', + }, + ), + ], + ); + }); + + test('encrypt', () async { + await nativeCrypto.encrypt( + Uint8List(0), + Uint8List(0), + 'aes', + ); + expect( + log, + [ + isMethodCall( + 'encrypt', + arguments: { + 'data': Uint8List(0), + 'key': Uint8List(0), + 'algorithm': 'aes', + }, + ), + ], + ); + }); + + test('decrypt', () async { + await nativeCrypto.decrypt( + Uint8List(0), + Uint8List(0), + 'aes', + ); + expect( + log, + [ + isMethodCall( + 'decrypt', + arguments: { + 'data': Uint8List(0), + 'key': Uint8List(0), + 'algorithm': 'aes', + }, + ), + ], + ); + }); + + }); +} diff --git a/packages/native_crypto_platform_interface/test/platform_interface/native_crypto_platform_test.dart b/packages/native_crypto_platform_interface/test/platform_interface/native_crypto_platform_test.dart new file mode 100644 index 0000000..acd0f9d --- /dev/null +++ b/packages/native_crypto_platform_interface/test/platform_interface/native_crypto_platform_test.dart @@ -0,0 +1,166 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: native_crypto_platform_test.dart +// Created Date: 25/05/2022 21:43:25 +// Last Modified: 25/05/2022 23:26:18 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:native_crypto_platform_interface/src/platform_interface/native_crypto_platform.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + late ExtendsNativeCryptoPlatform nativeCryptoPlatform; + + group('$NativeCryptoPlatform', () { + setUpAll(() { + nativeCryptoPlatform = ExtendsNativeCryptoPlatform(); + }); + test('Constructor', () { + expect(nativeCryptoPlatform, isA()); + expect(nativeCryptoPlatform, isA()); + }); + + test('get.instance', () { + expect( + NativeCryptoPlatform.instance, + isA(), + ); + }); + test('set.instance', () { + NativeCryptoPlatform.instance = ExtendsNativeCryptoPlatform(); + expect( + NativeCryptoPlatform.instance, + isA(), + ); + }); + + test('Cannot be implemented with `implements`', () { + expect( + () { + NativeCryptoPlatform.instance = ImplementsNativeCryptoPlatform(); + }, + throwsA(isInstanceOf()), + ); + }); + + test('Can be mocked with `implements`', () { + final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); + NativeCryptoPlatform.instance = mock; + }); + + test('Can be extended', () { + NativeCryptoPlatform.instance = ExtendsNativeCryptoPlatform(); + }); + + test('throws if .digest() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.digest(Uint8List(0), 'sha256'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'digest is not implemented', + ), + ), + ); + }); + + test('throws if .generateSecretKey() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.generateSecretKey(256), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'generateSecretKey is not implemented', + ), + ), + ); + }); + + test('throws if .pbkdf2() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.pbkdf2('password', 'salt', 0, 0, 'sha256'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'pbkdf2 is not implemented', + ), + ), + ); + }); + + test('throws if .encryptAsList() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.encryptAsList( + Uint8List(0), + Uint8List(0), + 'aes', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'encryptAsList is not implemented', + ), + ), + ); + }); + + test('throws if .decryptAsList() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform + .decryptAsList([Uint8List(0)], Uint8List(0), 'aes'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'decryptAsList is not implemented', + ), + ), + ); + }); + + test('throws if .encrypt() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.encrypt(Uint8List(0), Uint8List(0), 'aes'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'encrypt is not implemented', + ), + ), + ); + }); + + test('throws if .decrypt() not implemented', () async { + await expectLater( + () => nativeCryptoPlatform.decrypt(Uint8List(0), Uint8List(0), 'aes'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'decrypt is not implemented', + ), + ), + ); + }); + }); +} + +class ExtendsNativeCryptoPlatform extends NativeCryptoPlatform {} + +class ImplementsNativeCryptoPlatform extends Mock + implements NativeCryptoPlatform {} + +class MockNativeCryptoPlatform extends Mock + with MockPlatformInterfaceMixin + implements NativeCryptoPlatform {} From 9bfe969c7d88a27fdc752e9f45c2761d6e424243 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 16:25:35 +0200 Subject: [PATCH 28/39] test: (WIP) add mocks and tests for secret key --- packages/native_crypto/pubspec.yaml | 5 +- .../mocks/mock_native_crypto_platform.dart | 192 ++++++++++++++++++ .../test/src/secret_key_test.dart | 125 ++++++++++++ 3 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 packages/native_crypto/test/mocks/mock_native_crypto_platform.dart create mode 100644 packages/native_crypto/test/src/secret_key_test.dart diff --git a/packages/native_crypto/pubspec.yaml b/packages/native_crypto/pubspec.yaml index a613749..72b0d59 100644 --- a/packages/native_crypto/pubspec.yaml +++ b/packages/native_crypto/pubspec.yaml @@ -2,7 +2,7 @@ name: native_crypto description: Fast and secure cryptography for Flutter. version: 0.1.1 -# publish_to: 'none' +publish_to: 'none' environment: sdk: ">=2.17.0 <3.0.0" @@ -34,6 +34,9 @@ dev_dependencies: flutter_test: sdk: flutter + mockito: ^5.2.0 + plugin_platform_interface: ^2.1.2 + wyatt_analysis: git: url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages diff --git a/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart b/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart new file mode 100644 index 0000000..6e69cb9 --- /dev/null +++ b/packages/native_crypto/test/mocks/mock_native_crypto_platform.dart @@ -0,0 +1,192 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: mock_native_crypto_platform.dart +// Created Date: 25/05/2022 23:34:34 +// Last Modified: 26/05/2022 11:40:24 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockNativeCryptoPlatform extends Fake + with MockPlatformInterfaceMixin + implements NativeCryptoPlatform { + Uint8List? data; + List? dataAsList; + Uint8List? key; + String? algorithm; + int? bitsCount; + String? password; + String? salt; + int? keyBytesCount; + int? iterations; + + Uint8List? Function()? response; + List? Function()? responseAsList; + + // ignore: use_setters_to_change_properties + void setResponse(Uint8List? Function()? response) { + this.response = response; + } + + // ignore: use_setters_to_change_properties + void setResponseAsList(List? Function()? responseAsList) { + this.responseAsList = responseAsList; + } + + void setDecryptExpectations({ + required Uint8List data, + required Uint8List key, + required String algorithm, + }) { + this.data = data; + this.key = key; + this.algorithm = algorithm; + } + + @override + Future decrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) async { + expect(data, this.data); + expect(key, this.key); + expect(algorithm, this.algorithm); + return response?.call(); + } + + void setDecryptAsListExpectations({ + required List data, + required Uint8List key, + required String algorithm, + }) { + dataAsList = data; + this.key = key; + this.algorithm = algorithm; + } + + @override + Future decryptAsList( + List data, + Uint8List key, + String algorithm, + ) async { + expect(data, dataAsList); + expect(key, this.key); + expect(algorithm, this.algorithm); + + return response?.call(); + } + + void setDigestExpectations({ + required Uint8List data, + required String algorithm, + }) { + this.data = data; + this.algorithm = algorithm; + } + + @override + Future digest(Uint8List data, String algorithm) async { + expect(data, this.data); + expect(algorithm, this.algorithm); + + return response?.call(); + } + + void setEncryptExpectations({ + required Uint8List data, + required Uint8List key, + required String algorithm, + }) { + this.data = data; + this.key = key; + this.algorithm = algorithm; + } + + @override + Future encrypt( + Uint8List data, + Uint8List key, + String algorithm, + ) async { + expect(data, this.data); + expect(key, this.key); + expect(algorithm, this.algorithm); + + return response?.call(); + } + + void setEncryptAsListExpectations({ + required Uint8List data, + required Uint8List key, + required String algorithm, + }) => + setEncryptExpectations( + data: data, + key: key, + algorithm: algorithm, + ); + + @override + Future?> encryptAsList( + Uint8List data, + Uint8List key, + String algorithm, + ) async { + expect(data, this.data); + expect(key, this.key); + expect(algorithm, this.algorithm); + + return responseAsList?.call(); + } + + // ignore: use_setters_to_change_properties + void setGenerateKeyExpectations({required int bitsCount}) { + this.bitsCount = bitsCount; + } + + @override + Future generateSecretKey(int bitsCount) async { + expect(bitsCount, this.bitsCount); + + return response?.call(); + } + + void setPbkdf2Expectations({ + required String password, + required String salt, + required int keyBytesCount, + required int iterations, + required String algorithm, + }) { + this.password = password; + this.salt = salt; + this.iterations = iterations; + this.keyBytesCount = keyBytesCount; + this.algorithm = algorithm; + } + + @override + Future pbkdf2( + String password, + String salt, + int keyBytesCount, + int iterations, + String algorithm, + ) async { + expect(password, this.password); + expect(salt, this.salt); + expect(keyBytesCount, this.keyBytesCount); + expect(iterations, this.iterations); + expect(algorithm, this.algorithm); + + return response?.call(); + } +} diff --git a/packages/native_crypto/test/src/secret_key_test.dart b/packages/native_crypto/test/src/secret_key_test.dart new file mode 100644 index 0000000..1a98e24 --- /dev/null +++ b/packages/native_crypto/test/src/secret_key_test.dart @@ -0,0 +1,125 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: secret_key_test.dart +// Created Date: 26/05/2022 10:52:41 +// Last Modified: 26/05/2022 12:07:33 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/src/keys/secret_key.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_platform.dart'; + +void main() { + final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); + NativeCryptoPlatform.instance = mock; + + group('fromSecureRandom', () { + test('handles returning random bytes', () async { + mock + ..setGenerateKeyExpectations(bitsCount: 5) + ..setResponse(() => Uint8List.fromList([1, 2, 3, 4, 5])); + + final SecretKey secretKey = await SecretKey.fromSecureRandom(5); + + expect( + secretKey.bytes, + Uint8List.fromList([1, 2, 3, 4, 5]), + ); + }); + + test('handles returning empty list', () async { + mock + ..setGenerateKeyExpectations(bitsCount: 5) + ..setResponse(() => Uint8List(0)); + + await expectLater( + () => SecretKey.fromSecureRandom(5), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles returning null', () async { + mock + ..setGenerateKeyExpectations(bitsCount: 5) + ..setResponse(() => null); + + await expectLater( + () => SecretKey.fromSecureRandom(5), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles throwing PlatformException', () async { + mock + ..setGenerateKeyExpectations(bitsCount: 5) + ..setResponse( + () => throw PlatformException( + code: 'native_crypto', + message: 'dummy error', + ), + ); + + await expectLater( + () => SecretKey.fromSecureRandom(5), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'PlatformException(native_crypto, dummy error, null, null)', + ) + .having( + (e) => e.code, + 'code', + 'failed_to_generate_secret_key', + ), + ), + ); + }); + }); + + group('Constructors', () { + test('handles Uint8List', () { + final SecretKey key = SecretKey(Uint8List.fromList([1, 2, 3, 4, 5])); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles base16', () { + final SecretKey key = SecretKey.fromBase16('0102030405'); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles base64', () { + final SecretKey key = SecretKey.fromBase64('AQIDBAU='); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles utf8', () { + final SecretKey key = SecretKey.fromUtf8('ABCDE'); + + expect(key.bytes, Uint8List.fromList([65, 66, 67, 68, 69])); + }); + }); +} From 6939a8df7e390bf5e8f91bdf72a47f7c6d12b831 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 16:26:16 +0200 Subject: [PATCH 29/39] refactor: (WIP) optimize exceptions and bytearray --- packages/native_crypto/lib/native_crypto.dart | 3 +- .../lib/src/core/cipher_text.dart | 193 ++++++++++++------ .../lib/src/core/cipher_text_wrapper.dart | 89 ++++++++ .../lib/src/interfaces/byte_array.dart | 47 +++-- .../lib/src/keys/secret_key.dart | 17 +- .../native_crypto/lib/src/utils/convert.dart | 23 --- .../native_crypto/lib/src/utils/encoding.dart | 10 + .../lib/src/utils/extensions.dart | 95 +++++++++ .../lib/src/utils/exception.dart | 78 +++---- 9 files changed, 390 insertions(+), 165 deletions(-) create mode 100644 packages/native_crypto/lib/src/core/cipher_text_wrapper.dart delete mode 100644 packages/native_crypto/lib/src/utils/convert.dart create mode 100644 packages/native_crypto/lib/src/utils/encoding.dart create mode 100644 packages/native_crypto/lib/src/utils/extensions.dart diff --git a/packages/native_crypto/lib/native_crypto.dart b/packages/native_crypto/lib/native_crypto.dart index c42ea91..2b294a4 100644 --- a/packages/native_crypto/lib/native_crypto.dart +++ b/packages/native_crypto/lib/native_crypto.dart @@ -3,7 +3,7 @@ // ----- // File: native_crypto.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 25/05/2022 10:48:20 +// Last Modified: 26/05/2022 12:10:42 // ----- // Copyright (c) 2021 @@ -23,7 +23,6 @@ export 'src/kdf/kdf.dart'; export 'src/keys/keys.dart'; // Utils export 'src/utils/cipher_algorithm.dart'; -export 'src/utils/convert.dart'; export 'src/utils/hash_algorithm.dart'; export 'src/utils/kdf_algorithm.dart'; diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index fcf09aa..76a9399 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,81 +3,144 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 24/05/2022 21:27:44 +// Last Modified: 26/05/2022 16:22:49 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/interfaces/byte_array.dart'; +import 'package:native_crypto/src/utils/cipher_algorithm.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -class CipherText extends ByteArray { +/// Represents a cipher text in Native Crypto. +/// +/// It is represented as a [List] of [Uint8List] like: +/// ```txt +/// [[NONCE], [MESSAGE + TAG]] +/// ``` +/// where: +/// - `[NONCE]` is a [Uint8List] of length [CipherText.ivLength] +/// - `[MESSAGE + TAG]` is a [Uint8List] of length [CipherText.dataLength] +/// +/// To +/// +/// So accessing just the Message or just the Tag is costly and should be +/// done only when needed. +class CipherText { final int _ivLength; - final int _dataLength; + final int _messageLength; final int _tagLength; - final Uint8List _iv; - - CipherText(Uint8List iv, Uint8List data, Uint8List? tag) - : _ivLength = iv.length, - _dataLength = data.length, - _tagLength = tag?.length ?? 0, - _iv = iv, - super((tag != null) ? Uint8List.fromList(data + tag) : data); - - CipherText.fromBytes( - Uint8List bytes, { - required int ivLength, - required int dataLength, - int tagLength = 0, - }) : _ivLength = ivLength, - _dataLength = dataLength, - _tagLength = tagLength, - _iv = bytes.sublist(0, ivLength), - super(bytes.sublist(ivLength, bytes.length - tagLength)); - - const CipherText.fromIvAndBytes( - Uint8List iv, - super.data, { - required int dataLength, - int tagLength = 0, - }) : _ivLength = iv.length, - _dataLength = dataLength, - _tagLength = tagLength, - _iv = iv; - - CipherText.fromPairIvAndBytes( - List pair, { - required int dataLength, - int tagLength = 0, - }) : _ivLength = pair.first.length, - _dataLength = dataLength, - _tagLength = tagLength, - _iv = pair.first, - super(pair.last); - - /// Gets the CipherText IV. - Uint8List get iv => _iv; - - /// Gets the CipherText data. - Uint8List get data => _tagLength > 0 - ? bytes.sublist(0, _dataLength - _tagLength) - : bytes; - - /// Gets the CipherText tag. - Uint8List get tag => _tagLength > 0 - ? bytes.sublist(_dataLength - _tagLength, _dataLength) - : Uint8List(0); - - /// Gets the CipherText data and tag. - Uint8List get payload => bytes; - - /// Gets the CipherText IV length. + final CipherAlgorithm? _cipherAlgorithm; + + final Uint8List? _iv; + final Uint8List? _data; // Contains the message + tag (if any) + + CipherText._( + this._ivLength, + this._messageLength, + this._tagLength, + this._cipherAlgorithm, + this._iv, + this._data, + ); + + /// Gets the [CipherAlgorithm] used to encrypt the [CipherText]. + CipherAlgorithm get cipherAlgorithm { + if (_cipherAlgorithm.isNotNull) { + return _cipherAlgorithm!; + } else { + throw NativeCryptoException( + message: 'Cipher algorithm is not specified', + code: NativeCryptoExceptionCode.invalid_cipher.code, + ); + } + } + + /// Gets the [Uint8List] of the [CipherText]'s IV. + Uint8List get iv { + if (_iv.isNotNull) { + return _iv!; + } else { + throw NativeCryptoException( + message: 'IV is not specified', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } + + /// Gets the length of the [CipherText]'s IV. int get ivLength => _ivLength; - /// Gets the CipherText data length. - int get dataLength => _dataLength; + /// Gets the [Uint8List] of the [CipherText]'s data. + Uint8List get data { + if (_data.isNotNull) { + return _data!; + } else { + throw NativeCryptoException( + message: 'Data is not specified', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } + + /// Gets the length of the [CipherText]'s data. + int get dataLength => _messageLength + _tagLength; + + // CipherText.fromBytes( + // Uint8List bytes, { + // required int ivLength, + // required int dataLength, + // int tagLength = 0, + // }) : _ivLength = ivLength, + // _dataLength = dataLength, + // _tagLength = tagLength, + // _iv = bytes.sublist(0, ivLength), + // super(bytes.sublist(ivLength, bytes.length - tagLength)); + + // const CipherText.fromIvAndBytes( + // Uint8List iv, + // super.data, { + // required int dataLength, + // int tagLength = 0, + // }) : _ivLength = iv.length, + // _dataLength = dataLength, + // _tagLength = tagLength, + // _iv = iv; + + // CipherText.fromPairIvAndBytes( + // List pair, { + // required int dataLength, + // int tagLength = 0, + // }) : _ivLength = pair.first.length, + // _dataLength = dataLength, + // _tagLength = tagLength, + // _iv = pair.first, + // super(pair.last); + + // /// Gets the CipherText IV. + // Uint8List get iv => _iv; + + // /// Gets the CipherText data. + // Uint8List get data => _tagLength > 0 + // ? bytes.sublist(0, _dataLength - _tagLength) + // : bytes; + + // /// Gets the CipherText tag. + // Uint8List get tag => _tagLength > 0 + // ? bytes.sublist(_dataLength - _tagLength, _dataLength) + // : Uint8List(0); + + // /// Gets the CipherText data and tag. + // Uint8List get payload => bytes; + + // /// Gets the CipherText IV length. + // int get ivLength => _ivLength; + + // /// Gets the CipherText data length. + // int get dataLength => _dataLength; - /// Gets the CipherText tag length. - int get tagLength => _tagLength; + // /// Gets the CipherText tag length. + // int get tagLength => _tagLength; } diff --git a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart new file mode 100644 index 0000000..a65cc23 --- /dev/null +++ b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart @@ -0,0 +1,89 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_text_wrapper.dart +// Created Date: 26/05/2022 14:27:32 +// Last Modified: 26/05/2022 15:53:46 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; + +class CipherTextWrapper { + final CipherText? _single; + final List? _list; + + CipherTextWrapper._(this._single, this._list); + + factory CipherTextWrapper.single(CipherText cipherText) => + CipherTextWrapper._(cipherText, null); + + factory CipherTextWrapper.list(List cipherTexts) => + CipherTextWrapper._(null, cipherTexts); + + factory CipherTextWrapper.fromBytes( + // Uint8List bytes, { + // required int ivLength, + // required int dataLength, + // int tagLength = 0, + // int? chunkSize, + // } + ) { + // TODO(hpcl): implement fromBytes + throw UnimplementedError(); + } + + bool get isSingle => _single.isNotNull; + bool get isList => _list.isNotNull; + + /// Gets the [CipherText] if it's a single one. + /// + /// Throws [NativeCryptoException] with + /// [NativeCryptoExceptionCode.invalid_data] if it's not a single one. + CipherText get single { + if (isSingle) { + return _single!; + } else { + throw NativeCryptoException( + message: 'CipherTextWrapper is not single', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } + + /// Gets the [List] of [CipherText] if it's a list. + /// + /// Throws [NativeCryptoException] with + /// [NativeCryptoExceptionCode.invalid_data] if it's not a list. + List get list { + if (isList) { + return _list!; + } else { + throw NativeCryptoException( + message: 'CipherTextWrapper is not list', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } + + /// Gets the raw [Uint8List] of the [CipherText] or [List] of [CipherText]. + Uint8List get raw { + if (isSingle) { + return single.bytes; + } else { + return list.map((cipherText) => cipherText.bytes).toList().sum(); + } + } + + int get chunkCount { + _single.isNull; + if (_single.isNotNull) { + return 1; + } else { + return _list?.length ?? 0; + } + } +} diff --git a/packages/native_crypto/lib/src/interfaces/byte_array.dart b/packages/native_crypto/lib/src/interfaces/byte_array.dart index 23d3ba4..d99d115 100644 --- a/packages/native_crypto/lib/src/interfaces/byte_array.dart +++ b/packages/native_crypto/lib/src/interfaces/byte_array.dart @@ -3,47 +3,58 @@ // ----- // File: byte_array.dart // Created Date: 16/12/2021 17:54:16 -// Last Modified: 23/05/2022 23:07:03 +// Last Modified: 26/05/2022 14:25:05 // ----- // Copyright (c) 2021 -import 'dart:convert' as convert; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; -import 'package:native_crypto/src/utils/convert.dart'; +import 'package:native_crypto/src/utils/encoding.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; @immutable abstract class ByteArray { final Uint8List _bytes; + /// Creates a [ByteArray] from a [Uint8List]. const ByteArray(this._bytes); - /// Creates an ByteArray object from a hexdecimal string. + /// Creates a [ByteArray] object from a hexdecimal string. ByteArray.fromBase16(String encoded) - : _bytes = Convert.decodeHexString(encoded); + : _bytes = encoded.toBytes(from: Encoding.base16); - /// Creates an ByteArray object from a Base64 string. + /// Creates a [ByteArray] object from a Base64 string. ByteArray.fromBase64(String encoded) - : _bytes = convert.base64.decode(encoded); + : _bytes = encoded.toBytes(from: Encoding.base64); - /// Creates an ByteArray object from a UTF-8 string. - ByteArray.fromUtf8(String input) - : _bytes = Uint8List.fromList(convert.utf8.encode(input)); + /// Creates a [ByteArray] object from an UTF-8 string. + ByteArray.fromUtf8(String encoded) + : _bytes = encoded.toBytes(from: Encoding.utf8); - /// Creates an ByteArray object from a length. + /// Creates a [ByteArray] object from an UTF-16 string. + ByteArray.fromUtf16(String encoded) : _bytes = encoded.toBytes(); + + /// Creates an empty [ByteArray] object from a length. ByteArray.fromLength(int length) : _bytes = Uint8List(length); - /// Gets the ByteArray bytes. - // ignore: unnecessary_getters_setters + /// Creates a [ByteArray] object from a [List] of [int]. + ByteArray.fromList(List list) : _bytes = list.toTypedList(); + + /// Gets the [ByteArray] bytes. Uint8List get bytes => _bytes; - /// Gets the ByteArray bytes as a Hexadecimal representation. - String get base16 => - _bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + /// Gets the [ByteArray] bytes as a Hexadecimal representation. + String get base16 => _bytes.toStr(to: Encoding.base16); + + /// Gets the [ByteArray] bytes as a Base64 representation. + String get base64 => _bytes.toStr(to: Encoding.base64); + + /// Gets the [ByteArray] bytes as an UTF-8 representation. + String get utf8 => _bytes.toStr(to: Encoding.utf8); - /// Gets the ByteArray bytes as a Base64 representation. - String get base64 => convert.base64.encode(_bytes); + /// Gets the [ByteArray] bytes as an UTF-16 representation. + String get utf16 => _bytes.toStr(); @override bool operator ==(Object other) { diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index 1aa9031..32f539b 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,7 +3,7 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 25/05/2022 10:45:55 +// Last Modified: 26/05/2022 11:56:06 // ----- // Copyright (c) 2021 @@ -24,13 +24,22 @@ class SecretKey extends Key { static Future fromSecureRandom(int bitsCount) async { try { - final Uint8List _key = - (await platform.generateSecretKey(bitsCount)) ?? Uint8List(0); + final Uint8List? _key = await platform.generateSecretKey(bitsCount); + + if (_key == null || _key.isEmpty) { + throw const KeyException( + message: 'Could not generate secret key, platform returned null', + code: 'platform_returned_null', + ); + } return SecretKey(_key); } catch (e, s) { + if (e is KeyException) { + rethrow; + } throw KeyException( - message: 'Failed to generate a secret key!', + message: '$e', code: 'failed_to_generate_secret_key', stackTrace: s, ); diff --git a/packages/native_crypto/lib/src/utils/convert.dart b/packages/native_crypto/lib/src/utils/convert.dart deleted file mode 100644 index f68bb78..0000000 --- a/packages/native_crypto/lib/src/utils/convert.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: convert.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 22:39:19 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; - -abstract class Convert { - static Uint8List decodeHexString(String input) { - assert(input.length.isEven, 'Input needs to be an even length.'); - - return Uint8List.fromList( - List.generate( - input.length ~/ 2, - (i) => int.parse(input.substring(i * 2, (i * 2) + 2), radix: 16), - ).toList(), - ); - } -} diff --git a/packages/native_crypto/lib/src/utils/encoding.dart b/packages/native_crypto/lib/src/utils/encoding.dart new file mode 100644 index 0000000..b7ddd80 --- /dev/null +++ b/packages/native_crypto/lib/src/utils/encoding.dart @@ -0,0 +1,10 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: encoding.dart +// Created Date: 26/05/2022 12:12:34 +// Last Modified: 26/05/2022 12:18:09 +// ----- +// Copyright (c) 2022 + +enum Encoding { utf8, utf16, base64, base16 } diff --git a/packages/native_crypto/lib/src/utils/extensions.dart b/packages/native_crypto/lib/src/utils/extensions.dart new file mode 100644 index 0000000..91b9949 --- /dev/null +++ b/packages/native_crypto/lib/src/utils/extensions.dart @@ -0,0 +1,95 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: extensions.dart +// Created Date: 26/05/2022 12:12:48 +// Last Modified: 26/05/2022 15:49:38 +// ----- +// Copyright (c) 2022 + +import 'dart:convert'; +import 'dart:developer' as developer; +import 'dart:typed_data'; + +import 'package:native_crypto/src/utils/encoding.dart'; + +extension ObjectX on Object? { + /// Returns `true` if the object is `null`. + bool get isNull => this == null; + + /// Returns `true` if the object is **not** `null`. + bool get isNotNull => this != null; + + /// Prints the object to the console. + void log() => developer.log(toString()); +} + +extension ListIntX on List { + /// Converts a [List] of int to a [Uint8List]. + Uint8List toTypedList() => Uint8List.fromList(this); +} + +extension ListUint8ListX on List { + /// Reduce a [List] of [Uint8List] to a [Uint8List]. + + Uint8List sum() { + for (var i = 1; i < length; i++) { + first.addAll(this[i]); + removeAt(i); + } + return first; + } +} + +extension StringX on String { + /// Converts a [String] to a [Uint8List] using the specified [Encoding]. + Uint8List toBytes({final Encoding from = Encoding.utf16}) { + Uint8List bytes; + switch (from) { + case Encoding.utf8: + bytes = utf8.encode(this).toTypedList(); + break; + case Encoding.utf16: + bytes = runes.toList().toTypedList(); + break; + case Encoding.base64: + bytes = base64.decode(this); + break; + case Encoding.base16: + assert(length.isEven, 'String needs to be an even length.'); + bytes = List.generate( + length ~/ 2, + (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), + ).toList().toTypedList(); + } + return bytes; + } +} + +extension Uint8ListX on Uint8List { + /// Converts a [Uint8List] to a [String] using the specified [Encoding]. + String toStr({final Encoding to = Encoding.utf16}) { + String str; + switch (to) { + case Encoding.utf8: + str = utf8.decode(this); + break; + case Encoding.utf16: + str = String.fromCharCodes(this); + break; + case Encoding.base64: + str = base64.encode(this); + break; + case Encoding.base16: + str = List.generate( + length, + (i) => this[i].toRadixString(16).padLeft(2, '0'), + ).join(); + } + return str; + } + + /// Returns a concatenation of this with the other [Uint8List]. + Uint8List operator +(final Uint8List other) => + [...this, ...other].toTypedList(); +} diff --git a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart index 196a2bd..5b571c2 100644 --- a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart +++ b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart @@ -3,21 +3,41 @@ // ----- // File: exception.dart // Created Date: 24/05/2022 18:54:48 -// Last Modified: 25/05/2022 10:43:29 +// Last Modified: 26/05/2022 15:36:56 // ----- // Copyright (c) 2022 +// ignore_for_file: constant_identifier_names + import 'dart:developer'; import 'package:flutter/services.dart'; +enum NativeCryptoExceptionCode { + unknown, + not_implemented, + invalid_argument, + invalid_key, + invalid_key_length, + invalid_algorithm, + invalid_padding, + invalid_mode, + invalid_cipher, + invalid_data, + platform_not_supported, + platform_returned_invalid_data, + platform_returned_empty_data, + platform_returned_null; + + String get code => toString().split('.').last.toLowerCase(); +} + class NativeCryptoException implements Exception { - const NativeCryptoException({ + NativeCryptoException({ this.message, String? code, this.stackTrace, - // ignore: unnecessary_this - }) : this.code = code ?? 'unknown'; + }) : code = code ?? NativeCryptoExceptionCode.unknown.code; /// The long form message of the exception. final String? message; @@ -73,7 +93,7 @@ class NativeCryptoException implements Exception { ) : null; - String code = 'unknown'; + String code = NativeCryptoExceptionCode.unknown.code; String message = platformException.message ?? ''; if (details != null) { @@ -103,51 +123,3 @@ class NativeCryptoException implements Exception { // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => message.hashCode ^ code.hashCode ^ stackTrace.hashCode; } - -class KeyException extends NativeCryptoException { - const KeyException({ - super.message, - super.code, - super.stackTrace, - }); -} - -class KeyDerivationException extends NativeCryptoException { - const KeyDerivationException({ - super.message, - super.code, - super.stackTrace, - }); -} - -class CipherInitException extends NativeCryptoException { - const CipherInitException({ - super.message, - super.code, - super.stackTrace, - }); -} - -class EncryptionException extends NativeCryptoException { - const EncryptionException({ - super.message, - super.code, - super.stackTrace, - }); -} - -class DecryptionException extends NativeCryptoException { - const DecryptionException({ - super.message, - super.code, - super.stackTrace, - }); -} - -class NotImplementedException extends NativeCryptoException { - const NotImplementedException({ - super.message, - super.code, - super.stackTrace, - }); -} From 48ebabb54ca64d2c7ea2cb71c7d2d24b3d13bf25 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 20:42:53 +0200 Subject: [PATCH 30/39] feat: rework bytearray and memory optimization, simplify API --- .../example/lib/pages/benchmark_page.dart | 10 +- .../example/lib/pages/cipher_page.dart | 41 +++-- .../example/lib/pages/kdf_page.dart | 10 +- packages/native_crypto/example/lib/utils.dart | 52 ------- .../native_crypto/lib/native_crypto_ext.dart | 11 ++ .../lib/src/builders/aes_builder.dart | 49 ------ .../lib/src/builders/builders.dart | 4 +- .../lib/src/builders/decryption_builder.dart | 46 ++++++ .../lib/src/ciphers/aes/aes.dart | 127 +++++++++------ .../lib/src/ciphers/aes/aes_key_size.dart | 6 +- .../lib/src/ciphers/aes/aes_mode.dart | 13 +- .../lib/src/core/cipher_text.dart | 146 ++++++------------ .../lib/src/core/cipher_text_list.dart | 26 ---- .../lib/src/core/cipher_text_wrapper.dart | 116 ++++++++++++-- packages/native_crypto/lib/src/core/core.dart | 4 +- .../lib/src/interfaces/base_key.dart | 22 +++ .../lib/src/interfaces/byte_array.dart | 11 +- .../lib/src/interfaces/cipher.dart | 30 ++-- .../lib/src/interfaces/interfaces.dart | 4 +- .../native_crypto/lib/src/interfaces/key.dart | 18 --- .../lib/src/interfaces/keyderivation.dart | 11 +- .../native_crypto/lib/src/kdf/pbkdf2.dart | 51 ++++-- .../lib/src/keys/secret_key.dart | 50 +++--- .../lib/src/utils/cipher_algorithm.dart | 4 +- .../lib/src/utils/extensions.dart | 4 +- .../lib/src/utils/hash_algorithm.dart | 5 +- .../lib/src/utils/kdf_algorithm.dart | 3 +- .../test/src/secret_key_test.dart | 12 +- 28 files changed, 482 insertions(+), 404 deletions(-) delete mode 100644 packages/native_crypto/example/lib/utils.dart create mode 100644 packages/native_crypto/lib/native_crypto_ext.dart delete mode 100644 packages/native_crypto/lib/src/builders/aes_builder.dart create mode 100644 packages/native_crypto/lib/src/builders/decryption_builder.dart delete mode 100644 packages/native_crypto/lib/src/core/cipher_text_list.dart create mode 100644 packages/native_crypto/lib/src/interfaces/base_key.dart delete mode 100644 packages/native_crypto/lib/src/interfaces/key.dart diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index 6cf3e63..ad1074f 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -3,7 +3,7 @@ // ----- // File: benchmark_page.dart // Created Date: 28/12/2021 15:12:39 -// Last Modified: 25/05/2022 17:16:12 +// Last Modified: 26/05/2022 20:19:28 // ----- // Copyright (c) 2021 @@ -40,7 +40,7 @@ class BenchmarkPage extends ConsumerWidget { return; } - List testedSizes = [2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50]; + List testedSizes = [2, 4, 8, 16, 32, 64, 128, 256]; int multiplier = pow(2, 20).toInt(); // MiB benchmarkStatus.print("[Benchmark] Sizes: ${testedSizes.join('/')}MiB\n"); @@ -86,7 +86,7 @@ class BenchmarkPage extends ConsumerWidget { if (usePc) { pc.decrypt(encryptedBigFile as Uint8List, state.secretKey.bytes); } else { - await cipher.decrypt(encryptedBigFile as CipherText); + await cipher.decrypt(encryptedBigFile as CipherTextWrapper); } after = DateTime.now(); benchmark = @@ -105,7 +105,7 @@ class BenchmarkPage extends ConsumerWidget { .appendln('[Benchmark] Finished: ${sum}MiB in $benchmark ms'); benchmarkStatus.appendln('[Benchmark] Check the console for csv data'); benchmarkStatus.appendln(csv); - print(csv); + debugPrint(csv); } void _clear() { @@ -135,7 +135,7 @@ class BenchmarkPage extends ConsumerWidget { } keyContent.print(state.secretKey.bytes.toString()); - AES cipher = AES(state.secretKey, AESMode.gcm); + AES cipher = AES(state.secretKey); return SingleChildScrollView( child: Padding( diff --git a/packages/native_crypto/example/lib/pages/cipher_page.dart b/packages/native_crypto/example/lib/pages/cipher_page.dart index e953a87..5733563 100644 --- a/packages/native_crypto/example/lib/pages/cipher_page.dart +++ b/packages/native_crypto/example/lib/pages/cipher_page.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_page.dart // Created Date: 28/12/2021 13:33:15 -// Last Modified: 25/05/2022 10:49:30 +// Last Modified: 26/05/2022 20:39:37 // ----- // Copyright (c) 2021 @@ -12,10 +12,10 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto/native_crypto_ext.dart'; import 'package:native_crypto_example/widgets/button.dart'; import '../session.dart'; -import '../utils.dart'; import '../widgets/output.dart'; // ignore: must_be_immutable @@ -26,8 +26,8 @@ class CipherPage extends ConsumerWidget { final Output encryptionStatus = Output(); final Output decryptionStatus = Output(); - final TextEditingController _plainTextController = TextEditingController(); - CipherText? cipherText; + final TextEditingController _plainTextController = TextEditingController()..text = 'PlainText'; + CipherTextWrapper? cipherText; Future _encrypt(WidgetRef ref, Cipher cipher) async { Session state = ref.read(sessionProvider.state).state; @@ -41,13 +41,10 @@ class CipherPage extends ConsumerWidget { } else { var stringToBytes = plainText.toBytes(); cipherText = await cipher.encrypt(stringToBytes); - encryptionStatus.print('String successfully encrypted.\n'); - encryptionStatus.append("Nonce: " + - cipherText!.iv.toString() + - "\nData: " + - cipherText!.data.toString() + - "\nTag: " + - cipherText!.tag.toString()); + encryptionStatus.print('String successfully encrypted:\n'); + + CipherText unwrap = cipherText!.unwrap(); + encryptionStatus.append(unwrap.base16); } } @@ -56,17 +53,19 @@ class CipherPage extends ConsumerWidget { decryptionStatus.print('Encrypt before altering CipherText!'); } else { // Add 1 to the first byte - Uint8List _altered = cipherText!.data; + Uint8List _altered = cipherText!.unwrap().bytes; _altered[0] += 1; // Recreate cipher text with altered data - cipherText = CipherText(cipherText!.iv, _altered, cipherText!.tag); - encryptionStatus.print('String successfully encrypted.\n'); - encryptionStatus.append("Nonce: " + - cipherText!.iv.toString() + - "\nData: " + - cipherText!.data.toString() + - "\nTag: " + - cipherText!.tag.toString()); + cipherText = CipherTextWrapper.fromBytes( + _altered, + 12, + _altered.length - 28, + 16, + ); + encryptionStatus.print('String successfully encrypted:\n'); + + CipherText unwrap = cipherText!.unwrap(); + encryptionStatus.appendln(unwrap.base16); decryptionStatus.print('CipherText altered!\nDecryption will fail.'); } } @@ -114,7 +113,7 @@ class CipherPage extends ConsumerWidget { } keyContent.print(state.secretKey.bytes.toString()); - AES cipher = AES(state.secretKey, AESMode.gcm); + AES cipher = AES(state.secretKey); return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/packages/native_crypto/example/lib/pages/kdf_page.dart b/packages/native_crypto/example/lib/pages/kdf_page.dart index 6eb9011..da04947 100644 --- a/packages/native_crypto/example/lib/pages/kdf_page.dart +++ b/packages/native_crypto/example/lib/pages/kdf_page.dart @@ -3,7 +3,7 @@ // ----- // File: kdf_page.dart // Created Date: 28/12/2021 13:40:34 -// Last Modified: 23/05/2022 22:49:06 +// Last Modified: 26/05/2022 20:30:31 // ----- // Copyright (c) 2021 @@ -12,10 +12,10 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto/native_crypto_ext.dart'; import 'package:native_crypto_example/widgets/button.dart'; import '../session.dart'; -import '../utils.dart'; import '../widgets/output.dart'; class KdfPage extends ConsumerWidget { @@ -26,8 +26,8 @@ class KdfPage extends ConsumerWidget { final Output pbkdf2Status = Output(); final Output hashStatus = Output(large: true); - final TextEditingController _pwdTextController = TextEditingController(); - final TextEditingController _messageTextController = TextEditingController(); + final TextEditingController _pwdTextController = TextEditingController()..text = 'Password'; + final TextEditingController _messageTextController = TextEditingController()..text = 'Message'; Future _generate(WidgetRef ref) async { Session state = ref.read(sessionProvider.state).state; @@ -66,7 +66,7 @@ class KdfPage extends ConsumerWidget { } else { Uint8List hash = await hasher.digest(message.toBytes()); hashStatus.print( - 'Message successfully hashed with $hasher :${hash.toStr(to: Encoding.hex)}'); + 'Message successfully hashed with $hasher :${hash.toStr(to: Encoding.base16)}'); } } diff --git a/packages/native_crypto/example/lib/utils.dart b/packages/native_crypto/example/lib/utils.dart deleted file mode 100644 index d93a923..0000000 --- a/packages/native_crypto/example/lib/utils.dart +++ /dev/null @@ -1,52 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: utils.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 28/12/2021 14:40:21 -// ----- -// Copyright (c) 2021 - -import 'dart:typed_data'; -import 'dart:convert'; - -enum Encoding { utf16, base64, hex } - -extension StringX on String { - Uint8List toBytes({final from = Encoding.utf16}) { - Uint8List bytes = Uint8List(0); - switch (from) { - case Encoding.utf16: - bytes = Uint8List.fromList(runes.toList()); - break; - case Encoding.base64: - bytes = base64.decode(this); - break; - case Encoding.hex: - bytes = Uint8List.fromList( - List.generate( - length ~/ 2, - (i) => int.parse(substring(i * 2, (i * 2) + 2), radix: 16), - ).toList(), - ); - } - return bytes; - } -} - -extension Uint8ListX on Uint8List { - String toStr({final to = Encoding.utf16}) { - String str = ""; - switch (to) { - case Encoding.utf16: - str = String.fromCharCodes(this); - break; - case Encoding.base64: - str = base64.encode(this); - break; - case Encoding.hex: - str = map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); - } - return str; - } -} diff --git a/packages/native_crypto/lib/native_crypto_ext.dart b/packages/native_crypto/lib/native_crypto_ext.dart new file mode 100644 index 0000000..0e9502e --- /dev/null +++ b/packages/native_crypto/lib/native_crypto_ext.dart @@ -0,0 +1,11 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: native_crypto_ext.dart +// Created Date: 26/05/2022 19:36:54 +// Last Modified: 26/05/2022 19:38:44 +// ----- +// Copyright (c) 2022 + +export 'src/utils/encoding.dart'; +export 'src/utils/extensions.dart'; diff --git a/packages/native_crypto/lib/src/builders/aes_builder.dart b/packages/native_crypto/lib/src/builders/aes_builder.dart deleted file mode 100644 index e256d29..0000000 --- a/packages/native_crypto/lib/src/builders/aes_builder.dart +++ /dev/null @@ -1,49 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: aes_builder.dart -// Created Date: 28/12/2021 12:03:11 -// Last Modified: 25/05/2022 10:47:11 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/ciphers/aes/aes.dart'; -import 'package:native_crypto/src/interfaces/builder.dart'; -import 'package:native_crypto/src/keys/secret_key.dart'; -import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; - -class AESBuilder implements Builder { - SecretKey? _sk; - Future? _fsk; - AESMode _mode = AESMode.gcm; - - AESBuilder withGeneratedKey(int bitsCount) { - _fsk = SecretKey.fromSecureRandom(bitsCount); - return this; - } - - AESBuilder withKey(SecretKey secretKey) { - _sk = secretKey; - return this; - } - - AESBuilder using(AESMode mode) { - _mode = mode; - return this; - } - - @override - Future build() async { - if (_sk == null) { - if (_fsk == null) { - throw const CipherInitException( - message: 'You must specify or generate a secret key.', - code: 'missing_key', - ); - } else { - _sk = await _fsk; - } - } - return AES(_sk!, _mode); - } -} diff --git a/packages/native_crypto/lib/src/builders/builders.dart b/packages/native_crypto/lib/src/builders/builders.dart index e20a4a6..d846197 100644 --- a/packages/native_crypto/lib/src/builders/builders.dart +++ b/packages/native_crypto/lib/src/builders/builders.dart @@ -3,8 +3,8 @@ // ----- // File: builders.dart // Created Date: 23/05/2022 22:56:03 -// Last Modified: 23/05/2022 22:56:12 +// Last Modified: 26/05/2022 19:22:19 // ----- // Copyright (c) 2022 -export 'aes_builder.dart'; +export 'decryption_builder.dart'; diff --git a/packages/native_crypto/lib/src/builders/decryption_builder.dart b/packages/native_crypto/lib/src/builders/decryption_builder.dart new file mode 100644 index 0000000..998fdcf --- /dev/null +++ b/packages/native_crypto/lib/src/builders/decryption_builder.dart @@ -0,0 +1,46 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: decryption_builder.dart +// Created Date: 26/05/2022 19:07:52 +// Last Modified: 26/05/2022 19:21:00 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; +import 'package:native_crypto/src/interfaces/cipher.dart'; + +class DecryptionBuilder extends StatelessWidget { + final Cipher cipher; + final CipherTextWrapper data; + final Widget Function(BuildContext context) onLoading; + final Widget Function(BuildContext context, Object error) onError; + final Widget Function(BuildContext context, Uint8List plainText) onSuccess; + + const DecryptionBuilder({ + super.key, + required this.cipher, + required this.data, + required this.onLoading, + required this.onError, + required this.onSuccess, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: cipher.decrypt(data), + builder: (context, snapshot) { + if (snapshot.hasData) { + return onSuccess(context, snapshot.data!); + } else if (snapshot.hasError) { + return onError(context, snapshot.error!); + } + return onLoading(context); + }, + ); + } +} diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index 267c91b..9a28105 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 25/05/2022 21:17:10 +// Last Modified: 26/05/2022 19:43:22 // ----- // Copyright (c) 2022 @@ -13,88 +13,120 @@ import 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; import 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; import 'package:native_crypto/src/core/cipher_text.dart'; -import 'package:native_crypto/src/core/cipher_text_list.dart'; +import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; import 'package:native_crypto/src/interfaces/cipher.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; import 'package:native_crypto/src/utils/cipher_algorithm.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -export 'package:native_crypto/src/ciphers/aes/aes_key_size.dart'; -export 'package:native_crypto/src/ciphers/aes/aes_mode.dart'; -export 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; - +/// An AES cipher. +/// +/// [AES] is a [Cipher] that can be used to encrypt or decrypt data. class AES implements Cipher { - final SecretKey key; + final SecretKey _key; final AESMode mode; final AESPadding padding; @override CipherAlgorithm get algorithm => CipherAlgorithm.aes; - AES(this.key, this.mode, {this.padding = AESPadding.none}) { - if (!AESKeySize.supportedSizes.contains(key.bytes.length * 8)) { - throw const CipherInitException( - message: 'Invalid key length!', - code: 'invalid_key_length', + AES(SecretKey key, [this.mode = AESMode.gcm, this.padding = AESPadding.none]) + : _key = key { + if (!AESKeySize.supportedSizes.contains(key.bitLength)) { + throw NativeCryptoException( + message: 'Invalid key size! ' + 'Expected: ${AESKeySize.supportedSizes.join(', ')} bits', + code: NativeCryptoExceptionCode.invalid_key_length.code, ); } - final Map> _supported = { - AESMode.gcm: [AESPadding.none], - }; - if (!_supported[mode]!.contains(padding)) { - throw const CipherInitException( - message: 'Invalid padding!', - code: 'invalid_padding', + if (!mode.supportedPaddings.contains(padding)) { + throw NativeCryptoException( + message: 'Invalid padding! ' + 'Expected: ${mode.supportedPaddings.join(', ')}', + code: NativeCryptoExceptionCode.invalid_padding.code, ); } } - Future _decrypt(CipherText cipherText) async { - return await platform.decryptAsList( - [cipherText.iv, cipherText.payload], - key.bytes, - algorithm.name, - ) ?? - Uint8List(0); + Future _decrypt(CipherText cipherText, + {int chunkCount = 0,}) async { + final Uint8List? decrypted = await platform.decrypt( + cipherText.bytes, + _key.bytes, + algorithm.name, + ); + + if (decrypted.isNull) { + throw NativeCryptoException( + message: 'Platform returned null when decrypting chunk #$chunkCount', + code: NativeCryptoExceptionCode.platform_returned_null.code, + ); + } else if (decrypted!.isEmpty) { + throw NativeCryptoException( + message: 'Platform returned no data when decrypting chunk #$chunkCount', + code: NativeCryptoExceptionCode.platform_returned_empty_data.code, + ); + } else { + return decrypted; + } } - Future _encrypt(Uint8List data) async { - final List cipherText = - await platform.encryptAsList(data, key.bytes, algorithm.name) ?? - List.empty(); - return CipherText.fromPairIvAndBytes( - cipherText, - dataLength: cipherText.last.length - 16, - tagLength: 16, + Future _encrypt(Uint8List data, {int chunkCount = 0}) async { + final Uint8List? encrypted = await platform.encrypt( + data, + _key.bytes, + algorithm.name, ); + + if (encrypted.isNull) { + throw NativeCryptoException( + message: 'Platform returned null when encrypting chunk #$chunkCount', + code: NativeCryptoExceptionCode.platform_returned_null.code, + ); + } else if (encrypted!.isEmpty) { + throw NativeCryptoException( + message: 'Platform returned no data when encrypting chunk #$chunkCount', + code: NativeCryptoExceptionCode.platform_returned_empty_data.code, + ); + } else { + return CipherText.fromBytes( + 12, + encrypted.length - 28, + 16, + CipherAlgorithm.aes, + encrypted, + ); + } } @override - Future decrypt(CipherText cipherText) async { + Future decrypt(CipherTextWrapper cipherText) async { final BytesBuilder decryptedData = BytesBuilder(copy: false); - if (cipherText is CipherTextList) { - for (final CipherText ct in cipherText.list) { - decryptedData.add(await _decrypt(ct)); + if (cipherText.isList) { + int chunkCount = 0; + for (final CipherText chunk in cipherText.list) { + decryptedData.add(await _decrypt(chunk, chunkCount: chunkCount++)); } } else { - decryptedData.add(await _decrypt(cipherText)); + decryptedData.add(await _decrypt(cipherText.single)); } return decryptedData.toBytes(); } @override - Future encrypt(Uint8List data) async { + Future encrypt(Uint8List data) async { + CipherTextWrapper cipherTextWrapper; Uint8List dataToEncrypt; + final int chunkNb = (data.length / Cipher.bytesCountPerChunk).ceil(); - final CipherTextList cipherTextList = CipherTextList(); - - if (data.length > Cipher.bytesCountPerChunk) { - final int chunkNb = (data.length / Cipher.bytesCountPerChunk).ceil(); + if (chunkNb > 1) { + cipherTextWrapper = CipherTextWrapper.empty(); for (var i = 0; i < chunkNb; i++) { dataToEncrypt = i < (chunkNb - 1) ? data.sublist( @@ -102,11 +134,12 @@ class AES implements Cipher { (i + 1) * Cipher.bytesCountPerChunk, ) : data.sublist(i * Cipher.bytesCountPerChunk); - cipherTextList.add(await _encrypt(dataToEncrypt)); + cipherTextWrapper.add(await _encrypt(dataToEncrypt, chunkCount: i)); } } else { - return _encrypt(data); + cipherTextWrapper = CipherTextWrapper.single(await _encrypt(data)); } - return cipherTextList; + + return cipherTextWrapper; } } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart index 8018f0b..befa22f 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_key_size.dart @@ -3,7 +3,7 @@ // ----- // File: aes_key_size.dart // Created Date: 23/05/2022 22:10:07 -// Last Modified: 23/05/2022 22:33:32 +// Last Modified: 26/05/2022 18:45:01 // ----- // Copyright (c) 2022 @@ -13,9 +13,13 @@ enum AESKeySize { bits192(192), bits256(256); + /// Returns the number of bits supported by an [AESKeySize]. static final List supportedSizes = [128, 192, 256]; + /// Returns the number of bits in this [AESKeySize]. final int bits; + + /// Returns the number of bytes in this [AESKeySize]. int get bytes => bits ~/ 8; const AESKeySize(this.bits); diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart index c1d98cb..a16d414 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -3,9 +3,18 @@ // ----- // File: aes_mode.dart // Created Date: 23/05/2022 22:09:16 -// Last Modified: 25/05/2022 09:23:54 +// Last Modified: 26/05/2022 18:41:31 // ----- // Copyright (c) 2022 +import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; + /// Defines the AES modes of operation. -enum AESMode { gcm } +enum AESMode { + gcm([AESPadding.none]); + + /// Returns the list of supported [AESPadding] for this [AESMode]. + final List supportedPaddings; + + const AESMode(this.supportedPaddings); +} diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index 76a9399..d20f344 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,144 +3,96 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 26/05/2022 16:22:49 +// Last Modified: 26/05/2022 19:43:57 // ----- // Copyright (c) 2021 import 'dart:typed_data'; +import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; +import 'package:native_crypto/src/interfaces/byte_array.dart'; +import 'package:native_crypto/src/interfaces/cipher.dart'; import 'package:native_crypto/src/utils/cipher_algorithm.dart'; import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -/// Represents a cipher text in Native Crypto. +/// Represents a cipher text in NativeCrypto. /// -/// It is represented as a [List] of [Uint8List] like: +/// [CipherText] is a [ByteArray] that can be used to store encrypted data. +/// It is represented like: /// ```txt -/// [[NONCE], [MESSAGE + TAG]] +/// [IV + MESSAGE + TAG] /// ``` /// where: -/// - `[NONCE]` is a [Uint8List] of length [CipherText.ivLength] -/// - `[MESSAGE + TAG]` is a [Uint8List] of length [CipherText.dataLength] +/// - IV's length is [CipherText.ivLength] bytes. +/// - MESSAGE's length is [CipherText.messageLength] bytes. +/// - TAG's length is [CipherText.tagLength] bytes. /// -/// To -/// -/// So accessing just the Message or just the Tag is costly and should be -/// done only when needed. -class CipherText { +/// Check [CipherTextWrapper] for more information. +class CipherText extends ByteArray { final int _ivLength; final int _messageLength; final int _tagLength; final CipherAlgorithm? _cipherAlgorithm; - final Uint8List? _iv; - final Uint8List? _data; // Contains the message + tag (if any) - - CipherText._( + const CipherText._( this._ivLength, this._messageLength, this._tagLength, this._cipherAlgorithm, - this._iv, - this._data, + super.bytes, ); - /// Gets the [CipherAlgorithm] used to encrypt the [CipherText]. - CipherAlgorithm get cipherAlgorithm { - if (_cipherAlgorithm.isNotNull) { - return _cipherAlgorithm!; - } else { + factory CipherText.fromBytes( + int ivLength, + int messageLength, + int tagLength, + CipherAlgorithm? cipherAlgorithm, + Uint8List bytes, + ) { + if (bytes.length != ivLength + messageLength + tagLength) { throw NativeCryptoException( - message: 'Cipher algorithm is not specified', - code: NativeCryptoExceptionCode.invalid_cipher.code, + message: 'Invalid cipher text length! ' + 'Expected: ${ivLength + messageLength + tagLength} bytes', + code: NativeCryptoExceptionCode.invalid_argument.code, ); } - } - /// Gets the [Uint8List] of the [CipherText]'s IV. - Uint8List get iv { - if (_iv.isNotNull) { - return _iv!; - } else { + if (messageLength > Cipher.bytesCountPerChunk) { throw NativeCryptoException( - message: 'IV is not specified', - code: NativeCryptoExceptionCode.invalid_data.code, + message: 'Cipher text is too big! Consider using chunks.', + code: NativeCryptoExceptionCode.invalid_argument.code, ); } - } - /// Gets the length of the [CipherText]'s IV. - int get ivLength => _ivLength; + return CipherText._( + ivLength, + messageLength, + tagLength, + cipherAlgorithm, + bytes, + ); + } - /// Gets the [Uint8List] of the [CipherText]'s data. - Uint8List get data { - if (_data.isNotNull) { - return _data!; + /// Gets the [CipherAlgorithm] used to encrypt the [CipherText]. + CipherAlgorithm get cipherAlgorithm { + if (_cipherAlgorithm.isNotNull) { + return _cipherAlgorithm!; } else { throw NativeCryptoException( - message: 'Data is not specified', - code: NativeCryptoExceptionCode.invalid_data.code, + message: 'Cipher algorithm is not specified', + code: NativeCryptoExceptionCode.invalid_cipher.code, ); } } - /// Gets the length of the [CipherText]'s data. - int get dataLength => _messageLength + _tagLength; - - // CipherText.fromBytes( - // Uint8List bytes, { - // required int ivLength, - // required int dataLength, - // int tagLength = 0, - // }) : _ivLength = ivLength, - // _dataLength = dataLength, - // _tagLength = tagLength, - // _iv = bytes.sublist(0, ivLength), - // super(bytes.sublist(ivLength, bytes.length - tagLength)); - - // const CipherText.fromIvAndBytes( - // Uint8List iv, - // super.data, { - // required int dataLength, - // int tagLength = 0, - // }) : _ivLength = iv.length, - // _dataLength = dataLength, - // _tagLength = tagLength, - // _iv = iv; - - // CipherText.fromPairIvAndBytes( - // List pair, { - // required int dataLength, - // int tagLength = 0, - // }) : _ivLength = pair.first.length, - // _dataLength = dataLength, - // _tagLength = tagLength, - // _iv = pair.first, - // super(pair.last); - - // /// Gets the CipherText IV. - // Uint8List get iv => _iv; - - // /// Gets the CipherText data. - // Uint8List get data => _tagLength > 0 - // ? bytes.sublist(0, _dataLength - _tagLength) - // : bytes; - - // /// Gets the CipherText tag. - // Uint8List get tag => _tagLength > 0 - // ? bytes.sublist(_dataLength - _tagLength, _dataLength) - // : Uint8List(0); - - // /// Gets the CipherText data and tag. - // Uint8List get payload => bytes; - - // /// Gets the CipherText IV length. - // int get ivLength => _ivLength; + /// Gets the length of the [CipherText]'s IV. + int get ivLength => _ivLength; - // /// Gets the CipherText data length. - // int get dataLength => _dataLength; + /// Gets the length of the [CipherText]'s Message. + int get messageLength => _messageLength; - // /// Gets the CipherText tag length. - // int get tagLength => _tagLength; + /// Gets the length of the [CipherText]'s Tag. + int get tagLength => _tagLength; } diff --git a/packages/native_crypto/lib/src/core/cipher_text_list.dart b/packages/native_crypto/lib/src/core/cipher_text_list.dart deleted file mode 100644 index dab2709..0000000 --- a/packages/native_crypto/lib/src/core/cipher_text_list.dart +++ /dev/null @@ -1,26 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: cipher_text_list.dart -// Created Date: 23/05/2022 22:59:02 -// Last Modified: 24/05/2022 20:18:26 -// ----- -// Copyright (c) 2022 - -import 'dart:typed_data'; - -import 'package:native_crypto/src/core/cipher_text.dart'; - -class CipherTextList extends CipherText { - final List _list; - - CipherTextList() - : _list = [], - super(Uint8List(0), Uint8List(0), Uint8List(0)); - - void add(CipherText cipherText) { - _list.add(cipherText); - } - - List get list => _list; -} diff --git a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart index a65cc23..08dc915 100644 --- a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart +++ b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text_wrapper.dart // Created Date: 26/05/2022 14:27:32 -// Last Modified: 26/05/2022 15:53:46 +// Last Modified: 26/05/2022 20:32:38 // ----- // Copyright (c) 2022 @@ -12,31 +12,85 @@ import 'dart:typed_data'; import 'package:native_crypto/native_crypto.dart'; import 'package:native_crypto/src/utils/extensions.dart'; +/// Wrapper for [CipherText] +/// +/// Typically, this object is the result of an encryption operation. +/// For decryption you have to build this before using it. class CipherTextWrapper { final CipherText? _single; final List? _list; CipherTextWrapper._(this._single, this._list); + /// Creates a [CipherTextWrapper] from a [CipherText]. factory CipherTextWrapper.single(CipherText cipherText) => CipherTextWrapper._(cipherText, null); + /// Creates a [CipherTextWrapper] from a [List] of [CipherText]. factory CipherTextWrapper.list(List cipherTexts) => CipherTextWrapper._(null, cipherTexts); + /// Creates an empty [List] in a [CipherTextWrapper]. + /// + /// This is useful when you want to create a [CipherTextWrapper] then + /// fill it with data. + factory CipherTextWrapper.empty() => CipherTextWrapper._(null, []); + + /// Creates a [CipherTextWrapper] from a [Uint8List]. + /// + /// This is a convenience method to create a [CipherTextWrapper] + /// from a [Uint8List]. It tries to detect if the [Uint8List] is a + /// single [CipherText] or a list of [CipherText]. + /// + /// You can customize the chunk size by passing a [chunkSize] parameter. + /// The default chunk size is [Cipher.bytesCountPerChunk]. + /// + /// Throw an [NativeCryptoExceptionCode] with + /// [NativeCryptoExceptionCode.invalid_argument] if the [Uint8List] is + /// not a valid [CipherText] or a [List] of [CipherText]. factory CipherTextWrapper.fromBytes( - // Uint8List bytes, { - // required int ivLength, - // required int dataLength, - // int tagLength = 0, - // int? chunkSize, - // } - ) { - // TODO(hpcl): implement fromBytes - throw UnimplementedError(); + Uint8List bytes, + int ivLength, + int messageLength, + int tagLength, { + CipherAlgorithm? cipherAlgorithm, + int? chunkSize, + }) { + chunkSize ??= Cipher.bytesCountPerChunk; + Cipher.bytesCountPerChunk = chunkSize; + + if (bytes.length <= chunkSize) { + return CipherTextWrapper.single( + CipherText.fromBytes( + ivLength, + messageLength, + tagLength, + cipherAlgorithm, + bytes, + ), + ); + } else { + final cipherTexts = []; + for (var i = 0; i < bytes.length; i += chunkSize) { + final chunk = bytes.sublist(i, i + chunkSize); + cipherTexts.add( + CipherText.fromBytes( + ivLength, + messageLength, + tagLength, + cipherAlgorithm, + chunk, + ), + ); + } + return CipherTextWrapper.list(cipherTexts); + } } + /// Checks if the [CipherText] is a single [CipherText]. bool get isSingle => _single.isNotNull; + + /// Checks if the [CipherText] is a [List] of [CipherText]. bool get isList => _list.isNotNull; /// Gets the [CipherText] if it's a single one. @@ -70,7 +124,7 @@ class CipherTextWrapper { } /// Gets the raw [Uint8List] of the [CipherText] or [List] of [CipherText]. - Uint8List get raw { + Uint8List get bytes { if (isSingle) { return single.bytes; } else { @@ -78,6 +132,9 @@ class CipherTextWrapper { } } + /// Gets the number of parts of the [CipherText] or [List] of [CipherText]. + /// + /// Check [Cipher.bytesCountPerChunk] for more information. int get chunkCount { _single.isNull; if (_single.isNotNull) { @@ -86,4 +143,41 @@ class CipherTextWrapper { return _list?.length ?? 0; } } + + /// Gets the [CipherText] or the [List] of [CipherText]. + /// + /// Throws [NativeCryptoException] with + /// [NativeCryptoExceptionCode.invalid_data] if it's not a single or a list or + /// if [T] is not [CipherText] or [List] of [CipherText]. + T unwrap() { + if (isSingle && T == CipherText) { + return single as T; + } else if (isList && T == List) { + return list as T; + } else { + final String type = + isSingle ? 'CipherText' : (isList ? 'List' : 'unknown'); + throw NativeCryptoException( + message: 'CipherTextWrapper is not a $T but a $type, ' + 'you should use unwrap<$type>()', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } + + void add(CipherText cipherText) { + if (isSingle) { + throw NativeCryptoException( + message: 'CipherTextWrapper is already single', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } else if (isList) { + _list!.add(cipherText); + } else { + throw NativeCryptoException( + message: 'CipherTextWrapper is not single or list', + code: NativeCryptoExceptionCode.invalid_data.code, + ); + } + } } diff --git a/packages/native_crypto/lib/src/core/core.dart b/packages/native_crypto/lib/src/core/core.dart index 5ab875c..32ad783 100644 --- a/packages/native_crypto/lib/src/core/core.dart +++ b/packages/native_crypto/lib/src/core/core.dart @@ -3,9 +3,9 @@ // ----- // File: core.dart // Created Date: 23/05/2022 23:05:26 -// Last Modified: 25/05/2022 10:44:32 +// Last Modified: 26/05/2022 17:10:25 // ----- // Copyright (c) 2022 export 'cipher_text.dart'; -export 'cipher_text_list.dart'; +export 'cipher_text_wrapper.dart'; diff --git a/packages/native_crypto/lib/src/interfaces/base_key.dart b/packages/native_crypto/lib/src/interfaces/base_key.dart new file mode 100644 index 0000000..e8ac27e --- /dev/null +++ b/packages/native_crypto/lib/src/interfaces/base_key.dart @@ -0,0 +1,22 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: base_key.dart +// Created Date: 16/12/2021 16:28:00 +// Last Modified: 26/05/2022 17:40:38 +// ----- +// Copyright (c) 2021 + +import 'package:native_crypto/src/interfaces/byte_array.dart'; + +/// Represents a key in NativeCrypto. +/// +/// [BaseKey] is a [ByteArray] that can be used to store keys. +/// +/// This interface is implemented by all the key classes. +abstract class BaseKey extends ByteArray { + const BaseKey(super.bytes); + BaseKey.fromBase16(super.encoded) : super.fromBase16(); + BaseKey.fromBase64(super.encoded) : super.fromBase64(); + BaseKey.fromUtf8(super.input) : super.fromUtf8(); +} diff --git a/packages/native_crypto/lib/src/interfaces/byte_array.dart b/packages/native_crypto/lib/src/interfaces/byte_array.dart index d99d115..7cdef3a 100644 --- a/packages/native_crypto/lib/src/interfaces/byte_array.dart +++ b/packages/native_crypto/lib/src/interfaces/byte_array.dart @@ -3,7 +3,7 @@ // ----- // File: byte_array.dart // Created Date: 16/12/2021 17:54:16 -// Last Modified: 26/05/2022 14:25:05 +// Last Modified: 26/05/2022 17:13:27 // ----- // Copyright (c) 2021 @@ -13,6 +13,9 @@ import 'package:flutter/foundation.dart'; import 'package:native_crypto/src/utils/encoding.dart'; import 'package:native_crypto/src/utils/extensions.dart'; +/// Represents a byte array. +/// +/// [ByteArray] wraps a [Uint8List] and provides some useful conversion methods. @immutable abstract class ByteArray { final Uint8List _bytes; @@ -56,6 +59,12 @@ abstract class ByteArray { /// Gets the [ByteArray] bytes as an UTF-16 representation. String get utf16 => _bytes.toStr(); + /// Gets the [ByteArray] length in bytes. + int get length => _bytes.length; + + /// Gets the [ByteArray] length in bits. + int get bitLength => _bytes.length * 8; + @override bool operator ==(Object other) { if (other is ByteArray) { diff --git a/packages/native_crypto/lib/src/interfaces/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart index 8941ee8..c95f34b 100644 --- a/packages/native_crypto/lib/src/interfaces/cipher.dart +++ b/packages/native_crypto/lib/src/interfaces/cipher.dart @@ -3,44 +3,48 @@ // ----- // File: cipher.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 24/05/2022 19:55:38 +// Last Modified: 26/05/2022 17:38:26 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/core/cipher_text.dart'; +import 'package:native_crypto/src/core/cipher_text_wrapper.dart'; import 'package:native_crypto/src/utils/cipher_algorithm.dart'; -/// Represents a cipher. +/// Represents a cipher in NativeCrypto. /// -/// In cryptography, a cipher is an algorithm for performing encryption +/// In cryptography, a [Cipher] is an algorithm for performing encryption /// or decryption - a series of well-defined steps that can /// be followed as a procedure. +/// +/// This interface is implemented by all the ciphers in NativeCrypto. abstract class Cipher { - /// Returns the size of a chunk of data - /// that can be processed by the cipher. static int _bytesCountPerChunk = 33554432; + /// Returns the size of a chunk of data + /// that can be processed by the [Cipher]. static int get bytesCountPerChunk => Cipher._bytesCountPerChunk; + /// Sets the size of a chunk of data + /// that can be processed by the [Cipher]. static set bytesCountPerChunk(int bytesCount) { _bytesCountPerChunk = bytesCount; } - /// Returns the standard algorithm name for this cipher + /// Returns the standard algorithm for this [Cipher]. CipherAlgorithm get algorithm; - /// Encrypts data. + /// Encrypts the [data]. /// /// Takes [Uint8List] data as parameter. - /// Returns a [CipherText]. - Future encrypt(Uint8List data); + /// Returns a [CipherTextWrapper]. + Future encrypt(Uint8List data); - /// Decrypts cipher text. + /// Decrypts the [cipherText] /// - /// Takes [CipherText] as parameter. + /// Takes [CipherTextWrapper] as parameter. /// And returns plain text data as [Uint8List]. - Future decrypt(CipherText cipherText); + Future decrypt(CipherTextWrapper cipherText); } diff --git a/packages/native_crypto/lib/src/interfaces/interfaces.dart b/packages/native_crypto/lib/src/interfaces/interfaces.dart index 3ff3855..ef47be3 100644 --- a/packages/native_crypto/lib/src/interfaces/interfaces.dart +++ b/packages/native_crypto/lib/src/interfaces/interfaces.dart @@ -3,12 +3,12 @@ // ----- // File: interfaces.dart // Created Date: 23/05/2022 23:03:47 -// Last Modified: 23/05/2022 23:10:15 +// Last Modified: 26/05/2022 17:41:06 // ----- // Copyright (c) 2022 +export 'base_key.dart'; export 'builder.dart'; export 'byte_array.dart'; export 'cipher.dart'; -// export 'key.dart'; export 'keyderivation.dart'; diff --git a/packages/native_crypto/lib/src/interfaces/key.dart b/packages/native_crypto/lib/src/interfaces/key.dart deleted file mode 100644 index 3c99f0c..0000000 --- a/packages/native_crypto/lib/src/interfaces/key.dart +++ /dev/null @@ -1,18 +0,0 @@ -// Author: Hugo Pointcheval -// Email: git@pcl.ovh -// ----- -// File: key.dart -// Created Date: 16/12/2021 16:28:00 -// Last Modified: 23/05/2022 23:02:10 -// ----- -// Copyright (c) 2021 - -import 'package:native_crypto/src/interfaces/byte_array.dart'; - -/// A class representing a key. -abstract class Key extends ByteArray { - const Key(super.bytes); - Key.fromBase16(super.encoded) : super.fromBase16(); - Key.fromBase64(super.encoded) : super.fromBase64(); - Key.fromUtf8(super.input) : super.fromUtf8(); -} diff --git a/packages/native_crypto/lib/src/interfaces/keyderivation.dart b/packages/native_crypto/lib/src/interfaces/keyderivation.dart index cddb1bc..ebc3841 100644 --- a/packages/native_crypto/lib/src/interfaces/keyderivation.dart +++ b/packages/native_crypto/lib/src/interfaces/keyderivation.dart @@ -3,18 +3,21 @@ // ----- // File: kdf.dart // Created Date: 18/12/2021 11:56:43 -// Last Modified: 23/05/2022 22:37:04 +// Last Modified: 26/05/2022 18:47:15 // ----- // Copyright (c) 2021 import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/utils/kdf_algorithm.dart'; -/// Represents a Key Derivation Function +/// Represents a Key Derivation Function (KDF) in NativeCrypto. +/// +/// [KeyDerivation] function is a function that takes some +/// parameters and returns a [SecretKey]. abstract class KeyDerivation { - /// Returns the standard algorithm name for this key derivation function + /// Returns the standard algorithm for this key derivation function KdfAlgorithm get algorithm; - /// Derive key + /// Derive a [SecretKey]. Future derive(); } diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index 7d8acc2..ae7d3b4 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -3,7 +3,7 @@ // ----- // File: pbkdf2.dart // Created Date: 17/12/2021 14:50:42 -// Last Modified: 25/05/2022 10:45:00 +// Last Modified: 26/05/2022 18:51:59 // ----- // Copyright (c) 2021 @@ -12,10 +12,15 @@ import 'dart:typed_data'; import 'package:native_crypto/src/interfaces/keyderivation.dart'; import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto/src/utils/hash_algorithm.dart'; import 'package:native_crypto/src/utils/kdf_algorithm.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; +/// Represent a PBKDF2 Key Derivation Function (KDF) in NativeCrypto. +/// +/// [Pbkdf2] is a function that takes password, salt, iteration count and +/// derive a [SecretKey] of specified length. class Pbkdf2 extends KeyDerivation { final int _keyBytesCount; final int _iterations; @@ -35,20 +40,42 @@ class Pbkdf2 extends KeyDerivation { @override Future derive({String? password, String? salt}) async { if (password == null || salt == null) { - throw const KeyDerivationException( - message: "Password or Salt can't be null!", - code: 'invalid_password_or_salt', + throw NativeCryptoException( + message: 'Password and salt cannot be null. ' + 'Here is the password: $password, here is the salt: $salt', + code: NativeCryptoExceptionCode.invalid_argument.code, ); } - final Uint8List derivation = (await platform.pbkdf2( - password, - salt, - _keyBytesCount, - _iterations, - _hash.name, - )) ?? - Uint8List(0); + final Uint8List? derivation = await platform.pbkdf2( + password, + salt, + _keyBytesCount, + _iterations, + _hash.name, + ); + + if (derivation.isNull) { + throw NativeCryptoException( + message: 'Failed to derive a key! Platform returned null.', + code: NativeCryptoExceptionCode.platform_returned_null.code, + ); + } + + if (derivation!.isEmpty) { + throw NativeCryptoException( + message: 'Failed to derive a key! Platform returned no data.', + code: NativeCryptoExceptionCode.platform_returned_empty_data.code, + ); + } + + if (derivation.length != _keyBytesCount) { + throw NativeCryptoException( + message: 'Failed to derive a key! Platform returned ' + '${derivation.length} bytes, but expected $_keyBytesCount bytes.', + code: NativeCryptoExceptionCode.platform_returned_invalid_data.code, + ); + } return SecretKey(derivation); } diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index 32f539b..f660181 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,46 +3,54 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 26/05/2022 11:56:06 +// Last Modified: 26/05/2022 19:26:35 // ----- // Copyright (c) 2021 import 'dart:typed_data'; -import 'package:native_crypto/src/interfaces/key.dart'; +import 'package:native_crypto/src/interfaces/base_key.dart'; +import 'package:native_crypto/src/interfaces/cipher.dart'; import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; -/// A class representing a secret key. -/// A secret key is a key that is not accessible by anyone else. -/// It is used to encrypt and decrypt data. -class SecretKey extends Key { +/// Represents a secret key in NativeCrypto. +/// +/// [SecretKey] is a [BaseKey] that can be used to store secret keys. +/// A [SecretKey] is a key that can be used to encrypt or decrypt data with +/// a symmetric [Cipher]. +class SecretKey extends BaseKey { const SecretKey(super.bytes); SecretKey.fromBase16(super.encoded) : super.fromBase16(); SecretKey.fromBase64(super.encoded) : super.fromBase64(); SecretKey.fromUtf8(super.input) : super.fromUtf8(); static Future fromSecureRandom(int bitsCount) async { + Uint8List? key; try { - final Uint8List? _key = await platform.generateSecretKey(bitsCount); - - if (_key == null || _key.isEmpty) { - throw const KeyException( - message: 'Could not generate secret key, platform returned null', - code: 'platform_returned_null', - ); - } - - return SecretKey(_key); + key = await platform.generateSecretKey(bitsCount); } catch (e, s) { - if (e is KeyException) { - rethrow; - } - throw KeyException( + throw NativeCryptoException( message: '$e', - code: 'failed_to_generate_secret_key', + code: NativeCryptoExceptionCode.platform_throws.code, stackTrace: s, ); } + if (key.isNull) { + throw NativeCryptoException( + message: 'Failed to generate a secret key! Platform returned null.', + code: NativeCryptoExceptionCode.platform_returned_null.code, + ); + } + + if (key!.isEmpty) { + throw NativeCryptoException( + message: 'Failed to generate a secret key! ' + 'Platform returned no data.', + code: NativeCryptoExceptionCode.platform_returned_empty_data.code, + ); + } + return SecretKey(key); } } diff --git a/packages/native_crypto/lib/src/utils/cipher_algorithm.dart b/packages/native_crypto/lib/src/utils/cipher_algorithm.dart index 5498bbc..2ba968c 100644 --- a/packages/native_crypto/lib/src/utils/cipher_algorithm.dart +++ b/packages/native_crypto/lib/src/utils/cipher_algorithm.dart @@ -3,9 +3,9 @@ // ----- // File: cipher_algorithm.dart // Created Date: 23/05/2022 22:07:54 -// Last Modified: 23/05/2022 22:33:56 +// Last Modified: 26/05/2022 18:52:32 // ----- // Copyright (c) 2022 /// Represents different cipher algorithms -enum CipherAlgorithm { aes, rsa } +enum CipherAlgorithm { aes } diff --git a/packages/native_crypto/lib/src/utils/extensions.dart b/packages/native_crypto/lib/src/utils/extensions.dart index 91b9949..b6399e0 100644 --- a/packages/native_crypto/lib/src/utils/extensions.dart +++ b/packages/native_crypto/lib/src/utils/extensions.dart @@ -3,7 +3,7 @@ // ----- // File: extensions.dart // Created Date: 26/05/2022 12:12:48 -// Last Modified: 26/05/2022 15:49:38 +// Last Modified: 26/05/2022 18:52:48 // ----- // Copyright (c) 2022 @@ -30,8 +30,8 @@ extension ListIntX on List { } extension ListUint8ListX on List { + /// Reduce a [List] of [Uint8List] to a [Uint8List]. - Uint8List sum() { for (var i = 1; i < length; i++) { first.addAll(this[i]); diff --git a/packages/native_crypto/lib/src/utils/hash_algorithm.dart b/packages/native_crypto/lib/src/utils/hash_algorithm.dart index 4e914cd..d9c6ced 100644 --- a/packages/native_crypto/lib/src/utils/hash_algorithm.dart +++ b/packages/native_crypto/lib/src/utils/hash_algorithm.dart @@ -3,7 +3,7 @@ // ----- // File: hash_algorithm.dart // Created Date: 23/05/2022 22:01:59 -// Last Modified: 23/05/2022 22:47:08 +// Last Modified: 26/05/2022 18:53:38 // ----- // Copyright (c) 2022 @@ -11,12 +11,13 @@ import 'dart:typed_data'; import 'package:native_crypto/src/platform.dart'; +/// Defines the hash algorithms. enum HashAlgorithm { sha256, sha384, sha512; - /// Hashes a message + /// Digest the [data] using this [HashAlgorithm]. Future digest(Uint8List data) async { final Uint8List hash = (await platform.digest(data, name)) ?? Uint8List(0); diff --git a/packages/native_crypto/lib/src/utils/kdf_algorithm.dart b/packages/native_crypto/lib/src/utils/kdf_algorithm.dart index 58ff68a..68d6a76 100644 --- a/packages/native_crypto/lib/src/utils/kdf_algorithm.dart +++ b/packages/native_crypto/lib/src/utils/kdf_algorithm.dart @@ -3,8 +3,9 @@ // ----- // File: kdf_algorithm.dart // Created Date: 23/05/2022 22:36:24 -// Last Modified: 23/05/2022 22:36:36 +// Last Modified: 26/05/2022 18:53:50 // ----- // Copyright (c) 2022 +/// Represents different key derivation functions enum KdfAlgorithm { pbkdf2 } diff --git a/packages/native_crypto/test/src/secret_key_test.dart b/packages/native_crypto/test/src/secret_key_test.dart index 1a98e24..e8deb2e 100644 --- a/packages/native_crypto/test/src/secret_key_test.dart +++ b/packages/native_crypto/test/src/secret_key_test.dart @@ -3,7 +3,7 @@ // ----- // File: secret_key_test.dart // Created Date: 26/05/2022 10:52:41 -// Last Modified: 26/05/2022 12:07:33 +// Last Modified: 26/05/2022 19:24:44 // ----- // Copyright (c) 2022 @@ -42,10 +42,10 @@ void main() { await expectLater( () => SecretKey.fromSecureRandom(5), throwsA( - isA().having( + isA().having( (e) => e.code, 'code', - 'platform_returned_null', + 'platform_returned_empty_data', ), ), ); @@ -59,7 +59,7 @@ void main() { await expectLater( () => SecretKey.fromSecureRandom(5), throwsA( - isA().having( + isA().having( (e) => e.code, 'code', 'platform_returned_null', @@ -81,7 +81,7 @@ void main() { await expectLater( () => SecretKey.fromSecureRandom(5), throwsA( - isA() + isA() .having( (e) => e.message, 'message', @@ -90,7 +90,7 @@ void main() { .having( (e) => e.code, 'code', - 'failed_to_generate_secret_key', + 'platform_throws', ), ), ); From c5d42feef4ca72753c4653f483db2fdfc6847eb7 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 20:43:43 +0200 Subject: [PATCH 31/39] feat(api): add exception code for platform throw --- .../lib/src/utils/exception.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart index 5b571c2..b46bb4a 100644 --- a/packages/native_crypto_platform_interface/lib/src/utils/exception.dart +++ b/packages/native_crypto_platform_interface/lib/src/utils/exception.dart @@ -3,7 +3,7 @@ // ----- // File: exception.dart // Created Date: 24/05/2022 18:54:48 -// Last Modified: 26/05/2022 15:36:56 +// Last Modified: 26/05/2022 20:36:04 // ----- // Copyright (c) 2022 @@ -25,6 +25,7 @@ enum NativeCryptoExceptionCode { invalid_cipher, invalid_data, platform_not_supported, + platform_throws, platform_returned_invalid_data, platform_returned_empty_data, platform_returned_null; @@ -51,7 +52,7 @@ class NativeCryptoException implements Exception { @override String toString() { - String output = '[NativeException/$code] $message'; + String output = '[NativeCryptoException/$code] $message'; if (stackTrace != null) { output += '\n\n${stackTrace.toString()}'; From 96f9aad1b314bb749c1cf1997f41b09c949fe797 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 23:22:14 +0200 Subject: [PATCH 32/39] test: (WIP) add some tests --- .../test/src/cipher_text_test.dart | 192 ++++++++++ .../test/src/cipher_text_wrapper_test.dart | 327 ++++++++++++++++++ .../test/src/hash_algorithm_test.dart | 128 +++++++ .../native_crypto/test/src/pbkdf2_test.dart | 280 +++++++++++++++ .../test/src/secret_key_test.dart | 54 +-- 5 files changed, 954 insertions(+), 27 deletions(-) create mode 100644 packages/native_crypto/test/src/cipher_text_test.dart create mode 100644 packages/native_crypto/test/src/cipher_text_wrapper_test.dart create mode 100644 packages/native_crypto/test/src/hash_algorithm_test.dart create mode 100644 packages/native_crypto/test/src/pbkdf2_test.dart diff --git a/packages/native_crypto/test/src/cipher_text_test.dart b/packages/native_crypto/test/src/cipher_text_test.dart new file mode 100644 index 0000000..b466b55 --- /dev/null +++ b/packages/native_crypto/test/src/cipher_text_test.dart @@ -0,0 +1,192 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_text_test.dart +// Created Date: 26/05/2022 20:45:38 +// Last Modified: 26/05/2022 21:29:51 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; + +void main() { + setUp(() { + Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; + }); + + group('fromBytes', () { + test('throws if length is not the one expected', () { + final Uint8List bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + expect( + () => CipherText.fromBytes( + bytes, + ivLength: 1, + messageLength: 1, + tagLength: 1, + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('Invalid cipher text length'), + ), + ), + ); + }); + + test('throws if length is bigger than expected', () { + final Uint8List bytes = Uint8List.fromList([1, 3, 3, 3, 1]); + Cipher.bytesCountPerChunk = 2; + expect( + () => CipherText.fromBytes( + bytes, + ivLength: 1, + messageLength: 3, + tagLength: 1, + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('Cipher text is too big'), + ), + ), + ); + }); + + test('throws if data is empty', () { + final Uint8List bytes = Uint8List(0); + expect( + () => CipherText.fromBytes( + bytes, + ivLength: 1, + messageLength: 3, + tagLength: 1, + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('Passed data is empty'), + ), + ), + ); + }); + + test('throws if one of the length is negative', () { + final Uint8List bytes = Uint8List(0); + expect( + () => CipherText.fromBytes( + bytes, + ivLength: -1, + messageLength: 1, + tagLength: 1, + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('Invalid length'), + ), + ), + ); + }); + }); + + group('get.cipherAlgorithm', () { + test('throws if not set', () { + final CipherText cipherText = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect( + () => cipherText.cipherAlgorithm, + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_cipher', + ) + .having( + (e) => e.message, + 'message', + contains('Cipher algorithm is not specified'), + ), + ), + ); + }); + + test('returns the expected value', () { + final CipherText cipherText = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + cipherAlgorithm: CipherAlgorithm.aes, + ); + expect(cipherText.cipherAlgorithm, CipherAlgorithm.aes); + }); + }); + + group('Lengths', () { + test('get.ivLength returns the expected value', () { + final CipherText cipherText = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect(cipherText.ivLength, 1); + }); + + test('get.messageLength returns the expected value', () { + final CipherText cipherText = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect(cipherText.messageLength, 1); + }); + + test('get.tagLength returns the expected value', () { + final CipherText cipherText = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect(cipherText.tagLength, 1); + }); + }); +} diff --git a/packages/native_crypto/test/src/cipher_text_wrapper_test.dart b/packages/native_crypto/test/src/cipher_text_wrapper_test.dart new file mode 100644 index 0000000..7477900 --- /dev/null +++ b/packages/native_crypto/test/src/cipher_text_wrapper_test.dart @@ -0,0 +1,327 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: cipher_text_wrapper_test.dart +// Created Date: 26/05/2022 21:35:41 +// Last Modified: 26/05/2022 22:27:31 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; + +void main() { + late CipherText single; + late List list; + + setUp(() { + Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; + single = CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + list = [ + CipherText.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ), + CipherText.fromBytes( + Uint8List.fromList([4, 5, 6]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ), + ]; + }); + + group('single', () { + test('makes isSingle true', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.isSingle, isTrue); + }); + + test('makes isList false', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.isList, isFalse); + }); + + test('makes CipherText the single value', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.single, single); + }); + + test('throws when trying to get list', () { + final wrapper = CipherTextWrapper.single(single); + expect( + () => wrapper.list, + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('is not list'), + ), + ), + ); + }); + + test('makes wrapper returns bytes of CipherText', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.bytes, single.bytes); + }); + + test('makes chunkCount = 1', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.chunkCount, 1); + }); + + test('makes unwrap() returns only CipherText', () { + final wrapper = CipherTextWrapper.single(single); + expect(wrapper.unwrap(), single); + }); + + test('makes unwrap() throws when trying to unwrap List', () { + final wrapper = CipherTextWrapper.single(single); + expect( + () => wrapper.unwrap>(), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('you should use unwrap'), + ), + ), + ); + }); + + test('makes adding is not supported', () { + final wrapper = CipherTextWrapper.single(single); + expect( + () => wrapper.add(single), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('is already single'), + ), + ), + ); + }); + }); + + group('list', () { + test('makes isList true', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.isList, isTrue); + }); + + test('makes isSingle false', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.isSingle, isFalse); + }); + + test('makes List the list value', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.list, list); + }); + + test('throws when trying to get single', () { + final wrapper = CipherTextWrapper.list(list); + expect( + () => wrapper.single, + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('is not single'), + ), + ), + ); + }); + + test('makes wrapper returns bytes of all CipherText joined', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.bytes, Uint8List.fromList([1, 2, 3, 4, 5, 6])); + }); + + test('makes chunkCount = 2', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.chunkCount, 2); + }); + + test('makes unwrap() returns List', () { + final wrapper = CipherTextWrapper.list(list); + expect(wrapper.unwrap>(), list); + }); + + test('makes unwrap() throws when trying to unwrap single', () { + final wrapper = CipherTextWrapper.list(list); + expect( + () => wrapper.unwrap(), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('you should use unwrap'), + ), + ), + ); + }); + + test('makes adding is supported', () { + final originalList = List.from(list); + final wrapper = CipherTextWrapper.list(list)..add(single); + printOnFailure(list.length.toString()); + expect(wrapper.list, [...originalList, single]); + }); + }); + + group('empty', () { + test('makes isList true', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.isList, isTrue); + }); + + test('makes isSingle false', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.isSingle, isFalse); + }); + + test('makes List the list value', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.list, []); + }); + + test('throws when trying to get single', () { + final wrapper = CipherTextWrapper.empty(); + expect( + () => wrapper.single, + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('is not single'), + ), + ), + ); + }); + + test('makes wrapper returns empty bytes', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.bytes, Uint8List.fromList([])); + }); + + test('makes chunkCount = 0', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.chunkCount, 0); + }); + + test('makes unwrap() returns empty List', () { + final wrapper = CipherTextWrapper.empty(); + expect(wrapper.unwrap>(), []); + }); + + test('makes unwrap() throws when trying to unwrap single', () { + final wrapper = CipherTextWrapper.empty(); + expect( + () => wrapper.unwrap(), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_data', + ) + .having( + (e) => e.message, + 'message', + contains('you should use unwrap'), + ), + ), + ); + }); + + test('makes adding is supported', () { + final wrapper = CipherTextWrapper.empty()..add(single); + expect(wrapper.list, [single]); + }); + }); + + group('fromBytes', () { + test('creates single from bytes when no too big', () { + final wrapper = CipherTextWrapper.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect(wrapper.isSingle, isTrue); + expect(wrapper.single, single); + }); + + test('creates list from bytes when too big', () { + Cipher.bytesCountPerChunk = 3; + final wrapper = CipherTextWrapper.fromBytes( + Uint8List.fromList([1, 2, 3, 4, 5, 6]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + ); + expect(wrapper.isList, isTrue); + expect(wrapper.list, list); + }); + + test('modifies Cipher.bytesCountPerChunk', () { + expect(Cipher.bytesCountPerChunk, Cipher.defaultBytesCountPerChunk); + CipherTextWrapper.fromBytes( + Uint8List.fromList([1, 2, 3]), + ivLength: 1, + messageLength: 1, + tagLength: 1, + chunkSize: 3, + ); + expect(Cipher.bytesCountPerChunk, 3); + }); + }); +} diff --git a/packages/native_crypto/test/src/hash_algorithm_test.dart b/packages/native_crypto/test/src/hash_algorithm_test.dart new file mode 100644 index 0000000..b7911b1 --- /dev/null +++ b/packages/native_crypto/test/src/hash_algorithm_test.dart @@ -0,0 +1,128 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: hash_algorithm_test.dart +// Created Date: 26/05/2022 22:28:53 +// Last Modified: 26/05/2022 23:03:03 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/src/utils/hash_algorithm.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_platform.dart'; + +void main() { + final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); + NativeCryptoPlatform.instance = mock; + + group('name', () { + test('is sha256 for HashAlgorithm.sha256', () { + expect(HashAlgorithm.sha256.name, 'sha256'); + }); + test('is sha384 for HashAlgorithm.sha384', () { + expect(HashAlgorithm.sha384.name, 'sha384'); + }); + test('is sha512 for HashAlgorithm.sha512', () { + expect(HashAlgorithm.sha512.name, 'sha512'); + }); + }); + + group('digest', () { + test('handles returning empty list', () async { + mock + ..setDigestExpectations( + data: Uint8List.fromList([1, 2, 3]), + algorithm: 'sha256', + ) + ..setResponse(() => Uint8List(0)); + + await expectLater( + () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_empty_data', + ), + ), + ); + }); + + test('handles returning null', () async { + mock + ..setDigestExpectations( + data: Uint8List.fromList([1, 2, 3]), + algorithm: 'sha256', + ) + ..setResponse(() => null); + + await expectLater( + () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles throwing PlatformException', () async { + mock + ..setDigestExpectations( + data: Uint8List.fromList([1, 2, 3]), + algorithm: 'sha256', + ) + ..setResponse( + () => throw PlatformException( + code: 'native_crypto', + message: 'dummy error', + ), + ); + + await expectLater( + () => HashAlgorithm.sha256.digest(Uint8List.fromList([1, 2, 3])), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'PlatformException(native_crypto, dummy error, null, null)', + ) + .having( + (e) => e.code, + 'code', + 'platform_throws', + ), + ), + ); + }); + + test('returns data on success', () async { + final hash = Uint8List.fromList([4, 5, 6]); + mock + ..setDigestExpectations( + data: Uint8List.fromList([1, 2, 3]), + algorithm: 'sha256', + ) + ..setResponse(() => hash); + + final result = await HashAlgorithm.sha256.digest( + Uint8List.fromList( + [1, 2, 3], + ), + ); + + expect( + result, + hash, + ); + }); + }); +} diff --git a/packages/native_crypto/test/src/pbkdf2_test.dart b/packages/native_crypto/test/src/pbkdf2_test.dart new file mode 100644 index 0000000..f0ae084 --- /dev/null +++ b/packages/native_crypto/test/src/pbkdf2_test.dart @@ -0,0 +1,280 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: pbkdf2_test.dart +// Created Date: 26/05/2022 22:37:27 +// Last Modified: 26/05/2022 23:20:11 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_platform.dart'; + +void main() { + final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); + NativeCryptoPlatform.instance = mock; + + group('Constructor', () { + test('throws if keyBytesCount is negative', () { + expect( + () => Pbkdf2(keyBytesCount: -1, iterations: 10000), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('must be positive'), + ), + ), + ); + }); + + test('throws if iterations is negative or 0', () { + expect( + () => Pbkdf2(keyBytesCount: 32, iterations: -1), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('must be strictly positive'), + ), + ), + ); + }); + }); + + group('derive', () { + test('throws if password is null', () async { + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + await expectLater( + () => pbkdf2.derive( + salt: 'salt', + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('cannot be null'), + ), + ), + ); + }); + + test('throws if salt is null', () async { + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + await expectLater( + () => pbkdf2.derive( + password: 'password', + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('cannot be null'), + ), + ), + ); + }); + + test('handles returning empty list', () async { + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 32, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse(() => Uint8List(0)); + + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + + await expectLater( + () => pbkdf2.derive( + password: 'password', + salt: 'salt', + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_empty_data', + ), + ), + ); + }); + + test('handles returning null', () async { + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 32, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse(() => null); + + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + + await expectLater( + () => pbkdf2.derive( + password: 'password', + salt: 'salt', + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles returning data with wrong length', () async { + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 32, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse(() => Uint8List(33)); + + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + + await expectLater( + () => pbkdf2.derive( + password: 'password', + salt: 'salt', + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_invalid_data', + ), + ), + ); + }); + + test('handles throwing PlatformException', () async { + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 32, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse( + () => throw PlatformException( + code: 'native_crypto', + message: 'dummy error', + ), + ); + + final pbkdf2 = Pbkdf2(keyBytesCount: 32, iterations: 10000); + + await expectLater( + () => pbkdf2.derive( + password: 'password', + salt: 'salt', + ), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'PlatformException(native_crypto, dummy error, null, null)', + ) + .having( + (e) => e.code, + 'code', + 'platform_throws', + ), + ), + ); + }); + + test('returns SecretKey on success', () async { + final data = Uint8List.fromList([1, 2, 3, 4, 5, 6]); + final sk = SecretKey(data); + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 6, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse(() => data); + + final pbkdf = Pbkdf2(keyBytesCount: 6, iterations: 10000); + final result = await pbkdf.derive( + password: 'password', + salt: 'salt', + ); + + expect( + result, + sk, + ); + }); + + test('return empty SecretKey when keyBytesCount is set to 0', () async { + final sk = SecretKey(Uint8List(0)); + mock + ..setPbkdf2Expectations( + password: 'password', + salt: 'salt', + keyBytesCount: 0, + iterations: 10000, + algorithm: 'sha256', + ) + ..setResponse(() => Uint8List(0)); + + final pbkdf = Pbkdf2(keyBytesCount: 0, iterations: 10000); + final result = await pbkdf.derive( + password: 'password', + salt: 'salt', + ); + + expect( + result, + sk, + ); + }); + }); +} diff --git a/packages/native_crypto/test/src/secret_key_test.dart b/packages/native_crypto/test/src/secret_key_test.dart index e8deb2e..df2f159 100644 --- a/packages/native_crypto/test/src/secret_key_test.dart +++ b/packages/native_crypto/test/src/secret_key_test.dart @@ -3,7 +3,7 @@ // ----- // File: secret_key_test.dart // Created Date: 26/05/2022 10:52:41 -// Last Modified: 26/05/2022 19:24:44 +// Last Modified: 26/05/2022 22:38:07 // ----- // Copyright (c) 2022 @@ -20,6 +20,32 @@ void main() { final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); NativeCryptoPlatform.instance = mock; + group('Constructors', () { + test('handles Uint8List', () { + final SecretKey key = SecretKey(Uint8List.fromList([1, 2, 3, 4, 5])); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles base16', () { + final SecretKey key = SecretKey.fromBase16('0102030405'); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles base64', () { + final SecretKey key = SecretKey.fromBase64('AQIDBAU='); + + expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); + }); + + test('handles utf8', () { + final SecretKey key = SecretKey.fromUtf8('ABCDE'); + + expect(key.bytes, Uint8List.fromList([65, 66, 67, 68, 69])); + }); + }); + group('fromSecureRandom', () { test('handles returning random bytes', () async { mock @@ -96,30 +122,4 @@ void main() { ); }); }); - - group('Constructors', () { - test('handles Uint8List', () { - final SecretKey key = SecretKey(Uint8List.fromList([1, 2, 3, 4, 5])); - - expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); - }); - - test('handles base16', () { - final SecretKey key = SecretKey.fromBase16('0102030405'); - - expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); - }); - - test('handles base64', () { - final SecretKey key = SecretKey.fromBase64('AQIDBAU='); - - expect(key.bytes, Uint8List.fromList([1, 2, 3, 4, 5])); - }); - - test('handles utf8', () { - final SecretKey key = SecretKey.fromUtf8('ABCDE'); - - expect(key.bytes, Uint8List.fromList([65, 66, 67, 68, 69])); - }); - }); } From ebdcf00c151c98606be91d23dc4b700a1292866d Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 26 May 2022 23:23:13 +0200 Subject: [PATCH 33/39] fix: update code to pass all tests --- .../example/lib/pages/cipher_page.dart | 12 ++-- .../example/lib/pages/kdf_page.dart | 14 +++-- .../lib/src/ciphers/aes/aes.dart | 14 +++-- .../lib/src/ciphers/aes/aes_mode.dart | 16 ++++- .../lib/src/core/cipher_text.dart | 33 +++++++--- .../lib/src/core/cipher_text_wrapper.dart | 28 ++++----- .../lib/src/interfaces/cipher.dart | 8 ++- .../native_crypto/lib/src/kdf/pbkdf2.dart | 63 ++++++++++++++----- .../lib/src/keys/secret_key.dart | 6 +- .../lib/src/utils/extensions.dart | 17 ++--- .../lib/src/utils/hash_algorithm.dart | 31 ++++++++- 11 files changed, 171 insertions(+), 71 deletions(-) diff --git a/packages/native_crypto/example/lib/pages/cipher_page.dart b/packages/native_crypto/example/lib/pages/cipher_page.dart index 5733563..81b3fc4 100644 --- a/packages/native_crypto/example/lib/pages/cipher_page.dart +++ b/packages/native_crypto/example/lib/pages/cipher_page.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_page.dart // Created Date: 28/12/2021 13:33:15 -// Last Modified: 26/05/2022 20:39:37 +// Last Modified: 26/05/2022 21:07:54 // ----- // Copyright (c) 2021 @@ -26,7 +26,8 @@ class CipherPage extends ConsumerWidget { final Output encryptionStatus = Output(); final Output decryptionStatus = Output(); - final TextEditingController _plainTextController = TextEditingController()..text = 'PlainText'; + final TextEditingController _plainTextController = TextEditingController() + ..text = 'PlainText'; CipherTextWrapper? cipherText; Future _encrypt(WidgetRef ref, Cipher cipher) async { @@ -58,9 +59,10 @@ class CipherPage extends ConsumerWidget { // Recreate cipher text with altered data cipherText = CipherTextWrapper.fromBytes( _altered, - 12, - _altered.length - 28, - 16, + ivLength: AESMode.gcm.ivLength, + messageLength: + _altered.length - (AESMode.gcm.ivLength + AESMode.gcm.tagLength), + tagLength: AESMode.gcm.tagLength, ); encryptionStatus.print('String successfully encrypted:\n'); diff --git a/packages/native_crypto/example/lib/pages/kdf_page.dart b/packages/native_crypto/example/lib/pages/kdf_page.dart index da04947..371d883 100644 --- a/packages/native_crypto/example/lib/pages/kdf_page.dart +++ b/packages/native_crypto/example/lib/pages/kdf_page.dart @@ -3,7 +3,7 @@ // ----- // File: kdf_page.dart // Created Date: 28/12/2021 13:40:34 -// Last Modified: 26/05/2022 20:30:31 +// Last Modified: 26/05/2022 21:09:47 // ----- // Copyright (c) 2021 @@ -26,8 +26,10 @@ class KdfPage extends ConsumerWidget { final Output pbkdf2Status = Output(); final Output hashStatus = Output(large: true); - final TextEditingController _pwdTextController = TextEditingController()..text = 'Password'; - final TextEditingController _messageTextController = TextEditingController()..text = 'Message'; + final TextEditingController _pwdTextController = TextEditingController() + ..text = 'Password'; + final TextEditingController _messageTextController = TextEditingController() + ..text = 'Message'; Future _generate(WidgetRef ref) async { Session state = ref.read(sessionProvider.state).state; @@ -50,7 +52,11 @@ class KdfPage extends ConsumerWidget { if (password.isEmpty) { pbkdf2Status.print('Password is empty'); } else { - Pbkdf2 _pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); + Pbkdf2 _pbkdf2 = Pbkdf2( + keyBytesCount: 32, + iterations: 1000, + algorithm: HashAlgorithm.sha512, + ); SecretKey sk = await _pbkdf2.derive(password: password, salt: 'salt'); state.setKey(sk); pbkdf2Status.print('Key successfully derived.'); diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index 9a28105..49644fd 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 19:43:22 +// Last Modified: 26/05/2022 21:07:01 // ----- // Copyright (c) 2022 @@ -21,6 +21,10 @@ import 'package:native_crypto/src/utils/cipher_algorithm.dart'; import 'package:native_crypto/src/utils/extensions.dart'; import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; +export 'aes_key_size.dart'; +export 'aes_mode.dart'; +export 'aes_padding.dart'; + /// An AES cipher. /// /// [AES] is a [Cipher] that can be used to encrypt or decrypt data. @@ -94,11 +98,11 @@ class AES implements Cipher { ); } else { return CipherText.fromBytes( - 12, - encrypted.length - 28, - 16, - CipherAlgorithm.aes, encrypted, + ivLength: 12, + messageLength: encrypted.length - 28, + tagLength: 16, + cipherAlgorithm: CipherAlgorithm.aes, ); } } diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart index a16d414..4bbc7c4 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes_mode.dart @@ -3,7 +3,7 @@ // ----- // File: aes_mode.dart // Created Date: 23/05/2022 22:09:16 -// Last Modified: 26/05/2022 18:41:31 +// Last Modified: 26/05/2022 21:03:26 // ----- // Copyright (c) 2022 @@ -11,10 +11,20 @@ import 'package:native_crypto/src/ciphers/aes/aes_padding.dart'; /// Defines the AES modes of operation. enum AESMode { - gcm([AESPadding.none]); + gcm([AESPadding.none], 12, 16); /// Returns the list of supported [AESPadding] for this [AESMode]. final List supportedPaddings; - const AESMode(this.supportedPaddings); + /// Returns the default IV length for this [AESMode]. + final int ivLength; + + /// Returns the default tag length for this [AESMode]. + final int tagLength; + + const AESMode( + this.supportedPaddings, [ + this.ivLength = 16, + this.tagLength = 0, + ]); } diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index d20f344..e0c62e3 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 26/05/2022 19:43:57 +// Last Modified: 26/05/2022 22:20:40 // ----- // Copyright (c) 2021 @@ -27,7 +27,7 @@ import 'package:native_crypto_platform_interface/native_crypto_platform_interfac /// - IV's length is [CipherText.ivLength] bytes. /// - MESSAGE's length is [CipherText.messageLength] bytes. /// - TAG's length is [CipherText.tagLength] bytes. -/// +/// /// Check [CipherTextWrapper] for more information. class CipherText extends ByteArray { final int _ivLength; @@ -45,16 +45,33 @@ class CipherText extends ByteArray { ); factory CipherText.fromBytes( - int ivLength, - int messageLength, - int tagLength, + Uint8List bytes, { + required int ivLength, + required int messageLength, + required int tagLength, CipherAlgorithm? cipherAlgorithm, - Uint8List bytes, - ) { + }) { + if (ivLength.isNegative || + messageLength.isNegative || + tagLength.isNegative) { + throw NativeCryptoException( + message: 'Invalid length! Must be positive.', + code: NativeCryptoExceptionCode.invalid_argument.code, + ); + } + + if (bytes.isEmpty) { + throw NativeCryptoException( + message: 'Passed data is empty!', + code: NativeCryptoExceptionCode.invalid_argument.code, + ); + } + if (bytes.length != ivLength + messageLength + tagLength) { throw NativeCryptoException( message: 'Invalid cipher text length! ' - 'Expected: ${ivLength + messageLength + tagLength} bytes', + 'Expected: ${ivLength + messageLength + tagLength} bytes ' + 'got: ${bytes.length} bytes.', code: NativeCryptoExceptionCode.invalid_argument.code, ); } diff --git a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart index 08dc915..285ba6a 100644 --- a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart +++ b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text_wrapper.dart // Created Date: 26/05/2022 14:27:32 -// Last Modified: 26/05/2022 20:32:38 +// Last Modified: 26/05/2022 22:11:42 // ----- // Copyright (c) 2022 @@ -49,10 +49,10 @@ class CipherTextWrapper { /// [NativeCryptoExceptionCode.invalid_argument] if the [Uint8List] is /// not a valid [CipherText] or a [List] of [CipherText]. factory CipherTextWrapper.fromBytes( - Uint8List bytes, - int ivLength, - int messageLength, - int tagLength, { + Uint8List bytes, { + required int ivLength, + required int messageLength, + required int tagLength, CipherAlgorithm? cipherAlgorithm, int? chunkSize, }) { @@ -62,11 +62,11 @@ class CipherTextWrapper { if (bytes.length <= chunkSize) { return CipherTextWrapper.single( CipherText.fromBytes( - ivLength, - messageLength, - tagLength, - cipherAlgorithm, bytes, + ivLength: ivLength, + messageLength: messageLength, + tagLength: tagLength, + cipherAlgorithm: cipherAlgorithm, ), ); } else { @@ -75,11 +75,11 @@ class CipherTextWrapper { final chunk = bytes.sublist(i, i + chunkSize); cipherTexts.add( CipherText.fromBytes( - ivLength, - messageLength, - tagLength, - cipherAlgorithm, chunk, + ivLength: ivLength, + messageLength: messageLength, + tagLength: tagLength, + cipherAlgorithm: cipherAlgorithm, ), ); } @@ -128,7 +128,7 @@ class CipherTextWrapper { if (isSingle) { return single.bytes; } else { - return list.map((cipherText) => cipherText.bytes).toList().sum(); + return list.map((cipherText) => cipherText.bytes).toList().combine(); } } diff --git a/packages/native_crypto/lib/src/interfaces/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart index c95f34b..5d87661 100644 --- a/packages/native_crypto/lib/src/interfaces/cipher.dart +++ b/packages/native_crypto/lib/src/interfaces/cipher.dart @@ -3,7 +3,7 @@ // ----- // File: cipher.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 17:38:26 +// Last Modified: 26/05/2022 21:21:07 // ----- // Copyright (c) 2021 @@ -20,7 +20,11 @@ import 'package:native_crypto/src/utils/cipher_algorithm.dart'; /// /// This interface is implemented by all the ciphers in NativeCrypto. abstract class Cipher { - static int _bytesCountPerChunk = 33554432; + static const int _bytesCountPerChunkDefault = 33554432; + static int _bytesCountPerChunk = _bytesCountPerChunkDefault; + + /// Returns the default number of bytes per chunk. + static int get defaultBytesCountPerChunk => _bytesCountPerChunkDefault; /// Returns the size of a chunk of data /// that can be processed by the [Cipher]. diff --git a/packages/native_crypto/lib/src/kdf/pbkdf2.dart b/packages/native_crypto/lib/src/kdf/pbkdf2.dart index ae7d3b4..8ccdadd 100644 --- a/packages/native_crypto/lib/src/kdf/pbkdf2.dart +++ b/packages/native_crypto/lib/src/kdf/pbkdf2.dart @@ -3,7 +3,7 @@ // ----- // File: pbkdf2.dart // Created Date: 17/12/2021 14:50:42 -// Last Modified: 26/05/2022 18:51:59 +// Last Modified: 26/05/2022 23:19:46 // ----- // Copyright (c) 2021 @@ -29,31 +29,64 @@ class Pbkdf2 extends KeyDerivation { @override KdfAlgorithm get algorithm => KdfAlgorithm.pbkdf2; - Pbkdf2( - int keyBytesCount, - int iterations, { + Pbkdf2({ + required int keyBytesCount, + required int iterations, HashAlgorithm algorithm = HashAlgorithm.sha256, }) : _keyBytesCount = keyBytesCount, _iterations = iterations, - _hash = algorithm; + _hash = algorithm { + if (keyBytesCount < 0) { + throw NativeCryptoException( + message: 'keyBytesCount must be positive.', + code: NativeCryptoExceptionCode.invalid_argument.code, + ); + } + + if (iterations <= 0) { + throw NativeCryptoException( + message: 'iterations must be strictly positive.', + code: NativeCryptoExceptionCode.invalid_argument.code, + ); + } + } @override Future derive({String? password, String? salt}) async { - if (password == null || salt == null) { + Uint8List? derivation; + + if (_keyBytesCount == 0) { + return SecretKey(Uint8List(0)); + } + if (password.isNull) { + throw NativeCryptoException( + message: 'Password cannot be null.', + code: NativeCryptoExceptionCode.invalid_argument.code, + ); + } + + if (salt.isNull) { throw NativeCryptoException( - message: 'Password and salt cannot be null. ' - 'Here is the password: $password, here is the salt: $salt', + message: 'Salt cannot be null.', code: NativeCryptoExceptionCode.invalid_argument.code, ); } - final Uint8List? derivation = await platform.pbkdf2( - password, - salt, - _keyBytesCount, - _iterations, - _hash.name, - ); + try { + derivation = await platform.pbkdf2( + password!, + salt!, + _keyBytesCount, + _iterations, + _hash.name, + ); + } catch (e, s) { + throw NativeCryptoException( + message: '$e', + code: NativeCryptoExceptionCode.platform_throws.code, + stackTrace: s, + ); + } if (derivation.isNull) { throw NativeCryptoException( diff --git a/packages/native_crypto/lib/src/keys/secret_key.dart b/packages/native_crypto/lib/src/keys/secret_key.dart index f660181..e30b87b 100644 --- a/packages/native_crypto/lib/src/keys/secret_key.dart +++ b/packages/native_crypto/lib/src/keys/secret_key.dart @@ -3,7 +3,7 @@ // ----- // File: secret_key.dart // Created Date: 28/12/2021 13:36:54 -// Last Modified: 26/05/2022 19:26:35 +// Last Modified: 26/05/2022 23:13:10 // ----- // Copyright (c) 2021 @@ -28,6 +28,10 @@ class SecretKey extends BaseKey { static Future fromSecureRandom(int bitsCount) async { Uint8List? key; + if (bitsCount == 0) { + return SecretKey(Uint8List(0)); + } + try { key = await platform.generateSecretKey(bitsCount); } catch (e, s) { diff --git a/packages/native_crypto/lib/src/utils/extensions.dart b/packages/native_crypto/lib/src/utils/extensions.dart index b6399e0..5daa30b 100644 --- a/packages/native_crypto/lib/src/utils/extensions.dart +++ b/packages/native_crypto/lib/src/utils/extensions.dart @@ -3,7 +3,7 @@ // ----- // File: extensions.dart // Created Date: 26/05/2022 12:12:48 -// Last Modified: 26/05/2022 18:52:48 +// Last Modified: 26/05/2022 22:15:33 // ----- // Copyright (c) 2022 @@ -19,7 +19,7 @@ extension ObjectX on Object? { /// Returns `true` if the object is **not** `null`. bool get isNotNull => this != null; - + /// Prints the object to the console. void log() => developer.log(toString()); } @@ -30,14 +30,10 @@ extension ListIntX on List { } extension ListUint8ListX on List { - /// Reduce a [List] of [Uint8List] to a [Uint8List]. - Uint8List sum() { - for (var i = 1; i < length; i++) { - first.addAll(this[i]); - removeAt(i); - } - return first; + Uint8List combine() { + if (isEmpty) return Uint8List(0); + return reduce((value, element) => value.plus(element)); } } @@ -90,6 +86,5 @@ extension Uint8ListX on Uint8List { } /// Returns a concatenation of this with the other [Uint8List]. - Uint8List operator +(final Uint8List other) => - [...this, ...other].toTypedList(); + Uint8List plus(final Uint8List other) => [...this, ...other].toTypedList(); } diff --git a/packages/native_crypto/lib/src/utils/hash_algorithm.dart b/packages/native_crypto/lib/src/utils/hash_algorithm.dart index d9c6ced..4538ef5 100644 --- a/packages/native_crypto/lib/src/utils/hash_algorithm.dart +++ b/packages/native_crypto/lib/src/utils/hash_algorithm.dart @@ -3,23 +3,48 @@ // ----- // File: hash_algorithm.dart // Created Date: 23/05/2022 22:01:59 -// Last Modified: 26/05/2022 18:53:38 +// Last Modified: 26/05/2022 22:59:04 // ----- // Copyright (c) 2022 import 'dart:typed_data'; import 'package:native_crypto/src/platform.dart'; +import 'package:native_crypto/src/utils/extensions.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; /// Defines the hash algorithms. enum HashAlgorithm { sha256, sha384, sha512; - + /// Digest the [data] using this [HashAlgorithm]. Future digest(Uint8List data) async { - final Uint8List hash = (await platform.digest(data, name)) ?? Uint8List(0); + Uint8List? hash; + try { + hash = await platform.digest(data, name); + } catch (e, s) { + throw NativeCryptoException( + message: '$e', + code: NativeCryptoExceptionCode.platform_throws.code, + stackTrace: s, + ); + } + + if (hash.isNull) { + throw NativeCryptoException( + message: 'Failed to digest data! Platform returned null.', + code: NativeCryptoExceptionCode.platform_returned_null.code, + ); + } + + if (hash!.isEmpty) { + throw NativeCryptoException( + message: 'Failed to digest data! Platform returned no data.', + code: NativeCryptoExceptionCode.platform_returned_empty_data.code, + ); + } return hash; } From cefa73ec3da38a2730dcc346dbab8501ea92080e Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 27 May 2022 16:41:25 +0200 Subject: [PATCH 34/39] test: add cipher tests --- .../test/src/aes_cipher_test.dart | 323 ++++++++++++++++++ .../test/src/cipher_text_wrapper_test.dart | 32 +- 2 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 packages/native_crypto/test/src/aes_cipher_test.dart diff --git a/packages/native_crypto/test/src/aes_cipher_test.dart b/packages/native_crypto/test/src/aes_cipher_test.dart new file mode 100644 index 0000000..f1d0f39 --- /dev/null +++ b/packages/native_crypto/test/src/aes_cipher_test.dart @@ -0,0 +1,323 @@ +// Author: Hugo Pointcheval +// Email: git@pcl.ovh +// ----- +// File: aes_cipher_test.dart +// Created Date: 26/05/2022 23:20:53 +// Last Modified: 27/05/2022 16:39:44 +// ----- +// Copyright (c) 2022 + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_platform_interface/native_crypto_platform_interface.dart'; + +import '../mocks/mock_native_crypto_platform.dart'; + +void main() { + final MockNativeCryptoPlatform mock = MockNativeCryptoPlatform(); + NativeCryptoPlatform.instance = mock; + + setUp(() { + Cipher.bytesCountPerChunk = Cipher.defaultBytesCountPerChunk; + }); + + group('Constructor', () { + test('throws on invalid key length', () { + expect( + () => AES(SecretKey(Uint8List(0))), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_key_length', + ) + .having( + (e) => e.message, + 'message', + contains('Invalid key'), + ), + ), + ); + }); + + test('creates a valid instance', () { + expect( + AES( + SecretKey(Uint8List(16)), + ), + isA(), + ); + }); + }); + + group('encrypt', () { + test('returns a valid cipher text wrapper', () async { + mock + ..setEncryptExpectations( + data: Uint8List(16), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(16 + 28)); + + final aes = AES(SecretKey(Uint8List(16))); + + expect( + await aes.encrypt(Uint8List(16)), + isA().having((e) => e.isSingle, 'is single', isTrue), + ); + }); + + test('returns a valid cipher text with multiple chunks', () async { + mock + ..setEncryptExpectations( + data: Uint8List(16), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(16 + 28)); // Returns 1 encrypted chunk + Cipher.bytesCountPerChunk = 16; + final aes = AES(SecretKey(Uint8List(16))); + + expect( + await aes.encrypt(Uint8List(16 * 3)), + isA().having((e) => e.isList, 'is list', isTrue), + ); + }); + + test('handles returning empty list', () async { + mock + ..setEncryptExpectations( + data: Uint8List(16), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(0)); + + final aes = AES(SecretKey(Uint8List(16))); + + await expectLater( + () => aes.encrypt(Uint8List(16)), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_empty_data', + ), + ), + ); + }); + + test('handles returning null', () async { + mock + ..setEncryptExpectations( + data: Uint8List(16), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => null); + + final aes = AES(SecretKey(Uint8List(16))); + + await expectLater( + () => aes.encrypt(Uint8List(16)), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles throwing PlatformException', () async { + mock + ..setEncryptExpectations( + data: Uint8List(16), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse( + () => throw PlatformException( + code: 'native_crypto', + message: 'dummy error', + ), + ); + + final aes = AES(SecretKey(Uint8List(16))); + + await expectLater( + () => aes.encrypt(Uint8List(16)), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + contains( + 'PlatformException(native_crypto, dummy error, null, null)', + ), + ) + .having( + (e) => e.code, + 'code', + 'platform_throws', + ), + ), + ); + }); + }); + + group('decrypt', () { + test('returns a valid Uint8List', () async { + mock + ..setDecryptExpectations( + data: Uint8List(16 + 28), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(16)); + + final aes = AES(SecretKey(Uint8List(16))); + final bytes = Uint8List(16 + 28); + final wrapper = CipherTextWrapper.fromBytes( + bytes, + ivLength: 12, + tagLength: 16, + ); + + expect( + await aes.decrypt(wrapper), + isA().having((e) => e.length, 'length', 16), + ); + }); + + test('returns a valid Uint8List on decrypting multiple chunks', () async { + const int chunkSize = 8; + mock + ..setDecryptExpectations( + data: Uint8List(chunkSize + 28), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(chunkSize)); + Cipher.bytesCountPerChunk = chunkSize; + final aes = AES(SecretKey(Uint8List(16))); + final bytes = Uint8List((chunkSize + 28) * 3); + final wrapper = CipherTextWrapper.fromBytes( + bytes, + ivLength: 12, + tagLength: 16, + ); + + expect( + await aes.decrypt(wrapper), + isA().having((e) => e.length, 'length', chunkSize * 3), + ); + }); + + test('handles returning empty list', () async { + mock + ..setDecryptExpectations( + data: Uint8List(16 + 28), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => Uint8List(0)); + + final aes = AES(SecretKey(Uint8List(16))); + final bytes = Uint8List(16 + 28); + final wrapper = CipherTextWrapper.fromBytes( + bytes, + ivLength: 12, + tagLength: 16, + ); + + await expectLater( + () => aes.decrypt(wrapper), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_empty_data', + ), + ), + ); + }); + + test('handles returning null', () async { + mock + ..setDecryptExpectations( + data: Uint8List(16 + 28), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse(() => null); + + final aes = AES(SecretKey(Uint8List(16))); + final bytes = Uint8List(16 + 28); + final wrapper = CipherTextWrapper.fromBytes( + bytes, + ivLength: 12, + tagLength: 16, + ); + + await expectLater( + () => aes.decrypt(wrapper), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'platform_returned_null', + ), + ), + ); + }); + + test('handles throwing PlatformException', () async { + mock + ..setDecryptExpectations( + data: Uint8List(16 + 28), + key: Uint8List(16), + algorithm: 'aes', + ) + ..setResponse( + () => throw PlatformException( + code: 'native_crypto', + message: 'dummy error', + ), + ); + + final aes = AES(SecretKey(Uint8List(16))); + final bytes = Uint8List(16 + 28); + final wrapper = CipherTextWrapper.fromBytes( + bytes, + ivLength: 12, + tagLength: 16, + ); + + await expectLater( + () => aes.decrypt(wrapper), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + contains( + 'PlatformException(native_crypto, dummy error, null, null)', + ), + ) + .having( + (e) => e.code, + 'code', + 'platform_throws', + ), + ), + ); + }); + }); +} diff --git a/packages/native_crypto/test/src/cipher_text_wrapper_test.dart b/packages/native_crypto/test/src/cipher_text_wrapper_test.dart index 7477900..8dc0116 100644 --- a/packages/native_crypto/test/src/cipher_text_wrapper_test.dart +++ b/packages/native_crypto/test/src/cipher_text_wrapper_test.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text_wrapper_test.dart // Created Date: 26/05/2022 21:35:41 -// Last Modified: 26/05/2022 22:27:31 +// Last Modified: 27/05/2022 13:46:54 // ----- // Copyright (c) 2022 @@ -293,7 +293,6 @@ void main() { final wrapper = CipherTextWrapper.fromBytes( Uint8List.fromList([1, 2, 3]), ivLength: 1, - messageLength: 1, tagLength: 1, ); expect(wrapper.isSingle, isTrue); @@ -301,11 +300,10 @@ void main() { }); test('creates list from bytes when too big', () { - Cipher.bytesCountPerChunk = 3; + Cipher.bytesCountPerChunk = 1; final wrapper = CipherTextWrapper.fromBytes( Uint8List.fromList([1, 2, 3, 4, 5, 6]), ivLength: 1, - messageLength: 1, tagLength: 1, ); expect(wrapper.isList, isTrue); @@ -317,11 +315,35 @@ void main() { CipherTextWrapper.fromBytes( Uint8List.fromList([1, 2, 3]), ivLength: 1, - messageLength: 1, tagLength: 1, chunkSize: 3, ); expect(Cipher.bytesCountPerChunk, 3); }); + + test('throws if trying to build list with bad parameters', () { + Cipher.bytesCountPerChunk = 1; // length of a message + + expect( + () => CipherTextWrapper.fromBytes( + Uint8List.fromList([1, 2, 3, 4, 5, 6]), + ivLength: 2, + tagLength: 1, + ), + throwsA( + isA() + .having( + (e) => e.code, + 'code', + 'invalid_argument', + ) + .having( + (e) => e.message, + 'message', + contains('on chunk #'), + ), + ), + ); + }); }); } From 1b00d20ec5afe37bdc64d410bf1779caa6119a16 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 27 May 2022 16:41:43 +0200 Subject: [PATCH 35/39] fix: update code to pass tests --- .../lib/src/ciphers/aes/aes.dart | 74 +++++++++++++------ .../lib/src/core/cipher_text.dart | 6 +- .../lib/src/core/cipher_text_wrapper.dart | 38 ++++++---- .../lib/src/utils/extensions.dart | 13 +++- 4 files changed, 92 insertions(+), 39 deletions(-) diff --git a/packages/native_crypto/lib/src/ciphers/aes/aes.dart b/packages/native_crypto/lib/src/ciphers/aes/aes.dart index 49644fd..b1113fc 100644 --- a/packages/native_crypto/lib/src/ciphers/aes/aes.dart +++ b/packages/native_crypto/lib/src/ciphers/aes/aes.dart @@ -3,7 +3,7 @@ // ----- // File: aes.dart // Created Date: 16/12/2021 16:28:00 -// Last Modified: 26/05/2022 21:07:01 +// Last Modified: 27/05/2022 12:13:28 // ----- // Copyright (c) 2022 @@ -46,7 +46,6 @@ class AES implements Cipher { ); } - if (!mode.supportedPaddings.contains(padding)) { throw NativeCryptoException( message: 'Invalid padding! ' @@ -56,13 +55,25 @@ class AES implements Cipher { } } - Future _decrypt(CipherText cipherText, - {int chunkCount = 0,}) async { - final Uint8List? decrypted = await platform.decrypt( - cipherText.bytes, - _key.bytes, - algorithm.name, - ); + Future _decrypt( + CipherText cipherText, { + int chunkCount = 0, + }) async { + Uint8List? decrypted; + + try { + decrypted = await platform.decrypt( + cipherText.bytes, + _key.bytes, + algorithm.name, + ); + } catch (e, s) { + throw NativeCryptoException( + message: '$e', + code: NativeCryptoExceptionCode.platform_throws.code, + stackTrace: s, + ); + } if (decrypted.isNull) { throw NativeCryptoException( @@ -80,11 +91,21 @@ class AES implements Cipher { } Future _encrypt(Uint8List data, {int chunkCount = 0}) async { - final Uint8List? encrypted = await platform.encrypt( - data, - _key.bytes, - algorithm.name, - ); + Uint8List? encrypted; + + try { + encrypted = await platform.encrypt( + data, + _key.bytes, + algorithm.name, + ); + } catch (e, s) { + throw NativeCryptoException( + message: '$e on chunk #$chunkCount', + code: NativeCryptoExceptionCode.platform_throws.code, + stackTrace: s, + ); + } if (encrypted.isNull) { throw NativeCryptoException( @@ -97,13 +118,21 @@ class AES implements Cipher { code: NativeCryptoExceptionCode.platform_returned_empty_data.code, ); } else { - return CipherText.fromBytes( - encrypted, - ivLength: 12, - messageLength: encrypted.length - 28, - tagLength: 16, - cipherAlgorithm: CipherAlgorithm.aes, - ); + try { + return CipherText.fromBytes( + encrypted, + ivLength: 12, + messageLength: encrypted.length - 28, + tagLength: 16, + cipherAlgorithm: CipherAlgorithm.aes, + ); + } on NativeCryptoException catch (e, s) { + throw NativeCryptoException( + message: '${e.message} on chunk #$chunkCount', + code: e.code, + stackTrace: s, + ); + } } } @@ -125,6 +154,9 @@ class AES implements Cipher { @override Future encrypt(Uint8List data) async { + if (data.isEmpty) { + return CipherTextWrapper.empty(); + } CipherTextWrapper cipherTextWrapper; Uint8List dataToEncrypt; final int chunkNb = (data.length / Cipher.bytesCountPerChunk).ceil(); diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index e0c62e3..09cc4d6 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text.dart // Created Date: 16/12/2021 16:59:53 -// Last Modified: 26/05/2022 22:20:40 +// Last Modified: 27/05/2022 12:09:47 // ----- // Copyright (c) 2021 @@ -47,10 +47,12 @@ class CipherText extends ByteArray { factory CipherText.fromBytes( Uint8List bytes, { required int ivLength, - required int messageLength, required int tagLength, + int? messageLength, CipherAlgorithm? cipherAlgorithm, }) { + messageLength ??= bytes.length - ivLength - tagLength; + if (ivLength.isNegative || messageLength.isNegative || tagLength.isNegative) { diff --git a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart index 285ba6a..75a9dae 100644 --- a/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart +++ b/packages/native_crypto/lib/src/core/cipher_text_wrapper.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_text_wrapper.dart // Created Date: 26/05/2022 14:27:32 -// Last Modified: 26/05/2022 22:11:42 +// Last Modified: 27/05/2022 13:43:29 // ----- // Copyright (c) 2022 @@ -51,7 +51,6 @@ class CipherTextWrapper { factory CipherTextWrapper.fromBytes( Uint8List bytes, { required int ivLength, - required int messageLength, required int tagLength, CipherAlgorithm? cipherAlgorithm, int? chunkSize, @@ -59,29 +58,38 @@ class CipherTextWrapper { chunkSize ??= Cipher.bytesCountPerChunk; Cipher.bytesCountPerChunk = chunkSize; - if (bytes.length <= chunkSize) { + final int messageLength = bytes.length - ivLength - tagLength; + + if (messageLength <= chunkSize) { return CipherTextWrapper.single( CipherText.fromBytes( bytes, ivLength: ivLength, - messageLength: messageLength, tagLength: tagLength, cipherAlgorithm: cipherAlgorithm, ), ); } else { final cipherTexts = []; - for (var i = 0; i < bytes.length; i += chunkSize) { - final chunk = bytes.sublist(i, i + chunkSize); - cipherTexts.add( - CipherText.fromBytes( - chunk, - ivLength: ivLength, - messageLength: messageLength, - tagLength: tagLength, - cipherAlgorithm: cipherAlgorithm, - ), - ); + for (var i = 0; i < bytes.length; i += chunkSize + ivLength + tagLength) { + final chunk = bytes.trySublist(i, i + chunkSize + ivLength + tagLength); + + try { + cipherTexts.add( + CipherText.fromBytes( + chunk, + ivLength: ivLength, + tagLength: tagLength, + cipherAlgorithm: cipherAlgorithm, + ), + ); + } on NativeCryptoException catch (e, s) { + throw NativeCryptoException( + message: '${e.message} on chunk #$i', + code: e.code, + stackTrace: s, + ); + } } return CipherTextWrapper.list(cipherTexts); } diff --git a/packages/native_crypto/lib/src/utils/extensions.dart b/packages/native_crypto/lib/src/utils/extensions.dart index 5daa30b..fc2a799 100644 --- a/packages/native_crypto/lib/src/utils/extensions.dart +++ b/packages/native_crypto/lib/src/utils/extensions.dart @@ -3,7 +3,7 @@ // ----- // File: extensions.dart // Created Date: 26/05/2022 12:12:48 -// Last Modified: 26/05/2022 22:15:33 +// Last Modified: 27/05/2022 12:26:55 // ----- // Copyright (c) 2022 @@ -87,4 +87,15 @@ extension Uint8ListX on Uint8List { /// Returns a concatenation of this with the other [Uint8List]. Uint8List plus(final Uint8List other) => [...this, ...other].toTypedList(); + + /// Returns a sublist of this from the [start] index to the [end] index. + /// If [end] is greater than the length of the list, it is set to the length + Uint8List trySublist(int start, [int? end]) { + if (isEmpty) return this; + + int ending = end ?? length; + if (ending > length) ending = length; + + return sublist(start, ending); + } } From ac35cd89ca6775b55d626e7061ca4f8cbd8d4ee1 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 27 May 2022 17:34:53 +0200 Subject: [PATCH 36/39] style: format all + melos test command --- melos.yaml | 18 ++++++++++++++++++ .../example/lib/pages/benchmark_page.dart | 4 ++-- .../example/lib/pages/cipher_page.dart | 4 +--- .../lib/src/core/cipher_text.dart | 2 +- .../lib/src/interfaces/cipher.dart | 7 +++---- .../lib/src/interfaces/keyderivation.dart | 4 ++-- .../test/src/cipher_text_test.dart | 2 +- .../method_channel_native_crypto_test.dart | 3 +-- 8 files changed, 29 insertions(+), 15 deletions(-) diff --git a/melos.yaml b/melos.yaml index f034109..a311fc5 100644 --- a/melos.yaml +++ b/melos.yaml @@ -15,6 +15,24 @@ scripts: lint:all: run: melos run analyze && melos run format description: Run all static analysis checks. + + test:all: + run: | + melos run test --no-select + description: | + Run all tests available. + + test: + run: | + melos exec -c 6 --fail-fast -- \ + "flutter test --no-pub --no-test-assets" + description: Run `flutter test` for a specific package. + select-package: + dir-exists: + - test + ignore: + - "*web*" + - "*example*" analyze: run: | diff --git a/packages/native_crypto/example/lib/pages/benchmark_page.dart b/packages/native_crypto/example/lib/pages/benchmark_page.dart index ad1074f..6396152 100644 --- a/packages/native_crypto/example/lib/pages/benchmark_page.dart +++ b/packages/native_crypto/example/lib/pages/benchmark_page.dart @@ -91,8 +91,8 @@ class BenchmarkPage extends ConsumerWidget { after = DateTime.now(); benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - benchmarkStatus - .appendln('[Benchmark] ${size}MiB => Decryption took $benchmark ms'); + benchmarkStatus.appendln( + '[Benchmark] ${size}MiB => Decryption took $benchmark ms'); csvLine.write(';$benchmark'); } csv += csvLine.toString() + '\n'; diff --git a/packages/native_crypto/example/lib/pages/cipher_page.dart b/packages/native_crypto/example/lib/pages/cipher_page.dart index 81b3fc4..b0843c7 100644 --- a/packages/native_crypto/example/lib/pages/cipher_page.dart +++ b/packages/native_crypto/example/lib/pages/cipher_page.dart @@ -3,7 +3,7 @@ // ----- // File: cipher_page.dart // Created Date: 28/12/2021 13:33:15 -// Last Modified: 26/05/2022 21:07:54 +// Last Modified: 27/05/2022 16:42:10 // ----- // Copyright (c) 2021 @@ -60,8 +60,6 @@ class CipherPage extends ConsumerWidget { cipherText = CipherTextWrapper.fromBytes( _altered, ivLength: AESMode.gcm.ivLength, - messageLength: - _altered.length - (AESMode.gcm.ivLength + AESMode.gcm.tagLength), tagLength: AESMode.gcm.tagLength, ); encryptionStatus.print('String successfully encrypted:\n'); diff --git a/packages/native_crypto/lib/src/core/cipher_text.dart b/packages/native_crypto/lib/src/core/cipher_text.dart index 09cc4d6..6accd59 100644 --- a/packages/native_crypto/lib/src/core/cipher_text.dart +++ b/packages/native_crypto/lib/src/core/cipher_text.dart @@ -52,7 +52,7 @@ class CipherText extends ByteArray { CipherAlgorithm? cipherAlgorithm, }) { messageLength ??= bytes.length - ivLength - tagLength; - + if (ivLength.isNegative || messageLength.isNegative || tagLength.isNegative) { diff --git a/packages/native_crypto/lib/src/interfaces/cipher.dart b/packages/native_crypto/lib/src/interfaces/cipher.dart index 5d87661..58b0d4a 100644 --- a/packages/native_crypto/lib/src/interfaces/cipher.dart +++ b/packages/native_crypto/lib/src/interfaces/cipher.dart @@ -17,7 +17,7 @@ import 'package:native_crypto/src/utils/cipher_algorithm.dart'; /// In cryptography, a [Cipher] is an algorithm for performing encryption /// or decryption - a series of well-defined steps that can /// be followed as a procedure. -/// +/// /// This interface is implemented by all the ciphers in NativeCrypto. abstract class Cipher { static const int _bytesCountPerChunkDefault = 33554432; @@ -26,7 +26,7 @@ abstract class Cipher { /// Returns the default number of bytes per chunk. static int get defaultBytesCountPerChunk => _bytesCountPerChunkDefault; - /// Returns the size of a chunk of data + /// Returns the size of a chunk of data /// that can be processed by the [Cipher]. static int get bytesCountPerChunk => Cipher._bytesCountPerChunk; @@ -35,11 +35,10 @@ abstract class Cipher { static set bytesCountPerChunk(int bytesCount) { _bytesCountPerChunk = bytesCount; } - + /// Returns the standard algorithm for this [Cipher]. CipherAlgorithm get algorithm; - /// Encrypts the [data]. /// /// Takes [Uint8List] data as parameter. diff --git a/packages/native_crypto/lib/src/interfaces/keyderivation.dart b/packages/native_crypto/lib/src/interfaces/keyderivation.dart index ebc3841..ccdbb4b 100644 --- a/packages/native_crypto/lib/src/interfaces/keyderivation.dart +++ b/packages/native_crypto/lib/src/interfaces/keyderivation.dart @@ -11,8 +11,8 @@ import 'package:native_crypto/src/keys/secret_key.dart'; import 'package:native_crypto/src/utils/kdf_algorithm.dart'; /// Represents a Key Derivation Function (KDF) in NativeCrypto. -/// -/// [KeyDerivation] function is a function that takes some +/// +/// [KeyDerivation] function is a function that takes some /// parameters and returns a [SecretKey]. abstract class KeyDerivation { /// Returns the standard algorithm for this key derivation function diff --git a/packages/native_crypto/test/src/cipher_text_test.dart b/packages/native_crypto/test/src/cipher_text_test.dart index b466b55..16d98a5 100644 --- a/packages/native_crypto/test/src/cipher_text_test.dart +++ b/packages/native_crypto/test/src/cipher_text_test.dart @@ -157,7 +157,7 @@ void main() { expect(cipherText.cipherAlgorithm, CipherAlgorithm.aes); }); }); - + group('Lengths', () { test('get.ivLength returns the expected value', () { final CipherText cipherText = CipherText.fromBytes( diff --git a/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart b/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart index 5f59f97..28c01a1 100644 --- a/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart +++ b/packages/native_crypto_platform_interface/test/method_channel/method_channel_native_crypto_test.dart @@ -150,7 +150,7 @@ void main() { ], ); }); - + test('decrypt', () async { await nativeCrypto.decrypt( Uint8List(0), @@ -171,6 +171,5 @@ void main() { ], ); }); - }); } From cf4227fb582a7487194dc51d4631919c296790a8 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 27 May 2022 18:18:58 +0200 Subject: [PATCH 37/39] ci: add drone config file --- .drone.yml | 13 +++++++++++++ .../example/lib/pointycastle/aes_gcm.dart | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..0f2f2a6 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,13 @@ +kind: pipeline +type: docker +name: default + +steps: +- name: test:all + image: cirrusci/flutter:stable + commands: + - PATH=$PATH:$HOME/.pub-cache/bin + - flutter doctor + - flutter pub global activate melos + - melos bs + - melos run test:all \ No newline at end of file diff --git a/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart b/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart index a0aa0be..967c384 100644 --- a/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart +++ b/packages/native_crypto/example/lib/pointycastle/aes_gcm.dart @@ -3,10 +3,12 @@ // ----- // File: aes_gcm.dart // Created Date: 24/05/2022 16:34:54 -// Last Modified: 24/05/2022 17:15:22 +// Last Modified: 27/05/2022 17:36:31 // ----- // Copyright (c) 2022 +// ignore_for_file: implementation_imports + import 'dart:typed_data'; import 'package:pointycastle/export.dart'; From d28a49a67bc14b5bf1ac4b0cbd7f43d132bdd262 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 1 Jun 2022 12:21:49 +0200 Subject: [PATCH 38/39] docs: update readme --- README.md | 44 +++++++++++++++++++++++--------- packages/native_crypto/README.md | 44 +++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e9b0b7c..decc847 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,6 @@ To digest a message, you can use the following function: Uint8List hash = await HashAlgorithm.sha256.digest(message); ``` -Note that you can find a `toBytes()` method in the example app, to convert a `String` to a `Uint8List`. - > In NativeCrypto, you can use the following hash functions: SHA-256, SHA-384, SHA-512 #### Keys @@ -70,7 +68,11 @@ You can derive a `SecretKey` using **PBKDF2**. First, you need to initialize a `Pbkdf2` object. ```dart -Pbkdf2 pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); +Pbkdf2 pbkdf2 = Pbkdf2( + keyBytesCount: 32, + iterations: 1000, + algorithm: HashAlgorithm.sha512, +); ``` Then, you can derive a `SecretKey` from a password and salt. @@ -88,22 +90,40 @@ And now, you can use the `SecretKey` to encrypt/decrypt a message. First, you need to initialize a `Cipher` object. ```dart -AES cipher = AES(secretKey, AESMode.gcm); +AES cipher = AES(secretKey); ``` -Then, you can encrypt/decrypt your message. +Then, you can encrypt your message. ```dart -CipherText encrypted = await cipher.encrypt(message); -Uint8List decrypted = await cipher.decrypt(encrypted); +CipherTextWrapper wrapper = await cipher.encrypt(message); + +CipherText cipherText = wrapper.unwrap(); +// same as +CipherText cipherText = wrapper.single; + +// or + +List cipherTexts = wrapper.unwrap>(); +// same as +List cipherTexts = wrapper.list; ``` -After an encryption, you can use the `CipherText` object to access underlying data. +After an encryption you obtain a `CipherTextWrapper` which contains `CipherText` or `List` depending on the message size. It's up to you to know how to unwrap the `CipherTextWrapper` depending the chunk size you configured. + +Uppon receiving encrypted message, you can decrypt it. +You have to reconstruct the wrapper before decrypting. ```dart -Uint8List iv = encrypted.iv; -Uint8List data = encrypted.data; -Uint8List tag = encrypted.tag; +CipherTextWrapper wrapper = CipherTextWrapper.fromBytes( + data, + ivLength: AESMode.gcm.ivLength, + tagLength: AESMode.gcm.tagLength, +); ``` -Note that data and tag are costly to access, so you should only use them if you need to ! \ No newline at end of file +Then, you can decrypt your message. + +```dart +Uint8List message = await cipher.decrypt(wrapper); +``` \ No newline at end of file diff --git a/packages/native_crypto/README.md b/packages/native_crypto/README.md index e9b0b7c..decc847 100644 --- a/packages/native_crypto/README.md +++ b/packages/native_crypto/README.md @@ -47,8 +47,6 @@ To digest a message, you can use the following function: Uint8List hash = await HashAlgorithm.sha256.digest(message); ``` -Note that you can find a `toBytes()` method in the example app, to convert a `String` to a `Uint8List`. - > In NativeCrypto, you can use the following hash functions: SHA-256, SHA-384, SHA-512 #### Keys @@ -70,7 +68,11 @@ You can derive a `SecretKey` using **PBKDF2**. First, you need to initialize a `Pbkdf2` object. ```dart -Pbkdf2 pbkdf2 = Pbkdf2(32, 1000, algorithm: HashAlgorithm.sha512); +Pbkdf2 pbkdf2 = Pbkdf2( + keyBytesCount: 32, + iterations: 1000, + algorithm: HashAlgorithm.sha512, +); ``` Then, you can derive a `SecretKey` from a password and salt. @@ -88,22 +90,40 @@ And now, you can use the `SecretKey` to encrypt/decrypt a message. First, you need to initialize a `Cipher` object. ```dart -AES cipher = AES(secretKey, AESMode.gcm); +AES cipher = AES(secretKey); ``` -Then, you can encrypt/decrypt your message. +Then, you can encrypt your message. ```dart -CipherText encrypted = await cipher.encrypt(message); -Uint8List decrypted = await cipher.decrypt(encrypted); +CipherTextWrapper wrapper = await cipher.encrypt(message); + +CipherText cipherText = wrapper.unwrap(); +// same as +CipherText cipherText = wrapper.single; + +// or + +List cipherTexts = wrapper.unwrap>(); +// same as +List cipherTexts = wrapper.list; ``` -After an encryption, you can use the `CipherText` object to access underlying data. +After an encryption you obtain a `CipherTextWrapper` which contains `CipherText` or `List` depending on the message size. It's up to you to know how to unwrap the `CipherTextWrapper` depending the chunk size you configured. + +Uppon receiving encrypted message, you can decrypt it. +You have to reconstruct the wrapper before decrypting. ```dart -Uint8List iv = encrypted.iv; -Uint8List data = encrypted.data; -Uint8List tag = encrypted.tag; +CipherTextWrapper wrapper = CipherTextWrapper.fromBytes( + data, + ivLength: AESMode.gcm.ivLength, + tagLength: AESMode.gcm.tagLength, +); ``` -Note that data and tag are costly to access, so you should only use them if you need to ! \ No newline at end of file +Then, you can decrypt your message. + +```dart +Uint8List message = await cipher.decrypt(wrapper); +``` \ No newline at end of file From 4fde7d0aa22a1ca0fa20cbed5c754abe38ec6023 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Wed, 1 Jun 2022 13:47:38 +0200 Subject: [PATCH 39/39] ci: add badge --- .drone.yml | 6 +++++- README.md | 4 ++++ packages/native_crypto/README.md | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 0f2f2a6..74ade5c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,4 +10,8 @@ steps: - flutter doctor - flutter pub global activate melos - melos bs - - melos run test:all \ No newline at end of file + - melos run test:all + +trigger: + branch: + - master \ No newline at end of file diff --git a/README.md b/README.md index decc847..507b7ec 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Maintained with Melos + + +Build Status +

--- diff --git a/packages/native_crypto/README.md b/packages/native_crypto/README.md index decc847..507b7ec 100644 --- a/packages/native_crypto/README.md +++ b/packages/native_crypto/README.md @@ -11,6 +11,10 @@ Maintained with Melos + + +Build Status +

---