mirror of
https://github.com/nostr-protocol/nips.git
synced 2024-12-14 19:36:22 +00:00
Introduce NIP-44 encryption standard
This commit is contained in:
parent
a5047326d4
commit
95103569b5
170
44.md
Normal file
170
44.md
Normal file
@ -0,0 +1,170 @@
|
||||
NIP-44
|
||||
======
|
||||
|
||||
Encrypted Direct Message (Versioned)
|
||||
------------------------------------
|
||||
|
||||
`optional` `author:paulmillr` `author:staab`
|
||||
|
||||
The NIP introduces versioned encryption, allowing multiple algorithm choices to exist simultaneously.
|
||||
|
||||
The 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.
|
||||
|
||||
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 0
|
||||
|
||||
Version 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.
|
||||
|
||||
# Version 1
|
||||
|
||||
Params:
|
||||
|
||||
1. `nonce`: base64-encoded xchacha nonce
|
||||
2. `ciphertext`: base64-encoded xchacha ciphertext, created from (key, nonce) against `plaintext`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
{
|
||||
"ciphertext": "FvQi1H4atMwU+FzUR/0CJ7kowjs+",
|
||||
"nonce": "3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f",
|
||||
"v": 1
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
## Code Samples
|
||||
|
||||
### Javascript
|
||||
|
||||
```javascript
|
||||
import {xchacha20} from "@noble/ciphers/chacha"
|
||||
import {secp256k1} from "@noble/curves/secp256k1"
|
||||
import {sha256} from "@noble/hashes/sha256"
|
||||
import {randomBytes} from "@noble/hashes/utils"
|
||||
import {base64} from "@scure/base"
|
||||
|
||||
export const utf8Decoder = new TextDecoder()
|
||||
|
||||
export const utf8Encoder = new TextEncoder()
|
||||
|
||||
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) {
|
||||
throw new Error("NIP44: unknown encryption version")
|
||||
}
|
||||
|
||||
const key = getSharedSecret(privkey, pubkey)
|
||||
const nonce = randomBytes(24)
|
||||
const plaintext = utf8Encoder.encode(text)
|
||||
const ciphertext = xchacha20(key, nonce, plaintext)
|
||||
|
||||
return JSON.stringify({
|
||||
ciphertext: base64.encode(ciphertext),
|
||||
nonce: base64.encode(nonce),
|
||||
v,
|
||||
})
|
||||
}
|
||||
|
||||
export function decrypt(privkey: string, pubkey: string, payload: string) {
|
||||
try {
|
||||
payload = JSON.parse(payload) as {
|
||||
ciphertext: string
|
||||
nonce: string
|
||||
v: number
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("NIP44: failed to parse payload")
|
||||
}
|
||||
|
||||
if (data.v !== 1) {
|
||||
throw new Error("NIP44: unknown encryption version")
|
||||
}
|
||||
|
||||
const key = getSharedSecret(privkey, pubkey)
|
||||
const nonce = base64.decode(data.nonce)
|
||||
const ciphertext = base64.decode(data.ciphertext)
|
||||
const plaintext = xchacha20(key, nonce, ciphertext)
|
||||
|
||||
return utf8Decoder.decode(plaintext)
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin
|
||||
|
||||
```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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user