clone

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

main
nip.guide / site / nips / 44.html
44.html 40.3K
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-44 - Encrypted Payloads (Versioned) — Nostr Instruction Manual</title>7<meta name="description" content="NIP-44: versioned keypair-based encryption for Nostr. ECDH conversation keys, HKDF, ChaCha20, HMAC-SHA256, padding, and the base64 payload format. Complete manual page.">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 44 - 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="44">44</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 44</div>50 <h1>Encrypted Payloads (Versioned)</h1>51 <p class="sub">A versioned encryption format for keypair-to-keypair payloads: ECDH, HKDF, ChaCha20, HMAC-SHA256, padding, and base64, audited and test-vectored.</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>This NIP defines a data format for keypair-based encryption. Two users derive a shared secret from their keys. The sender encrypts a payload with that secret. The recipient decrypts it with the same secret. Nobody else can read it.</p>60 <p>The format is <strong>versioned</strong>. The first byte of every payload names an encryption algorithm suite. This lets multiple algorithm choices exist at the same time. Today only version 2 is defined and usable.</p>61 <p>The format may carry many things. It MUST be used inside a signed event as defined in <a href="01.html">NIP-01</a>. The outer Schnorr signature authenticates the full payload. This NIP defines <em>no</em> event kinds. It is only the encryption layer that a messaging standard can build on. It SHOULD NOT be used as a drop-in replacement for <a href="04.html">NIP-04</a> payloads.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>Version 2 of this format was audited by Cure53 in December 2023. The audit report is published as <code>audit-2023.12.pdf</code> in the <code>paulmillr/nip44</code> repository and on the auditor's website. Reference implementations in many languages live in the same repository.</p>66 </div>6768 <h2 class="bar" id="spec"><span class="num">2</span> Versions</h2>69 <p>The first decoded byte of a payload is the version. Three values are defined:</p>70 <div class="tscroll"><table class="ref">71 <caption>Version bytes</caption>72 <tr><th>Version</th><th>Status</th><th>Algorithms</th></tr>73 <tr><td class="btn">0x00</td><td>Reserved</td><td>None. Do not use.</td></tr>74 <tr><td class="btn">0x01</td><td>Deprecated and undefined</td><td>None. Do not use.</td></tr>75 <tr><td class="btn">0x02</td><td>Current</td><td>secp256k1 ECDH - HKDF - padding - ChaCha20 - HMAC-SHA256 - base64</td></tr>76 </table></div>77 <p>A payload that starts with the character <code>#</code> signals a future non-base64 encoding. When a decoder sees an unknown version or a leading <code>#</code>, it MUST report "encryption version not supported". It must not report a base64 error.</p>7879 <h2 class="bar" id="limits"><span class="num">3</span> Threat Model and Limitations</h2>80 <p>Every Nostr user has a public key. This solves the key distribution problem of other systems. But the relay architecture makes strong private-messaging features hard: metadata hiding, forward secrecy, and post-compromise secrecy are out of scope here.</p>81 <p>The goal of this NIP is a <em>simple</em> way to encrypt payloads inside a signed event. Weigh your users' threat model before you apply it. For high-risk situations, users should chat in specialized E2EE messaging software. They should limit Nostr to exchanging contacts.</p>82 <p>On its own, this scheme has these shortcomings:</p>83 <ul>84 <li><strong>No deniability.</strong> Anyone can prove that a particular key signed an event.</li>85 <li><strong>No forward secrecy.</strong> A compromised key decrypts all previous conversations.</li>86 <li><strong>No post-compromise security.</strong> A compromised key decrypts all future conversations.</li>87 <li><strong>No post-quantum security.</strong> A powerful quantum computer could decrypt the messages.</li>88 <li><strong>IP address leak.</strong> Relays and intermediaries can see the user's IP address.</li>89 <li><strong>Date leak.</strong> The <code>created_at</code> field of a <a href="01.html">NIP-01</a> event is public.</li>90 <li><strong>Limited message size leak.</strong> Padding only partially hides the true message length.</li>91 <li><strong>No attachments.</strong> The format does not support them.</li>92 </ul>93 <div class="box warn">94 <span class="tag">Warning!</span>95 <p>Lack of forward secrecy is only <em>partially</em> mitigated. Send messages to trusted relays only. Ask relays to delete stored messages after a set duration.</p>96 </div>9798 <h2 class="bar" id="design"><span class="num">4</span> Version 2 Design Choices</h2>99 <p>Version 2 makes deliberate trade-offs. Each primitive was chosen for a reason:</p>100 <div class="tscroll"><table class="ref">101 <caption>Why these algorithms</caption>102 <tr><th>Choice</th><th>Instead of</th><th>Reason</th></tr>103 <tr><td class="btn">MAC before signing</td><td>MAC after</td><td>Events are already signed per <a href="01.html">NIP-01</a>. The outer signature authenticates the full payload. Validate it before you decrypt.</td></tr>104 <tr><td class="btn">ChaCha20</td><td>AES</td><td>ChaCha is faster and has better security against multi-key attacks.</td></tr>105 <tr><td class="btn">ChaCha20</td><td>XChaCha</td><td>XChaCha is not standardized. Its improved nonce collision resistance is unnecessary: every message gets a fresh (key, nonce) pair.</td></tr>106 <tr><td class="btn">HMAC-SHA256</td><td>Poly1305</td><td>Polynomial MACs are much easier to forge.</td></tr>107 <tr><td class="btn">SHA256</td><td>SHA3 / BLAKE</td><td>SHA256 is already used in Nostr. BLAKE's speed advantage shrinks in non-parallel environments.</td></tr>108 <tr><td class="btn">Custom padding</td><td>padmé</td><td>Better leakage reduction for small messages.</td></tr>109 <tr><td class="btn">Base64</td><td>other encodings</td><td>Widely available and already used in Nostr.</td></tr>110 </table></div>111112 <h2 class="bar" id="keys"><span class="num">5</span> Key Derivation</h2>113 <p>Two key derivation stages exist. The <strong>conversation key</strong> is long-term and shared by one pair of users. The <strong>message keys</strong> are unique per message.</p>114 <h3>Conversation key</h3>115 <p>Execute ECDH: multiply public key B by private key a (scalar multiplication as defined in BIP-340). The output <code>shared_x</code> must be the <em>unhashed</em> 32-byte encoded x coordinate of the shared point. Then run HKDF-extract with SHA256, <code>IKM=shared_x</code> and <code>salt=utf8_encode('nip44-v2')</code>. The output is the <code>conversation_key</code>. It is symmetric: <code>conv(a, B) == conv(b, A)</code>.</p>116 <p>Validate both keys per BIP-340. The public key must be a valid non-zero point on the secp256k1 curve. The private key must be a scalar in the range <code>[1, secp256k1_order - 1]</code>.</p>117 <div class="box warn">118 <span class="tag">Warning!</span>119 <p>NIP-44 does <em>not</em> hash the ECDH output. Some libraries hash it with sha256 by default. In libsecp256k1, the unhashed version is available as <code>secp256k1_ec_pubkey_tweak_mul</code>.</p>120 </div>121 <h3>Message keys</h3>122 <p>Generate a random 32-byte <code>nonce</code> for every message. Always use a CSPRNG. Do not derive the nonce from message content. Do not reuse a nonce between messages: reuse makes those messages decryptable, though it does not leak the long-term key.</p>123 <p>Validate that <code>conversation_key</code> and <code>nonce</code> are both 32 bytes. Run HKDF-expand with SHA256, <code>PRK=conversation_key</code>, <code>info=nonce</code>, and <code>L=76</code>. Slice the 76-byte output:</p>124 <div class="tscroll"><table class="ref">125 <caption>Message key slices (HKDF-expand output, 76 bytes)</caption>126 <tr><th>Slice</th><th>Bytes</th><th>Size</th><th>Used for</th></tr>127 <tr><td class="btn">chacha_key</td><td><code>0..32</code></td><td>32 bytes</td><td>ChaCha20 encryption key</td></tr>128 <tr><td class="btn">chacha_nonce</td><td><code>32..44</code></td><td>12 bytes</td><td>ChaCha20 nonce</td></tr>129 <tr><td class="btn">hmac_key</td><td><code>44..76</code></td><td>32 bytes</td><td>HMAC-SHA256 key</td></tr>130 </table></div>131132 <figure class="diagram">133 <div class="frame">134 <svg viewBox="0 0 560 210" role="img" aria-label="Private key a and public key B flow through ECDH and HKDF-extract to the conversation key; with a random nonce, HKDF-expand yields chacha key, chacha nonce, and hmac key">135 <rect class="d-actor" x="10" y="16" width="110" height="40" rx="8"/>136 <text class="d-actor-label" x="65" y="41" text-anchor="middle">priv a - pub B</text>137 <rect class="d-actor" x="170" y="16" width="90" height="40" rx="8"/>138 <text class="d-actor-label" x="215" y="41" text-anchor="middle">ECDH</text>139 <rect class="d-actor" x="310" y="16" width="120" height="40" rx="8"/>140 <text class="d-actor-label" x="370" y="34" text-anchor="middle">HKDF-extract</text>141 <text class="d-note" x="370" y="50" text-anchor="middle">salt "nip44-v2"</text>142 <rect class="d-actor" x="180" y="100" width="150" height="40" rx="8"/>143 <text class="d-actor-label" x="255" y="118" text-anchor="middle">HKDF-expand</text>144 <text class="d-note" x="255" y="134" text-anchor="middle">info=nonce, L=76</text>145 <rect class="d-actor" x="400" y="76" width="140" height="26" rx="6"/>146 <text class="d-actor-label" x="470" y="93" text-anchor="middle">chacha_key 32B</text>147 <rect class="d-actor" x="400" y="108" width="140" height="26" rx="6"/>148 <text class="d-actor-label" x="470" y="125" text-anchor="middle">chacha_nonce 12B</text>149 <rect class="d-actor" x="400" y="140" width="140" height="26" rx="6"/>150 <text class="d-actor-label" x="470" y="157" text-anchor="middle">hmac_key 32B</text>151 <rect class="d-actor" x="10" y="100" width="120" height="40" rx="8"/>152 <text class="d-actor-label" x="70" y="118" text-anchor="middle">nonce 32B</text>153 <text class="d-note" x="70" y="134" text-anchor="middle">CSPRNG</text>154 <path class="d-msg d-flow" d="M122,36 L167,36"/>155 <path class="d-msg d-flow" d="M262,36 L307,36"/>156 <path class="d-msg blue" d="M370,58 C370,80 320,90 300,97"/>157 <text class="d-label" x="420" y="66" text-anchor="middle">conversation_key</text>158 <path class="d-msg d-flow" d="M132,120 L177,120"/>159 <path class="d-msg blue" d="M332,110 L397,92"/>160 <path class="d-msg blue" d="M332,120 L397,121"/>161 <path class="d-msg blue" d="M332,130 L397,150"/>162 <circle r="4" fill="#f26a00">163 <animateMotion dur="2.6s" repeatCount="indefinite" path="M122,36 L167,36 L307,36"/>164 </circle>165 </svg>166 </div>167 <figcaption>Fig. 44-1 — <b>Key derivation.</b> ECDH plus HKDF-extract makes the long-term conversation key. HKDF-expand with a fresh nonce makes the per-message keys.</figcaption>168 </figure>169170 <h2 class="bar" id="padding"><span class="num">6</span> Padding</h2>171 <p>Padding hides part of the true message length. Encode the content from UTF-8 into a byte array first. Then validate the plaintext length: minimum 1 byte, maximum 4,294,967,295 bytes.</p>172 <p>The padded form is a length prefix, the plaintext, then zero bytes. The prefix is big-endian and has two shapes:</p>173 <ul>174 <li>Length below 65536: 2-byte <code>u16</code> prefix. Format: <code>[plaintext_length: u16][plaintext][zero_bytes]</code>.</li>175 <li>Length 65536 or greater: 6-byte prefix, 2 zero bytes plus a <code>u32</code>. Format: <code>[0x00, 0x00][plaintext_length: u32][plaintext][zero_bytes]</code>.</li>176 </ul>177 <p>A zero value in the first 2 bytes signals the extended format. This is unambiguous: valid plaintext is at least 1 byte, so a <code>u16</code> length of 0 is otherwise invalid.</p>178 <p>The padded size follows a powers-of-two rule with a minimum of 32 bytes. Messages of 32 bytes or less pad to exactly 32 bytes. Larger messages round up to the next multiple of a chunk. The chunk is 32 bytes while the next power of two is at most 256; above that, the chunk is one eighth of the next power of two.</p>179 <div class="crt">180 <div class="titlebar">Pseudocode - padding</div>181<pre><span class="c"># Calculates length of the padded byte array.</span>182def calc_padded_len(unpadded_len):183 next_power = 1 &lt;&lt; (floor(log2(unpadded_len - 1))) + 1184 if next_power &lt;= 256: chunk = 32185 else: chunk = next_power / 8186 if unpadded_len &lt;= 32: return 32187 else: return chunk * (floor((unpadded_len - 1) / chunk) + 1)188189<span class="c"># Converts unpadded plaintext to padded bytearray.</span>190def pad(plaintext):191 unpadded = utf8_encode(plaintext)192 unpadded_len = len(unpadded)193 if (unpadded_len &lt; c.min_plaintext_size or194 unpadded_len &gt; c.max_plaintext_size): raise <span class="s">'invalid plaintext length'</span>195 if unpadded_len &gt;= c.extended_prefix_threshold:196 prefix = concat([0, 0], write_u32_be(unpadded_len)) <span class="c"># 6 bytes</span>197 else:198 prefix = write_u16_be(unpadded_len) <span class="c"># 2 bytes</span>199 suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)200 return concat(prefix, unpadded, suffix)201202<span class="c"># Converts padded bytearray to unpadded plaintext.</span>203def unpad(padded):204 first_two = read_uint16_be(padded[0:2])205 if first_two == 0:206 unpadded_len = read_uint32_be(padded[2:6])207 if unpadded_len &lt; c.extended_prefix_threshold: raise <span class="s">'invalid padding'</span>208 prefix_len = 6209 else:210 unpadded_len = first_two211 prefix_len = 2212 unpadded = padded[prefix_len : prefix_len + unpadded_len]213 if (unpadded_len == 0 or214 len(unpadded) != unpadded_len or215 len(padded) != prefix_len + calc_padded_len(unpadded_len)): raise <span class="s">'invalid padding'</span>216 return utf8_decode(unpadded)</pre>217 </div>218 <p>Note that <code>unpad</code> rejects an extended prefix that carries a length below 65536. Encoding a small length in the 6-byte form is invalid padding.</p>219 <div class="box warn">220 <span class="tag">Warning!</span>221 <p><code>calc_padded_len</code> can return values up to 2<sup>32</sup>. That exceeds the range of unsigned 32-bit integers. Use 64-bit or larger arithmetic for padding calculations.</p>222 </div>223224 <h2 class="bar" id="encrypt"><span class="num">7</span> Encryption</h2>225 <p>Follow these seven steps to encrypt a message from user A to user B:</p>226 <ol>227 <li><strong>Calculate the conversation key.</strong> ECDH of public key B by private key a, then HKDF-extract with salt <code>nip44-v2</code>. See §5.</li>228 <li><strong>Generate a random 32-byte nonce</strong> from a CSPRNG. Never derive it from content. Never reuse it.</li>229 <li><strong>Calculate the message keys.</strong> Validate both inputs are 32 bytes. HKDF-expand to 76 bytes; slice into <code>chacha_key</code>, <code>chacha_nonce</code>, <code>hmac_key</code>. See §5.</li>230 <li><strong>Add padding.</strong> UTF-8 encode, validate the length (1 to 4,294,967,295 bytes), prepend the length prefix, append zero bytes. See §6.</li>231 <li><strong>Encrypt the padded content</strong> with ChaCha20, using the key and nonce from step 3.</li>232 <li><strong>Calculate the MAC.</strong> Use AAD (additional authenticated data): compute HMAC-SHA256 over the concatenation of <code>nonce</code> and <code>ciphertext</code>, not over the ciphertext alone. Validate that the AAD (the nonce) is 32 bytes.</li>233 <li><strong>Base64-encode</strong> (with padding) the value <code>concat(version, nonce, ciphertext, mac)</code>.</li>234 </ol>235 <p>The encrypted payload MUST then be placed in an event, hashed, and signed as defined in <a href="01.html">NIP-01</a>, using the Schnorr signature scheme over secp256k1.</p>236237 <figure class="diagram">238 <div class="frame">239 <svg viewBox="0 0 560 130" role="img" aria-label="Payload byte layout: one version byte, 32-byte nonce, variable-length ciphertext, 32-byte MAC, all wrapped in base64">240 <text class="d-actor-label" x="10" y="20">base64( ... )</text>241 <rect x="10" y="34" width="50" height="40" rx="4" fill="#2e5f9e"/>242 <text class="d-label" x="35" y="58" text-anchor="middle" fill="#fff">0x02</text>243 <rect x="66" y="34" width="120" height="40" rx="4" fill="#f26a00"/>244 <text class="d-label" x="126" y="58" text-anchor="middle" fill="#fff">nonce</text>245 <rect x="192" y="34" width="230" height="40" rx="4" fill="#7d8794"/>246 <text class="d-label" x="307" y="58" text-anchor="middle" fill="#fff">ciphertext</text>247 <rect x="428" y="34" width="120" height="40" rx="4" fill="#2c7a3f"/>248 <text class="d-label" x="488" y="58" text-anchor="middle" fill="#fff">mac</text>249 <text class="d-note" x="35" y="92" text-anchor="middle">1 byte</text>250 <text class="d-note" x="126" y="92" text-anchor="middle">32 bytes</text>251 <text class="d-note" x="307" y="92" text-anchor="middle">len(padded plaintext)</text>252 <text class="d-note" x="488" y="92" text-anchor="middle">32 bytes</text>253 <text class="d-note" x="280" y="116" text-anchor="middle">metadata overhead is always 65 bytes - MAC covers nonce + ciphertext</text>254 </svg>255 </div>256 <figcaption>Fig. 44-2 — <b>Payload layout.</b> Version, nonce, ciphertext, and MAC are concatenated, then base64-encoded with padding.</figcaption>257 </figure>258259 <h2 class="bar" id="decrypt"><span class="num">8</span> Decryption</h2>260 <p>Before decryption, validate the event's <code>pubkey</code> and <code>sig</code> as defined in <a href="01.html">NIP-01</a>. The public key MUST be a valid non-zero secp256k1 curve point. The signature must be a valid secp256k1 Schnorr signature. For exact validation rules, refer to BIP-340. Then follow these seven steps, in this order:</p>261 <ol>262 <li><strong>Check the first character.</strong> If it is <code>#</code>, the payload uses a future non-base64 encoding. <code>#</code> is not in the base64 alphabet. Do not report a base64 error; report that the encryption version is not yet supported.</li>263 <li><strong>Decode base64</strong> into <code>version, nonce, ciphertext, mac</code>. If the version is unknown, report that the version is not supported. First validate the minimum base64 length: at least 132 characters. This prevents DoS on the base64 decoder. Then validate the decoded length: at least 99 bytes.</li>264 <li><strong>Calculate the conversation key.</strong> Same as encryption step 1 (§5).</li>265 <li><strong>Calculate the message keys.</strong> Same as encryption step 3 (§5).</li>266 <li><strong>Calculate the MAC with AAD and compare.</strong> Compute HMAC-SHA256 over <code>concat(nonce, ciphertext)</code>. Compare it to the decoded MAC with a <em>constant-time</em> algorithm. If it does not match, stop and throw an error.</li>267 <li><strong>Decrypt the ciphertext</strong> with ChaCha20, using the key and nonce from step 4.</li>268 <li><strong>Remove the padding.</strong> Read the first 2 bytes as a big-endian <code>u16</code>. If zero, read the next 4 bytes as a big-endian <code>u32</code> plaintext length (6-byte prefix total). Otherwise, use the <code>u16</code> as the plaintext length (2-byte prefix total). Verify that the sliced plaintext matches the decoded length. Verify that the total padded length equals prefix length plus <code>calc_padded_len</code> of the plaintext length.</li>269 </ol>270271 <figure class="diagram">272 <div class="frame">273 <svg viewBox="0 0 560 96" role="img" aria-label="Decryption validation order: signature check, version and length checks, MAC comparison, ChaCha20 decryption, padding check">274 <rect class="d-actor" x="10" y="24" width="90" height="44" rx="8"/>275 <text class="d-actor-label" x="55" y="43" text-anchor="middle">NIP-01</text>276 <text class="d-note" x="55" y="59" text-anchor="middle">sig valid?</text>277 <rect class="d-actor" x="128" y="24" width="96" height="44" rx="8"/>278 <text class="d-actor-label" x="176" y="43" text-anchor="middle">Decode</text>279 <text class="d-note" x="176" y="59" text-anchor="middle">version + length</text>280 <rect class="d-actor" x="252" y="24" width="90" height="44" rx="8"/>281 <text class="d-actor-label" x="297" y="43" text-anchor="middle">MAC</text>282 <text class="d-note" x="297" y="59" text-anchor="middle">constant-time</text>283 <rect class="d-actor" x="370" y="24" width="90" height="44" rx="8"/>284 <text class="d-actor-label" x="415" y="43" text-anchor="middle">ChaCha20</text>285 <text class="d-note" x="415" y="59" text-anchor="middle">decrypt</text>286 <rect class="d-actor" x="488" y="24" width="62" height="44" rx="8"/>287 <text class="d-actor-label" x="519" y="43" text-anchor="middle">unpad</text>288 <text class="d-note" x="519" y="59" text-anchor="middle">verify</text>289 <path class="d-msg d-flow" d="M102,46 L125,46"/>290 <path class="d-msg d-flow" d="M226,46 L249,46"/>291 <path class="d-msg d-flow" d="M344,46 L367,46"/>292 <path class="d-msg d-flow" d="M462,46 L485,46"/>293 <text class="d-note" x="280" y="88" text-anchor="middle">any failed check stops decryption with an error</text>294 </svg>295 </div>296 <figcaption>Fig. 44-3 — <b>Validation order.</b> Signature first, then decode checks, then the MAC, and only then decryption and unpadding.</figcaption>297 </figure>298299 <h3>Constants and limits</h3>300 <div class="tscroll"><table class="ref">301 <caption>Limits (version 2)</caption>302 <tr><th>Name</th><th>Value</th><th>Meaning</th></tr>303 <tr><td class="btn">min_plaintext_size</td><td>1 byte</td><td>A 1-byte message pads to 32 bytes.</td></tr>304 <tr><td class="btn">max_plaintext_size</td><td>4,294,967,295 bytes</td><td>2<sup>32</sup> - 1, about 4 GB.</td></tr>305 <tr><td class="btn">extended_prefix_threshold</td><td>65,536</td><td>Lengths below use a 2-byte u16 prefix. Lengths at or above use the 6-byte prefix.</td></tr>306 <tr><td class="btn">min padded size</td><td>32 bytes</td><td>Smallest padded plaintext.</td></tr>307 <tr><td class="btn">metadata size</td><td>65 bytes</td><td>Version (1) + nonce (32) + MAC (32). Constant overhead.</td></tr>308 <tr><td class="btn">min base64 length</td><td>132 chars</td><td>Reject shorter payloads before base64 decoding.</td></tr>309 <tr><td class="btn">min decoded length</td><td>99 bytes</td><td>65 metadata + 34 (32 padded + 2 prefix). Reject shorter decoder output.</td></tr>310 <tr><td class="btn">raw payload (small)</td><td>99 to 65,603 bytes</td><td>65 + 34 up to 65 + 65,536 + 2, for plaintext below 65,536.</td></tr>311 <tr><td class="btn">raw payload (large)</td><td>65,607 to 4,294,967,367 bytes</td><td>65 + 65,542 up to 65 + 2<sup>32</sup> + 6, for plaintext at or above 65,536.</td></tr>312 </table></div>313314 <h2 class="bar" id="impl"><span class="num">9</span> Implementation Notes</h2>315 <p>The theoretical maximum plaintext is about 4 GB. Implementations SHOULD enforce their own maximum payload size based on platform and resource limits. Reject oversized payloads early, in <code>decode_payload</code> before base64 decoding, to prevent denial of service. Decryption can need several times the payload size in working memory: base64 decoding, byte array slicing, and padding all make copies. JVM systems are limited to about 2 GB per contiguous array. Mobile devices may have much less memory.</p>316 <h3>Primitives and helper functions</h3>317 <p>The specification is written against these named methods:</p>318 <div class="tscroll"><table class="ref">319 <caption>Cryptographic methods</caption>320 <tr><th>Method</th><th>Definition</th></tr>321 <tr><td class="btn">secure_random_bytes(length)</td><td>Fetches randomness from a CSPRNG.</td></tr>322 <tr><td class="btn">hkdf(IKM, salt, info, L)</td><td>HKDF (RFC 5869) with SHA256, split into <code>hkdf_extract(IKM, salt)</code> and <code>hkdf_expand(OKM, info, L)</code>.</td></tr>323 <tr><td class="btn">chacha20(key, nonce, data)</td><td>ChaCha20 (RFC 8439) with the starting counter set to 0.</td></tr>324 <tr><td class="btn">hmac_sha256(key, message)</td><td>HMAC (RFC 2104) with SHA256.</td></tr>325 <tr><td class="btn">secp256k1_ecdh(priv_a, pub_b)</td><td>Multiplication of point B by scalar a, per BIP-340. Encode the shared point's 32-byte x coordinate with <code>bytes(P)</code> from BIP-340. Output is unhashed.</td></tr>326 </table></div>327 <div class="tscroll"><table class="ref">328 <caption>Helper functions and operators</caption>329 <tr><th>Function</th><th>Definition</th></tr>330 <tr><td class="btn">x[i:j]</td><td>Byte-slice operator: a <code>(j - i)</code>-byte copy from byte <code>i</code> (inclusive) to byte <code>j</code> (exclusive).</td></tr>331 <tr><td class="btn">base64_encode / base64_decode</td><td>Base64 (RFC 4648), with padding.</td></tr>332 <tr><td class="btn">concat</td><td>Byte array concatenation.</td></tr>333 <tr><td class="btn">is_equal_ct(a, b)</td><td>Constant-time equality check of two byte arrays.</td></tr>334 <tr><td class="btn">utf8_encode / utf8_decode</td><td>String to byte array and back.</td></tr>335 <tr><td class="btn">write_u8(number)</td><td>Restricts to 0..255; encodes as a big-endian uint8 byte array.</td></tr>336 <tr><td class="btn">write_u16_be(number)</td><td>Restricts to 0..65535; encodes as a big-endian uint16 byte array.</td></tr>337 <tr><td class="btn">write_u32_be(number)</td><td>Restricts to 0..4294967295; encodes as a big-endian uint32 byte array.</td></tr>338 <tr><td class="btn">read_uint16_be(bytes)</td><td>Reads 2 bytes as a big-endian unsigned 16-bit integer.</td></tr>339 <tr><td class="btn">read_uint32_be(bytes)</td><td>Reads 4 bytes as a big-endian unsigned 32-bit integer.</td></tr>340 <tr><td class="btn">zeros(length)</td><td>Byte array of the given non-negative length, filled with zeros.</td></tr>341 <tr><td class="btn">floor / log2</td><td>The well-known mathematical functions.</td></tr>342 </table></div>343 <div class="box tech">344 <span class="tag">Tech Note</span>345 <p>The source spec defines the slice operator <code>x[i:j]</code> "where <code>i, j &lt;= 0</code>". That condition is a typo: indexes must satisfy <code>0 &lt;= i &lt;= j</code>. Also, decryption step 7 tells you to verify padding against "step 3 of the encryption process"; padding is encryption step <em>4</em>. Step 3 derives message keys.</p>346 </div>347 <div class="crt">348 <div class="titlebar">Pseudocode - encrypt / decrypt core</div>349<pre>def decode_payload(payload):350 plen = len(payload)351 if plen == 0 or payload[0] == <span class="s">'#'</span>: raise <span class="s">'unknown version'</span>352 if plen &lt; 132: raise <span class="s">'invalid payload size'</span>353 data = base64_decode(payload)354 dlen = len(data)355 if dlen &lt; 99: raise <span class="s">'invalid data size'</span>356 vers = data[0]357 if vers != 2: raise <span class="s">'unknown version'</span>358 nonce = data[1:33]359 ciphertext = data[33 : dlen - 32]360 mac = data[dlen - 32 : dlen]361 return (nonce, ciphertext, mac)362363def hmac_aad(key, message, aad):364 if len(aad) != 32: raise <span class="s">'AAD associated data must be 32 bytes'</span>365 return hmac(sha256, key, concat(aad, message))366367<span class="c"># Long-term key between A and B: get_key(Apriv, Bpub) == get_key(Bpriv, Apub)</span>368def get_conversation_key(private_key_a, public_key_b):369 shared_x = secp256k1_ecdh(private_key_a, public_key_b)370 return hkdf_extract(IKM=shared_x, salt=utf8_encode(<span class="s">'nip44-v2'</span>))371372<span class="c"># Unique per-message keys</span>373def get_message_keys(conversation_key, nonce):374 if len(conversation_key) != 32: raise <span class="s">'invalid conversation_key length'</span>375 if len(nonce) != 32: raise <span class="s">'invalid nonce length'</span>376 keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)377 return (keys[0:32], keys[32:44], keys[44:76])378379def encrypt(plaintext, conversation_key, nonce):380 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)381 padded = pad(plaintext)382 ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)383 mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)384 return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))385386def decrypt(payload, conversation_key):387 (nonce, ciphertext, mac) = decode_payload(payload)388 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)389 calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)390 if not is_equal_ct(calculated_mac, mac): raise <span class="s">'invalid MAC'</span>391 padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)392 return unpad(padded_plaintext)</pre>393 </div>394395 <h2 class="bar" id="examples"><span class="num">10</span> Worked Example</h2>396 <p>The transcript below encrypts the one-byte message <code>a</code>. It uses a published test vector, so every intermediate value is reproducible. The nonce is fixed only for the test; real nonces come from a CSPRNG.</p>397 <div class="crt">398 <div class="titlebar">Test vector - encrypt "a" from sec1 to sec2</div>399<pre><span class="c"># inputs</span>400<span class="k">sec1</span> = <span class="s">0000000000000000000000000000000000000000000000000000000000000001</span>401<span class="k">sec2</span> = <span class="s">0000000000000000000000000000000000000000000000000000000000000002</span>402<span class="k">nonce</span> = <span class="s">0000000000000000000000000000000000000000000000000000000000000001</span>403404<span class="send">→ conversation_key = get_conversation_key(sec1, pub2)</span>405<span class="recv">← c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d</span>406407<span class="send">→ payload = encrypt("a", conversation_key, nonce)</span>408<span class="recv">← AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtD409 KfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb</span>410411<span class="c"># "a" is 1 byte -&gt; padded to 32 bytes (2-byte prefix + 1 + 29 zeros = 34)</span>412<span class="c"># decoded payload = 65 metadata + 34 ciphertext = 99 bytes, the minimum</span>413414<span class="send">→ decrypt(payload, get_conversation_key(sec2, pub1))</span>415<span class="recv">← "a"</span></pre>416 </div>417 <p>The full test vector file contains many more cases, including invalid ones. Instead of embedding the file, the spec pins its checksum:</p>418 <div class="crt">419 <div class="titlebar">nip44.vectors.json - sha256</div>420<pre><span class="s">269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040</span> nip44.vectors.json</pre>421 </div>422 <p>The vector file groups cases by purpose:</p>423 <div class="tscroll"><table class="ref">424 <caption>Test vector groups</caption>425 <tr><th>Group</th><th>What it checks</th></tr>426 <tr><td class="btn">valid.get_conversation_key</td><td>Calculate <code>conversation_key</code> from secret key <code>sec1</code> and public key <code>pub2</code>.</td></tr>427 <tr><td class="btn">valid.get_message_keys</td><td>Calculate <code>chacha_key</code>, <code>chacha_nonce</code>, <code>hmac_key</code> from <code>conversation_key</code> and <code>nonce</code>.</td></tr>428 <tr><td class="btn">valid.calc_padded_len</td><td>Take an unpadded length; calculate the padded length.</td></tr>429 <tr><td class="btn">valid.encrypt_decrypt</td><td>Emulate a real conversation: derive keys both ways, encrypt, verify the payload, decrypt, verify the plaintext.</td></tr>430 <tr><td class="btn">valid.encrypt_decrypt_long_msg</td><td>Same, but plaintext and payload are given as checksums.</td></tr>431 <tr><td class="btn">invalid.encrypt_msg_lengths</td><td>Plaintext lengths that must be rejected.</td></tr>432 <tr><td class="btn">invalid.get_conversation_key</td><td>Key pairs where conversation-key calculation must throw an error.</td></tr>433 <tr><td class="btn">invalid.decrypt</td><td>Payloads where decryption must throw an error.</td></tr>434 </table></div>435 <h3>Extended length prefix boundary</h3>436 <p>Three vectors exercise the boundary between the 2-byte and 6-byte prefixes. They reuse the <code>conversation_key</code> and <code>nonce</code> above. The plaintext is the byte <code>0x61</code> (<code>a</code>) repeated. The payloads are too large to inline, so sha256 checksums are given, following the <code>encrypt_decrypt_long_msg</code> pattern:</p>437 <div class="tscroll"><table class="ref">438 <caption>Extended prefix test vectors</caption>439 <tr><th>plaintext_len</th><th>prefix</th><th>padded_len</th><th>plaintext_sha256</th><th>payload_sha256</th></tr>440 <tr><td class="btn">65535</td><td>u16 (2 bytes)</td><td>65536</td><td><code>6e1bebca6a8229364a162a72ef064826c4cd7457bf54f190ef782bd9deff3e42</code></td><td><code>6d8c2810d1e870fbaa1f0a0937126cca837a15f9260e27060c331d70a3c0bc84</code></td></tr>441 <tr><td class="btn">65536</td><td>extended (6 bytes)</td><td>65536</td><td><code>bf718b6f653bebc184e1479f1935b8da974d701b893afcf49e701f3e2f9f9c5a</code></td><td><code>b7b4edb36ba92e267d322d56d9aebc22e7fa96ff52e3c12adc07f07a43cbc616</code></td></tr>442 <tr><td class="btn">65537</td><td>extended (6 bytes)</td><td>81920</td><td><code>008ffc88d3c96a9f307524eb361e47c5222a887fc45fa0c1fb8d429c5c23b430</code></td><td><code>eeb7c7c5373894ea2c1547cfd3ccb15d5a0b2d619da852e5c79df792dcc9e435</code></td></tr>443 </table></div>444 <p>Note: 65,535 and 65,536 both pad to 65,536 bytes, but the prefixed totals differ: 65,538 (2 + 65,536) versus 65,542 (6 + 65,536). The jump to 65,537 triggers the next padding bucket at 81,920.</p>445446 <h2 class="bar" id="history"><span class="num">11</span> Version History</h2>447 <p>The number 44 briefly held an unrelated "Video Events" draft (December 7 to 9, 2023) that moved to <a href="71.html">NIP-71</a>. The encryption standard landed days later, after roughly a year of design revisions and a professional audit. Notable changes, newest first:</p>448 <ul class="patchnotes">449 <li><span class="ver">2026</span><span class="when">2026-06</span>Extended 6-byte length prefix added; maximum plaintext raised from 65,535 bytes to 2<sup>32</sup> - 1. Implementation considerations and boundary test vectors added.</li>450 <li class="minor"><span class="ver">2024</span><span class="when">2024-12</span>Clarified units, base64 explanation, and other nits.</li>451 <li><span class="ver">2024</span><span class="when">2024-09</span>Clarified that the ECDH output is unhashed; named the libsecp256k1 function <code>secp256k1_ec_pubkey_tweak_mul</code>.</li>452 <li><span class="ver">2023</span><span class="when">2023-12</span>Cleanup separated encryption from messaging; added the note that this is not a drop-in replacement for <a href="04.html">NIP-04</a>; added the Cure53 audit notice.</li>453 <li><span class="ver">Origin</span><span class="when">2023-12-20</span>Paul Miller merged "NIP44 encryption standard, revision 3" (#746) after a year of iteration on versions 1 and 2.</li>454 </ul>455456 <div class="credits">457 <h3 class="cardhead">Original Proposal - Credits</h3>458 <table>459 <tr><td>Author</td><td>Paul Miller (with fiatjaf, Jon Staab, and the nip44 working group)</td></tr>460 <tr><td>First commit</td><td>2023-12-20 — “NIP44 encryption standard, revision 3 (#746)”</td></tr>461 <tr><td>Pull request</td><td><a href="https://github.com/nostr-protocol/nips/pull/746">#746</a></td></tr>462 <tr><td>Status</td><td>active - optional — audited by Cure53, 2023-12</td></tr>463 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/44.md">44.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/44.md">full change history</a></td></tr>464 </table>465 </div>466467 <h2 class="bar"><span class="num">12</span> See Also</h2>468 <ul>469 <li><a href="01.html">NIP-01</a> — the signed event that must wrap every payload; Schnorr signatures over secp256k1.</li>470 <li><a href="04.html">NIP-04</a> — the older encrypted direct message scheme this format does not replace in-place.</li>471 <li><a href="17.html">NIP-17</a> — private direct messages; a messaging standard built on this encryption.</li>472 <li><a href="59.html">NIP-59</a> — gift wrap; seals and wraps events using NIP-44 encryption.</li>473 <li><a href="07.html">NIP-07</a> — browser signer extensions that expose <code>nip44.encrypt</code> and <code>nip44.decrypt</code>.</li>474 </ul>475 </div>476477 <aside>478 <div class="cartridge">479 <div class="inner">480 <h3 class="cardhead">Spec Card</h3>481 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>482 <dl>483 <dt>Status</dt><dd><span class="chip optional">optional</span></dd>484 <dt>Defines</dt><dd>versioned encrypted payload format - version 2 algorithm suite</dd>485 <dt>Kinds</dt><dd>none — encryption only, no messaging kinds</dd>486 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> (signed events)</dd>487 <dt>Crypto</dt><dd>secp256k1 ECDH - HKDF-SHA256 - ChaCha20 - HMAC-SHA256 - base64</dd>488 <dt>Audit</dt><dd>Cure53, December 2023</dd>489 <dt>First commit</dt><dd>2023-12-20</dd>490 <dt>Last updated</dt><dd>2026-06-28</dd>491 </dl>492 </div>493 </div>494 </aside>495 </main>496497 <nav class="navbtns">498 <a href="43.html">◀ NIP-43: Relay Access Metadata and Requests</a>499 <a href="45.html">NIP-45: Event Counts ▶</a>500 </nav>501502 <footer class="pagefoot">503 <a href="../index.html">Nostr Protocol Manual</a>504 <span class="pageno">NIP 44 / F4</span>505 </footer>506</div>507508<script src="../assets/webmcp.js" defer></script>509</body>510</html>