diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml new file mode 100644 index 0000000..51fc4a5 --- /dev/null +++ b/.github/workflows/flutter.yml @@ -0,0 +1,45 @@ +name: Flutter CI + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out the repository + uses: actions/checkout@v2 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.6' + + - name: Cache Flutter dependencies + uses: actions/cache@v3 + with: + path: | + ~/.pub-cache + flutter/bin/cache + # key: ${{ runner.os }}-pub-cache-${{ hashFiles('pubspec.yaml') }} + key: ${{ runner.os }}-flutter-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-flutter- + + - name: Install dependencies + run: flutter pub get + + - name: Run tests & generate coverage + run: flutter test --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + files: ./coverage/lcov.info + flags: flutter + name: code-coverage-report + token: 2f59bcbb-7263-4e0c-9a37-e710e39705bb + fail_ci_if_error: true diff --git a/LICENSE b/LICENSE index ba75c69..6dd70ce 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,20 @@ -TODO: Add your license here. +MIT License + +Copyright (c) 2024 LamNguyen176 +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 index e35381d..fa272a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,135 @@ # flutter_crypto_algorithm -A new Flutter plugin project. +[![Native language](https://img.shields.io/badge/native_language-Kotlin_&_Swift-green)](https://pub.dev/packages/flutter_crypto_algorithm) +[![Code cov](https://codecov.io/gh/LamNguyen17/flutter_crypto_algorithm/branch/master/graph/badge.svg)](https://app.codecov.io/github/LamNguyen17/flutter_crypto_algorithm/blob/master/lib) +[![License](https://img.shields.io/badge/license-MIT-8A2BE2)](https://github.com/LamNguyen17/flutter_crypto_algorithm/blob/master/LICENSE) +[![Author](https://img.shields.io/badge/author-Forest_Nguyen-f59642)](https://github.com/LamNguyen17) + +A Flutter package for secure encryption algorithms, providing efficient tools for data protection and encryption operations + +## Installation +Run this command with Flutter: +```sh +flutter pub add encryption_algorithm +``` + +## Usage +### Methods +#### 🚀 AES +```dart +import 'package:flutter_crypto_algorithm/flutter_crypto_algorithm.dart'; +``` +```dart +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _encrypt = ''; + String _decrypt = ''; + final _crypto = Crypto(); + + @override + void initState() { + super.initState(); + crypto(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future crypto() async { + String encrypt; + String decrypt; + try { + encrypt = + await _crypto.encrypt('Hello123', 'Hello') ?? + 'Unknown encrypt'; + decrypt = await _crypto.decrypt(encrypt, 'Hello') ?? + 'Unknown decrypt'; + } on PlatformException { + encrypt = 'Failed encrypt.'; + decrypt = 'Failed decrypt.'; + } + if (!mounted) return; + setState(() { + _encrypt = encrypt; + _decrypt = decrypt; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Flutter Crypto Algorithm'), + ), + body: SingleChildScrollView( + child: Column( + children: [ + Section(title: 'AES', children: [ + _buildText('Encrypt: ', _encrypt), + _buildText('Decrypt: ', _decrypt), + ]), + ], + ), + ), + ), + ); + } + + Widget _buildText(String label, String value) { + return Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 2, + TextSpan( + text: label, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red), + children: [ + TextSpan( + text: value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black), + ), + ], + ), + ); + } +} + +class Section extends StatelessWidget { + final String title; + final List children; + + const Section({super.key, required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ...children, + ], + ), + ); + } +} +``` ## Getting Started diff --git a/android/build.gradle b/android/build.gradle index 878bef0..27c9af5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -50,6 +50,8 @@ android { } dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' } diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..6689b85 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/src/main/kotlin/com/example/flutter_crypto_algorithm/AesAlgorithm.kt b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/AesAlgorithm.kt new file mode 100644 index 0000000..2527de5 --- /dev/null +++ b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/AesAlgorithm.kt @@ -0,0 +1,35 @@ +package com.example.flutter_crypto_algorithm + +import javax.crypto.* +import android.util.* + +class AesAlgorithm { + private val cryptoHelper = CryptoHelper() + + fun encrypt(value: String, privateKey: String, ivKey: String?): String { + val ivSpec = cryptoHelper.genIvKey(ivKey) + val secretKey = cryptoHelper.genSecretKey(CryptoHelper.AES.ALGORITHM, privateKey) + val cipher = Cipher.getInstance(CryptoHelper.AES.TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) + val encryptedBytes = cipher.doFinal(value.toByteArray()) + return Base64.encodeToString(encryptedBytes, Base64.DEFAULT) + } + + fun decrypt(value: String, privateKey: String, ivKey: String?): String { + return try { + val ivSpec = cryptoHelper.genIvKey(ivKey) + val secretKey = cryptoHelper.genSecretKey(CryptoHelper.AES.ALGORITHM, privateKey) + val decodedBytes = Base64.decode(value, Base64.DEFAULT) + val cipher = Cipher.getInstance(CryptoHelper.AES.TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) + val decryptedBytes = cipher.doFinal(decodedBytes) + String(decryptedBytes, Charsets.UTF_8) + } catch (e: BadPaddingException) { + CryptoHelper.CONSTANTS.ERR_VALID_KEY + } catch (e: IllegalBlockSizeException) { + CryptoHelper.CONSTANTS.ERR_INCORRECT_BLOCK_SIZE + } catch (e: Exception) { + CryptoHelper.CONSTANTS.ERR_UNEXPECTED_ERROR + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/example/flutter_crypto_algorithm/CryptoHelper.kt b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/CryptoHelper.kt new file mode 100644 index 0000000..81f9569 --- /dev/null +++ b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/CryptoHelper.kt @@ -0,0 +1,52 @@ +package com.example.flutter_crypto_algorithm + +import android.util.Base64 +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.* + +class CryptoHelper { + object CONSTANTS { + const val ERR_VALID_KEY = "Invalid key or corrupted data" + const val ERR_INCORRECT_BLOCK_SIZE = "Incorrect block size" + const val ERR_UNEXPECTED_ERROR = "Unexpected error" + const val KEY_256 = 256 + } + + object AES { + const val ALGORITHM = "AES" + const val TRANSFORMATION = "AES/CBC/PKCS5PADDING" + val EMPTY_IV_SPEC = IvParameterSpec(ByteArray(16) { 0x00.toByte() }) + val EMPTY_SECRET_KEY = SecretKeySpec(ByteArray(32) { 0x00 }, "AES") + } + + private fun genExactlyCharacterLengthKey(key: String, length: Int): String { + val bytes = key.toByteArray(Charsets.UTF_8) + val hexString = bytes.joinToString("") { "%02x".format(it) } + val hexLength = hexString.take(length).padEnd(length, '0'); + return hexLength; + } + + fun genSecretKey(algorithmType: String, secretKey: String?): SecretKey { + return when (algorithmType) { + AES.ALGORITHM -> { + if (secretKey != null) { + val transform = genExactlyCharacterLengthKey(secretKey, 32) + SecretKeySpec(transform.toByteArray(), AES.ALGORITHM) + } else { + AES.EMPTY_SECRET_KEY + } + } + else -> AES.EMPTY_SECRET_KEY + } + } + + fun genIvKey(ivKey: String?): IvParameterSpec { + return if (ivKey != null) { + val transform = genExactlyCharacterLengthKey(ivKey, 16) + IvParameterSpec(transform.toByteArray()) + } else { + AES.EMPTY_IV_SPEC + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/example/flutter_crypto_algorithm/FlutterCryptoAlgorithmPlugin.kt b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/FlutterCryptoAlgorithmPlugin.kt index db8861b..0785894 100644 --- a/android/src/main/kotlin/com/example/flutter_crypto_algorithm/FlutterCryptoAlgorithmPlugin.kt +++ b/android/src/main/kotlin/com/example/flutter_crypto_algorithm/FlutterCryptoAlgorithmPlugin.kt @@ -7,29 +7,94 @@ 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 kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch -/** FlutterCryptoAlgorithmPlugin */ -class FlutterCryptoAlgorithmPlugin: 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(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_crypto_algorithm") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() +class FlutterCryptoAlgorithmPlugin : FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + private lateinit var aesAlgorithm: AesAlgorithm + private lateinit var cryptoHelper: CryptoHelper + + private companion object { + const val CRYPTO_CHANNEL = "flutter_crypto_algorithm" + const val ENCRYPT_METHOD = "encrypt" + const val DECRYPT_METHOD = "decrypt" + } + + private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + aesAlgorithm = AesAlgorithm() + cryptoHelper = CryptoHelper() + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CRYPTO_CHANNEL) + channel.setMethodCallHandler(this) } - } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + ENCRYPT_METHOD -> { + val value = call.argument("value") + val privateKey = call.argument("privateKey") + val ivKey = call.argument("ivKey") ?: "" + if (value != null && privateKey != null) { + activityScope.launch { + flow { + val encryptedData = aesAlgorithm.encrypt(value, privateKey, ivKey) + emit(encryptedData) + }.flowOn(Dispatchers.IO).catch { + result.error( + CryptoHelper.CONSTANTS.ERR_VALID_KEY, + "Data to encrypt is null", + null + ) + }.collect { encryptData -> + result.success(encryptData) + } + } + } else { + result.error( + CryptoHelper.CONSTANTS.ERR_VALID_KEY, + "Data to encrypt is null", + null + ) + } + } + + DECRYPT_METHOD -> { + val value = call.argument("value") + val privateKey = call.argument("privateKey") + val ivKey = call.argument("ivKey") ?: "" + if (value != null && privateKey != null) { + activityScope.launch { + flow { + val decryptData = aesAlgorithm.decrypt(value, privateKey, ivKey) + emit(decryptData) + }.flowOn(Dispatchers.IO).catch { + result.error( + CryptoHelper.CONSTANTS.ERR_VALID_KEY, + "Data to decrypt is null", + null + ) + }.collect { decryptData -> + result.success(decryptData) + } + } + } else { + result.error( + CryptoHelper.CONSTANTS.ERR_VALID_KEY, + "Data to decrypt is null", + null + ) + } + } + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + // Cancel the CoroutineScope to avoid memory leaks + activityScope.cancel() + } } diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..28ba097 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,34 @@ +SF:lib/flutter_crypto_algorithm.dart +DA:4,1 +DA:5,1 +DA:6,1 +DA:9,1 +DA:10,1 +DA:11,1 +LF:6 +LH:6 +end_of_record +SF:lib/flutter_crypto_algorithm_platform_interface.dart +DA:7,6 +DA:9,6 +DA:11,3 +DA:16,2 +DA:21,1 +DA:22,2 +LF:6 +LH:6 +end_of_record +SF:lib/flutter_crypto_algorithm_method_channel.dart +DA:13,1 +DA:17,3 +DA:18,1 +DA:19,1 +DA:20,0 +DA:27,1 +DA:31,3 +DA:32,1 +DA:33,1 +DA:34,0 +LF:10 +LH:8 +end_of_record diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index e523f54..c738a5e 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -16,10 +16,11 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getPlatformVersion test', (WidgetTester tester) async { - final FlutterCryptoAlgorithm plugin = FlutterCryptoAlgorithm(); - final String? version = await plugin.getPlatformVersion(); + final Crypto plugin = Crypto(); + final String? encryptData = await plugin.encrypt('Hello123', 'Hello'); + // final String? version = await plugin.getPlatformVersion(); // The version string depends on the host platform running the test, so // just assert that some non-empty string is returned. - expect(version?.isNotEmpty, true); + expect(encryptData?.isNotEmpty, true); }); } diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.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__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..c4e6496 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,39 @@ +PODS: + - CryptoSwift (1.8.3) + - Flutter (1.0.0) + - flutter_crypto_algorithm (0.0.1): + - CryptoSwift (= 1.8.3) + - Flutter + - RxSwift (= 6.7.1) + - integration_test (0.0.1): + - Flutter + - RxSwift (6.7.1) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_crypto_algorithm (from `.symlinks/plugins/flutter_crypto_algorithm/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +SPEC REPOS: + trunk: + - CryptoSwift + - RxSwift + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_crypto_algorithm: + :path: ".symlinks/plugins/flutter_crypto_algorithm/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_crypto_algorithm: e0b5068b94eb0b67f3e86413a9a4bcc166650d5b + integration_test: 13825b8a9334a850581300559b8839134b124670 + RxSwift: b9a93a26031785159e11abd40d1a55bcb8057e52 + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 83b3385..ac8050c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,9 +11,11 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7A527D5497784DEDACAC9248 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07B9FF9AB471833D8CBA33F5 /* Pods_Runner.framework */; }; 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 */; }; + B26EBC66CECBEE9F86850458 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96C2C2DECC7F545D5D9B160D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,14 +42,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 07B9FF9AB471833D8CBA33F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5268A135E6281D556367B266 /* 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 = ""; }; + 5E649F149B2BB34D39D7C221 /* 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 = ""; }; 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 = ""; }; + 96C2C2DECC7F545D5D9B160D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; @@ -55,13 +61,26 @@ 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 = ""; }; + C52C87975794A5A9B04C1229 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DE8D221F96FCC502A23CFF56 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EA6F942AA36D9DBDF62C33E1 /* 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 = ""; }; + EB91AC448C84DB87BE82625E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5F03FF1F8FCC64FEEA535523 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B26EBC66CECBEE9F86850458 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7A527D5497784DEDACAC9248 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +113,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + C7EAEA6A2A36B86B97DA8A07 /* Pods */, + B74C51CD52D83462C0698897 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +142,29 @@ path = Runner; sourceTree = ""; }; + B74C51CD52D83462C0698897 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 07B9FF9AB471833D8CBA33F5 /* Pods_Runner.framework */, + 96C2C2DECC7F545D5D9B160D /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C7EAEA6A2A36B86B97DA8A07 /* Pods */ = { + isa = PBXGroup; + children = ( + EA6F942AA36D9DBDF62C33E1 /* Pods-Runner.debug.xcconfig */, + 5E649F149B2BB34D39D7C221 /* Pods-Runner.release.xcconfig */, + 5268A135E6281D556367B266 /* Pods-Runner.profile.xcconfig */, + DE8D221F96FCC502A23CFF56 /* Pods-RunnerTests.debug.xcconfig */, + C52C87975794A5A9B04C1229 /* Pods-RunnerTests.release.xcconfig */, + EB91AC448C84DB87BE82625E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 5FA17AECB70E993CB5B7A72A /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 5F03FF1F8FCC64FEEA535523 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + D94AD727B417D3BC7D991EA1 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C85E79AEDC6253378385178D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -238,6 +286,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5FA17AECB70E993CB5B7A72A /* [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-RunnerTests-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; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +323,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C85E79AEDC6253378385178D /* [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; + }; + D94AD727B417D3BC7D991EA1 /* [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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DE8D221F96FCC502A23CFF56 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C52C87975794A5A9B04C1229 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EB91AC448C84DB87BE82625E /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..497328a 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -70,6 +70,13 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard index f3c2851..7e6915b 100644 --- a/example/ios/Runner/Base.lproj/Main.storyboard +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 72fdc28..68657f6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; import 'dart:async'; - +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + import 'package:flutter_crypto_algorithm/flutter_crypto_algorithm.dart'; void main() { @@ -16,34 +16,38 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; - final _flutterCryptoAlgorithmPlugin = FlutterCryptoAlgorithm(); + String _encrypt = ''; + String _decrypt = ''; + final _crypto = Crypto(); @override void initState() { super.initState(); - initPlatformState(); + crypto(); } // 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. + Future crypto() async { + String encrypt; + String decrypt; try { - platformVersion = - await _flutterCryptoAlgorithmPlugin.getPlatformVersion() ?? 'Unknown platform version'; + encrypt = + await _crypto.encrypt('Hello123', 'Hello') ?? + 'Unknown encrypt'; + decrypt = await _crypto.decrypt(encrypt, 'Hello') ?? + 'Unknown decrypt'; } on PlatformException { - platformVersion = 'Failed to get platform version.'; + encrypt = 'Failed encrypt.'; + decrypt = 'Failed decrypt.'; } - // 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; + print('encrypt: $encrypt'); + setState(() { - _platformVersion = platformVersion; + _encrypt = encrypt; + _decrypt = decrypt; }); } @@ -52,12 +56,64 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('Plugin example app'), + title: const Text('Flutter Crypto Algorithm'), ), - body: Center( - child: Text('Running on: $_platformVersion\n'), + body: SingleChildScrollView( + child: Column( + children: [ + Section(title: 'AES', children: [ + _buildText('Encrypt: ', _encrypt), + _buildText('Decrypt: ', _decrypt), + ]), + ], + ), ), ), ); } + + Widget _buildText(String label, String value) { + return Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 2, + TextSpan( + text: label, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red), + children: [ + TextSpan( + text: value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black), + ), + ], + ), + ); + } +} + +class Section extends StatelessWidget { + final String title; + final List children; + + const Section({super.key, required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ...children, + ], + ), + ); + } } diff --git a/ios/Classes/AesAlgorithm.swift b/ios/Classes/AesAlgorithm.swift new file mode 100644 index 0000000..3639cac --- /dev/null +++ b/ios/Classes/AesAlgorithm.swift @@ -0,0 +1,37 @@ +import Foundation +import CryptoSwift + +class AesAlgorithm { + private let cryptoHelper = CryptoHelper() + + func encrypt(value: String, privateKey: String, ivKey: String?) -> String? { + let iv = cryptoHelper.genAESIvKey(ivKey: ivKey) + let key = cryptoHelper.genAESSecretKey(secretKey: privateKey) + + do { + let aes = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs5) + let encryptedBytes = try aes.encrypt(Array(value.utf8)) + let encryptedData = Data(encryptedBytes) + return encryptedData.base64EncodedString() + } catch { + print("Encryption error: \(error)") + return nil + } + } + + func decrypt(value: String, privateKey: String, ivKey: String?) -> String { + let iv = cryptoHelper.genAESIvKey(ivKey: ivKey) + let key = cryptoHelper.genAESSecretKey(secretKey: privateKey) + do { + let aes = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs5) + if let data = Data(base64Encoded: value) { + let decryptedBytes = try aes.decrypt([UInt8](data)) + return String(bytes: decryptedBytes, encoding: .utf8) ?? CryptoHelper.errValidKey + } else { + return CryptoHelper.errValidKey + } + } catch { + return CryptoHelper.errUnexpectedError + } + } +} \ No newline at end of file diff --git a/ios/Classes/CryptoHelper.swift b/ios/Classes/CryptoHelper.swift new file mode 100644 index 0000000..4f63a6a --- /dev/null +++ b/ios/Classes/CryptoHelper.swift @@ -0,0 +1,58 @@ +import Foundation +import CryptoSwift + +class CryptoHelper { + enum CryptoType { + case encrypt + case decrypt + } + struct Aes { + static let algorithm = "AES" + static let emptyIvSpec = Array(repeating: 0x00, count: 16) + static let emptySecretKey = Array(repeating: 0x00, count: 32) + } + internal static let errValuePrivateKeyNull = "Value and privateKey must not be null or empty" + internal static let errValuePrivateRequired = "Value and privateKey are required" + internal static let errValidKey = "Invalid key or corrupted data" + internal static let errIncorrectBlockSize = "Incorrect block size" + internal static let errUnexpectedError = "Unexpected error" + + private func genExactlyCharacterLengthKey(key: String, length: Int) -> String { + let bytes = Array(key.utf8) + let hexString = bytes.map { String(format: "%02x", $0) }.joined() + let hexLength = hexString.prefix(length).padding(toLength: length, withPad: "0", startingAt: 0) + return String(hexLength) + } + + func genSecretKey(algorithmType: String, secretKey: String?) -> Array { + switch algorithmType { + case CryptoHelper.Aes.algorithm: + if let secretKey = secretKey { + let transform = genExactlyCharacterLengthKey(key: secretKey, length: 32) + return Array(transform.utf8) + } else { + return Aes.emptySecretKey + } + default: + return Aes.emptySecretKey + } + } + + func genAESSecretKey(secretKey: String?) -> Array { + if let secretKey = secretKey { + let transform = genExactlyCharacterLengthKey(key: secretKey, length: 32) + return Array(transform.utf8) + } else { + return Aes.emptySecretKey + } + } + + func genAESIvKey(ivKey: String?) -> Array { + if let ivKey = ivKey { + let transform = genExactlyCharacterLengthKey(key: ivKey, length: 16) + return Array(transform.utf8) + } else { + return Aes.emptyIvSpec + } + } +} diff --git a/ios/Classes/FlutterCryptoAlgorithmPlugin.swift b/ios/Classes/FlutterCryptoAlgorithmPlugin.swift index abf8664..18a20ff 100644 --- a/ios/Classes/FlutterCryptoAlgorithmPlugin.swift +++ b/ios/Classes/FlutterCryptoAlgorithmPlugin.swift @@ -1,19 +1,92 @@ import Flutter import UIKit +import RxSwift -public class FlutterCryptoAlgorithmPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_crypto_algorithm", binaryMessenger: registrar.messenger()) - let instance = FlutterCryptoAlgorithmPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } +let CRYPTO_CHANNEL = "flutter_crypto_algorithm" +let ENCRYPT_METHOD = "encrypt" +let DECRYPT_METHOD = "decrypt" - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("iOS " + UIDevice.current.systemVersion) - default: - result(FlutterMethodNotImplemented) +public class FlutterCryptoAlgorithmPlugin: NSObject, FlutterPlugin { + private let disposeBag = DisposeBag() // For RxSwift memory management + private let cryptoHelper = CryptoHelper() + private let aesAlgorithm = AesAlgorithm() + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: CRYPTO_CHANNEL, binaryMessenger: registrar.messenger()) + let instance = FlutterCryptoAlgorithmPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + private func cryptoMethod(_ algorithmType: String, _ cryptoType: CryptoHelper.CryptoType, _ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + if let arguments = call.arguments as? [String: Any] { + if let value = arguments["value"] as? String, !value.isEmpty, + let privateKey = arguments["privateKey"] as? String, !privateKey.isEmpty { + let ivKey = arguments["ivKey"] as? String + activityLaunch(algorithmType: algorithmType, cryptoType: cryptoType, + value: value, privateKey: privateKey, ivKey: ivKey, result: result) + } else { + result(NSError(domain: CryptoHelper.errValuePrivateKeyNull, code: 0, userInfo: nil)) + } + } else { + result(NSError(domain: CryptoHelper.errValuePrivateRequired, code: 0, userInfo: nil)) + } + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case ENCRYPT_METHOD: + cryptoMethod(CryptoHelper.Aes.algorithm, CryptoHelper.CryptoType.encrypt, call, result) + case DECRYPT_METHOD: + cryptoMethod(CryptoHelper.Aes.algorithm, CryptoHelper.CryptoType.decrypt, call, result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func activityLaunch(algorithmType: String, cryptoType: CryptoHelper.CryptoType, value: String, privateKey: String, ivKey: String?, result: @escaping FlutterResult) { + Observable.just(value) + .flatMap { data -> Observable in + switch algorithmType { + case CryptoHelper.Aes.algorithm: + switch cryptoType { + case CryptoHelper.CryptoType.encrypt: + return Observable.create { observer in + guard let encryptedData = self.aesAlgorithm.encrypt(value: data, privateKey: privateKey, ivKey: ivKey) else { + observer.onError(NSError(domain: CryptoHelper.errValidKey, code: 0, userInfo: nil)) + return Disposables.create() + } + observer.onNext(encryptedData) + observer.onCompleted() + return Disposables.create() + } + case CryptoHelper.CryptoType.decrypt: + return Observable.create { observer in + let decryptedData = self.aesAlgorithm.decrypt(value: data, privateKey: privateKey, ivKey: ivKey) + if decryptedData == CryptoHelper.errValidKey || + decryptedData == CryptoHelper.errIncorrectBlockSize || + decryptedData == CryptoHelper.errUnexpectedError { + observer.onError(NSError(domain: CryptoHelper.errValidKey, code: 0, userInfo: [NSLocalizedDescriptionKey: decryptedData])) + } else { + observer.onNext(decryptedData) + observer.onCompleted() + } + return Disposables.create() + } + } + default: + return Observable.error(NSError(domain: CryptoHelper.errValidKey, code: 0, userInfo: nil)) + } + } + .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .observe(on: MainScheduler.instance) // Observe result on main thread + .subscribe( + onNext: { data in + result(data) + }, + onError: { error in + result(error.localizedDescription) + } + ) + .disposed(by: disposeBag) } - } } diff --git a/ios/flutter_crypto_algorithm.podspec b/ios/flutter_crypto_algorithm.podspec index 67cfbd1..07ff264 100644 --- a/ios/flutter_crypto_algorithm.podspec +++ b/ios/flutter_crypto_algorithm.podspec @@ -20,4 +20,7 @@ A new Flutter plugin project. # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' + # Flutter Setup dependency ios + s.dependency "CryptoSwift", "1.8.3" + s.dependency "RxSwift", "6.7.1" end diff --git a/lib/flutter_crypto_algorithm.dart b/lib/flutter_crypto_algorithm.dart index 8e59380..34ee3da 100644 --- a/lib/flutter_crypto_algorithm.dart +++ b/lib/flutter_crypto_algorithm.dart @@ -1,8 +1,13 @@ - import 'flutter_crypto_algorithm_platform_interface.dart'; -class FlutterCryptoAlgorithm { - Future getPlatformVersion() { - return FlutterCryptoAlgorithmPlatform.instance.getPlatformVersion(); +class Crypto { + Future encrypt(String value, String privateKey, {String? ivKey}) { + return FlutterCryptoAlgorithmPlatform.instance + .encrypt(value, privateKey, ivKey); + } + + Future decrypt(String value, String privateKey, {String? ivKey}) { + return FlutterCryptoAlgorithmPlatform.instance + .decrypt(value, privateKey, ivKey); } } diff --git a/lib/flutter_crypto_algorithm_method_channel.dart b/lib/flutter_crypto_algorithm_method_channel.dart index 3e7de76..7a8a59d 100644 --- a/lib/flutter_crypto_algorithm_method_channel.dart +++ b/lib/flutter_crypto_algorithm_method_channel.dart @@ -4,14 +4,37 @@ import 'package:flutter/services.dart'; import 'flutter_crypto_algorithm_platform_interface.dart'; /// An implementation of [FlutterCryptoAlgorithmPlatform] that uses method channels. -class MethodChannelFlutterCryptoAlgorithm extends FlutterCryptoAlgorithmPlatform { +class MethodChannelFlutterCryptoAlgorithm + extends FlutterCryptoAlgorithmPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('flutter_crypto_algorithm'); @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; + Future encrypt( + String value, String privateKey, String? ivKey) async { + try { + return await methodChannel.invokeMethod('encrypt', { + 'value': value, + 'privateKey': privateKey, + if (ivKey != null) 'ivKey': ivKey + }); + } catch (e) { + return null; + } + } + + @override + Future decrypt( + String value, String privateKey, String? ivKey) async { + try { + return await methodChannel.invokeMethod('decrypt', { + 'value': value, + 'privateKey': privateKey, + if (ivKey != null) 'ivKey': ivKey + }); + } catch (e) { + return null; + } } } diff --git a/lib/flutter_crypto_algorithm_platform_interface.dart b/lib/flutter_crypto_algorithm_platform_interface.dart index ecb4d5e..ea5a4ae 100644 --- a/lib/flutter_crypto_algorithm_platform_interface.dart +++ b/lib/flutter_crypto_algorithm_platform_interface.dart @@ -23,7 +23,7 @@ abstract class FlutterCryptoAlgorithmPlatform extends PlatformInterface { _instance = instance; } - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } + Future encrypt(String value, String privateKey, String? ivKey); + + Future decrypt(String value, String privateKey, String? ivKey); } diff --git a/pubspec.yaml b/pubspec.yaml index 2446ad3..9c50f77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,9 @@ name: flutter_crypto_algorithm -description: "A new Flutter plugin project." -version: 0.0.1 -homepage: +description: "A Flutter package for secure encryption algorithms, providing efficient tools for data protection and encryption operations" +version: 0.1.0 +repository: https://github.com/LamNguyen17/flutter_crypto_algorithm +issue_tracker: https://github.com/LamNguyen17/flutter_crypto_algorithm/issues +homepage: https://github.com/LamNguyen17/flutter_crypto_algorithm environment: sdk: '>=3.3.4 <4.0.0' diff --git a/test/flutter_crypto_algorithm_method_channel_test.dart b/test/flutter_crypto_algorithm_method_channel_test.dart index c3a9931..7b9d494 100644 --- a/test/flutter_crypto_algorithm_method_channel_test.dart +++ b/test/flutter_crypto_algorithm_method_channel_test.dart @@ -12,7 +12,12 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( channel, (MethodCall methodCall) async { - return '42'; + switch (methodCall.method) { + case 'encrypt': + return 'IVVM1yR+Cn2Bbxo7RnkAQw=='; + case 'decrypt': + return 'Hello123'; + } }, ); }); @@ -21,7 +26,11 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); }); - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); + test('encrypt', () async { + expect(await platform.encrypt('Hello123', 'Hello', null), 'IVVM1yR+Cn2Bbxo7RnkAQw=='); + }); + + test('decrypt', () async { + expect(await platform.decrypt('IVVM1yR+Cn2Bbxo7RnkAQw==', 'Hello', null), 'Hello123'); }); } diff --git a/test/flutter_crypto_algorithm_test.dart b/test/flutter_crypto_algorithm_test.dart index 7bc1d32..edc09e6 100644 --- a/test/flutter_crypto_algorithm_test.dart +++ b/test/flutter_crypto_algorithm_test.dart @@ -9,7 +9,10 @@ class MockFlutterCryptoAlgorithmPlatform implements FlutterCryptoAlgorithmPlatform { @override - Future getPlatformVersion() => Future.value('42'); + Future encrypt(String value, String privateKey, String? ivKey) => Future.value('IVVM1yR+Cn2Bbxo7RnkAQw=='); + + @override + Future decrypt(String value, String privateKey, String? ivKey) => Future.value('Hello123'); } void main() { @@ -19,11 +22,17 @@ void main() { expect(initialPlatform, isInstanceOf()); }); - test('getPlatformVersion', () async { - FlutterCryptoAlgorithm flutterCryptoAlgorithmPlugin = FlutterCryptoAlgorithm(); + test('encrypt', () async { + Crypto crypto = Crypto(); MockFlutterCryptoAlgorithmPlatform fakePlatform = MockFlutterCryptoAlgorithmPlatform(); FlutterCryptoAlgorithmPlatform.instance = fakePlatform; + expect(await crypto.encrypt('Hello123', 'Hello'), 'IVVM1yR+Cn2Bbxo7RnkAQw=='); + }); - expect(await flutterCryptoAlgorithmPlugin.getPlatformVersion(), '42'); + test('decrypt', () async { + Crypto crypto = Crypto(); + MockFlutterCryptoAlgorithmPlatform fakePlatform = MockFlutterCryptoAlgorithmPlatform(); + FlutterCryptoAlgorithmPlatform.instance = fakePlatform; + expect(await crypto.decrypt('IVVM1yR+Cn2Bbxo7RnkAQw==', 'Hello'), 'Hello123'); }); }