clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / site / nips / 49.html
49.html 22.8K
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-49 - Private Key Encryption (ncryptsec) — Nostr Instruction Manual</title>7<meta name="description" content="NIP-49: encrypt a Nostr private key with a password. Full scrypt derivation, XChaCha20-Poly1305 encryption, and the ncryptsec bech32 format.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 49 - Encryption</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="49">49</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 49</div>50 <h1>Private Key Encryption (<code>ncryptsec</code>)</h1>51 <p class="sub">Lock a private key with a password. The result is one portable <code>ncryptsec</code> string that only the password can open.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>A Nostr identity is a <a href="01.html">secp256k1 private key</a>. If an attacker copies the key, the attacker owns the identity forever. This NIP defines how a client encrypts the private key with a password. It also defines the reverse: how a client decrypts it.</p>60 <p>The output is a bech32 string with the prefix <code>ncryptsec</code>. The string carries everything needed for decryption except the password: a version byte, the scrypt work factor, the salt, the nonce, a key-security flag, and the ciphertext. A user can move this one string between clients and computers.</p>61 <p>The scheme has two stages. First, <strong>key derivation</strong>: scrypt stretches the password into a 32-byte symmetric key. Second, <strong>symmetric encryption</strong>: XChaCha20-Poly1305 encrypts the private key under that symmetric key.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>An <code>ncryptsec</code> string is a locked box, not a disguise. A weak password is a weak lock. The scrypt work factor slows an attacker down; it does not replace a strong password.</p>66 </div>6768 <h2 class="bar" id="spec"><span class="num">2</span> Symmetric Key Derivation</h2>69 <p>A password makes a poor cryptographic key. Derive the real key from the password with scrypt. The derivation uses four inputs:</p>70 <ul>71 <li><strong>PASSWORD</strong> — read from the user. Normalize the password to Unicode <strong>NFKC</strong> form before use. Normalization makes the same visible password produce the same bytes on every computer and every client.</li>72 <li><strong>LOG_N</strong> — one byte. It is a power of two that sets the scrypt round count (for example, 18 represents 262,144 rounds). The user or the implementer chooses it. Larger values need more time and more memory, and give better protection.</li>73 <li><strong>SALT</strong> — 16 random bytes. Generate a fresh salt for each encryption.</li>74 <li>scrypt parameters <strong>r = 8</strong> and <strong>p = 1</strong>, fixed.</li>75 </ul>7677 <div class="crt">78 <div class="titlebar">Key derivation</div>79<pre><span class="k">PASSWORD</span> = <span class="c">nfkc_normalize(user input)</span>80<span class="k">LOG_N</span> = <span class="c">1 byte, chosen work factor</span>81<span class="k">SALT</span> = <span class="c">16 random bytes</span>82<span class="k">SYMMETRIC_KEY</span> = scrypt(password=<span class="k">PASSWORD</span>, salt=<span class="k">SALT</span>,83 log_n=<span class="k">LOG_N</span>, r=<span class="s">8</span>, p=<span class="s">1</span>) <span class="c">→ 32 bytes</span></pre>84 </div>85 <p>The symmetric key must be 32 bytes long. The key is temporary. Zero it and discard it after use. Do not store it. Do not reuse it for any other purpose.</p>8687 <div class="tscroll"><table class="ref">88 <caption>LOG_N cost table</caption>89 <tr><th>LOG_N</th><th>Rounds (2^n)</th><th>Memory required</th><th>Approx. time on a fast computer</th></tr>90 <tr><td class="btn">16</td><td>65,536</td><td>64 MiB</td><td>100 ms</td></tr>91 <tr><td class="btn">18</td><td>262,144</td><td>256 MiB</td><td>—</td></tr>92 <tr><td class="btn">20</td><td>1,048,576</td><td>1 GiB</td><td>2 seconds</td></tr>93 <tr><td class="btn">21</td><td>2,097,152</td><td>2 GiB</td><td>—</td></tr>94 <tr><td class="btn">22</td><td>4,194,304</td><td>4 GiB</td><td>—</td></tr>95 </table></div>9697 <figure class="diagram">98 <div class="frame">99 <svg viewBox="0 0 560 120" role="img" aria-label="Password flows through NFKC normalization and scrypt to produce the 32-byte symmetric key">100 <rect class="d-actor" x="10" y="35" width="120" height="50" rx="8"/>101 <text class="d-actor-label" x="70" y="57" text-anchor="middle">Password</text>102 <text class="d-note" x="70" y="74" text-anchor="middle">from the user</text>103 <rect class="d-actor" x="180" y="35" width="110" height="50" rx="8"/>104 <text class="d-actor-label" x="235" y="57" text-anchor="middle">NFKC</text>105 <text class="d-note" x="235" y="74" text-anchor="middle">normalize</text>106 <rect class="d-actor" x="340" y="35" width="90" height="50" rx="8"/>107 <text class="d-actor-label" x="385" y="57" text-anchor="middle">scrypt</text>108 <text class="d-note" x="385" y="74" text-anchor="middle">salt, log_n, r=8, p=1</text>109 <rect class="d-actor" x="480" y="35" width="70" height="50" rx="8"/>110 <text class="d-actor-label" x="515" y="57" text-anchor="middle">Key</text>111 <text class="d-note" x="515" y="74" text-anchor="middle">32 bytes</text>112 <path class="d-msg d-flow" d="M132,60 L177,60"/>113 <path class="d-msg d-flow" d="M292,60 L337,60"/>114 <path class="d-msg d-flow" d="M432,60 L477,60"/>115 </svg>116 </div>117 <figcaption>Fig. 49-1 — <b>Key derivation.</b> Normalize the password, then stretch it with scrypt into a temporary 32-byte key.</figcaption>118 </figure>119120 <h2 class="bar" id="encrypt"><span class="num">3</span> Encrypting a Private Key</h2>121 <p>The plaintext is the user's secp256k1 private key as <strong>32 raw bytes</strong>. Do not encrypt the hex form. Do not encrypt the bech32 (<code>nsec</code>) form.</p>122 <p>One extra byte travels with the key: the <strong>KEY_SECURITY_BYTE</strong>. It records whether the key was ever handled insecurely, for example stored unencrypted or cut and pasted unencrypted. This byte is the associated data of the AEAD cipher. It is authenticated but not encrypted.</p>123124 <div class="tscroll"><table class="ref">125 <caption>KEY_SECURITY_BYTE values</caption>126 <tr><th>Value</th><th>Meaning</th></tr>127 <tr><td class="btn">0x00</td><td>The key is known to have been handled insecurely.</td></tr>128 <tr><td class="btn">0x01</td><td>The key is NOT known to have been handled insecurely.</td></tr>129 <tr><td class="btn">0x02</td><td>The client does not track this data.</td></tr>130 </table></div>131132 <p>Encrypt with <strong>XChaCha20-Poly1305</strong>. Generate a fresh 24-byte random nonce for each encryption. The cipher output is the 32-byte encrypted key plus a 16-byte Poly1305 authentication tag: 48 bytes total.</p>133134 <div class="crt">135 <div class="titlebar">Encryption</div>136<pre><span class="k">PRIVATE_KEY</span> = <span class="c">32 raw bytes (not hex, not bech32!)</span>137<span class="k">ASSOCIATED_DATA</span> = <span class="k">KEY_SECURITY_BYTE</span>138<span class="k">NONCE</span> = <span class="c">24 random bytes</span>139<span class="k">CIPHERTEXT</span> = XChaCha20-Poly1305(140 plaintext=<span class="k">PRIVATE_KEY</span>,141 associated_data=<span class="k">ASSOCIATED_DATA</span>,142 nonce=<span class="k">NONCE</span>,143 key=<span class="k">SYMMETRIC_KEY</span>144 ) <span class="c">→ 48 bytes</span></pre>145 </div>146147 <h2 class="bar" id="payload"><span class="num">4</span> The ncryptsec Payload</h2>148 <p>Set the <strong>VERSION_NUMBER</strong> to <code>0x02</code>. Concatenate the six parts in this exact order. Then encode the result with bech32 under the human-readable prefix <code>ncryptsec</code>.</p>149150 <div class="crt">151 <div class="titlebar">Concatenation and encoding</div>152<pre><span class="k">VERSION_NUMBER</span> = <span class="s">0x02</span>153<span class="k">PAYLOAD</span> = concat(<span class="k">VERSION_NUMBER</span>, <span class="k">LOG_N</span>, <span class="k">SALT</span>,154 <span class="k">NONCE</span>, <span class="k">ASSOCIATED_DATA</span>, <span class="k">CIPHERTEXT</span>) <span class="c">→ 91 bytes</span>155<span class="k">ENCRYPTED_PRIVATE_KEY</span> = bech32_encode(<span class="s">'ncryptsec'</span>, <span class="k">PAYLOAD</span>)</pre>156 </div>157158 <div class="tscroll"><table class="ref">159 <caption>Payload byte layout (91 bytes before bech32)</caption>160 <tr><th>Offset</th><th>Length</th><th>Field</th><th>Content</th></tr>161 <tr><td>0</td><td>1</td><td class="btn">VERSION_NUMBER</td><td>Always <code>0x02</code>.</td></tr>162 <tr><td>1</td><td>1</td><td class="btn">LOG_N</td><td>The scrypt work factor byte.</td></tr>163 <tr><td>2</td><td>16</td><td class="btn">SALT</td><td>The random scrypt salt.</td></tr>164 <tr><td>18</td><td>24</td><td class="btn">NONCE</td><td>The random XChaCha20 nonce.</td></tr>165 <tr><td>42</td><td>1</td><td class="btn">ASSOCIATED_DATA</td><td>The KEY_SECURITY_BYTE.</td></tr>166 <tr><td>43</td><td>48</td><td class="btn">CIPHERTEXT</td><td>32 encrypted key bytes + 16-byte Poly1305 tag.</td></tr>167 </table></div>168169 <figure class="diagram">170 <div class="frame">171 <svg viewBox="0 0 560 130" role="img" aria-label="Byte layout of the 91-byte ncryptsec payload: version, log_n, salt, nonce, key security byte, ciphertext">172 <text class="d-actor-label" x="10" y="20">91-byte payload → bech32("ncryptsec", …)</text>173 <rect x="10" y="45" width="32" height="30" rx="4" fill="#7d8794"/>174 <rect x="42" y="45" width="32" height="30" rx="4" fill="#2c7a3f"/>175 <rect x="74" y="45" width="96" height="30" rx="4" fill="#2e5f9e"/>176 <rect x="170" y="45" width="140" height="30" rx="4" fill="#2e5f9e"/>177 <rect x="310" y="45" width="32" height="30" rx="4" fill="#2c7a3f"/>178 <rect x="342" y="45" width="208" height="30" rx="4" fill="#f26a00"/>179 <text class="d-label" x="26" y="64" text-anchor="middle" fill="#fff">02</text>180 <text class="d-label" x="58" y="64" text-anchor="middle" fill="#fff">n</text>181 <text class="d-label" x="122" y="64" text-anchor="middle" fill="#fff">salt</text>182 <text class="d-label" x="240" y="64" text-anchor="middle" fill="#fff">nonce</text>183 <text class="d-label" x="326" y="64" text-anchor="middle" fill="#fff">ad</text>184 <text class="d-label" x="446" y="64" text-anchor="middle" fill="#fff">ciphertext + tag</text>185 <text class="d-note" x="26" y="92" text-anchor="middle">1 B</text>186 <text class="d-note" x="58" y="107" text-anchor="middle">1 B</text>187 <text class="d-note" x="122" y="92" text-anchor="middle">16 B</text>188 <text class="d-note" x="240" y="92" text-anchor="middle">24 B</text>189 <text class="d-note" x="326" y="107" text-anchor="middle">1 B</text>190 <text class="d-note" x="446" y="92" text-anchor="middle">32 B + 16 B</text>191 </svg>192 </div>193 <figcaption>Fig. 49-2 — <b>Payload layout.</b> Six fields, fixed sizes, fixed order. The total is always 91 bytes before bech32 encoding.</figcaption>194 </figure>195196 <p><strong>Decryption operates in reverse.</strong> Decode the bech32 string. Split the payload at the fixed offsets above. Re-run scrypt with the stored salt and <code>LOG_N</code> and the user's NFKC-normalized password. Then decrypt the ciphertext with XChaCha20-Poly1305 under the stored nonce and associated data. The Poly1305 tag fails if the password is wrong or the data is corrupt.</p>197198 <div class="box tech">199 <span class="tag">Tech Note</span>200 <p>Three soft spots in the source spec. First, the version byte is <code>0x02</code>, but no version <code>0x01</code> is documented and no rule says how to handle an unknown version byte; reject anything that is not <code>0x02</code>. Second, decryption is specified only as "the reverse" — error handling (bad tag, bad length, bad prefix) is left to the implementer. Third, the LOG_N cost table leaves the approximate-time cells for 18, 21, and 22 empty.</p>201 </div>202203 <h2 class="bar" id="testdata"><span class="num">5</span> Test Vectors</h2>204 <h3>Password Unicode normalization</h3>205 <p>The visible password <code>ÅΩẛ̣</code> can arrive from a keyboard in decomposed or compatibility form. Convert it to NFKC before scrypt:</p>206 <div class="tscroll"><table class="ref">207 <caption>NFKC normalization vector</caption>208 <tr><th>Stage</th><th>Unicode codepoints</th><th>UTF-8 bytes</th></tr>209 <tr><td class="btn">input</td><td><code>U+212B U+2126 U+1E9B U+0323</code></td><td><code>E2 84 AB E2 84 A6 E1 BA 9B CC A3</code></td></tr>210 <tr><td class="btn">NFKC</td><td><code>U+00C5 U+03A9 U+1E69</code></td><td><code>C3 85 CE A9 E1 B9 A9</code></td></tr>211 </table></div>212 <h3>Encryption</h3>213 <p>There is no fixed encryption vector. The process is non-deterministic: the random salt and the random nonce change on every run.</p>214 <h3>Decryption</h3>215 <p>The following <code>ncryptsec</code> string, decrypted with password <code>nostr</code> and <code>log_n = 16</code>, yields the hex private key below:</p>216 <div class="crt">217 <div class="titlebar">Decryption vector</div>218<pre><span class="k">ncryptsec</span> = <span class="s">ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcx219 fqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vp220 zmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p</span>221<span class="k">password</span> = <span class="s">"nostr"</span> <span class="k">log_n</span> = <span class="s">16</span>222<span class="k">private key (hex)</span> =223 <span class="s">3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683</span></pre>224 </div>225226 <h2 class="bar" id="examples"><span class="num">6</span> Worked Example</h2>227 <p>The transcript below decrypts the test vector step by step. Every value is real. Orange lines are inputs the client computes or reads. Blue lines are outputs.</p>228 <div class="crt">229 <div class="titlebar">Decryption walkthrough</div>230<pre><span class="c">-- 1. bech32-decode, check prefix "ncryptsec", get 91 bytes --</span>231<span class="send">→ payload[0] version = 02</span>232<span class="send">→ payload[1] log_n = 16 (65,536 rounds)</span>233<span class="send">→ payload[2:18] salt = 52d7c3f8580e7b41953381e5bc49646b</span>234<span class="send">→ payload[18:42] nonce = c33f02a7dcaac8bdd8da23cd44978324235 0b6ebc12edeea7bf</span>236<span class="send">→ payload[42] ad = 00 (key was handled insecurely)</span>237<span class="send">→ payload[43:91] ct+tag = b8e8803440de7b3e9519c3e734cb2ac9238 a211ea2dc52312e5117a11a3022d813a239 b438719ca0b504a1193be510c3aee776</span>240241<span class="c">-- 2. derive the key --</span>242<span class="send">→ password = nfkc("nostr")</span>243<span class="recv">← key = scrypt(password, salt, log_n=16, r=8, p=1) # 32 bytes</span>244245<span class="c">-- 3. decrypt and verify the Poly1305 tag --</span>246<span class="recv">← plaintext = xchacha20_poly1305_open(key, nonce, ad, ct+tag)</span>247<span class="recv">← 3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683</span>248249<span class="c">-- 4. zero the key and the password buffers --</span></pre>250 </div>251252 <h2 class="bar" id="design"><span class="num">7</span> Design Notes</h2>253 <p><strong>Why a key derivation function?</strong> Two reasons, straight from the spec. First, the symmetric cipher assumes a key with a uniform, functionally random bit distribution; a KDF turns a password into such a key deterministically. Second, the KDF injects a slow, irreversible step, so brute-force password guessing is severely hampered.</p>254 <p><strong>Why scrypt?</strong> scrypt has been proven maximally memory hard. Several cryptographers told the NIP author it is a better choice than argon2, even though argon2 won a competition in 2015.</p>255 <p><strong>Why XChaCha20-Poly1305?</strong> Cryptographers typically favor it over AES, and it is less associated with the U.S. government. The cipher (or its earlier non-X variant) sees wide use in TLS and OpenSSH, and most modern crypto libraries provide it.</p>256257 <h2 class="bar" id="recs"><span class="num">8</span> Recommendations</h2>258 <div class="box warn">259 <span class="tag">Warning!</span>260 <p>Do not publish <code>ncryptsec</code> strings to Nostr. When an attacker can amass many encrypted private keys, cracking any one key may become easier.</p>261 </div>262 <div class="box warn">263 <span class="tag">Warning!</span>264 <p>Zero out the memory that holds passwords and private keys before you free it. The derived symmetric key gets the same treatment: zero it, discard it, never store or reuse it.</p>265 </div>266267 <h2 class="bar" id="history"><span class="num">9</span> Version History</h2>268 <p>NIP-49 has been stable since early 2024. One change was breaking. Notable changes, newest first:</p>269 <ul class="patchnotes">270 <li><span class="ver">2026</span><span class="when">2026-06</span>Title standardized to "Private Key Encryption (<code>ncryptsec</code>)" in a repository-wide renaming pass.</li>271 <li><span class="ver">2024</span><span class="when">2024-02</span>Breaking change: the password must be Unicode NFKC-normalized before scrypt. An NFKC test vector was added (#1054).</li>272 <li class="minor"><span class="ver">2024</span><span class="when">2024-02</span>Typo fixes; the term PASSPHRASE was corrected to PASSWORD throughout.</li>273 <li><span class="ver">Origin</span><span class="when">2024-01</span>Michael Dilger published the spec as "NIP-49: Private key encryption" (#133).</li>274 </ul>275276 <div class="credits">277 <h3 class="cardhead">Original Proposal - Credits</h3>278 <table>279 <tr><td>Author</td><td>Michael Dilger</td></tr>280 <tr><td>First commit</td><td>2024-01-30 — “NIP-49: Private key encryption (#133)”</td></tr>281 <tr><td>Pull request</td><td><a href="https://github.com/nostr-protocol/nips/pull/133">nostr-protocol/nips #133</a></td></tr>282 <tr><td>Status</td><td>draft - optional — clients choose whether to offer it</td></tr>283 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/49.md">49.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/49.md">full change history</a></td></tr>284 </table>285 </div>286287 <h2 class="bar"><span class="num">10</span> See Also</h2>288 <ul>289 <li><a href="01.html">NIP-01</a> — defines the secp256k1 keypair that this NIP protects.</li>290 <li><a href="19.html">NIP-19</a> — the bech32 encoding family; <code>ncryptsec</code> follows the same style as <code>nsec</code> and <code>npub</code>.</li>291 <li><a href="06.html">NIP-06</a> — derives the private key from a mnemonic seed phrase instead of storing it.</li>292 <li><a href="46.html">NIP-46</a> — remote signing; keeps the key on another device so clients never hold it.</li>293 <li><a href="44.html">NIP-44</a> — encrypted payloads between users; a different scheme for a different job.</li>294 </ul>295 </div>296297 <aside>298 <div class="cartridge">299 <div class="inner">300 <h3 class="cardhead">Spec Card</h3>301 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>302 <dl>303 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip">optional</span></dd>304 <dt>Defines</dt><dd><code>ncryptsec</code> encoding - key derivation - key security byte</dd>305 <dt>Kind(s)</dt><dd>none — no events defined</dd>306 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> keys; bech32 as in <a href="19.html">NIP-19</a></dd>307 <dt>Crypto</dt><dd>scrypt (r=8, p=1) - XChaCha20-Poly1305 - NFKC - bech32</dd>308 <dt>First commit</dt><dd>2024-01-30</dd>309 <dt>Last updated</dt><dd>2026-06-02</dd>310 </dl>311 </div>312 </div>313 </aside>314 </main>315316 <nav class="navbtns">317 <a href="48.html">◀ NIP-48: Bridged events</a>318 <a href="50.html">NIP-50: Search Capability ▶</a>319 </nav>320321 <footer class="pagefoot">322 <a href="../index.html">Nostr Protocol Manual</a>323 <span class="pageno">NIP 49 / F4</span>324 </footer>325</div>326327<script src="../assets/webmcp.js" defer></script>328</body>329</html>