mirror of
https://github.com/nostr-protocol/nips.git
synced 2025-12-08 16:18:50 +00:00
Compare commits
12 Commits
f310614122
...
staab-NIP-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2977bcc5 | ||
|
|
2239f96541 | ||
|
|
c78bd4cef1 | ||
|
|
a1f8a82e73 | ||
|
|
3a37d7c8b9 | ||
|
|
06f8dbadc8 | ||
|
|
4d95efcf87 | ||
|
|
e2eeda5200 | ||
|
|
94330ffb10 | ||
|
|
30696049cc | ||
|
|
00a8f9532e | ||
|
|
95103569b5 |
4
04.md
4
04.md
@@ -1,10 +1,12 @@
|
|||||||
|
> __Warning__ `unrecommended`: deprecated in favor of [NIP-44](44.md)
|
||||||
|
|
||||||
NIP-04
|
NIP-04
|
||||||
======
|
======
|
||||||
|
|
||||||
Encrypted Direct Message
|
Encrypted Direct Message
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
`final` `optional` `author:arcbtc`
|
`final` `unrecommended` `author:arcbtc`
|
||||||
|
|
||||||
A special event with kind `4`, meaning "encrypted direct message". It is supposed to have the following attributes:
|
A special event with kind `4`, meaning "encrypted direct message". It is supposed to have the following attributes:
|
||||||
|
|
||||||
|
|||||||
6
07.md
6
07.md
@@ -18,8 +18,10 @@ async window.nostr.signEvent(event: Event): Event // takes an event object, adds
|
|||||||
Aside from these two basic above, the following functions can also be implemented optionally:
|
Aside from these two basic above, the following functions can also be implemented optionally:
|
||||||
```
|
```
|
||||||
async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} } // returns a basic map of relay urls to relay policies
|
async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} } // returns a basic map of relay urls to relay policies
|
||||||
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04
|
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated)
|
||||||
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04
|
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated)
|
||||||
|
async window.nostr.nip44.encrypt(pubkey, plaintext, v): string // returns encrypted payload as specified in nip-44
|
||||||
|
async window.nostr.nip44.decrypt(pubkey, payload): string // takes encrypted payload as specified in nip-44
|
||||||
```
|
```
|
||||||
|
|
||||||
### Implementation
|
### Implementation
|
||||||
|
|||||||
162
44.md
Normal file
162
44.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
NIP-44
|
||||||
|
======
|
||||||
|
|
||||||
|
Encrypted Payloads (Versioned)
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
`optional` `author:paulmillr` `author:staab`
|
||||||
|
|
||||||
|
The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```
|
||||||
|
AZKyMIHbfVYFlAAK7Ci5wuM5GFOLaeI7LQKDzWJY
|
||||||
|
```
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
const payload = new Uint8Array(1 + 24 + ciphertext.length)
|
||||||
|
payload.set([version], 0)
|
||||||
|
payload.set(nonce, 1)
|
||||||
|
payload.set(ciphertext, 1 + 24)
|
||||||
|
|
||||||
|
return base64.encode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(privkey: string, pubkey: string, blob: string) {
|
||||||
|
const payload = base64.decode(blob)
|
||||||
|
if (payload[0] !== 1) {
|
||||||
|
throw new Error('NIP44: unknown encryption version')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = payload.subarray(1, 25)
|
||||||
|
const ciphertext = payload.subarray(25)
|
||||||
|
|
||||||
|
const key = getSharedSecret(privkey, pubkey)
|
||||||
|
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)
|
||||||
|
}
|
||||||
10
46.md
10
46.md
@@ -82,12 +82,18 @@ These are mandatory methods the remote signer app MUST implement:
|
|||||||
- **get_relays**
|
- **get_relays**
|
||||||
- params []
|
- params []
|
||||||
- result `{ [url: string]: {read: boolean, write: boolean} }`
|
- result `{ [url: string]: {read: boolean, write: boolean} }`
|
||||||
- **nip04_encrypt**
|
- **nip04_encrypt** (deprecated)
|
||||||
- params [`pubkey`, `plaintext`]
|
- params [`pubkey`, `plaintext`]
|
||||||
- result `nip4 ciphertext`
|
- result `nip4 ciphertext`
|
||||||
- **nip04_decrypt**
|
- **nip04_decrypt** (deprecated)
|
||||||
- params [`pubkey`, `nip4 ciphertext`]
|
- params [`pubkey`, `nip4 ciphertext`]
|
||||||
- result [`plaintext`]
|
- result [`plaintext`]
|
||||||
|
- **nip44_encrypt**
|
||||||
|
- params [`pubkey`, `plaintext`, `v`]
|
||||||
|
- result `nip44 encrypted payload`
|
||||||
|
- **nip44_decrypt**
|
||||||
|
- params [`pubkey`, `nip44 encrypted payload`]
|
||||||
|
- result [`plaintext`]
|
||||||
|
|
||||||
|
|
||||||
NOTICE: `pubkey` and `signature` are hex-encoded strings.
|
NOTICE: `pubkey` and `signature` are hex-encoded strings.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
|||||||
- [NIP-01: Basic protocol flow description](01.md)
|
- [NIP-01: Basic protocol flow description](01.md)
|
||||||
- [NIP-02: Contact List and Petnames](02.md)
|
- [NIP-02: Contact List and Petnames](02.md)
|
||||||
- [NIP-03: OpenTimestamps Attestations for Events](03.md)
|
- [NIP-03: OpenTimestamps Attestations for Events](03.md)
|
||||||
- [NIP-04: Encrypted Direct Message](04.md)
|
- [NIP-04: Encrypted Direct Message](04.md) --- **unrecommended**: deprecated in favor of [NIP-44](44.md)
|
||||||
- [NIP-05: Mapping Nostr keys to DNS-based internet identifiers](05.md)
|
- [NIP-05: Mapping Nostr keys to DNS-based internet identifiers](05.md)
|
||||||
- [NIP-06: Basic key derivation from mnemonic seed phrase](06.md)
|
- [NIP-06: Basic key derivation from mnemonic seed phrase](06.md)
|
||||||
- [NIP-07: `window.nostr` capability for web browsers](07.md)
|
- [NIP-07: `window.nostr` capability for web browsers](07.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user