{"id":13608003,"url":"https://github.com/adrielcafe/satchel","last_synced_at":"2025-04-23T16:34:01.457Z","repository":{"id":149190605,"uuid":"279437068","full_name":"adrielcafe/satchel","owner":"adrielcafe","description":":school_satchel: A fast, secure and modular key-value storage with batteries-included for Android and JVM.","archived":false,"fork":false,"pushed_at":"2020-08-12T23:59:55.000Z","size":529,"stargazers_count":71,"open_issues_count":0,"forks_count":8,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-11-07T13:38:54.524Z","etag":null,"topics":["android","android-library","coroutines","encrypted-store","encryption","jose4j","jvm","key-value","key-value-database","key-value-store","kotlin","kotlin-android","kotlin-library","kryo","protobuf","serialization","storage","tink"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrielcafe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"ko_fi":"adrielcafe"}},"created_at":"2020-07-13T23:58:22.000Z","updated_at":"2023-08-31T15:24:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"af028324-646b-4a9d-be2c-ed1ff2044a51","html_url":"https://github.com/adrielcafe/satchel","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fsatchel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fsatchel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fsatchel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fsatchel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrielcafe","download_url":"https://codeload.github.com/adrielcafe/satchel/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223930258,"owners_count":17227044,"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":["android","android-library","coroutines","encrypted-store","encryption","jose4j","jvm","key-value","key-value-database","key-value-store","kotlin","kotlin-android","kotlin-library","kryo","protobuf","serialization","storage","tink"],"created_at":"2024-08-01T19:01:23.485Z","updated_at":"2024-11-10T08:27:04.450Z","avatar_url":"https://github.com/adrielcafe.png","language":"Kotlin","funding_links":["https://ko-fi.com/adrielcafe"],"categories":["Kotlin"],"sub_categories":[],"readme":"[![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/satchel.svg?style=for-the-badge)](https://jitpack.io/#adrielcafe/satchel) \n[![Android API](https://img.shields.io/badge/api-16%2B-brightgreen.svg?style=for-the-badge)](https://android-arsenal.com/api?level=16) \n[![Github Actions](https://img.shields.io/github/workflow/status/adrielcafe/satchel/main/master?style=for-the-badge)](https://github.com/adrielcafe/satchel/actions) \n[![Codacy](https://img.shields.io/codacy/grade/e072b5e37b094518a7cd672086ac390a.svg?style=for-the-badge)](https://www.codacy.com/app/adriel_cafe/satchel) \n[![Kotlin](https://img.shields.io/github/languages/top/adrielcafe/satchel.svg?style=for-the-badge)](https://kotlinlang.org/) \n[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg?style=for-the-badge)](https://ktlint.github.io/) \n[![License MIT](https://img.shields.io/github/license/adrielcafe/satchel.svg?style=for-the-badge\u0026color=yellow)](https://opensource.org/licenses/MIT)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/adrielcafe/satchel/blob/master/satchel.png?raw=true\"\u003e\n\u003c/p\u003e\n\n### *Satchel* is a powerful and flexible key-value storage with batteries-included for Android and JVM.\n\nIt's backed by [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) and great third-party libraries ([Tink](https://github.com/google/tink), [Kryo](https://github.com/EsotericSoftware/kryo) and [Protobuf](https://github.com/protocolbuffers/protobuf) to name a few).\n\n# Features\n* Fast: see the [Benchmark](#benchmark) results\n* Small: the [core library](#setup) has ~35kb and contains everything you need to get started\n* Simple: has an easy to use [API](#api)\n* Modular: 10 (optional) built-in [modules](#modules) to choose from\n* Extensible: create your own [Storer](#build-your-own-storer), [Encrypter](#build-your-own-encrypter) and [Serializer](#build-your-own-serializer)\n\n## Supported types\n- [x] `Double` and `List\u003cDouble\u003e`\n- [x] `Float` and `List\u003cFloat\u003e`\n- [x] `Int` and `List\u003cInt\u003e`\n- [x] `Long` and `List\u003cLong\u003e`\n- [x] `Boolean` and `List\u003cBoolean\u003e`\n- [x] `String` and `List\u003cString\u003e`\n- [x] `Serializable`¹\n\n¹ *Not supported by `satchel-serializer-protobuf-lite`*\n\n# Setup\n1. Add the JitPack repository to your project level `build.gradle`:\n```gradle\nallprojects {\n    repositories {\n        maven { url 'https://jitpack.io' }\n    }\n}\n```\n\n2. Next, add the desired dependencies to the module `build.gradle`:\n```gradle\ndependencies {\n    // Core (required)\n    implementation \"com.github.adrielcafe.satchel:satchel-core:$currentVersion\"\n\n    // Storers\n    implementation \"com.github.adrielcafe.satchel:satchel-storer-encrypted-file:$currentVersion\"\n\n    // Encrypters\n    implementation \"com.github.adrielcafe.satchel:satchel-encrypter-cipher:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-encrypter-jose4j:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-encrypter-tink-android:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-encrypter-tink-jvm:$currentVersion\"\n\n    // Serializers\n    implementation \"com.github.adrielcafe.satchel:satchel-serializer-base64-android:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-serializer-base64-jvm:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-serializer-gzip:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-serializer-kryo:$currentVersion\"\n    implementation \"com.github.adrielcafe.satchel:satchel-serializer-protobuf-lite:$currentVersion\"\n}\n```\nCurrent version: [![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/satchel.svg?style=flat-square)](https://jitpack.io/#adrielcafe/satchel)\n\n# Usage\nTake a look at the [sample app](https://github.com/adrielcafe/satchel/tree/master/sample/src/main/java/cafe/adriel/satchel/sample) for a working example.\n\n## Global instance\nFirst initialize Satchel's global instance by calling `Satchel.init()`:\n```kotlin\nSatchel.init(\n    storer = FileSatchelStorer(storageFile),\n    encrypter = BypassSatchelEncrypter,\n    serializer = RawSatchelSerializer\n)\n```\n\nNow you can use `Satchel.storage` everywhere:\n```kotlin\nSatchel.storage[\"key\"] = \"value\"\n```\n\nIt's also possible to check if Satchel was already initialized:\n```kotlin\nif (Satchel.isInitialized.not()) {\n    // Init\n}\n```\n\n## Local instance\nUse `Satchel.with()` to create a local instance:\n```kotlin\nval satchel = Satchel.with(\n    storer = FileSatchelStorer(storageFile),\n    encrypter = BypassSatchelEncrypter,\n    serializer = RawSatchelSerializer\n)\n```\n\nAnd start using it:\n```kotlin\nsatchel[\"key\"] = \"value\"\n```\n\n## API\nSatchel has a simple and familiar [API](https://github.com/adrielcafe/satchel/blob/master/satchel-core/src/main/java/cafe/adriel/satchel/SatchelStorage.kt) based on [MutableMap](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-map/) and [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences):\n```kotlin\nsatchel.apply {\n    val firstName = get\u003cString\u003e(\"firstName\")\n\n    val notificationsEnabled = getOrDefault(\"notificationsEnabled\", false)\n\n    val favoritePostIds = getOrDefault(\"favoritePostIds\") { emptySet\u003cInt\u003e() }\n\n    val registeredAt = getOrSet(\"registeredAt\", currentTimestamp)\n\n    val lastName = getOrSet(\"lastName\") { \"Doe\" }\n\n    set(\"username\", \"john.doe\")\n\n    setIfAbsent(\"lastName\", lastName)\n\n    keys.forEach { key -\u003e\n        // ...\n    }\n\n    when {\n        isEmpty -\u003e { /* ... */ }\n        size == 1 -\u003e { /* ... */ }\n        contains(\"username\") -\u003e { /* ... */ }\n    }\n\n    remove(\"favoritePostIds\")\n\n    clear()\n}\n```\n\nBut unlike `SharedPreferences`, there's no `apply()` or `commit()`. Changes are **saved asynchronously** every time a write operation (`set()`, `remove()` and `clear()`) happens.\n\n### Delegates\nIt's possible to delegate the job of `get` and `set` the value of a specific key:\n```kotlin\nprivate var favoritePostIds by satchel.value(key = \"favoritePostIds\", defaultValue = emptySet\u003cInt\u003e())\n\n// Will call set(key, value)\nfavoritePostIds = setOf(1, 2, 3)\n\n// Will call getOrDefault(key, defaultValue)\nshowFavoritePosts(favoritePostIds)\n```\n\nIf you doesn't specify a default value, it will return a nullable value:\n```kotlin\nprivate var username by satchel.value\u003cString\u003e(\"username\")\n\nusername?.let(::showProfile)\n```\n\n### Events\nYou can be notified every time the storage changes, just call `addListener()` to register a listener in the specified `CoroutineScope`:\n```kotlin\nsatchel.addListener(lifecycleScope) { event -\u003e\n    when (event) {\n        is SatchelEvent.Set -\u003e { /* ... */ }\n        is SatchelEvent.Remove -\u003e { /* ... */ }\n        is SatchelEvent.Clear -\u003e { /* ... */ }\n    }\n}\n```\n\n## Modules\nSatchel has 3 different categories of modules:\n* **Storers**: responsible for reading and writing to the file system\n* **Encrypters**: responsible for encryption and decryption\n* **Serializers**: responsible for serialization and deserialization \n\nThe core library comes with one stock module for each category: [FileSatchelStorer](#FileSatchelStorer), [BypassSatchelEncrypter](#BypassSatchelEncrypter) and [RawSatchelSerializer](#RawSatchelSerializer). All the other libraries are *optional*.\n\n### Storers\nIf you are developing for Android, I recommend to use the [Context.filesDir](https://developer.android.com/training/data-storage/app-specific) as the parent folder. If you want to save in the external storage remember to [ask for write permission](https://developer.android.com/training/data-storage#permissions) first.\n```kotlin\nval file = File(context.filesDir, \"satchel.storage\")\n```\n\n#### [FileSatchelStorer](https://github.com/adrielcafe/satchel/blob/master/satchel-core/src/main/java/cafe/adriel/satchel/storer/file/FileSatchelStorer.kt)\nUses the `FileOutputStream` and `FileInputStream` to read and write without do any modification.\n```kotlin\nval storer = FileSatchelStorer(file)\n```\n\n#### [EncryptedFileSatchelStorer](https://github.com/adrielcafe/satchel/blob/master/satchel-storer-encrypted-file/src/main/java/cafe/adriel/satchel/storer/encryptedfile/EncryptedFileSatchelStorer.kt)\nUses the `EncryptedFile` from [Jetpack Security](https://developer.android.com/topic/security/data.md) to read/write and also takes care of encryption/decryption.\n```kotlin\nval storer = EncryptedFileSatchelStorer.with(applicationContext, file)\n```\n\n#### Build your own Storer\nCreate a `class` or `object` that implements the `SatchelStorer` interface: \n```kotlin\nobject MySatchelStorer : SatchelStorer {\n    \n    suspend fun store(data: ByteArray) {\n        // Save the ByteArray wherever you want\n    }\n\n    fun retrieve(): ByteArray {\n        // Load and return the stored ByteArray\n    }\n}\n```\n\n### Encrypters\n:warning: Satchel doesn't store your crypto keys, it only uses it. So make sure to store them in a safe place.\n\n#### [BypassSatchelEncrypter](https://github.com/adrielcafe/satchel/blob/master/satchel-core/src/main/java/cafe/adriel/satchel/encrypter/bypass/BypassSatchelEncrypter.kt)\nJust bypass the encryption/decryption.\n```kotlin\nval encrypter = BypassSatchelEncrypter\n```\n\n#### [CipherSatchelEncrypter](https://github.com/adrielcafe/satchel/blob/master/satchel-encrypter-cipher/src/main/java/cafe/adriel/satchel/encrypter/cipher/CipherSatchelEncrypter.kt)\nUses the [Cipher](https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html) for encryption/decryption.\n```kotlin\nval transformation = \"AES\"\nval key = KeyGenerator\n    .getInstance(transformation)\n    .apply { init(256) }\n    .generateKey()\nval cipherKey = CipherKey.SecretKey(key)\nval encrypter = CipherSatchelEncrypter.with(cipherKey, transformation)\n```\n\n#### [Jose4jSatchelEncrypter](https://github.com/adrielcafe/satchel/blob/master/satchel-encrypter-jose4j/src/main/java/cafe/adriel/satchel/encrypter/jose4j/Jose4jSatchelEncrypter.kt)\nUses the [Jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) library for encryption/decryption.\n```kotlin\nval jwk = RsaJwkGenerator.generateJwk(2048)\nval encrypter = Jose4jSatchelEncrypter.with(jwk)\n```\n\n#### [TinkSatchelEncrypter](https://github.com/adrielcafe/satchel/blob/master/satchel-encrypter-tink-jvm/src/main/java/cafe/adriel/satchel/encrypter/tink/jvm/TinkSatchelEncrypter.kt) (JVM)\nUses the [Tink](https://github.com/google/tink) JVM library for encryption/decryption.\n```kotlin\nval keyset = KeysetHandle.generateNew(AesGcmKeyManager.aes256GcmTemplate())\nval encrypter = TinkSatchelEncrypter.with(keyset)\n```\n\n#### [TinkSatchelEncrypter](https://github.com/adrielcafe/satchel/blob/master/satchel-encrypter-tink-android/src/main/java/cafe/adriel/satchel/encrypter/tink/android/TinkSatchelEncrypter.kt) (Android)\nUses the [Tink](https://github.com/google/tink) Android library for encryption/decryption.\n```kotlin\nval encrypter = TinkSatchelEncrypter.with(applicationContext)\n```\n\n#### Build your own Encrypter\nCreate a `class` or `object` that implements the `SatchelEncrypter` interface: \n```kotlin\nobject MySatchelEncrypter : SatchelEncrypter {\n    \n    suspend fun encrypt(data: ByteArray): ByteArray {\n        // Return a encrypted ByteArray\n    }\n\n    fun decrypt(data: ByteArray): ByteArray {\n        // Return a decrypted ByteArray\n    }\n}\n```\n\n### Serializers\n\n#### [RawSatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-core/src/main/java/cafe/adriel/satchel/serializer/raw/RawSatchelSerializer.kt)\nUses the `ObjectOutputStream`/`ObjectInputStream` for serialization/deserialization.\n```kotlin\nval serializer = RawSatchelSerializer\n```\n\n#### [GzipSatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-serializer-gzip/src/main/java/cafe/adriel/satchel/serializer/gzip/GzipSatchelSerializer.kt)\nUses the `GZIPOutputStream`/`GZIPInputStream` for serialization/deserialization.\n```kotlin\nval serializer = GzipSatchelSerializer\n```\n\n#### [Base64SatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-serializer-base64-jvm/src/main/java/cafe/adriel/satchel/serializer/base64/jvm/Base64SatchelSerializer.kt) (JVM)\nUses the `Base64` from [Java 8](https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html) for serialization/deserialization.\n```kotlin\nval serializer = Base64SatchelSerializer\n```\n\n#### [Base64SatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-serializer-base64-android/src/main/java/cafe/adriel/satchel/serializer/base64/android/Base64SatchelSerializer.kt) (Android)\nUses the `Base64` from [Android](https://developer.android.com/reference/android/util/Base64) for serialization/deserialization.\n```kotlin\nval serializer = Base64SatchelSerializer\n```\n\n#### [KryoSatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-serializer-kryo/src/main/java/cafe/adriel/satchel/serializer/kryo/KryoSatchelSerializer.kt)\nUses the [Kryo](https://github.com/EsotericSoftware/kryo) library for serialization/deserialization.\n```kotlin\nval serializer = KryoSatchelSerializer\n```\n:warning: At the moment Kryo 5 only works on Android API 26 and later, [this issue](https://github.com/EsotericSoftware/kryo/issues/691) explains how to make it work in previous versions.\n\n#### [ProtobufLiteSatchelSerializer](https://github.com/adrielcafe/satchel/blob/master/satchel-serializer-protobuf-lite/src/main/java/cafe/adriel/satchel/serializer/protobuf/lite/ProtobufLiteSatchelSerializer.kt)\nUses the [Protocol Buffers Java Lite](https://github.com/protocolbuffers/protobuf/blob/master/java/lite.md) library for serialization/deserialization.\n```kotlin\nval serializer = ProtobufLiteSatchelSerializer\n```\n:warning: The current implementation doesn't supports [Serializable](https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html) objects.\n\n#### Build your own Serializer\nCreate a `class` or `object` that implements the `SatchelSerializer` interface: \n```kotlin\nobject MySatchelSerializer : SatchelSerializer {\n\n    override suspend fun serialize(data: Map\u003cString, Any\u003e): ByteArray {\n        // Transform the Map into a ByteArray\n    }\n\n    override fun deserialize(data: ByteArray): Map\u003cString, Any\u003e {\n        // Transform the ByteArray into a Map\n    }\n}\n```\n\n# Benchmark\nThe following benchmark consists in reading and writing 1k strings on Satchel and similar libraries. Also we compared all modules (storers, encrypters and serializers) individually to help you choose the fastest ones (if performance is a must for you).\n\nYou can run the benchmark by yourself, just execute the following command:\n```shell script\n./gradlew benchmark:connectedCheck\n```\n\nThe benchmark below was made on a [Samsung Galaxy S20](https://www.gsmarena.com/samsung_galaxy_s20-10081.php).\n\n## Similar libraries\nFor this benchmark, we use a local Satchel instance with the stock modules (`FileSatchelStorer`, `BypassSatchelEncrypter` and `RawSatchelSerializer`) from the core library.\n\nKeep in mind that by using different modules you can get best or worse performance results (see the modules benchmarks below for a detailed comparison).\n\n|                                           | Read (ns)   | Write (ns)    |\n|-------------------------------------------|-------------|---------------|\n| **Satchel**                               | **23.054** | **217.000**    |\n| [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences) | 341.693 | 279.346 |\n| [MMKV](https://github.com/Tencent/MMKV)   | 461.807   | 551.308         |\n| [Paper](https://github.com/pilgr/Paper)   | 71.388.808  | 427.568.730   |\n| [Hawk](https://github.com/orhanobut/hawk) | 18.698.000  | 1.829.687.614 |\n\n## Storers\n|                              | Read (ns) | Write (ns) |\n|------------------------------|-----------|------------|\n| `FileSatchelStorer`          | 55.302    | 47.811     |\n| `EncryptedFileSatchelStorer` | 261.962   | 322.577    |\n\n## Encrypters\n|                          | Read (ns) | Write (ns) |\n|--------------------------|-----------|------------|\n| `BypassSatchelEncrypter` | 0         | 0          |\n| `CipherSatchelEncrypter` | 189.423   | 202.577    |\n| `Jose4jSatchelEncrypter` | 394.654   | 498.538    |\n| `TinkSatchelEncrypter`   | 46.439    | 55.134     |\n\n## Serializers\n|                                     | Read (ns) | Write (ns) |\n|-------------------------------------|-----------|------------|\n| `RawSatchelSerializer`              | 652.769   | 1.001.346  |\n| `GzipSatchelSerializer`             | 741.230   | 1.425.924  |\n| `Base64SatchelSerializer` (Android) | 683.231   | 1.029.077  |\n| `Base64SatchelSerializer` (JVM)     | 703.769   | 1.041.000  |\n| `KryoSatchelSerializer`             | 209.923   | 170.654    |\n| `ProtobufLiteSatchelSerializer`     | 629.116   | 1.319.961  |","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Fsatchel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrielcafe%2Fsatchel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Fsatchel/lists"}