Open-source project
orhanobut/hawk avatar
orhanobut/hawk

Hawk: Android key-value storage where the encryption behaviour changed between major versions

✔️ Secure, simple key-value storage for Android

3,993 stars397 forksKotlinApache-2.0

At a glance

What is it?
A pluggable key-value store over app-private SharedPreferences, with a Gson parser and an encryption layer you can swap. The published artifact is from 2018, the branch is modern Kotlin, and the two versions disagree about whether encryption failure falls back to plaintext.
Who is it for?
Hawk is a good fit for a small Android app that wants typed key-value access with encryption it controls, and a poor fit for anything sensitive on a device you cannot guarantee. The thing to understand before writing a line of code is that 2.0.1 and the unreleased 3.0 are different products in a way that matters: 3.0 fails loudly when the Keystore is unavailable while 2.0.1 quietly drops to Base64, and 3.0 cannot read 2's ciphertext at all.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A key-value store with a pipeline you can replace at every layer

The pitch is one line: save a value with a key and read it back without writing a database schema. The feature list behind it covers primitives, strings, custom objects, lists, sets and maps, persisted in app-private SharedPreferences, with parsing, conversion, encryption, serialization and storage all replaceable, and a small API for checking, counting and deleting entries.

That last point is the architectural decision. Every stage of the path a value takes is an interface you can supply your own implementation of, and the README shows it directly:

kotlin
Hawk.init(applicationContext)
    .setParser(GsonParser(GsonBuilder().create()))
    .setStorage(myStorage)
    .setEncryption(myEncryption)
    .build()

Gson is the default parser. You pass a configured one when your models need custom type adapters, or replace the whole pipeline interface when they do not. A `NoEncryption()` implementation is available for data that needs no confidentiality, and the README is careful to say what it does: it only Base64-encodes values, which is encoding rather than protection.

The daily API is small enough to hold in your head:

kotlin
Hawk.init(applicationContext).build()

val saved = Hawk.put("name", "Ada")
val name: String? = Hawk.get("name")
val visits: Int = Hawk.get("visits", 0)

Hawk.put("tags", listOf("android", "kotlin"))
val tags: List<String>? = Hawk.get("tags")

Hawk.contains("name")
Hawk.count()
Hawk.delete("name")
Hawk.deleteAll()

Three details in that block shape how you use it. `put` returns whether the value was actually saved, so it is a boolean you can and should check. Passing null deletes the key. And a missing key returns null, or the default you supply, which is why `visits` can be read as a non-nullable Int.

The synchronous part is the one to plan around. Reads and writes are synchronous, so the README advises keeping substantial work off the main thread. `deleteAll` clears stored values but deliberately retains encryption keys, a decision that returns later in the migration story.

Installing 2.0.1 gets you the 2018 code, not the Keystore version

The install snippet is ordinary Maven Central:

kotlin
repositories {
    mavenCentral()
}

dependencies {
    implementation("com.orhanobut:hawk:2.0.1")
}

What that coordinate resolves to is a release published on 2018-04-03. The repository is not archived and the last push was on 2026-10-03, and the default branch looks nothing like a 2018 Android project: the language is Kotlin, the tree carries `build.gradle.kts`, `settings.gradle.kts`, `gradle.properties`, `gradlew`, a `benchmark/` directory, a `scripts/` directory, `CHANGELOG.md`, `MAINTAINING.md` and a `.idea/` folder.

So the repository is actively developed and the published artifact is eight years old, and the README is explicit about the gap. It opens with a notice that Hawk 3 is coming, describing Android Keystore-backed AES-GCM encryption, fewer dependencies and a modern Kotlin codebase, then states in the same breath that Hawk 3 is not released and the current published version is 2.0.1. The install section repeats the point: the examples use APIs available in 2.0.1, the next version is unreleased and requires Android 6.0, API 23, or newer, and the published 2.0.1 artifact declares API 10 as its minimum.

That minimum SDK line is a real decision point rather than trivia. An artifact declaring API 10 can be consumed by applications with a low minimum, which 3.0's API 23 floor cannot. If your app still supports older devices, the version choice is made for you before you weigh any other factor.

