SkillAgentSearch skills...

Satchel

:school_satchel: A fast, secure and modular key-value storage with batteries-included for Android and JVM.

Install / Use

/learn @adrielcafe/Satchel

README

JitPack Android API Github Actions Codacy Kotlin ktlint License MIT

<p align="center"> <img src="https://github.com/adrielcafe/satchel/blob/master/satchel.png?raw=true"> </p>

Satchel is a powerful and flexible key-value storage with batteries-included for Android and JVM.

It's backed by Coroutines and great third-party libraries (Tink, Kryo and Protobuf to name a few).

Features

Supported types

  • [x] Double and List<Double>
  • [x] Float and List<Float>
  • [x] Int and List<Int>
  • [x] Long and List<Long>
  • [x] Boolean and List<Boolean>
  • [x] String and List<String>
  • [x] Serializable¹

¹ Not supported by satchel-serializer-protobuf-lite

Setup

  1. Add the JitPack repository to your project level build.gradle:
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
  1. Next, add the desired dependencies to the module build.gradle:
dependencies {
    // Core (required)
    implementation "com.github.adrielcafe.satchel:satchel-core:$currentVersion"

    // Storers
    implementation "com.github.adrielcafe.satchel:satchel-storer-encrypted-file:$currentVersion"

    // Encrypters
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-cipher:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-jose4j:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-tink-android:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-tink-jvm:$currentVersion"

    // Serializers
    implementation "com.github.adrielcafe.satchel:satchel-serializer-base64-android:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-base64-jvm:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-gzip:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-kryo:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-protobuf-lite:$currentVersion"
}

Current version: JitPack

Usage

Take a look at the sample app for a working example.

Global instance

First initialize Satchel's global instance by calling Satchel.init():

Satchel.init(
    storer = FileSatchelStorer(storageFile),
    encrypter = BypassSatchelEncrypter,
    serializer = RawSatchelSerializer
)

Now you can use Satchel.storage everywhere:

Satchel.storage["key"] = "value"

It's also possible to check if Satchel was already initialized:

if (Satchel.isInitialized.not()) {
    // Init
}

Local instance

Use Satchel.with() to create a local instance:

val satchel = Satchel.with(
    storer = FileSatchelStorer(storageFile),
    encrypter = BypassSatchelEncrypter,
    serializer = RawSatchelSerializer
)

And start using it:

satchel["key"] = "value"

API

Satchel has a simple and familiar API based on MutableMap and SharedPreferences:

satchel.apply {
    val firstName = get<String>("firstName")

    val notificationsEnabled = getOrDefault("notificationsEnabled", false)

    val favoritePostIds = getOrDefault("favoritePostIds") { emptySet<Int>() }

    val registeredAt = getOrSet("registeredAt", currentTimestamp)

    val lastName = getOrSet("lastName") { "Doe" }

    set("username", "john.doe")

    setIfAbsent("lastName", lastName)

    keys.forEach { key ->
        // ...
    }

    when {
        isEmpty -> { /* ... */ }
        size == 1 -> { /* ... */ }
        contains("username") -> { /* ... */ }
    }

    remove("favoritePostIds")

    clear()
}

But unlike SharedPreferences, there's no apply() or commit(). Changes are saved asynchronously every time a write operation (set(), remove() and clear()) happens.

Delegates

It's possible to delegate the job of get and set the value of a specific key:

private var favoritePostIds by satchel.value(key = "favoritePostIds", defaultValue = emptySet<Int>())

// Will call set(key, value)
favoritePostIds = setOf(1, 2, 3)

// Will call getOrDefault(key, defaultValue)
showFavoritePosts(favoritePostIds)

If you doesn't specify a default value, it will return a nullable value:

private var username by satchel.value<String>("username")

username?.let(::showProfile)

Events

You can be notified every time the storage changes, just call addListener() to register a listener in the specified CoroutineScope:

satchel.addListener(lifecycleScope) { event ->
    when (event) {
        is SatchelEvent.Set -> { /* ... */ }
        is SatchelEvent.Remove -> { /* ... */ }
        is SatchelEvent.Clear -> { /* ... */ }
    }
}

Modules

Satchel has 3 different categories of modules:

  • Storers: responsible for reading and writing to the file system
  • Encrypters: responsible for encryption and decryption
  • Serializers: responsible for serialization and deserialization

The core library comes with one stock module for each category: FileSatchelStorer, BypassSatchelEncrypter and RawSatchelSerializer. All the other libraries are optional.

Storers

If you are developing for Android, I recommend to use the Context.filesDir as the parent folder. If you want to save in the external storage remember to ask for write permission first.

val file = File(context.filesDir, "satchel.storage")

FileSatchelStorer

Uses the FileOutputStream and FileInputStream to read and write without do any modification.

val storer = FileSatchelStorer(file)

EncryptedFileSatchelStorer

Uses the EncryptedFile from Jetpack Security to read/write and also takes care of encryption/decryption.

val storer = EncryptedFileSatchelStorer.with(applicationContext, file)

Build your own Storer

Create a class or object that implements the SatchelStorer interface:

object MySatchelStorer : SatchelStorer {
    
    suspend fun store(data: ByteArray) {
        // Save the ByteArray wherever you want
    }

    fun retrieve(): ByteArray {
        // Load and return the stored ByteArray
    }
}

Encrypters

:warning: Satchel doesn't store your crypto keys, it only uses it. So make sure to store them in a safe place.

BypassSatchelEncrypter

Just bypass the encryption/decryption.

val encrypter = BypassSatchelEncrypter

CipherSatchelEncrypter

Uses the Cipher for encryption/decryption.

val transformation = "AES"
val key = KeyGenerator
    .getInstance(transformation)
    .apply { init(256) }
    .generateKey()
val cipherKey = CipherKey.SecretKey(key)
val encrypter = CipherSatchelEncrypter.with(cipherKey, transformation)

Jose4jSatchelEncrypter

Uses the Jose4j library for encryption/decryption. `

Related Skills

View on GitHub
GitHub Stars71
CategoryData
Updated1mo ago
Forks8

Languages

Kotlin

Security Score

100/100

Audited on Feb 23, 2026

No findings