Storing Notes and Secrets in PIV Data Objects
This guide shows how to store small notes or secrets on the PIV application of a YubiKey or an RP2350-based key (such as RS-Key), protected by the PIV PIN.
Contents
- How It Works
- Storage Limits
- Choosing a Data Object
- Prerequisites
- Writing a Secret
- Reading a Secret
- Writing Without a Plaintext File
- Security Considerations
- Cleanup
- Quick Reference
How It Works
The PIV application exists to hold cryptographic keys and certificates. It also
holds data objects: small blobs, each addressed by an object ID (for example
0x5FC121). ykman can write any file into a data object and read it back.
Data objects are independent of the PIV keys and certificates. You do not need to generate keys or certificates first.
Several data objects can only be read after the PIN is entered. Those objects make a convenient, portable place to keep a short list of critical secrets.
Storage Limits
Space is small, so keep the stored text short.
| Device | Max size per object | Total PIV storage |
|---|---|---|
| YubiKey 4 / 5 | ~3,052 bytes | ~51,200 bytes (shared with certificates) |
| YubiKey NEO | ~2,025 bytes | — |
| RS-Key (RP2350) | 1,900 bytes | 1,408 KB (shared with all applets) |
YubiKey figures come from Yubico's documentation. Certificates and data objects share the total storage, so a key full of certificates has less room for notes.
RS-Key figures come from the firmware source. The per-object cap (MAX_OBJECT
in crates/rsk-piv) keeps a response inside the 2 KiB CCID buffer. The total is
the KVMAIN flash partition, which FIDO, PIV, OATH, and OpenPGP all share.
A 16 MB board uses the same 1,408 KB partition as a 4 MB board; the extra flash
is left unused. A 2 MB build (KVMAIN=896K) has 896 KB.
Real-World Example
My full secrets file was 12,371 bytes. I cut it to 3,223 bytes by removing whitespace, comments, and redundant entries. The trimmed file keeps only the most important secrets, especially the accounts that give access to everything else.
At 3,223 bytes the file was still too large for one object. gzip -9 reduced it to
1,465 bytes, which fits in a single object on both RS-Key and YubiKey.
Choosing a Data Object
Recommended Objects
These three objects are rarely used for their intended purpose, and all three require the PIN to read. That makes them the best choices for secret storage.
| Object ID | Name | Suggested use |
|---|---|---|
0x5FC121 |
Cardholder Iris Images | Primary vault. No conflict with management keys or certificates. |
0x5FC108 |
Cardholder Facial Image | Secondary vault. A second file, a split file, or backup secrets. |
0x5FC103 |
Cardholder Fingerprints | Tertiary vault. Emergency codes or other small secrets. |
I use 0x5FC121 as my primary vault and keep 0x5FC103 for emergency codes. I may
use 0x5FC108 for a PGP private key later.
Caution
Do not use 0x5FC109 (Printed Information). YubiKey uses it to store a
PIN-protected management key, and ykman refuses to write it in that case.
Objects that do not require the PIN (such as CHUID or the certificate objects)
can be read by anyone who holds the key.
All Standard PIV Data Objects
Most of these objects are needed for normal PIV authentication, or have a reserved purpose. They are listed for reference.
| Object ID | Name | Read access | Write access | Purpose |
|---|---|---|---|---|
0x5FC102 |
Cardholder Unique Identifier (CHUID) | Always | Management key | Card identifier, expiry, and optional signature. Used by physical access control systems (PACS). |
0x5FC105 |
X.509 Certificate for PIV Authentication | Always | Management key | Certificate for slot 9a. Used for workstation and system logins. |
0x5FC10A |
X.509 Certificate for Digital Signature | Always | Management key | Certificate for slot 9c. Used to sign documents, Git commits, and email (S/MIME). |
0x5FC10B |
X.509 Certificate for Key Management | Always | Management key | Certificate for slot 9d. Used for asymmetric decryption. |
0x5FC101 |
X.509 Certificate for Card Authentication | Always | Management key | Certificate for slot 9e. Used by door readers and services that do not ask for a PIN. |
0x5FC109 |
Printed Information | PIN | Management key | Badge text (name, employee ID). YubiKey reuses it for the PIN-protected management key. |
0x5FC103 |
Cardholder Fingerprints | PIN | Management key | Fingerprint templates (CBEFF format). |
0x5FC108 |
Cardholder Facial Image | PIN | Management key | Facial photo (JPEG / JPEG 2000). |
0x5FC121 |
Cardholder Iris Images | PIN | Management key | Iris images (CBEFF format). |
0x5FC107 |
Card Capability Container (CCC) | Always | Management key | Describes the card's data model and supported features. |
0x5FC106 |
Security Object | Always | Management key | Signed SHA-256 digests of the other data objects, for tamper evidence. |
0x5FC10C |
Key History Object | Always | Management key | Tracks retired key management certificates in slots 82–95. |
0x5FFF01 |
YubiKey Attestation Certificate | Always | Factory | Yubico extension for slot f9. Proves that keys were generated on the device. |
Prerequisites
- A YubiKey or RP2350 key with the PIV application enabled.
ykmaninstalled.- A PIV PIN. The factory default is
123456. Change it before you store secrets. - The PIV management key. Writing a data object needs the management key, not the PIN.
If your management key is PIN-protected,
ykmanasks only for the PIN.
To change the PIN, and to replace the default management key with a random, PIN-protected one:
ykman piv access change-pin
ykman piv access change-management-key --generate --protect
The shell examples below use bash or zsh syntax. For fish, see the note on fish.
Writing a Secret
1. Create the Secret File
Work in $XDG_RUNTIME_DIR (usually /run/user/<uid>). That directory lives in RAM
and only your user can read it. /tmp can be on disk and is visible to other users.
nano "$XDG_RUNTIME_DIR/secret.txt"
2. Check the Size
wc -c "$XDG_RUNTIME_DIR/secret.txt"
# 1465 /run/user/1000/secret.txt
Compare the number with the storage limits.
3a. Write the File As-Is
If the file fits, write it directly:
ykman piv objects import 0x5fc121 "$XDG_RUNTIME_DIR/secret.txt"
# Enter PIN:
# Object imported.
3b. Compress, Then Write
If the file is too large, compress it. xz often wins on larger text files, and
gzip often wins on very small ones, so try both:
gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt" | wc -c
xz -9 -e -c "$XDG_RUNTIME_DIR/secret.txt" | wc -c
Write the smaller result straight to the key. Process substitution passes the
compressed data to ykman without writing a compressed file to disk:
# gzip
ykman piv objects import 0x5fc121 <(gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt")
# xz
ykman piv objects import 0x5fc121 <(xz -9 -e -c "$XDG_RUNTIME_DIR/secret.txt")
If neither result fits, trim the file further or split it across two objects.
Important
Do not pipe data into ykman ... import -. When stdin is not a terminal, ykman
reads the PIN or management key from the first line of stdin. That line is then
consumed as a PIN, and the stored data is incomplete.
4. Verify the Write
Read the object back and compare it with the source. No output means a match.
# Uncompressed
cmp <(ykman piv objects export 0x5fc121 -) "$XDG_RUNTIME_DIR/secret.txt"
# gzip or xz: decompress the stored copy before you compare
cmp <(ykman piv objects export 0x5fc121 - | gunzip) "$XDG_RUNTIME_DIR/secret.txt"
cmp <(ykman piv objects export 0x5fc121 - | xz -d) "$XDG_RUNTIME_DIR/secret.txt"
Then clean up.
Reading a Secret
Export to - to send the data to stdout. The PIN prompt goes to stderr, so the
secret goes to the terminal and never touches the disk.
# Uncompressed
ykman piv objects export 0x5fc121 -
# Compressed with gzip
ykman piv objects export 0x5fc121 - | gunzip
# Compressed with xz
ykman piv objects export 0x5fc121 - | xz -d
Append | less to page through a long file. To save a copy to a file instead:
ykman piv objects export 0x5fc121 - | gunzip > "$XDG_RUNTIME_DIR/secret.txt"
Writing Without a Plaintext File
These methods write a short secret without creating a plaintext file.
Prompt for the secret (recommended). -s hides the typed text, and the secret
stays out of your shell history:
read -rsp "Secret: " secret && echo &&
ykman piv objects import 0x5fc121 <(printf '%s' "$secret"); unset secret
Type the secret into the command. This is quick, but the secret is saved in
your shell history unless you prevent that (in bash and zsh, start the command
with a space when HISTCONTROL=ignorespace or setopt HIST_IGNORE_SPACE is set):
ykman piv objects import 0x5fc121 <(printf '%s' "my secret text")
fish Shell
fish has no <(...) process substitution. Use psub instead:
ykman piv objects import 0x5fc121 (gzip -9 -c $XDG_RUNTIME_DIR/secret.txt | psub)
read -s -P "Secret: " secret; and ykman piv objects import 0x5fc121 (printf '%s' $secret | psub); set -e secret
Reading with export 0x5fc121 - | gunzip works in fish unchanged.
Security Considerations
- The PIN is the only protection. Data on the key is not encrypted. Anyone who
has the key and the PIN can read the objects. For extra protection, encrypt the
file first (for example with
ageorgpg). Compress before you encrypt, because encrypted data does not compress. - The PIN has limited retries. After 3 wrong attempts (by default) the PIN is
blocked. You then need the PUK to unblock it (default PUK:
12345678— change it). - Reset erases everything.
ykman piv resetdeletes all data objects. - The key is not a backup. Keep another copy of these secrets in a separate safe place, in case the key is lost or damaged.
- Change the default management key. With the default key, anyone who holds your key can overwrite or delete your objects without knowing the PIN. They cannot read the PIN-protected objects.
- Temporary files leak. Delete every plaintext and compressed copy when you are
done. Editors can also leave swap or backup files (for example
.secret.txt.swporsecret.txt~).
Cleanup
Keep any copy you need in a secure place first. Then remove the working files:
rm -f "$XDG_RUNTIME_DIR"/secret.txt* "$XDG_RUNTIME_DIR"/.secret.txt*
# Check that nothing is left
ls -la "$XDG_RUNTIME_DIR" | grep -i secret
$XDG_RUNTIME_DIR is in RAM, so rm is enough and the data is gone at the next
reboot.
If you worked in a directory on disk, overwrite the files before you delete them:
shred -u /path/to/secret.txt*
Note
shred is reliable only on hard disk drives with non-journaling file systems.
On SSDs, and on journaling or copy-on-write file systems (ext4 with data
journaling, Btrfs, ZFS), old copies can survive. Full-disk encryption, or working
in RAM from the start, is the real protection.
Quick Reference
# Write (compressed)
ykman piv objects import 0x5fc121 <(gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt")
# Read (compressed)
ykman piv objects export 0x5fc121 - | gunzip
# Write a short secret from a hidden prompt
read -rsp "Secret: " secret && echo &&
ykman piv objects import 0x5fc121 <(printf '%s' "$secret"); unset secret
# Clean up
rm -f "$XDG_RUNTIME_DIR"/secret.txt*