From 6b8bde098c3d2dbe5ddc5b103e2f7fd8ecdc88c5 Mon Sep 17 00:00:00 2001 From: t0theRANCH Date: Sun, 4 Feb 2024 16:27:18 -0500 Subject: [PATCH 1/2] uses native ios and android keyring/keystore functionality to store sensitive data --- MANIFEST.in | 1 + examples/keystore/buildozer.spec | 452 ++++++++++++++++++ examples/keystore/ios_readme.md | 55 +++ examples/keystore/main.py | 59 +++ plyer/facades/keystore.py | 12 +- plyer/platforms/android/keystore.py | 131 ++++- plyer/platforms/ios/keystore.py | 73 ++- plyer/platforms/linux/keystore.py | 4 +- plyer/platforms/macosx/keystore.py | 4 +- plyer/platforms/win/keystore.py | 4 +- .../Headers/KeychainBridge.h | 9 + .../tools/KeychainBridge.framework/Info.plist | Bin 0 -> 758 bytes .../KeychainBridge.framework/KeychainBridge | Bin 0 -> 88352 bytes .../Modules/module.modulemap | 6 + .../_CodeSignature/CodeResources | 124 +++++ setup.py | 3 +- 16 files changed, 909 insertions(+), 28 deletions(-) create mode 100644 examples/keystore/buildozer.spec create mode 100644 examples/keystore/ios_readme.md create mode 100644 examples/keystore/main.py create mode 100755 plyer/tools/KeychainBridge.framework/Headers/KeychainBridge.h create mode 100755 plyer/tools/KeychainBridge.framework/Info.plist create mode 100755 plyer/tools/KeychainBridge.framework/KeychainBridge create mode 100755 plyer/tools/KeychainBridge.framework/Modules/module.modulemap create mode 100755 plyer/tools/KeychainBridge.framework/_CodeSignature/CodeResources diff --git a/MANIFEST.in b/MANIFEST.in index be9d07a6f..803196c9a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include *README.rst *LICENSE include *CHANGELOG.md +include plyer/tools/KeychainBridge.framework diff --git a/examples/keystore/buildozer.spec b/examples/keystore/buildozer.spec new file mode 100644 index 000000000..66fe81540 --- /dev/null +++ b/examples/keystore/buildozer.spec @@ -0,0 +1,452 @@ +[app] + +# (str) Title of your application +title = keystore example + +# (str) Package name +package.name = myapp + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.test + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas,whl + +# (list) List of inclusions using pattern matching +#source.include_patterns = assets/*,images/*.png + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin, venv + +# (list) List of exclusions using pattern matching +# Do not prefix with './' +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = 0.1 + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma separated e.g. requirements = sqlite3,kivy +requirements = python3,kivy,./plyer-2.2.0.dev0-py3-none-any.whl + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +#requirements.source.plyer = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (list) Supported orientations +# Valid options are: landscape, portrait, portrait-reverse or landscape-reverse +orientation = portrait + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# change the major version of python used by the app +osx.python_version = 3 + +# Kivy version to use +osx.kivy_version = 1.9.1 + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 0 + +# (string) Presplash background color (for android toolchain) +# Supported formats are: #RRGGBB #AARRGGBB or one of the following names: +# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, +# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, +# olive, purple, silver, teal. +#android.presplash_color = #FFFFFF + +# (string) Presplash animation using Lottie format. +# see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ +# for general documentation. +# Lottie files can be created using various tools, like Adobe After Effect or Synfig. +#android.presplash_lottie = "path/to/lottie/file.json" + +# (str) Adaptive icon of the application (used if Android API level is 26+ at runtime) +#icon.adaptive_foreground.filename = %(source.dir)s/data/icon_fg.png +#icon.adaptive_background.filename = %(source.dir)s/data/icon_bg.png + +# (list) Permissions +# (See https://python-for-android.readthedocs.io/en/latest/buildoptions/#build-options-1 for all the supported syntaxes and properties) +#android.permissions = android.permission.INTERNET, (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18) + +# (list) features (adds uses-feature -tags to manifest) +#android.features = android.hardware.usb.host + +# (int) Target Android API, should be as high as possible. +#android.api = 31 + +# (int) Minimum API your APK / AAB will support. +#android.minapi = 21 + +# (int) Android SDK version to use +#android.sdk = 20 + +# (str) Android NDK version to use +#android.ndk = 23b + +# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. +#android.ndk_api = 21 + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (bool) If True, then automatically accept SDK license +# agreements. This is intended for automation only. If set to False, +# the default, you will be shown the license when first running +# buildozer. +# android.accept_sdk_license = False + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.kivy.android.PythonActivity + +# (str) Full name including package path of the Java class that implements Android Activity +# use that parameter together with android.entrypoint to set custom Java class instead of PythonActivity +#android.activity_class_name = org.kivy.android.PythonActivity + +# (str) Extra xml to write directly inside the element of AndroidManifest.xml +# use that parameter to provide a filename from where to load your custom XML code +#android.extra_manifest_xml = ./src/android/extra_manifest.xml + +# (str) Extra xml to write directly inside the tag of AndroidManifest.xml +# use that parameter to provide a filename from where to load your custom XML arguments: +#android.extra_manifest_application_arguments = ./src/android/extra_manifest_application_arguments.xml + +# (str) Full name including package path of the Java class that implements Python Service +# use that parameter to set custom Java class which extends PythonService +#android.service_class_name = org.kivy.android.PythonService + +# (str) Android app theme, default is ok for Kivy-based app +# android.apptheme = "@android:style/Theme.NoTitleBar" + +# (list) Pattern to whitelist for the whole project +#android.whitelist = + +# (str) Path to a custom whitelist file +#android.whitelist_src = + +# (str) Path to a custom blacklist file +#android.blacklist_src = + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (list) Android AAR archives to add +#android.add_aars = + +# (list) Put these files or directories in the apk assets directory. +# Either form may be used, and assets need not be in 'source.include_exts'. +# 1) android.add_assets = source_asset_relative_path +# 2) android.add_assets = source_asset_path:destination_asset_relative_path +#android.add_assets = + +# (list) Put these files or directories in the apk res directory. +# The option may be used in three ways, the value may contain one or zero ':' +# Some examples: +# 1) A file to add to resources, legal resource names contain ['a-z','0-9','_'] +# android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png +# 2) A directory, here 'legal_icons' must contain resources of one kind +# android.add_resources = legal_icons:drawable +# 3) A directory, here 'legal_resources' must contain one or more directories, +# each of a resource kind: drawable, xml, etc... +# android.add_resources = legal_resources +#android.add_resources = + +# (list) Gradle dependencies to add +#android.gradle_dependencies = + +# (bool) Enable AndroidX support. Enable when 'android.gradle_dependencies' +# contains an 'androidx' package, or any package from Kotlin source. +# android.enable_androidx requires android.api >= 28 +#android.enable_androidx = True + +# (list) add java compile options +# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option +# see https://developer.android.com/studio/write/java8-support for further information +# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" + +# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} +# please enclose in double quotes +# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" +#android.add_gradle_repositories = + +# (list) packaging options to add +# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html +# can be necessary to solve conflicts in gradle_dependencies +# please enclose in double quotes +# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" +#android.add_packaging_options = + +# (list) Java classes to add as activities to the manifest. +#android.add_activities = com.example.ExampleActivity + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (list) Copy these files to src/main/res/xml/ (used for example with intent-filters) +#android.res_xml = PATH_TO_FILE, + +# (str) launchMode to set for the main activity +#android.manifest.launch_mode = standard + +# (str) screenOrientation to set for the main activity. +# Valid values can be found at https://developer.android.com/guide/topics/manifest/activity-element +#android.manifest.orientation = fullSensor + +# (list) Android additional libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_arm64_v8a = libs/android-v8/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (list) Android shared libraries which will be added to AndroidManifest.xml using tag +#android.uses_library = + +# (str) Android logcat filters to use +#android.logcat_filters = *:S python:D + +# (bool) Android logcat only display log for activity's pid +#android.logcat_pid_only = False + +# (str) Android additional adb arguments +#android.adb_args = -H host.docker.internal + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 +# In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time. +android.archs = arm64-v8a, armeabi-v7a + +# (int) overrides automatic versionCode computation (used in build.gradle) +# this is not the same as app version and should only be edited if you know what you're doing +# android.numeric_version = 1 + +# (bool) enables Android auto backup feature (Android API >=23) +android.allow_backup = True + +# (str) XML file for custom backup rules (see official auto backup documentation) +# android.backup_rules = + +# (str) If you need to insert variables into your AndroidManifest.xml file, +# you can do so with the manifestPlaceholders property. +# This property takes a map of key-value pairs. (via a string) +# Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"] +# android.manifest_placeholders = [:] + +# (bool) Skip byte compile for .py files +# android.no-byte-compile-python = False + +# (str) The format used to package the app for release mode (aab or apk or aar). +# android.release_artifact = aab + +# (str) The format used to package the app for debug mode (apk or aar). +# android.debug_artifact = apk + +# +# Python for android (p4a) specific +# + +# (str) python-for-android URL to use for checkout +#p4a.url = + +# (str) python-for-android fork to use in case if p4a.url is not specified, defaults to upstream (kivy) +#p4a.fork = kivy + +# (str) python-for-android branch to use, defaults to master +#p4a.branch = master + +# (str) python-for-android specific commit to use, defaults to HEAD, must be within p4a.branch +#p4a.commit = HEAD + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#p4a.source_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes = + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + +# Control passing the --use-setup-py vs --ignore-setup-py to p4a +# "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not +# Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py +# NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate +# setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. +#p4a.setup_py = false + +# (str) extra command line arguments to pass when invoking pythonforandroid.toolchain +#p4a.extra_args = + + + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../kivy-ios +# Alternately, specify the URL and branch of a git checkout: +ios.kivy_ios_url = https://github.com/kivy/kivy-ios +ios.kivy_ios_branch = master + +# Another platform dependency: ios-deploy +# Uncomment to use a custom checkout +#ios.ios_deploy_dir = ../ios_deploy +# Or specify URL and branch +ios.ios_deploy_url = https://github.com/phonegap/ios-deploy +ios.ios_deploy_branch = 1.10.0 + +# (bool) Whether or not to sign the code +ios.codesign.allowed = false + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) The development team to use for signing the debug version +#ios.codesign.development_team.debug = + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + +# (str) The development team to use for signing the release version +#ios.codesign.development_team.release = + +# (str) URL pointing to .ipa file to be installed +# This option should be defined along with `display_image_url` and `full_size_image_url` options. +#ios.manifest.app_url = + +# (str) URL pointing to an icon (57x57px) to be displayed during download +# This option should be defined along with `app_url` and `full_size_image_url` options. +#ios.manifest.display_image_url = + +# (str) URL pointing to a large icon (512x512px) to be used by iTunes +# This option should be defined along with `app_url` and `display_image_url` options. +#ios.manifest.full_size_image_url = + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +# build_dir = ./.buildozer + +# (str) Path to build output (i.e. .apk, .aab, .ipa) storage +# bin_dir = ./bin + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/examples/keystore/ios_readme.md b/examples/keystore/ios_readme.md new file mode 100644 index 000000000..c00de9280 --- /dev/null +++ b/examples/keystore/ios_readme.md @@ -0,0 +1,55 @@ +This is the process I used to compile my app for iOS. +I used a virtualbox VM on a Windows 10 host running a macOS +Catalina guest (I couldn't get the newer ones to work without boot looping). +If you are running a macOS host with an M1 chip, you may have to use a Rosetta Terminal for +some of these steps, for more information, see: +https://nrodrig1.medium.com/how-to-run-your-kivy-app-on-your-iphone-5926e0917216 +https://nrodrig1.medium.com/put-kivy-application-on-iphone-update-1cda12e79825 + +if you already have homebrew, Xcode (with command line tools), and python installed, you can skip to step 4. + +1. Install Homebrew (make sure to install Xcode command line tools when prompted): +
`/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` +2. Install Python: +
`brew install python` +3. Install Xcode (I use version 13.4 because it is the last version that I can run on macOS Monterey). Extract this +file in your application folder: +
https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_13.4/Xcode_13.4.xip + + +4. Just a suggestion, make a directory for virtual environments and one for your projects: +``` +mkdir ~/Documents/Environments +mkdir ~/Documents/kivy_builds +``` +5. create and then activate a virtual environment: +``` +cd kivy_builds +python -m venv venv_project_name` +source venv_projectP_name/bin/activate +``` +6. Install kivy-ios prerequisites: +``` +brew install autoconf automake libtool pkg-config +brew link libtool +``` +7. Install your project requirements: +
`pip install -r requirements.txt` +8. Install kivy-ios: +
`pip install kivy-ios` +9. At the very least, your project will need kivy and python. I would recommend executing the following +command and copying the project folder to have a base to start from for subsequent projects. All you need to do is copy +the project folder and rename it, this will take a while to run: +
`toolchain build python3 kivy` +10. Some of your dependencies will need the build command, some will need pip install, you'll just have to +figure out which ones need which: +``` +toolchain build pillow numpy +toolchain pip install kivymd keyring requests +``` +11. Create your project: +`toolchain create project_name /Users/$(whoami)/Documents/project_name` + +The last command should create a project folder in your Documents folder named project_name-ios. +To compile the project, go into the directory and find the project_name.xcodeproj file. Open it with Xcode and from there +you can build it like any other Xcode project for iOS. \ No newline at end of file diff --git a/examples/keystore/main.py b/examples/keystore/main.py new file mode 100644 index 000000000..326465746 --- /dev/null +++ b/examples/keystore/main.py @@ -0,0 +1,59 @@ +from kivy.app import App +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.textinput import TextInput +from kivy.uix.label import Label +from plyer import keystore + + +class KeyStoreTestApp(App): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.store = None + self.set_key_input = None + self.set_value_input = None + self.get_key_input = None + self.get_value_label = None + + def build(self): + self.store = keystore + + layout = BoxLayout(orientation='vertical', padding=10, spacing=10) + + # Input for setting a key + self.set_key_input = TextInput(text='Enter key to set', size_hint=(1, 0.1)) + self.set_value_input = TextInput(text='Enter value to set', size_hint=(1, 0.1)) + set_button = Button(text='Set Key', size_hint=(1, 0.1)) + set_button.bind(on_press=self.set_key) + + # Input for getting a key + self.get_key_input = TextInput(text='Enter key to get', size_hint=(1, 0.1)) + self.get_value_label = Label(text='Value will appear here', size_hint=(1, 0.1)) + get_button = Button(text='Get Key', size_hint=(1, 0.1)) + get_button.bind(on_press=self.get_key) + + layout.add_widget(self.set_key_input) + layout.add_widget(self.set_value_input) + layout.add_widget(set_button) + layout.add_widget(self.get_key_input) + layout.add_widget(self.get_value_label) + layout.add_widget(get_button) + + return layout + + def set_key(self, instance): + key = self.set_key_input.text + value = self.set_value_input.text + self.store.set_key('servicename', key, value) + self.set_key_input.text = '' + self.set_value_input.text = '' + + def get_key(self, instance): + key = self.get_key_input.text + value = self.store.get_key('servicename', key) + self.get_value_label.text = f'Value: {value}' + self.get_key_input.text = '' + + +if __name__ == '__main__': + KeyStoreTestApp().run() diff --git a/plyer/facades/keystore.py b/plyer/facades/keystore.py index 5d0f21d25..ff8b18f40 100644 --- a/plyer/facades/keystore.py +++ b/plyer/facades/keystore.py @@ -19,14 +19,14 @@ class Keystore: Keystore facade ''' - def set_key(self, servicename, key, value, **kwargs): - self._set_key(servicename, key, value, **kwargs) + def set_key(self, servicename, key, value, encrypt, **kwargs): + self._set_key(servicename, key, value, encrypt, **kwargs) - def _set_key(self, servicename, key, value, **kwargs): + def _set_key(self, servicename, key, value, encrypt, **kwargs): raise NotImplementedError() - def get_key(self, servicename, key, **kwargs): - return self._get_key(servicename, key) + def get_key(self, servicename, key, decrypt, **kwargs): + return self._get_key(servicename, key, decrypt, **kwargs) - def _get_key(self, servicename, key, **kwargs): + def _get_key(self, servicename, key, decrypt, **kwargs): raise NotImplementedError() diff --git a/plyer/platforms/android/keystore.py b/plyer/platforms/android/keystore.py index 0339948c2..e44f82a7d 100644 --- a/plyer/platforms/android/keystore.py +++ b/plyer/platforms/android/keystore.py @@ -1,25 +1,152 @@ +import json + +from jnius import autoclass, cast + from plyer.facades import Keystore from plyer.platforms.android import activity class AndroidKeystore(Keystore): + def _set_key(self, servicename, key, value, encrypt=True, **kwargs): + """ + Encrypts the value using a secret key and stores it in the Android Keystore system. + + Args: + servicename (str): The name of the service. + key (str): The key to store the value under. + value (str): The value to be encrypted and stored. + encrypt (bool, optional): Whether to encrypt the value. Defaults to True. + **kwargs: Additional keyword arguments. - def _set_key(self, servicename, key, value, **kwargs): + Returns: + None + """ + if encrypt: + cipher, iv = self.encrypt_key(value, servicename) + cipher, iv = self.cipher_text_wrapper(cipher, iv) + value = json.dumps({'cipher': cipher, 'iv': iv}) mode = kwargs.get("mode", 0) settings = activity.getSharedPreferences(servicename, mode) editor = settings.edit() editor.putString(key, value) editor.commit() - def _get_key(self, servicename, key, **kwargs): + @staticmethod + def cipher_text_wrapper(cipher, iv): + return ','.join([str(x) for x in cipher]), ','.join([str(x) for x in iv]) + + def encrypt_key(self, value, servicename): + """ + Encrypts the value using a secret key. + + Args: + value (str): The value to be encrypted. + servicename (str): The name of the service. + + Returns: + tuple: A tuple containing the encrypted value and the initialization vector. + + https://developer.android.com/reference/android/security/keystore/KeyProperties + https://developer.android.com/reference/javax/crypto/KeyGenerator + https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec + https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder + """ + KeyProperties = autoclass('android.security.keystore.KeyProperties') + KeyGenerator = autoclass('javax.crypto.KeyGenerator') + KeyGenParameterSpec = autoclass('android.security.keystore.KeyGenParameterSpec$Builder') + String = autoclass('java.lang.String') + + # Configure the KeyGenerator + kg = KeyGenParameterSpec(servicename, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + kg.setBlockModes(KeyProperties.BLOCK_MODE_GCM) + kg.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + key_generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + key_generator.init(kg.build()) + + # Generate the key + cipher = self.get_cipher() + cipher.init(1, cast('java.security.Key', key_generator.generateKey())) + return cipher.doFinal(String(value).getBytes("UTF-8")), cipher.getIV() + + @staticmethod + def get_cipher(): + """ + Returns an instance of the Cipher class. + """ + Cipher = autoclass('javax.crypto.Cipher') + return Cipher.getInstance("AES/GCM/NoPadding") + + def _get_key(self, servicename, key, decrypt=True, **kwargs): + """ + Retrieves the value associated with the given key from the Android Keystore system. + + Args: + servicename (str): The name of the service. + key (str): The key to retrieve the value for. + decrypt (bool, optional): Whether to decrypt the value. Defaults to True. + **kwargs: Additional keyword arguments. + + Returns: + str: The decrypted value associated with the key. + """ mode = kwargs.get("mode", 0) default = kwargs.get("default", "__None") settings = activity.getSharedPreferences(servicename, mode) ret = settings.getString(key, default) + if decrypt: + cipherTextWrapper = json.loads(ret) + ret = self.decrypt_key(servicename, cipherTextWrapper) if ret == "__None": ret = None return ret + def decrypt_key(self, servicename, cipherTextWrapper): + """ + Decrypts the value using a secret key. + + Args: + servicename (str): The name of the service. + cipherTextWrapper (CipherTextWrapper): An json string containing the encrypted value and the initialization vector. + + Returns: + str: The decrypted value. + https://developer.android.com/reference/javax/crypto/spec/GCMParameterSpec + Specifies the set of parameters required by a Cipher using the Galois/Counter Mode (GCM) mode. + """ + GCMParameterSpec = autoclass('javax.crypto.spec.GCMParameterSpec') + String = autoclass('java.lang.String') + + # Get the key and the cipher + secretKey = self.get_secret_key(servicename) + cipher = self.get_cipher() + + # separate the encrypted data and the initialization vector + iv = [int(x) for x in cipherTextWrapper['iv'].split(",")] + e = [int(x) for x in cipherTextWrapper['cipher'].split(",")] + + # decrypt the data + cipher.init(2, secretKey, GCMParameterSpec(128, iv)) + decryptedData = cipher.doFinal(e) + p = String(decryptedData, "UTF-8").toCharArray() + return ''.join(p) + + @staticmethod + def get_secret_key(servicename): + """ + Retrieves the secret key from the Android Keystore system. + + Args: + servicename (str): The name of the service. + + Returns: + Key: The secret key. + """ + KeyStore = autoclass('java.security.KeyStore') + keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(None) + return keyStore.getKey(servicename, None) + def instance(): return AndroidKeystore() + diff --git a/plyer/platforms/ios/keystore.py b/plyer/platforms/ios/keystore.py index 289a5c01e..1affb245e 100644 --- a/plyer/platforms/ios/keystore.py +++ b/plyer/platforms/ios/keystore.py @@ -1,22 +1,69 @@ +from importlib.resources import path + from plyer.facades import Keystore from pyobjus import autoclass, objc_str - -NSUserDefaults = autoclass('NSUserDefaults') +from pyobjus.dylib_manager import load_framework class IosKeystore(Keystore): + """ + In order to get this to work, I had to write an Objective-C class that contains the functions to interact with the + keychain. They were a part of a framework, and not a class, and are thus, unable to be accessed with pyobjus + normally. If for some reason you want to do this yourself, the original file (KeychainBridge.m) is in the 'tools' + directory. You can compile it yourself using the following commands(macOS): + + First compile the ios-sim dylib: + clang -target x86_64-apple-ios13.0-simulator -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) \ + -F$(xcrun --sdk iphonesimulator --show-sdk-path)/System/Library/Frameworks -framework Security -framework Foundation \ + -shared -o KeychainBridge_sim.dylib KeychainBridge.m + + + Then compile the ios dylib: + clang -target arm64-apple-ios13.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -F$(xcrun --sdk iphoneos \ + --show-sdk-path)/System/Library/Frameworks -framework Security -framework Foundation -shared -o \ + KeychainBridge_device.dylib KeychainBridge.m + + then run this command to create a fat dylib: + lipo -create -output KeychainBridge.dylib KeychainBridge_sim.dylib KeychainBridge_device.dylib + + This will give you a bridge from Objective-C to Python that will allow you to interact with the keychain, + this will work on both the Xcode iOS simulator (x86_64) and an iPhone (arm64). If you only want one or the other, + you can just use the sim or device dylib and ignore the other two commands. + + It comes with three functions, you can see usage examples in the class below: + - (BOOL)saveWithService:(NSString *)service account:(NSString *)account value:(NSString *)value; + - (NSString *)retrieveWithService:(NSString *)service account:(NSString *)account; + - (BOOL)deleteWithService:(NSString *)service account:(NSString *)account; + """ + + @staticmethod + def KeychainBridge(): + with path('plyer.tools.KeychainBridge', 'KeychainBridge.framework') as framework_path: + load_framework(str(framework_path)) + return autoclass('KeychainBridge') + + def _set_key(self, servicename, key, value, *args, **kwargs): + keychain_bridge = self.KeychainBridge() + keychain_bridge.deleteWithService_account_( + objc_str(servicename), objc_str(key) + ) + return keychain_bridge.saveWithService_account_value_(objc_str(servicename), objc_str(key), + objc_str(value)) - def _set_key(self, servicename, key, value, **kwargs): - NSUserDefaults.standardUserDefaults().setObject_forKey_( - objc_str(value), objc_str(key)) - - def _get_key(self, servicename, key, **kwargs): - ret = NSUserDefaults.standardUserDefaults().stringForKey_( - objc_str(key)) - if ret is not None: - return ret.UTF8String() - else: - return ret + def _get_key(self, servicename, key, *args, **kwargs): + keychain_bridge = self.KeychainBridge() + if result := keychain_bridge.retrieveWithService_account_( + objc_str(servicename), objc_str(key) + ): + result = result.UTF8String() + """ + for some reason, the ios-sim returns a bytes object, but the actual phone returns a str object that you + can't use isinstance on + """ + if 'bytes' in str(type(result)): + result = result.decode('utf-8') + return result + return None def instance(): diff --git a/plyer/platforms/linux/keystore.py b/plyer/platforms/linux/keystore.py index 105ebd314..3b9214dfb 100644 --- a/plyer/platforms/linux/keystore.py +++ b/plyer/platforms/linux/keystore.py @@ -8,10 +8,10 @@ class LinuxKeystore(Keystore): - def _set_key(self, servicename, key, value, **kwargs): + def _set_key(self, servicename, key, value, *args, **kwargs): keyring.set_password(servicename, key, value) - def _get_key(self, servicename, key, **kwargs): + def _get_key(self, servicename, key, *args, **kwargs): return keyring.get_password(servicename, key) diff --git a/plyer/platforms/macosx/keystore.py b/plyer/platforms/macosx/keystore.py index 11e60e897..417519094 100644 --- a/plyer/platforms/macosx/keystore.py +++ b/plyer/platforms/macosx/keystore.py @@ -8,10 +8,10 @@ class OSXKeystore(Keystore): - def _set_key(self, servicename, key, value, **kwargs): + def _set_key(self, servicename, key, value, *args, **kwargs): keyring.set_password(servicename, key, value) - def _get_key(self, servicename, key, **kwargs): + def _get_key(self, servicename, key, *args, **kwargs): return keyring.get_password(servicename, key) diff --git a/plyer/platforms/win/keystore.py b/plyer/platforms/win/keystore.py index 0065a6ab3..93099a8cd 100644 --- a/plyer/platforms/win/keystore.py +++ b/plyer/platforms/win/keystore.py @@ -8,10 +8,10 @@ class WinKeystore(Keystore): - def _set_key(self, servicename, key, value, **kwargs): + def _set_key(self, servicename, key, value, *args, **kwargs): keyring.set_password(servicename, key, value) - def _get_key(self, servicename, key, **kwargs): + def _get_key(self, servicename, key, *args, **kwargs): return keyring.get_password(servicename, key) diff --git a/plyer/tools/KeychainBridge.framework/Headers/KeychainBridge.h b/plyer/tools/KeychainBridge.framework/Headers/KeychainBridge.h new file mode 100755 index 000000000..3f3ac5281 --- /dev/null +++ b/plyer/tools/KeychainBridge.framework/Headers/KeychainBridge.h @@ -0,0 +1,9 @@ +#import + +@interface KeychainBridge : NSObject + ++ (BOOL)saveWithService:(NSString *)service account:(NSString *)account value:(NSString *)value; ++ (NSString *)retrieveWithService:(NSString *)service account:(NSString *)account; ++ (BOOL)deleteWithService:(NSString *)service account:(NSString *)account; + +@end diff --git a/plyer/tools/KeychainBridge.framework/Info.plist b/plyer/tools/KeychainBridge.framework/Info.plist new file mode 100755 index 0000000000000000000000000000000000000000..90d01a1824bb4ac74fae530bde2c04e4a5556a19 GIT binary patch literal 758 zcmY*W%Wl&^6rCBM6bg;gP-xQ>`rut;*@l!A3lftmN<%~JGyz316MN!}I)30s)JVVw zu;T+*lrR2CAn;a+;rg5v~N-4Ar zMr1M~%yBJZ2Tfv9$JV%|RC{(z^f17EMou-WMr5S}N<58QPn}hJ;K)=DqCED-hs5)v zs++3Mu@&8rOKAh^N7y9l*p0gRw5tpqFG!L~;8EK=ErzZe$;4)B zSuH!3OIbWwMpjen0I<=`4S_euJ z?zO7XCaF#;*rIH#afMd3N#2CiBgRxHV;A=+qX8xUp(O1U#fME|_iALU4>7e%9yQEp zA8Wc}@jk((hew3}UsZN_L9Nu=HAUowlj|MY7&`Hvz7RETE6!opWrUBa$rgN`v602= z4BI9zYC=mA%SEX}-8kj=s`!|fx+#l^Di$T7Ef&i~LFj+Jdyj)0I@xJq&)R(i=0OqE zfC=7$kKil#27ZFSa1n069ry(9!`JW#hVUKy06)Pm@H_kgf5ATpp*ggGGAN7ksDPfL dJ=8!g)JG^LdRfo5S$`-kYEDn{Xa;C=gj~B literal 0 HcmV?d00001 diff --git a/plyer/tools/KeychainBridge.framework/KeychainBridge b/plyer/tools/KeychainBridge.framework/KeychainBridge new file mode 100755 index 0000000000000000000000000000000000000000..784b4924e1cace62c2ebdbf3b26a5642b3bd5c85 GIT binary patch literal 88352 zcmeI52~<->wW9}*ZaPG*4p=+eTIF` z`JFLg-EjJ^+fUsnil)FhfcS#MJ5$sg%8Nu%Z9&?IDN3nKjUAK9MKygCHB~7=w})sV zV2X1}rHL&t2?}`6*i-On-AN(h6ci=m0>Z=2Vwu;KN~0++-Pn-B)?W8mKB+*+L3C?- zPJ%t?JLi;2efnf|qfuLX>jit0g#1LewnshFKF(eaYs$*j8comkcL;B&w_uL~9WX!F zDV1uy4(wQ*ZTlF(o;Y(f@`$Q(^A2_PWIJC4&S57XA75@^txHokpqEW#}Q3ZGTe*dy@nMM7Qmu z#WwNDy66k5MyZU6N{v!RCyhwqS4R;^1*hPiRKu(lbTTFuAuCL&%+xoI>htZD3Fe4B z>v45}co$0=_`*1F<4Pu;ZTrfgJaJCEM-koH9z^pCUq~11X>(MWEJ0^$?;|KroW&l| zU&ig94cxe*{G&n$D+J$0w=lC4k}dtTEDF9|LL%`h`0x_m<8+&TT2eS)uxDhm4QvK% zE4KEQRPfmup%l@r?b$cajT;AI(bistV9x@Z=+^e28cPxc8^%?mGN}Ze%sVc~J*q1B zaIz3ZbQ^m@3h$Bm8xNvXCMJv+78{em&t8au!6B}Qd%}a)C1ZJ=XhIGy!X2K(PMd~! zK8M1Pt1@NDhOvd}ER|L_*r3&9veFC#><#&P!xWnYiVB4q`3}=m1x%n2L9Y$5O*r61HkT5tmkTD=6jk86) z3rnXrElVjhn%Ep!qBh;2G8D>&SV$RVL-ct%7(SXPY=w!!?o-AoKjeIopjb#%VfsvU2xWGniq6)AQ|3eBwsk{j zYWX!sg?arANRvTaNt?PT66dNgSBpiZ)x9WleKck6PvSfkW_TZOiBlhi*@@Uz(`RaZ zDRWs#yE-m@xDbDXI#b(<#JA_;qxkqTv0q)8cq~Wp$h53fB!u*pTwAG(7uNQZ$i--16Duub5kk=Lb8Zot{ z+FxO=y+N1O22kdjL6rG11^o$!a)T6Rc-@Z+bNK=k=9+wJe@(t(zY|SOt?^ygt|p1v zPx4pyQkY2@GYQN1@Sh|c!iWEWaCyl@zHgoQJ~+|NH;IibiVBCf483ndo|+`Zet2E8 z<&zo7<&X7A4)(pymGcwxNWhcW&X+DpcFE%?>lDDyQj^aIALx|_nh z0pe<4Y+y`&Cp?dfc%EE&R$+b<(n&wBL!J__wS(lpL6z3HLV1`QWc-Us9GMF+x65En zbf=0&!8+JaL;l&21~1WiA8Ha$c~=L}r6m2Dm@>~Gb%=Ss1(q<{a>J**KwSQB(s z9mBdHV-2r@^KFCnR>Qoh2YyK;4(j5@L|C&MVQn_7OJTikC?R7njAg^vcc|mW81@gY ze5_>*As)t~OzhvV)-=Fp7}Q0^P6j^AS)Mx?I~b1$(hgXo7WT0I_SX!uT3`Oues~9b zc{Q1!hohk0PVoveSEq=NlTecL7U9wpUy`2u{B(F9x`@2PzJ5CV zrUh&&|1#ItuZD0(2p@Pc)AUm}6{p|R1a zHt|97#^0y-*Py0lD5Gi`J6dbXN?{Gtv}!g|rB=h+?xx6Ts_Z;Ak}|L+gO+VR3#DPR zSyQv6De^N3X-2IsGghb8Yrv1BG+H%zg&y9mC-o(zPiEC7V`PTj04*{`^0iW0oz_H> zW1xH_+fa6JD5GE^`zqvNir`R1NH7J`7(oqgqVd0v0PmY~&f4eR_g6IjB!J~NY9OkK zBje!)zYgFHLfj>qqTyY0^0Nj+&>;aNfCP{L5qS@G62=Ji&Pt@oyY@aqol@oqyrbs|em_=F;I`E_1%4;5!Mvv*7dZdU_R+ z|I19ulmE9%eDZ&yiSHr!o`MhWHG=ahg8!(@`7H&%mEd~|eq99@;2w&`Acl|t55WO{wFsl z)*Z?nLNs_gpYw~XlbQ{a9OSnE_}>F)%);B5)EM!S&_$u%TEX{-BL%#unS#Dn@IMp$ z)|@@BA|Ju`wejKqd4rP_aUOY8kcSslCHQTL3co*Kn=hVA=MwRO1dsp{Kmter2_OL^ zfCP{L5eDByl{v=D z6jrAZG*zBSZ(y@om609An(_>~S5(<~tdJ-qG-xK3R%a=fovl|lNr-B+qCF*ot3D~ z(V8eFs0k)ECt9Cd$VF?3Wg#+3Lkxftovkt&8=_;Nqb9)=-^3V|NyX=kGMNl1tYMl~ zEo2}OQEIh5PY0C|TO66j0-WhMR>vB&>SU-TU(dA~GB&m{Ne9M~1`mx^Mkhw4q$mTG zBT{0tYLiy4QyB`MNdi7xFSruJV^gCVOC_aGX4TLTehk^1TvMS^tx{(_YhTC-bBtC} zS~At_sG5Z^g{Y6N^7{nc@;er!DH`$++;@1i%rGl($YN$e7+=Dl=!~F~@}BVXEOiwS07vW- zo&Q}Eikc2O*o0B!-CMj)%yMokJRzM_%elkw1f?PUDz8J0y!*{YueZ@Xcw<0;cM=;t zgx4GF58?HO_PxUE4fSQ(=mj=!PHu{ge-q60AHo6m}KvYAzkBuH^ zqswjdG#j0a-$0N-ApJlTAWG&g5W_a1cwmJ`g5lBS1ny$leqQ z5(d&2gg6PMY<52rHqPb~n{3_B*+lu1-~~JB|KO;woCh|COOs|;*EAx&z!o#f!%O+zvO_my%ul>E-7q#yjvaKdU6X=S>0b=cn4~F zU3$Td$iR~8(cixX^?J4GERz^j)7a5kQtu=AbOYSMWHA6RTc67sWMiPOEa(R9G*&~JDQj9mkVciR&C4}rXXYF9)26ZonHhOg zr{?9RPsu7Y$p+_Xvo*2=oryK%uo|t(#2TeKy;i4T3vBF2)48lMYH0X|0P5t-vOPDc1I=vxBl`T^la>7Dw2=JZLH|<=EUY(c2 z>P)t{Y|7e#lIAou^ItLce}j=!|LmslvR#Bf0Q z_&-7(&_(r>sNp*DBP75lRjy16iuS0Mh$uIbTX66R_Ia+TAwjLrks4W5rUCXmY13Iv z#rFvxo)Q<5nifWVa_GtqpS6jDyT4z(G3-FScht2MI%2}+b1jDcuytcx#GShdovX@! z*=4rBeo`LzAckfpcz>I=)9Kt!=W`eTadAa*Rdvl?{>}XH?N>a&*C5cuU;Bb+2k4Q`n zj~E^j-DhYI$nOC{%DMEM8$b7r7B;eX++?rr2&IQ!I{HFBZ`vk=JWGv@TutMm(LdDrS0R!IyW4 z0l_To3I@{Uu8gyTIL*$Xl_)Jm-im2K6qi;me18&jYN@;{6G$RmTXoDG@gIa+VX*5qq74L||lC^nm`&?tFc{X$P%-9&LA4LxzQ?um6@nPR%L zH4VTiE~Y&xh<6bc(=>JI!TiT%V_oMRODvTJy|Uz7L5KI5+D-9yU84^L&3+U(A=dBb zn`6UTj!)IB%}x*6eRRc%Q~m$E=urCjXVC}kC+{x2ka==z?I6b%*JdAx>yYtg#D<#P z3AZE9Uh|qX{mo?yPgGoXt0<|uVJ^M6dExO&7V{(4dM<5hnZ ze0F2Tjv(J3UVU0LHXzS;U(3qdem{qP^R)AxwFw{noOWpO><#_PKK*j;TRuNgU#$~Y zo!S-YGvlKD_%%1PtXoV=7@iKTXu96c ziE#iAW-H@Q;sWg2Fg``zy{fcv%cnU%y+377!OLQc{h-T{or+6R^J1djGi{ zgraIgwpB@zTMAK32a?~z&d1K%`DsP#CFUI(m!&QDckQlrTXAdi$BYL__9Q*AgB9Yy zcsW=tK|2vFiKZ(A<^)Y`V_VhySv@O1Mie*rpmI}$PoQBg8ZtO6B7(#`H4b-()Dz`tY*GuEv*k~;S8 z+4%d8?<7nb+xFNG%gbl2@>zwCF4hmD%lcTYrd&aF}Ml!Kn7i3tg&#UG43 zIXiAdhrDjO&&_AY|1tB6Y***7>zbdw2p4;|z3|)Q5~U)PyhDT)Q@(@ZaDSxye%$=qaq8(Urr8P^Hx9DR6c`EWh1|-&?0L__W!wEgv_Df zKEd)(CM1mPB7K-(CPGmEBRkp4wv3-j%bop8zwa2AeAE8+#KPG@H^VlJPE5PCpzZvt z=3&_n96#*Lh-%sltV?!2H#4eirTNT>jMaftT2yWP>?gmThyCALJKgR5xCO7C_X!U< zdBk)0H{RMaU$r$nnBZ|?rgGK2F5*wq77w4`{=246KFaCdR(*pExD??Wp-jOzEy(B~kN2 z-k7zM8FOo#UGT!JU5h(hWsUWj6WId^m!_=?@n+x4U;b4PzhxBD(_s^x_RLcABukIm zMC=(ctU#-!+U)-E(ha7VCZ|;giCr=61U1@$P{Q%1*4Wd`tb0sP%bp{meL9kj!w8#B zety2y&cSUqYL$$cb+{q3h?(_CyIH&7%+acJYE}a0oGD9dl&BkyK30-mC{gJOCAo0E zL&#`^k2ppNeEMf|O?;UQD8mxbNk|=VV5Y+r0se-f)4-ofB;+F}6wqlTYQ0XQC5Nd| zqR)`z8QI=VD$3PESyCC~Y^a|cy}1Sz{80j$2R7kvD&!NE)MjaN+t>K8cM^jN_TFB& z>dGJatL{xZo6jUw`p+quOr9*FC3*UKOoN|6cU_gt9?`>UdoHgmF->c5Dr61SD z#Q$qj-?gnD4CtCEc_U=%Rk1k!9y7u+=M(y?ceA(c@mOYPocKBrY>WS*(!-{;9x4d!T@=uSe zoE^hI%FFA-6x$z#Eo{4RCz3xby}J1P*E=uWsop43MV zUv*f1`}P3&Kc70S`{MImm1*lWXScOpzJJ%zf%A^K-kQLeKm2(4N|)*`H}kr+_-e!0 zp;F)5$pb#WHRs&-S-o3HllF9Kw`ZeVdGp)9q9*OmuI;)her0M!P3ivS+lH>ZJ!RtR zA6`E_@_07~ZTYUp^S-zr5cYcQQpMvDdp6B}oZa2)hdoDE-g~w1$*mo~{Ceg6jgfIP z+A>chJ)AaPF+TY*uwP1h%EJ`+R%q3#?C`qbot7WjBkpo6YSNlL2P#A#E=@@~vHk4t zU4*?*)R!q1g)(jpd!LADdUa~{mXd4PiQwv_VbYl(i_VDVcCy@mU@tIQ?PK}*8bfA? zRKpq=c|&%3ZkHF{wD>|yC=6#u%U;?VlolYCix9t;*4yrnZvUP8W4MJ4oCv*|KM@*t zSr)!z=0DxY7$3r?3%NPEF)kJ(VzJdJHsI@H>vj!aFzl-?6sp4;@&z1)-A^@EvEd0!@k2^+`A8dL4$ zo$VyOysO5!Y_2Nv`>o&p+be!q_bB4wwh6UsuRm%re9eR3;>R-mg#v1gvq&z3t)lT8 zg1=F^nC{V7o|YHW?cg@$(|=GwPPT-6Q4RNu0iESirnAKI?f-zzX{kek`gR^P(4~K; zn55{`vB|L#xZW5|lH|0(i3!n?&OtI+6!}ailf|UQNRks1Qc@*gLnezI(OJ@2*pQmu z!XTqjmTVwj>6;1@!B`L!kZMet&QJ|+y-9N*Rs(;|80g~Me+pYTP_4;Q8M9bpe;Lsr zQe`lxNWi&2`LhTK3@b<-IXZKyjv1Dj9yU3zPx!cu9G%SACnAg)K=R3ijQ!z@ZLTcD za5eOoarF;$vAHM!C~9V+-$Sa$0;e9IoIlyIhu$w_K+jQe`M{Q~m`y~+QoO?5f_PDxt( zF8b9UZz+B7p6L2Fou)bZRbD!A7m){oz80y z_^2m2mLGWBmq{L8`JnPUU$1N9HqRU{30!;WbcK31|D_c^kpL1v0!RP}AOR$R1dsp{ zKmter2_OL^fCP{L5 + + + + files + + Headers/KeychainBridge.h + + odeLJnj9YP4p9ft+yxQU/lCXvVQ= + + Info.plist + + xkG1O0HQ344XE9rEhZinlez+YCY= + + Modules/module.modulemap + + d6HULAtvegRcWnLy1kBJbcqPFg8= + + + files2 + + Headers/KeychainBridge.h + + hash2 + + BiEfu4dGqFbti25FAq1Qw/5PGAaPJTTM2IJPWIPlnyE= + + + Modules/module.modulemap + + hash2 + + UKw0MBnkfdIjd4VJDRdac3Hgk3Vv2PaikG6UjihYsCE= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/setup.py b/setup.py index 80f65af73..408a41deb 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,8 @@ author_email='mat@kivy.org', url='https://plyer.readthedocs.org/en/latest/', packages=PACKAGES, - package_data={'': ['LICENSE', 'README.md']}, + package_data={'': ['LICENSE', 'README.md'], + 'plyer.tools.KeychainBridge': ['KeychainBridge.framework']}, package_dir={'plyer': 'plyer'}, include_package_data=True, license='MIT', From 824ca7c02eaf3ec2bf1b91a9527e31431438befb Mon Sep 17 00:00:00 2001 From: t0theRANCH Date: Sun, 4 Feb 2024 16:35:34 -0500 Subject: [PATCH 2/2] uses native ios and android keyring/keystore functionality to store sensitive data --- plyer/platforms/ios/keystore.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/plyer/platforms/ios/keystore.py b/plyer/platforms/ios/keystore.py index 1affb245e..2e2913f86 100644 --- a/plyer/platforms/ios/keystore.py +++ b/plyer/platforms/ios/keystore.py @@ -9,26 +9,7 @@ class IosKeystore(Keystore): """ In order to get this to work, I had to write an Objective-C class that contains the functions to interact with the keychain. They were a part of a framework, and not a class, and are thus, unable to be accessed with pyobjus - normally. If for some reason you want to do this yourself, the original file (KeychainBridge.m) is in the 'tools' - directory. You can compile it yourself using the following commands(macOS): - - First compile the ios-sim dylib: - clang -target x86_64-apple-ios13.0-simulator -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) \ - -F$(xcrun --sdk iphonesimulator --show-sdk-path)/System/Library/Frameworks -framework Security -framework Foundation \ - -shared -o KeychainBridge_sim.dylib KeychainBridge.m - - - Then compile the ios dylib: - clang -target arm64-apple-ios13.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -F$(xcrun --sdk iphoneos \ - --show-sdk-path)/System/Library/Frameworks -framework Security -framework Foundation -shared -o \ - KeychainBridge_device.dylib KeychainBridge.m - - then run this command to create a fat dylib: - lipo -create -output KeychainBridge.dylib KeychainBridge_sim.dylib KeychainBridge_device.dylib - - This will give you a bridge from Objective-C to Python that will allow you to interact with the keychain, - this will work on both the Xcode iOS simulator (x86_64) and an iPhone (arm64). If you only want one or the other, - you can just use the sim or device dylib and ignore the other two commands. + normally. I compiled it into a .framework to comply with Apple's App Store guidelines. This will give you a bridge from Objective-C to Python that will allow you to interact with the keychain. It comes with three functions, you can see usage examples in the class below: - (BOOL)saveWithService:(NSString *)service account:(NSString *)account value:(NSString *)value; @@ -38,7 +19,7 @@ class IosKeystore(Keystore): @staticmethod def KeychainBridge(): - with path('plyer.tools.KeychainBridge', 'KeychainBridge.framework') as framework_path: + with path('plyer.tools', 'KeychainBridge.framework') as framework_path: load_framework(str(framework_path)) return autoclass('KeychainBridge')