Read the README as two documents stapled together. Everything from the quick start through the configuration section describes 2.0.1. The compatibility and limitations section describes 3.0, and says so explicitly when it does, for instance by stating that Hawk 3.0 uses AES-256-GCM with an application-owned Keystore key and that `ConcealEncryption` has been removed. Keeping track of which paragraph belongs to which version is the main reading skill this project asks of you.

Encryption failure stopped falling back to plaintext

This is the most important difference between the two versions, and it runs in the safer direction for the newer one, which is worth stating clearly because silent fallback is the sort of thing that survives review for years.

What the 2.0.0 release notes say is that `EncryptionMethod` was removed, that by default it is encrypted with a fallback to no encryption mode if the crypto is not available, and that `NoEncryption` became available out of the box through `setEncryption`. So in the published 2.x line, an unavailable or failing crypto provider does not throw. It stores the value Base64-encoded. The README confirms this for 2.0.1 directly: it still uses legacy Conceal and may fall back to Base64-only storage, and its encryption is not changed by the installation snippet above.

What 3.0 does is the opposite. The README states that initialization fails if the Keystore is unavailable and that it never falls back to `NoEncryption`. Each write uses a fresh nonce and binds the ciphertext to its storage key, which closes a second gap in the older design where the same key material and a reused nonce pattern can leak relationships between values.

Both statements are in this project's own files, and they are not in conflict, because they describe different versions. The trap is assuming they are. Code written against 2.x can appear to encrypt data and silently not have done so, and there is no warning at the call site, since `put` returns a boolean about saving rather than about confidentiality.

The other half of the 3.0 story is that it cannot read 2's default ciphertext. Before upgrading you must read and migrate the values using the old library and save them again with the new encryption. Unmigrated values are not automatically cleared or converted, so they stay in storage while `get` returns null, or your default, when decryption fails. A null where you expected a session token is a confusing failure mode, and the migration is on you.

Backup rules decide whether your data survives at all

Because the key that decrypts the values lives in the Android Keystore rather than in the preferences file, restoring a device can hand you ciphertext with no way to read it. The README addresses this directly: Keystore keys are not restored with preferences or transferred to another installation, so `Hawk2.xml` has to be excluded from both cloud backup and device transfer using your app's backup rules. If you skip that, restored values cannot be decrypted.

The recovery guidance is blunt rather than generous. You are told to handle key loss by recovering data from an application-specific trusted source, and that Hawk never regenerates a key while decrypting an existing value. That second sentence is a design commitment worth respecting: silent re-keying would turn every unreadable value into a permanent loss while looking like a fix. Instead, the values stay put, `deleteAll()` retains the key so other stored values are not invalidated, and a custom `KeystoreEncryption` alias should be kept unchanged for as long as its data is retained.

There is also a logging caution: avoid logging sensitive data through a `LogInterceptor`. That interface exists because 2.0.0 removed `LogLevel` and delegated all messages to the interceptor, so whether anything gets printed is now your configuration's decision rather than the library's default.

Taken together, the backup rules and the key retention rules are the operational half of this library. Encryption in Hawk is not a property of a single file, it is a relationship between a preferences file, a Keystore entry, your backup configuration and your user's device, and all four have to stay in step. For an app holding a login token, that is a lot of moving parts for what reads like `Hawk.put` and `Hawk.get`.

Type discipline and R8 decide whether old values still read back

The compatibility section has a cluster of rules that all reduce to one idea: Hawk stores enough type information to fail badly when your model changes.

Use the same type when reading a key as when saving it. Collections record the class of the first element, so use homogeneous, non-nested collections whose first element, and first map key and value, is non-null. A list whose first entry happened to be null, or a map with mixed value types, stores a class that will not match what you read back later.

Model changes can prevent older data from being read, and Gson does not enforce Kotlin constructor defaults or non-null properties. So the type system will happily tell you a value is non-null while the deserializer produces something else, and a renamed or restructured model turns a stored object into a null on read.

Then there is shrinking. The README warns specifically about apps using R8: keep the names and fields of models persisted with Hawk, because the format stores class names and Gson uses reflection. The rule is given concretely:

