upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/44.md
diff options
context:
space:
mode:
authorJonathan Staab <shtaab@gmail.com>2023-08-11 08:34:18 -0700
committerJonathan Staab <shtaab@gmail.com>2023-08-11 08:34:18 -0700
commit95103569b54b14e5cff55e1da1879fcfe6076349 (patch)
treec4b2310d23bf2e8f858665d56a736000e3f5f3e4 /44.md
parenta5047326d4d7c28e66c5d1262c252b86a1c8fe67 (diff)
Introduce NIP-44 encryption standard
Diffstat (limited to '44.md')
-rw-r--r--44.md170
1 files changed, 170 insertions, 0 deletions
diff --git a/44.md b/44.md
new file mode 100644
index 0000000..9982156
--- /dev/null
+++ b/44.md
@@ -0,0 +1,170 @@
1NIP-44
2======
3
4Encrypted Direct Message (Versioned)
5------------------------------------
6
7`optional` `author:paulmillr` `author:staab`
8
9The NIP introduces versioned encryption, allowing multiple algorithm choices to exist simultaneously.
10
11The algorithm described in NIP4 is potentially vulnerable to [padding oracle attacks](https://en.wikipedia.org/wiki/Padding_oracle_attack) and uses keys which are not indistinguishable from random.
12
13An encrypted payload MUST be encoded as a JSON object. Different versions may have different parameters. Every format has a `v` field specifying its version.
14
15Currently defined encryption algorithms:
16
17- `0x00` - Reserved
18- `0x01` - XChaCha with same key `sha256(ecdh)` per conversation
19
20# Version 0
21
22Version 0 is not defined, however implementations depending on this NIP MAY choose to support the payload described in NIP 04 in the same places a NIP 44 payload would otherwise be expected. This is intended to allow a smooth transition while clients and signing software adopt the new standard.
23
24# Version 1
25
26Params:
27
281. `nonce`: base64-encoded xchacha nonce
292. `ciphertext`: base64-encoded xchacha ciphertext, created from (key, nonce) against `plaintext`.
30
31Example:
32
33```
34{
35 "ciphertext": "FvQi1H4atMwU+FzUR/0CJ7kowjs+",
36 "nonce": "3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f",
37 "v": 1
38}
39```
40
41**Note**: By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). We are using this exact implementation. In NIP4, unhashed shared point was used.
42
43## Code Samples
44
45### Javascript
46
47```javascript
48import {xchacha20} from "@noble/ciphers/chacha"
49import {secp256k1} from "@noble/curves/secp256k1"
50import {sha256} from "@noble/hashes/sha256"
51import {randomBytes} from "@noble/hashes/utils"
52import {base64} from "@scure/base"
53
54export const utf8Decoder = new TextDecoder()
55
56export const utf8Encoder = new TextEncoder()
57
58export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array =>
59 sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33))
60
61export function encrypt(privkey: string, pubkey: string, text: string, v = 1) {
62 if (v !== 1) {
63 throw new Error("NIP44: unknown encryption version")
64 }
65
66 const key = getSharedSecret(privkey, pubkey)
67 const nonce = randomBytes(24)
68 const plaintext = utf8Encoder.encode(text)
69 const ciphertext = xchacha20(key, nonce, plaintext)
70
71 return JSON.stringify({
72 ciphertext: base64.encode(ciphertext),
73 nonce: base64.encode(nonce),
74 v,
75 })
76}
77
78export function decrypt(privkey: string, pubkey: string, payload: string) {
79 try {
80 payload = JSON.parse(payload) as {
81 ciphertext: string
82 nonce: string
83 v: number
84 }
85 } catch (e) {
86 throw new Error("NIP44: failed to parse payload")
87 }
88
89 if (data.v !== 1) {
90 throw new Error("NIP44: unknown encryption version")
91 }
92
93 const key = getSharedSecret(privkey, pubkey)
94 const nonce = base64.decode(data.nonce)
95 const ciphertext = base64.decode(data.ciphertext)
96 const plaintext = xchacha20(key, nonce, ciphertext)
97
98 return utf8Decoder.decode(plaintext)
99}
100```
101
102### Kotlin
103
104```kotlin
105// implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
106// implementation "com.goterl:lazysodium-android:5.1.0@aar"
107// implementation "net.java.dev.jna:jna:5.12.1@aar"
108
109fun getSharedSecretNIP44(privKey: ByteArray, pubKey: ByteArray): ByteArray =
110 MessageDigest.getInstance("SHA-256").digest(
111 Secp256k1.get().pubKeyTweakMul(
112 Hex.decode("02") + pubKey,
113 privKey
114 ).copyOfRange(1, 33)
115 )
116
117fun encryptNIP44(msg: String, privKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
118 val nonce = ByteArray(24).apply {
119 SecureRandom.getInstanceStrong().nextBytes(this)
120 }
121
122 val cipher = streamXChaCha20Xor(
123 message = msg.toByteArray(),
124 nonce = nonce,
125 key = getSharedSecretNIP44(privKey, pubKey)
126 )
127
128 return EncryptedInfo(
129 ciphertext = Base64.getEncoder().encodeToString(cipher),
130 nonce = Base64.getEncoder().encodeToString(nonce),
131 v = Nip24Version.XChaCha20.code
132 )
133}
134
135fun decryptNIP44(encInfo: EncryptedInfo, privKey: ByteArray, pubKey: ByteArray): String? {
136 require(encInfo.v == Nip24Version.XChaCha20.code) { "NIP44: unknown encryption version" }
137
138 return streamXChaCha20Xor(
139 message = Base64.getDecoder().decode(encInfo.ciphertext),
140 nonce = Base64.getDecoder().decode(encInfo.nonce),
141 key = getSharedSecretNIP44(privKey, pubKey)
142 )?.decodeToString()
143}
144
145// This method is not exposed in AndroidSodium yet, but it will be in the next version.
146fun streamXChaCha20Xor(message: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray? {
147 return with (SodiumAndroid()) {
148 val resultCipher = ByteArray(message.size)
149
150 val isSuccessful = crypto_stream_chacha20_xor_ic(
151 resultCipher,
152 message,
153 message.size.toLong(),
154 nonce.drop(16).toByteArray(), // chacha nonce is just the last 8 bytes.
155 0,
156 ByteArray(32).apply {
157 crypto_core_hchacha20(this, nonce, key, null)
158 }
159 ) == 0
160
161 if (isSuccessful) resultCipher else null
162 }
163}
164
165data class EncryptedInfo(val ciphertext: String, val nonce: String, val v: Int)
166
167enum class Nip24Version(val code: Int) {
168 Reserved(0),
169 XChaCha20(1)
170}