A command-line tool for exporting and importing Aries Askar PostgreSQL wallet data.
askardedum export connects to PostgreSQL databases containing Aries Askar encrypted
wallet data, decrypts all items using the provided passkey, and streams the decrypted
data to JSON output. askardedum import reads such a dump back, re-encrypts it under
the target store's own keys, and writes it into one of its profiles.
Together they move a wallet's contents between stores whose passkeys, key derivation methods and layouts differ — for example from a store-per-wallet deployment into one profile of a multi-tenant store, or from one agent's database into another's.
- Decrypt Askar wallet data stored in PostgreSQL
- Load a dump into another store, including key manager entries (
kind=1) - Support for multi-tenant wallets with sub-wallet databases
- Streaming output to handle large datasets with minimal memory usage
- JSON and JSON Lines (JSONL) formats, auto-detected on import (
--formatoverrides) - Profile filtering for targeted extraction and loading
- Credentials can be passed via
ASKARDEDUM_DB_URI/ASKARDEDUM_PASSKEYinstead of argv
- Access to the PostgreSQL database containing the Askar wallet
- The wallet passkey used during wallet provisioning
- Network connectivity to the database server
# Native build
cargo build --release
# Cross-compile for Linux (from macOS)
cargo zigbuild --release --target x86_64-unknown-linux-muslThe binary will be located at:
- Native:
target/release/askardedum - Linux:
target/x86_64-unknown-linux-musl/release/askardedum
# Output to stdout
askardedum export --db-uri "postgresql://user:pass@host:5432/wallet_db" \
--passkey "your_wallet_passkey"
# Output to file
askardedum export --db-uri "postgresql://user:pass@host:5432/wallet_db" \
--passkey "your_wallet_passkey" \
--output wallet_data.jsonFor ACA-Py multi-tenant deployments with separate databases per sub-wallet:
askardedum export --db-uri "postgresql://user:pass@host:5432/base_wallet" \
--passkey "your_wallet_passkey" \
--sub-wallets "tenant1_db,tenant2_db,tenant3_db"For deployments that keep every tenant as a profile of one store, export the profile of interest. Its store key is the base wallet passkey:
askardedum export --db-uri "postgresql://user:pass@host:5432/multitenant_sub_wallet" \
--passkey "base_wallet_passkey" \
--profile "<wallet_id>" \
--format jsonl --output tenant.jsonl# Export only first 1000 items per profile
askardedum export --db-uri "postgresql://user:pass@host:5432/wallet_db" \
--passkey "your_wallet_passkey" \
--limit 1000| Option | Description |
|---|---|
--db-uri |
PostgreSQL connection URI (required; env ASKARDEDUM_DB_URI) |
--passkey |
Wallet passkey/password (required; env ASKARDEDUM_PASSKEY) |
--output, -o |
Output file path (default: stdout) |
--sub-wallets |
Comma-separated list of sub-wallet database names |
--schema |
Database schema (default: "public"; plain identifiers only) |
--profile |
Filter by specific profile name |
--limit |
Maximum items per profile in id order (0 = unlimited; negative values are rejected) |
--format |
Output format: json or jsonl (default: json) |
--include-raw |
Include raw encrypted hex values for debugging |
Items are exported in item id order, so --limit takes a stable prefix and two
exports of the same profile can be compared line by line.
If a wallet fails part-way — a row that cannot be fetched, an item or tag that does
not decrypt — the export fails rather than writing a dump that silently misses data.
With --sub-wallets, the remaining wallets still run, but the exit status reports
the failure.
Import writes into an existing profile of an existing store: create the wallet with
the owning agent first (for ACA-Py, POST /multitenancy/wallet) so that its wallet
record, store and profile are set up the way that agent expects, then load the items.
# Inspect the dump and verify the target without writing anything
askardedum import --db-uri "postgresql://user:pass@host:5432/target_db" \
--passkey "target_passkey" \
--input tenant.jsonl --dry-run
# Load it
askardedum import --db-uri "postgresql://user:pass@host:5432/target_db" \
--passkey "target_passkey" \
--input tenant.jsonlImport always reports what the input holds — item count, source wallet/profile, entry kinds and categories — before touching the target.
A dump may hold several source profiles, for instance when it was written with
--sub-wallets. Since import writes to a single profile it refuses to merge them, so
narrow the input first:
askardedum import --db-uri "..." --passkey "..." --input dump.jsonl \
--source-wallet multitenant_sub_wallet \
--source-profile "<wallet_id>" \
--skip-category acapy_version \
--skip-category acapy_storage_typeSkipping acapy_version and acapy_storage_type lets the target agent keep its own
version bookkeeping instead of inheriting the source's.
| Option | Description |
|---|---|
--db-uri |
PostgreSQL connection URI of the target store (required; env ASKARDEDUM_DB_URI) |
--passkey |
Target store passkey/password (required; env ASKARDEDUM_PASSKEY) |
--input, -i |
Dump written by export, json or jsonl (required) |
--format |
Input format, when the auto-detection should not decide |
--schema |
Database schema (default: "public"; plain identifiers only) |
--profile |
Target profile name (default: the store's default_profile) |
--source-profile |
Load only items exported from this source profile |
--source-wallet |
Load only items exported from this source wallet |
--skip-category |
Category to leave behind, repeatable |
--batch-size |
Items per transaction (default: 500) |
--on-conflict |
error (default), skip, or replace when the item already exists |
--dry-run |
Decode and re-encrypt every selected item and verify the target, without writing |
The input is read exactly once, before anything is written: pipes and process
substitutions work as --input, and the items that get written are the items that
were scanned and validated. Every item's value is decoded before the first row is
inserted, so a malformed item aborts a clean target instead of a half-written one.
If a later batch still fails (for instance on a conflict), the error reports how many
items had already been committed.
If the input itself holds the same (kind, category, name) twice — say, two
concatenated exports of one wallet — the default --on-conflict error refuses up
front and says so, instead of blaming the target store; skip keeps the first
occurrence of each, replace the last. --on-conflict replace also clears any
expiry left on the replaced row, which would otherwise keep the item invisible
to Askar.
Export the target and compare it with the input:
askardedum export --db-uri "postgresql://...target_db" --passkey "target_passkey" \
--format jsonl --output target.jsonl
diff <(jq -r '[.kind, .category] | @tsv' tenant.jsonl | sort | uniq -c) \
<(jq -r '[.kind, .category] | @tsv' target.jsonl | sort | uniq -c)[
{
"dump_version": 2,
"wallet_name": "wallet_db",
"profile_name": "e4b5c6d7-...",
"profile_id": 1,
"item_id": 123,
"kind": 2,
"category": "connection",
"name": "conn-abc123",
"value": {
"connection_id": "abc123",
"state": "active"
},
"value_text": "{\"connection_id\": \"abc123\", \"state\": \"active\"}",
"tags": [
{"name": "state", "value": "active", "plaintext": true}
]
}
]Each item is output as a single line, useful for streaming processing:
{"wallet_name":"wallet_db","profile_name":"...","item_id":1,...}
{"wallet_name":"wallet_db","profile_name":"...","item_id":2,...}
| Field | Meaning |
|---|---|
dump_version |
Dump schema version. Items without it (older dumps, or dumps reshaped by hand) are imported with a warning: their values are reconstructed on a best-effort basis. |
wallet_name, profile_name |
Where the item was read from. Import reads these to keep two source profiles from being merged into one target; items missing the labels are kept apart from every labelled source and can only be imported on their own. |
profile_id, item_id |
Source-side row ids, for reference only. Import assigns its own. |
kind |
2 for a stored item, 1 for a key manager entry (a key, whose value is CBOR and therefore appears hex-marked) |
category, name |
The item's identity, unique per profile together with kind |
value |
The value as JSON when it parses as JSON, as a string otherwise, and as "(binary:<hex>)" when it is not text |
value_text |
The value verbatim. Written only when value alone would not reproduce the stored bytes — for instance when a JSON value carries spacing that parsing drops. Authoritative on import. |
tags |
Decrypted tags. plaintext tags are stored unencrypted by Askar and can be queried directly; the ~ prefix used by the Askar language wrappers is not part of the stored name. |
raw_category, raw_name, raw_value |
The still-encrypted bytes, with --include-raw, for debugging |
Values round-trip byte for byte: export checks each rendering against the bytes it
read and falls back to value_text whenever the rendering alone would lose anything.
# Pretty print with jq
askardedum export --db-uri "..." --passkey "..." | jq .
# Filter specific categories
askardedum export --db-uri "..." --passkey "..." | jq '[.[] | select(.category == "credential")]'
# Count items by category
askardedum export --db-uri "..." --passkey "..." | jq 'group_by(.category) | map({category: .[0].category, count: length})'
# Save to file while viewing logs
askardedum export --db-uri "..." --passkey "..." > output.json- Logs are written to stderr, JSON output to stdout
- Export streams items and never holds a wallet in memory. Import reads the whole input once, up front, so it needs memory proportional to the dump; in exchange, pipes work as input and nothing is written until every item has been decoded.
- Item expiry is not carried through: ACA-Py does not set it, imported items are written without one, and
--on-conflict replaceclears any expiry left on the replaced row - Items are written in batches of
--batch-size, each batch as a couple of multi-row statements, so imports are not bound by per-item network round trips - Stop the agents using a store before importing into it, so that nothing writes to the profile concurrently
- Database connection URIs should be properly escaped if they contain special characters
Entry categories, names and tags are encrypted with a nonce derived from the message
itself, which is what makes Askar's lookups by category, name or tag work: equal
plaintexts must encrypt to equal bytes. Entry values are the one field never looked up
by ciphertext, and there import uses a random nonce — exactly as Askar does — so that
equal or unchanged values do not reveal themselves to anyone watching the stored bytes.
Askar reads the nonce prefixed to every field either way.
The encryption is covered by known-answer tests taken from an independent implementation
of the construction in askar-storage/src/protect/profile_key.rs — in the encrypt
direction for the deterministic fields, in the decrypt direction for values:
cargo testThis tool requires:
- Database access credentials
- The wallet encryption passkey
Both are sensitive, and so is the output: a dump holds private keys, link secrets and credential attributes in the clear. Ensure you:
- Pass credentials via the
ASKARDEDUM_DB_URIandASKARDEDUM_PASSKEYenvironment variables rather than argv — command-line arguments are visible to every local user inpsfor as long as the run takes - Run on trusted systems only
- Secure or delete output files containing decrypted data
Apache-2.0