proguard
-keep class com.example.app.storage.model.** { *; }

That is a normal, unglamorous ProGuard rule, and it is the difference between a release build that reads your saved state and one that silently loses it. It is the kind of thing that works in debug and breaks in production, which is the worst possible failure profile.

One more closes the loop: changing encryption, parsing or storage implementations does not migrate existing data. Hawk 2 data is incompatible with Hawk 1. The `Hawk2.xml` filename is a fossil of that history, and the 1.22 release notes record the point where old data support was removed from the 1.x line. Treat the storage format as write-once per major version and plan migrations as application work, because that is what it is.

What Hawk is for, next to a memory-mapped store

The honest case for Hawk is that it is a very small idea executed thoroughly. If you need typed reads, a swappable parser, an encryption layer you own, and no schema, it removes about a screen of boilerplate and gives you one `init` call. The pluggable storage interface also means you can start on SharedPreferences and move the backing store later without touching call sites, which is a migration path most preferences libraries do not offer.

The case against is equally concrete. Everything is synchronous, so you manage your own threading. The value model is Gson-shaped, which means reflection, R8 rules and a class-name-based storage format, none of which you would accept in a new design. And in the published version the encryption can quietly degrade to Base64.

Google's related searches for this project surface MMKV alongside Hawk itself, which is a fair signal about what people are weighing it against. The difference is one of approach rather than of feature list. Hawk is a SharedPreferences-backed typed store with encryption in the middle; a memory-mapped key-value library such as MMKV is built for throughput on a hot path, with mmap giving it write performance that a preferences file does not offer. If you are caching thousands of small values in a scroll-heavy screen, that is the axis to compare on. If you are storing a dozen settings and an auth token, Hawk's simplicity and its pluggable encryption are the better fit.

Whichever direction you go, the licensing is unambiguous: Apache License 2.0, with a LICENSE file in the repository and the README crediting Orhan Obut as copyright holder.

Editorial conclusion

Hawk is a good fit for a small Android app that wants typed key-value access with encryption it controls, and a poor fit for anything sensitive on a device you cannot guarantee. The thing to understand before writing a line of code is that 2.0.1 and the unreleased 3.0 are different products in a way that matters: 3.0 fails loudly when the Keystore is unavailable while 2.0.1 quietly drops to Base64, and 3.0 cannot read 2's ciphertext at all. Decide which one you are building against before you choose a version, because the migration is manual and there is no automatic path. If you ship on 2.0.1, exclude `Hawk2.xml` from backup now, and keep `-keep` rules for any model class Gson reflects over. Start with `Hawk.init(applicationContext).build()` and read the compatibility section before you store anything you would mind losing.

Frequently asked questions

What does Hawk store and where does it keep the data?

Hawk stores primitives, strings, custom objects, lists, sets and maps in app-private SharedPreferences. You need no database schema: you save a value under a key and read it back, and reads and writes are synchronous, so substantial work belongs off the main thread.

Which version of Hawk should I use?

Only 2.0.1 is published, from 2018-04-03, and it declares API 10 as its minimum while relying on legacy Conceal encryption. Hawk 3 is unreleased, requires Android 6.0 or API 23 and newer, and cannot read 2's default ciphertext, so upgrading requires migrating values manually.

What happens when Hawk cannot encrypt a value?

It depends on the version. In 2.x the default falls back to no encryption mode, which Base64-encodes the value, and the README says 2.0.1 may do exactly that. In the unreleased 3.0 initialization fails if the Keystore is unavailable and never falls back to `NoEncryption`.

Should Hawk data be excluded from Android cloud backup?

Yes. Keystore keys are not restored with preferences or transferred to another installation, so the README says to exclude `Hawk2.xml` from both cloud backup and device transfer in your app's backup rules. Otherwise restored values cannot be decrypted.

Does Hawk need ProGuard rules?

For apps using R8, yes. The storage format records class names and Gson reflects over your models, so the README asks you to keep the names and fields of persisted models with a rule such as `-keep class com.example.app.storage.model.** { *; }`.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. orhanobut/hawk on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/orhanobut-hawk.svg)](https://hysenlabs.com/projects/orhanobut-hawk)