# 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](#how-it-works) - [Storage Limits](#storage-limits) - [Choosing a Data Object](#choosing-a-data-object) - [Prerequisites](#prerequisites) - [Writing a Secret](#writing-a-secret) - [Reading a Secret](#reading-a-secret) - [Writing Without a Plaintext File](#writing-without-a-plaintext-file) - [Security Considerations](#security-considerations) - [Cleanup](#cleanup) - [Quick Reference](#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. - [`ykman`](https://developers.yubico.com/yubikey-manager/) installed. - 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, `ykman` asks only for the PIN. To change the PIN, and to replace the default management key with a random, PIN-protected one: ```bash 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](#fish-shell). ## Writing a Secret ### 1. Create the Secret File Work in `$XDG_RUNTIME_DIR` (usually `/run/user/`). That directory lives in RAM and only your user can read it. `/tmp` can be on disk and is visible to other users. ```bash nano "$XDG_RUNTIME_DIR/secret.txt" ``` ### 2. Check the Size ```bash wc -c "$XDG_RUNTIME_DIR/secret.txt" # 1465 /run/user/1000/secret.txt ``` Compare the number with the [storage limits](#storage-limits). ### 3a. Write the File As-Is If the file fits, write it directly: ```bash 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: ```bash 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: ```bash # 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. ```bash # 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](#cleanup). ## 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. ```bash # 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: ```bash 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: ```bash 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): ```bash ykman piv objects import 0x5fc121 <(printf '%s' "my secret text") ``` ### fish Shell fish has no `<(...)` process substitution. Use `psub` instead: ```fish 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 `age` or `gpg`). 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 reset` deletes 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.swp` or `secret.txt~`). ## Cleanup Keep any copy you need in a secure place first. Then remove the working files: ```bash 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: ```bash 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 ```bash # 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* ```