{"id":13608845,"url":"https://github.com/cioccarellia/ksprefs","last_synced_at":"2025-04-09T09:07:59.916Z","repository":{"id":144339077,"uuid":"135190985","full_name":"cioccarellia/ksprefs","owner":"cioccarellia","description":"Kotlin SharedPreferences wrapper \u0026 cryptographic android library.","archived":false,"fork":false,"pushed_at":"2024-02-28T00:25:56.000Z","size":13135,"stargazers_count":228,"open_issues_count":2,"forks_count":22,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-04-02T06:08:17.659Z","etag":null,"topics":["aes","android","android-sharedpreferences","base64","encryption","keystore","kotlin","library","maven","security","shared-preferences","sharedpreferences","sharedpreferences-wrapper","wrapper","xml"],"latest_commit_sha":null,"homepage":"https://cioccarellia.github.io/ksprefs/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cioccarellia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-05-28T17:31:47.000Z","updated_at":"2024-08-09T01:15:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"b78dc35b-7272-4b78-8b4e-dcfe1d038053","html_url":"https://github.com/cioccarellia/ksprefs","commit_stats":null,"previous_names":["andreacioccarelli/cryptoprefs"],"tags_count":41,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cioccarellia%2Fksprefs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cioccarellia%2Fksprefs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cioccarellia%2Fksprefs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cioccarellia%2Fksprefs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cioccarellia","download_url":"https://codeload.github.com/cioccarellia/ksprefs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248008630,"owners_count":21032556,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aes","android","android-sharedpreferences","base64","encryption","keystore","kotlin","library","maven","security","shared-preferences","sharedpreferences","sharedpreferences-wrapper","wrapper","xml"],"created_at":"2024-08-01T19:01:30.439Z","updated_at":"2025-04-09T09:07:59.896Z","avatar_url":"https://github.com/cioccarellia.png","language":"Kotlin","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/cioccarellia/ksprefs\" target=\"_blank\"\u003e\u003cimg width=\"100\" src=\"https://raw.githubusercontent.com/cioccarellia/ksprefs/master/extras/ksprefs.png\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003ch1 align=\"center\"\u003eKsPrefs\u003c/h1\u003e\n\u003cp align=\"center\"\u003eSecure SharedPreferences\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://search.maven.org/artifact/com.github.cioccarellia/ksprefs\"\u003e\u003cimg src=\"https://img.shields.io/maven-central/v/com.github.cioccarellia/ksprefs.svg?label=Maven%20Central\" alt=\"Download from MavenCentral\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://app.codacy.com/manual/cioccarellia/ksprefs/dashboard\"\u003e\u003cimg src=\"https://api.codacy.com/project/badge/Grade/f10cdbdbe7b84d0ea7a03b755c104e03\" alt=\"Codacy\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://kotlinlang.org/docs/releases.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/kotlin-1.9.22-orange.svg\" alt=\"Kotlin\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://source.android.com/setup/start/build-numbers\"\u003e\u003cimg src=\"https://img.shields.io/badge/min-19-00e676.svg\" alt=\"Android Min Sdk\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://source.android.com/setup/start/build-numbers\"\u003e\u003cimg src=\"https://img.shields.io/badge/compile-34-00e676.svg\" alt=\"Android Compile Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/cioccarellia/ksprefs/blob/master/LICENSE.md\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-Apache%202.0-blue.svg\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\n\u003cdetails open\u003e\u003csummary\u003eGradle\u003c/summary\u003e\n\n```gradle\ndependencies {\n    implementation 'com.github.cioccarellia:ksprefs:2.4.1'\n}\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eKotlin DSL\u003c/summary\u003e\n\n```gradle\ndependencies {\n    implementation(\"com.github.cioccarellia:ksprefs:2.4.1\")\n}\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eMaven\u003c/summary\u003e\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.cioccarellia\u003c/groupId\u003e\n    \u003cartifactId\u003eksprefs\u003c/artifactId\u003e\n    \u003cversion\u003e2.4.1\u003c/version\u003e\n    \u003ctype\u003epom\u003c/type\u003e\n\u003c/dependency\u003e\n```\n\u003c/details\u003e\n\n- Powerful SharedPreferences wrapper \u0026 API.\n- Easy to pick up \u0026 use right away.\n- Tested and production-ready.\n- Fully customizable behaviour.\n- Built-in cryptography \u0026 decoding engines (PlainText, Base64, AES-CBC, AES-ECB, Android KeyStore + AES-GCM / RSA KeyPair).\n- Extensive type \u0026 enum support.\n\n```kotlin\nval prefs = KsPrefs(applicationContext)\nval count = prefs.pull\u003cInt\u003e(\"app_start_key\")\n```\n\nTo _read_ from SharedPreferences, use `pull(key, fallback)`.\u003cbr\u003e\nTo _write_ to SharedPreferences, use `push(key, value)`.\n\n## Introduction\n\u003cimg src=\"https://raw.githubusercontent.com/cioccarellia/ksprefs/master/extras/dark/png/scheme.png\"\u003e\u003cbr\u003e\u003cbr\u003e\nKsPrefs (\u003cb\u003eK\u003c/b\u003eotlin \u003cb\u003eS\u003c/b\u003ehared \u003cb\u003ePref\u003c/b\u003eerences) is a wrapper for the default Android SharedPreferences (_SP_ for short) implementation.\u003cbr\u003e\nIts purposes are to bring security to preference storage through cryptography, to implement an elegant and practical SP API, and to do so with as little overhead as possible.\u003cbr\u003e\nKsprefs can be used as a replacement of direct _SP_ usage, which lacks both security and practicality, and which even Google is moving away from with [Jetpack DataStore](https://developer.android.com/topic/libraries/architecture/datastore).\u003cbr\u003e\nOn top of the _SP_ API, KsPrefs extends with numerous features and extra bits which come pre-packed with the library, and can be used to enhance the development experience and productivity.\n\n## Basics\n### Initialization\n\nYou should create `KsPrefs` only once in your codebase. \n```kotlin\nval prefs = KsPrefs(applicationContext)\n```\n\nIt is recommended to keep it inside your `Application` class, so that it's reachable everywhere from your code.\n\n```kotlin\nclass App : Application() {\n\n    companion object {\n        lateinit var appContext: Context\n        val prefs by lazy { KsPrefs(appContext) }\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        appContext = this\n    }\n}\n```\n\n### Terminology\n\n| Term                   | Description                                                                                                                                            |\n|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|\n| SP                     | Android Shared Preferences                                                                                                                             |\n| Entry                  | Key-Value pair stored by _SP_. Thus the basic data entity which can be pushed and pulled                                                               |\n| Persistent XML Storage | _SP_ XML file containing actual entries. Stored in the application [private storage](https://developer.android.com/training/data-storage/app-specific) |\n|                        |                                                                                                                                                        |\n|                        |                                                                                                                                                        |\n\n\n### Read \u003csmall\u003e(Pull)\u003c/small\u003e\nTo retrieve saved values from _SP_ you use `pull()`.\u003cbr\u003e\nKey uniquely identifies a record, fallback is the default value if none is found in _SP_.\n\n```kotlin\nval uname = prefs.pull(key = \"username\", fallback = nobody)\n```\n\n\nThere are 4 different variants of `pull`. \u003c!-- helo --\u003e\n\n- `pull\u003cT\u003e(key, fallback\u003cT\u003e)`: Scans the preferences with the given key. If a record is found, the value is read from the persistent XML storage, deserialized as the requested type and returned. If the key isn't contained inside the storage, the fallback value is returned.\n- `pull\u003cT\u003e(key)`: No fallback value is supplied\n- `pull\u003cT\u003e(key, kclass\u003cT\u003e)`\n- `pull\u003cT\u003e(key, jclass\u003cT\u003e)`\n\n\nA function is defined *safe* when you supply the fallback (Android SharedPreferences defines it `default`) value, so that, for *any* given key, you always have a concrete in-memory value to return.\u003cbr\u003e\nA function is *unsafe* because there is no guarantee it will return a concrete value, as it only relies on the supplied key to pull the value from the persistent XML storage\u003cbr\u003e\n\n\nEven though the standard SharedPreferences API always forces you to provide a default (KsPrefs defines it `fallback`) value, KsPrefs allows you to leave that out, because supplying an actual instance of an object, in some contexts is verbose and redundant if you are know that the key is present inside the persistent storage, or if for some clever intuition you know that the key holds a value at some specific time.\n\n```kotlin\nval username = prefs.pull(\"username\")\n```\n\n*:pushpin: #1: Using an unsafe version of `pull()` isn't dangerous, as long as you are sure the target key holds a value.*\u003cbr\u003e\n*:pushpin: #2: The 3 unsafe functions accept the type parameter as a kotlin class, as a java class or as a kotlin generic. For the latter, the bytecode of the function is inlined, in order for the generic type to be reified.*\u003cbr\u003e\n\n### Write \u003csmall\u003e(Push)\u003c/small\u003e\nTo save values to the preference storage you use `push()`\u003cbr\u003e\nPush takes a key and a value, and stores them inside the preferences, according to the `commitStrategy`, `autoSavePoliciy`.\n\n```kotlin\nprefs.push(\"username\", viewModel.username)\n```\n\n\n\n### Configuration\nKsPrefs is configurable at initialization time with specific parameters.\u003cbr\u003e\nEach parameters has a default value which will be used unless you specify otherwise.\u003cbr\u003e\nEach parameter changes the internal behaviour and the algorithms used, so it is vital to choose the appropriate settings.\u003cbr\u003e\n\n```kotlin\nval prefs = KsPrefs(applicationContext) {\n    // Configuration Parameters Lambda\n    encryptionType = PlainText()\n    autoSave = AutoSavePolicy.MANUAL\n    commitStrategy = CommitStrategy.COMMIT\n}\n```\n\n| Field          | Type           | Description                                                                                                                       | Default Value            |\n|----------------|----------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------|\n| encryptionType | EncryptionType | Encryption technique used to encrypt and decrypt data                                                                             | PlainText()              |\n| commitStrategy | CommitStrategy | Strategy to use at the moment of writing preferences entries to the persistent XML storage                                        | CommitStrategy.APPLY     |\n| autoSave       | AutoSavePolicy | Whether after a `push()` operation changes are saved to the persistent XML storage; saving strategy depending on `commitStrategy` | AutoSavePolicy.AUTOMATIC |\n| mode           | Int            | SharedPreferences access mode                                                                                                     | Context.MODE_PRIVATE     |\n| charset        | Charset        | Charset used for string-to-byte and byte-to-string conversions                                                                    | Charsets.UTF_8           |\n| keyRegex       | Regex?         | Regular Expression which, if non null, every key must match.                                                                      | null                     |\n\n\n\n### Saving, Auto Save Policies \u0026 Commit Strategies\nA pending transaction is a change which is registered in-memory, but not yet on the XML preference file.\nAndroid SharedPreferences works that way; indeed, you can stack up pending transactions, but at some point you have to _actually_ save them.\nIf the app were to shut down unexpectedly, those changes would be lost.\u003cbr\u003e\nTo commit any pending transaction to the persistent XML storage, in ksprefs you use `save()`. \nThis matches `commit()` and `apply()` SharedPreferences behaviour you may be accustomed to.\u003cbr\u003e\n\n#### Auto Save Policy\nBy default, `autoSave` is set to `AutoSavePolicy.AUTOMATIC`, and therefore changes are automatically synchronized with the underlying XML file, because after each `push()` call, a `save()` follows, in order to automatically commit and save the preference. Therefore, no pending transaction is kept.\n\nHowever, if `autoSave` is turned off (using `AutoSavePolicy.MANUAL`), `push()` will save the change in-memory, but is not going to write it to the XML preferences file until `save()` is invoked. This way it's going to create a pending transaction which will be kept in-memory until a `save()` operation happens.\n\nHere is a table representing when values are saved to the storage, depending on the policy in use.\n\n| `AutoSavePolicy`                  | AUTO               | MANUAL             |\n|-----------------------------------|--------------------|--------------------|\n| push()                            | :white_check_mark: | :x:                |\n| queue()                           | :x:                | :x:                |\n| save()                            | :white_check_mark: | :white_check_mark: |\n| SharedPreferences.Editor.commit() | :white_check_mark: | :white_check_mark: |\n| SharedPreferences.Editor.apply()  | :white_check_mark: | :white_check_mark: |\n\n*:pushpin: `AutoSavePolicy` chooses when to write changes to the persistent XML storage and when to keep them in memory.*\u003cbr\u003e\n\n#### Commit Strategy\nThe best (and default) practise while dealing with SharedPreferences is to use `APPLY`. It is asynchronous and fast. `COMMIT` is also available, though it should not be used unless you have a valid reason to, given its synchronous and strict nature, as well as `NONE`, for no-op (Does not save anything, used internally for `queue()`).\u003cbr\u003e\n`save()` and `push()` always refer to the commit strategy to decide how to save their updates to the persistent XML preference storage.\n\nHere is a table representing various features of different commit strategies. Check out the official documentation [here](https://developer.android.com/reference/android/content/SharedPreferences.Editor.html) and see [this](https://stackoverflow.com/questions/5960678/whats-the-difference-between-commit-and-apply-in-sharedpreferences) post for more intel.\n\n| `CommitStrategy` | APPLY              | COMMIT             | NONE                |\n|------------------|--------------------|--------------------|---------------------|\n| in-memory        | :white_check_mark: | :white_check_mark: | :white_check_mark:  |\n| XML              | :white_check_mark: | :white_check_mark: | :x:                 |\n| async            | :white_check_mark: | :x:                | :heavy_minus_sign:\t |\n| atomic           | :white_check_mark: | :white_check_mark: | :heavy_minus_sign:\t |\n| error report     | :x:                | :white_check_mark: | :heavy_minus_sign:\t |\n\n*:pushpin: The `CommitStrategy` involves how to write changes to the persistent XML storage.*\u003cbr\u003e\n\n### Queuing\nTo enqueue values to be written into the preference storage you use `queue()`. It follows `push()`'s syntax.\u003cbr\u003e\nWhile `push`, by default, _pushes_ the update immediately on the XML persistent storage (By default, changeable with `AutoSave`), `queue()` saves the update in-memory without writing it out to the storage.\u003cbr\u003e\nNot writing the changes to the file makes enqueuing a valid choice for both batch computing or resource-expensive and long-running operations.\u003cbr\u003e\n- `queue()` takes a key and a value, and saves the changes in-memory.\u003cbr\u003e\n- `queue()` does not actually send updates to the storage. You can do so by calling `save()` (Or by using `push()` subsequently).\n\u003cbr\u003e\u003cbr\u003e\n\nThis segment touches a broader concept, which is storing scope.\nThere are two storing scopes for SharedPreferences:\n- in-memory (key-value pairs are kept in memory). This is fast to read to/write from, but does not persist application restarts.\n- XML (key-value pairs are kept on a file). Writing to a file is mildly expensive but it allows preferences to survive across application restarts.\u003cbr\u003e\nHere is a table explaining how different methods inside KsPrefs touch and go through those storing scopes.\n\n| `StoringScope` | in-memory          | XML                             |\n|----------------|--------------------|---------------------------------|\n| push(k, v)     | :white_check_mark: | :white_check_mark: (By default) |\n| queue(k, v)    | :white_check_mark: | :x:                             |\n| save()         | :white_check_mark: | :white_check_mark:              |\n\n*:pushpin: The `StoringScope` determines at which level changes are propagated.*\u003cbr\u003e\n\nIn the following snippet (Given that `autoSavePolicy` is set to `AUTOMATIC`), `n` in-memory and `x` XML write operations are performed. This, given  `f(n)` and `f(x)` for how long those operations will take, takes `n×f(n) + m×f(x)`. Given that, if using `push()`, `m=n`, then it resolves to `n×(f(n) + f(x))`\n\n```kotlin\nfor ((index, pic) in picsArray.toList().withIndex()) {\n    // Long-running computation\n    prefs.push(\"pic-$index\", pic.url)\n}\n```\n\nEven though this isn't a significant speedup for small data sizes, as n (and m) grow the computation takes longer; since enqueuing values sets `m=1`, thus, `f(n) \u003c f(x)`. The time/op chart follows a much more gentle curve: `n×f(n) + f(x)`.\nThis improvements drastically optimizes performances for a large amount of operations.\n\n```kotlin\nfor ((index, pic) in picsArray.toList().withIndex()) {\n    // Long-running computation\n    prefs.queue(\"pic-$index\", pic.url)\n}\n\n// One save operation\nprefs.save()\n```\n\nPlease note, that if you set `autoSavePolicy` to `MANUAL`, `push()` will only change the in-memory values, and you will need to save them manually anyways.\n\n## API\nKsPrefs provides some customizable data structures, to abstract preference reads/writes to function calls.\n\n### Preferences Center\nA `PrefsCenter` is to be though as a task-specific abstractor. It is used to enclose and contain all the SP-specific operations, such as providing a key, doing some value specific post-read/pre-write operation, providing the fallback value or the explicit return type, handling logic / conditions and interacting with other app components.\n\n```kotlin\nobject StartCounterPrefCenter : PrefsCenter(App.prefs) {\n    private const val counterKey = \"start_counter\"\n    \n    fun increment() = prefs.push(counterKey,  read() + 1)\n    fun read() = prefs.pull(counterKey, 0)\n}\n```\n\n### Dynamic Delegates\nIt is really useful and fun to have dynamic properties whose value is a direct representation of what the underlying XML preferences file contains.\n\n```kotlin\nval accentColor by prefs.dynamic(\"accent_color\", \"#2106F3\")\n```\n\nWhen you set a value for this property, it is also updated on the XML preference file, as it is a dynamic reference to the preference.\n\n## Encryption\nKsPrefs provides with different levels of encryption. From no encryption at all (`EncryptionType.PlainText` and `EncryptionType.Base64`), to standard [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) with key size among 128, 192 \u0026 256-bit and ECB/CBC modes (base64-wrapped ciphertext), to [Android's own keystore system](https://developer.android.com/training/articles/keystore) (though the ksprefs implementation isn't nearly as fast as the AES-backed one).\u003c/br\u003e\nIt is recommended to store the AES key into a native library shipped with your application, which makes reverse engineering your code harder.\n\n## Sample App\n\u003cimg src=\"https://raw.githubusercontent.com/cioccarellia/ksprefs/master/art/demo-app.png\"\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcioccarellia%2Fksprefs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcioccarellia%2Fksprefs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcioccarellia%2Fksprefs/lists"}