2023-08-11 11:34:18 -04:00
NIP-44
======
2023-08-11 12:23:56 -04:00
Encrypted Payloads (Versioned)
------------------------------
2023-08-11 11:34:18 -04:00
`optional` `author:paulmillr` `author:staab`
2023-08-12 17:05:41 -04:00
The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously.
2023-08-11 11:34:18 -04:00
An encrypted payload MUST be encoded as a JSON object. Different versions may have different parameters. Every format has a `v` field specifying its version.
Currently defined encryption algorithms:
- `0x00` - Reserved
- `0x01` - XChaCha with same key `sha256(ecdh)` per conversation
# Version 1
Params:
1. `nonce` : base64-encoded xchacha nonce
2. `ciphertext` : base64-encoded xchacha ciphertext, created from (key, nonce) against `plaintext` .
Example:
2023-08-14 20:42:33 -04:00
- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a`
- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d`
Encrypting the message `hello` from Alice to Bob results in the base-64 encoded tlv payload:
2023-08-11 11:34:18 -04:00
```
2023-08-14 20:42:33 -04:00
AAEBARgeI8gcP/4mnw3mKgtMvD8aGYUnGBlhopoCBd94Ev9i
2023-08-11 11:34:18 -04:00
```
2023-08-11 16:09:17 -04:00
# Other Notes
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 NIP-94, unhashed shared point was used.
2023-08-11 11:34:18 -04:00
2023-08-12 17:05:41 -04:00
This encryption scheme replaces the one described in NIP-04, which is not secure. It used bad cryptographic building blocks and must not be used.
2023-08-11 16:09:17 -04:00
# Code Samples
2023-08-11 11:34:18 -04:00
2023-08-11 16:09:17 -04:00
## Javascript
2023-08-11 11:34:18 -04:00
```javascript
import {xchacha20} from "@noble/ciphers/chacha"
import {secp256k1} from "@noble/curves/secp256k1"
import {sha256} from "@noble/hashes/sha256"
2023-08-14 20:42:33 -04:00
import {randomBytes, concatBytes} from "@noble/hashes/utils"
2023-08-11 11:34:18 -04:00
import {base64} from "@scure/base"
export const utf8Decoder = new TextDecoder()
export const utf8Encoder = new TextEncoder()
2023-08-14 20:42:33 -04:00
export type TLV = {[t: number]: Uint8Array[]}
export function parseTLV(data: Uint8Array): TLV {
let result: TLV = {}
let rest = data
while (rest.length > 0) {
let t = rest[0]
let l = rest[1]
if (!l) throw new Error(`malformed TLV ${t}`)
let v = rest.slice(2, 2 + l)
rest = rest.slice(2 + l)
if (v.length < l ) throw new Error ( ` not enough data to read on TLV $ { t } ` )
result[t] = result[t] || []
result[t].push(v)
}
return result
}
export function encodeTLV(tlv: TLV): Uint8Array {
let entries: Uint8Array[] = []
Object.entries(tlv).forEach(([t, vs]) => {
vs.forEach(v => {
let entry = new Uint8Array(v.length + 2)
entry.set([parseInt(t)], 0)
entry.set([v.length], 1)
entry.set(v, 2)
entries.push(entry)
})
})
return concatBytes(...entries)
}
2023-08-11 11:34:18 -04:00
export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array =>
sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33))
export function encrypt(privkey: string, pubkey: string, text: string, v = 1) {
if (v !== 1) {
2023-08-14 20:42:33 -04:00
throw new Error('NIP44: unknown encryption version')
2023-08-11 11:34:18 -04:00
}
const key = getSharedSecret(privkey, pubkey)
const nonce = randomBytes(24)
const plaintext = utf8Encoder.encode(text)
const ciphertext = xchacha20(key, nonce, plaintext)
2023-08-14 20:42:33 -04:00
const tlv = encodeTLV({
0: [new Uint8Array([1])],
1: [nonce],
2: [ciphertext]
2023-08-11 11:34:18 -04:00
})
2023-08-14 20:42:33 -04:00
return base64.encode(tlv)
2023-08-11 11:34:18 -04:00
}
export function decrypt(privkey: string, pubkey: string, payload: string) {
2023-08-14 20:42:33 -04:00
let byteArray
try {
byteArray = base64.decode(payload)
} catch (e) {
throw new Error(`NIP44: failed to base64 decode payload: ${e}`)
}
let tlv
2023-08-11 11:34:18 -04:00
try {
2023-08-14 20:42:33 -04:00
tlv = parseTLV(byteArray)
2023-08-11 11:34:18 -04:00
} catch (e) {
2023-08-14 20:42:33 -04:00
throw new Error(`NIP44: failed to decode tlv: ${e}`)
}
if (tlv[0]?.[0]?.[0] !== 1) {
throw new Error(`NIP44: invalid version: ${tlv[0]?.[0]?.[0]}`)
}
if (tlv[1]?.[0]?.length !== 24) {
throw new Error(`NIP44: invalid nonce: ${tlv[1]?.[0]}`)
2023-08-11 11:34:18 -04:00
}
2023-08-14 20:42:33 -04:00
if (!tlv[2]?.[0]) {
throw new Error(`NIP44: missing ciphertext`)
2023-08-11 11:34:18 -04:00
}
2023-08-14 20:42:33 -04:00
const nonce = tlv[1][0]
const ciphertext = tlv[2][0]
2023-08-11 11:34:18 -04:00
const key = getSharedSecret(privkey, pubkey)
const plaintext = xchacha20(key, nonce, ciphertext)
return utf8Decoder.decode(plaintext)
}
```
2023-08-11 16:09:17 -04:00
## Kotlin
2023-08-11 11:34:18 -04:00
```kotlin
// implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
// implementation "com.goterl:lazysodium-android:5.1.0@aar"
// implementation "net.java.dev.jna:jna:5.12.1@aar"
fun getSharedSecretNIP44(privKey: ByteArray, pubKey: ByteArray): ByteArray =
MessageDigest.getInstance("SHA-256").digest(
Secp256k1.get().pubKeyTweakMul(
Hex.decode("02") + pubKey,
privKey
).copyOfRange(1, 33)
)
fun encryptNIP44(msg: String, privKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val nonce = ByteArray(24).apply {
SecureRandom.getInstanceStrong().nextBytes(this)
}
val cipher = streamXChaCha20Xor(
message = msg.toByteArray(),
nonce = nonce,
key = getSharedSecretNIP44(privKey, pubKey)
)
return EncryptedInfo(
ciphertext = Base64.getEncoder().encodeToString(cipher),
nonce = Base64.getEncoder().encodeToString(nonce),
v = Nip24Version.XChaCha20.code
)
}
fun decryptNIP44(encInfo: EncryptedInfo, privKey: ByteArray, pubKey: ByteArray): String? {
require(encInfo.v == Nip24Version.XChaCha20.code) { "NIP44: unknown encryption version" }
return streamXChaCha20Xor(
message = Base64.getDecoder().decode(encInfo.ciphertext),
nonce = Base64.getDecoder().decode(encInfo.nonce),
key = getSharedSecretNIP44(privKey, pubKey)
)?.decodeToString()
}
// This method is not exposed in AndroidSodium yet, but it will be in the next version.
fun streamXChaCha20Xor(message: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray? {
return with (SodiumAndroid()) {
val resultCipher = ByteArray(message.size)
val isSuccessful = crypto_stream_chacha20_xor_ic(
resultCipher,
message,
message.size.toLong(),
nonce.drop(16).toByteArray(), // chacha nonce is just the last 8 bytes.
0,
ByteArray(32).apply {
crypto_core_hchacha20(this, nonce, key, null)
}
) == 0
if (isSuccessful) resultCipher else null
}
}
data class EncryptedInfo(val ciphertext: String, val nonce: String, val v: Int)
enum class Nip24Version(val code: Int) {
Reserved(0),
XChaCha20(1)
}