Skip to content

Commit

Permalink
refactor: NIP44
Browse files Browse the repository at this point in the history
  • Loading branch information
ethicnology committed Jan 22, 2025
1 parent fff7804 commit 95f8ded
Show file tree
Hide file tree
Showing 6 changed files with 1,305 additions and 74 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ flutter pub add nostr
- [x] [NIP 21 nostr: URI scheme](https://github.com/nostr-protocol/nips/blob/master/21.md)
- [x] [NIP 23 Long-form Content](https://github.com/nostr-protocol/nips/blob/master/23.md)
- [x] [NIP 28 Public Chat](https://github.com/nostr-protocol/nips/blob/master/28.md)
- [x] [NIP 44 Encrypted Payloads (Versioned)](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [x] [NIP 50 Search Capability](https://github.com/nostr-protocol/nips/blob/master/50.md)
- [x] [NIP 51 Lists](https://github.com/nostr-protocol/nips/blob/master/51.md)

Expand Down
2 changes: 2 additions & 0 deletions lib/nostr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ export 'src/nips/nip_020.dart';
export 'src/nips/nip_021.dart';
export 'src/nips/nip_023.dart';
export 'src/nips/nip_028.dart';
export 'src/nips/nip_044.dart';
export 'src/nips/nip_044_utils.dart';
export 'src/nips/nip_051.dart';
56 changes: 31 additions & 25 deletions lib/src/nips/nip_044.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@ import 'package:elliptic/elliptic.dart';
import 'package:nostr/nostr.dart';
import 'package:nostr/src/nips/nip_044_utils.dart';

/// NIP-44 encryption and decryption functions.
/// The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously. This format may be used for many things, but MUST be used in the context of a signed event as described in NIP-01.
class Nip44 {
static Future<String> encryptMessage(
String plaintext,
String senderPrivateKey,
String recipientPublicKey, {
Uint8List? customNonce,
Uint8List? customConversationKey,
static Future<String> encrypt({
required String plaintext,
required String senderPrivateKey,
required String recipientPublicKey,
List<int>? customNonce,
List<int>? customConversationKey,
}) async {
// Step 1: Compute Shared Secret
final sharedSecret = customConversationKey ??
computeSharedSecret(senderPrivateKey, recipientPublicKey);
computeSharedSecret(
privateKeyHex: senderPrivateKey,
publicKeyHex: recipientPublicKey,
);

// Step 2: Derive Conversation Key
final conversationKey =
customConversationKey ?? deriveConversationKey(sharedSecret);
final conversationKey = customConversationKey ??
deriveConversationKey(sharedSecret: sharedSecret);

// Step 3: Generate or Use Custom Nonce
final nonce = customNonce ?? generateRandomBytes(32);
final nonce = customNonce ?? Uint8List.fromList(generateRandomBytes(32));

// Step 4: Derive Message Keys
final keys = deriveMessageKeys(conversationKey, nonce);
Expand All @@ -45,19 +48,22 @@ class Nip44 {
return constructPayload(nonce, ciphertext, mac);
}

static Future<String> decryptMessage(
String payload,
String recipientPrivateKey,
String senderPublicKey, {
Uint8List? customConversationKey,
static Future<String> decrypt({
required String payload,
required String recipientPrivateKey,
required String senderPublicKey,
List<int>? customConversationKey,
}) async {
// Step 1: Compute Shared Secret
final sharedSecret = customConversationKey ??
computeSharedSecret(recipientPrivateKey, senderPublicKey);
computeSharedSecret(
privateKeyHex: recipientPrivateKey,
publicKeyHex: senderPublicKey,
);

// Step 2: Derive Conversation Key
final conversationKey =
customConversationKey ?? deriveConversationKey(sharedSecret);
final conversationKey = customConversationKey ??
deriveConversationKey(sharedSecret: sharedSecret);

// Step 3: Parse Payload
final parsed = parsePayload(payload);
Expand All @@ -84,18 +90,18 @@ class Nip44 {
return utf8.decode(plaintextBytes);
}

static Uint8List computeSharedSecret(
String privateKeyHex,
String publicKeyHex,
) {
static List<int> computeSharedSecret({
required String privateKeyHex,
required String publicKeyHex,
}) {
final ec = getS256();
final privateKey = PrivateKey.fromHex(ec, privateKeyHex);
final publicKey = PublicKey.fromHex(ec, checkPublicKey(publicKeyHex));
final sec = computeSecret(privateKey, publicKey);
return Uint8List.fromList(sec);
return sec;
}

static Uint8List deriveConversationKey(Uint8List sharedSecret) {
static List<int> deriveConversationKey({required List<int> sharedSecret}) {
final salt = utf8.encode('nip44-v2');

final conversationKey = hkdfExtract(
Expand Down
108 changes: 61 additions & 47 deletions lib/src/nips/nip_044_utils.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:cryptography/cryptography.dart' as crypto;
import 'package:pointycastle/export.dart';

Map<String, Uint8List> deriveMessageKeys(
Uint8List conversationKey, Uint8List nonce) {
Map<String, List<int>> deriveMessageKeys(
List<int> conversationKey,
List<int> nonce,
) {
if (conversationKey.length != 32) {
throw FormatException('Invalid conversation key length');
}
Expand All @@ -26,14 +28,14 @@ Map<String, Uint8List> deriveMessageKeys(
};
}

Uint8List pad(Uint8List plaintext) {
int unpaddedLen = plaintext.length;
List<int> pad(List<int> plaintext) {
final unpaddedLen = plaintext.length;
if (unpaddedLen < 1 || unpaddedLen > 65535) {
throw Exception('Invalid plaintext length');
}

int paddedLen = calcPaddedLen(unpaddedLen);
Uint8List padded = Uint8List(paddedLen + 2);
final paddedLen = calcPaddedLen(unpaddedLen);
final padded = Uint8List(paddedLen + 2);

// First two bytes are the length in big-endian
padded[0] = (unpaddedLen >> 8) & 0xFF;
Expand All @@ -46,97 +48,105 @@ Uint8List pad(Uint8List plaintext) {
}

int calcPaddedLen(int unpaddedLen) {
int nextPower = 1 << ((unpaddedLen - 1).bitLength);
int chunk = nextPower <= 256 ? 32 : nextPower ~/ 8;
final nextPower = 1 << ((unpaddedLen - 1).bitLength);
final chunk = nextPower <= 256 ? 32 : nextPower ~/ 8;
if (unpaddedLen <= 32) {
return 32;
} else {
return chunk * ((unpaddedLen - 1) ~/ chunk + 1);
}
}

Future<Uint8List> encryptChaCha20(
Uint8List key, Uint8List nonce, Uint8List data) async {
final algorithm = Chacha20(macAlgorithm: MacAlgorithm.empty);
final skey = SecretKey(key);
Future<List<int>> encryptChaCha20(
List<int> key,
List<int> nonce,
List<int> data,
) async {
final algorithm = crypto.Chacha20(macAlgorithm: crypto.MacAlgorithm.empty);
final skey = crypto.SecretKey(key);
final secretBox = await algorithm.encrypt(
data,
secretKey: skey,
nonce: nonce,
);

return Uint8List.fromList(secretBox.cipherText);
return secretBox.cipherText;
}

Future<Uint8List> decryptChaCha20(
Uint8List key, Uint8List nonce, Uint8List ciphertext) async {
final algorithm = Chacha20(macAlgorithm: MacAlgorithm.empty);
final skey = SecretKey(key);
final secretBox = SecretBox(
Future<List<int>> decryptChaCha20(
List<int> key,
List<int> nonce,
List<int> ciphertext,
) async {
final algorithm = crypto.Chacha20(macAlgorithm: crypto.MacAlgorithm.empty);
final skey = crypto.SecretKey(key);
final secretBox = crypto.SecretBox(
ciphertext,
nonce: nonce,
mac: Mac.empty,
mac: crypto.Mac.empty,
);

final plaintext = await algorithm.decrypt(
secretBox,
secretKey: skey,
);

return Uint8List.fromList(plaintext);
return plaintext;
}

String constructPayload(Uint8List nonce, Uint8List ciphertext, Uint8List mac) {
Uint8List payloadBytes = Uint8List.fromList([
String constructPayload(List<int> nonce, List<int> ciphertext, List<int> mac) {
List<int> payloadBytes = [
0x02, // Version
...nonce,
...ciphertext,
...mac,
]);
];
return base64.encode(payloadBytes);
}

Uint8List hkdfExtract({required Uint8List ikm, required Uint8List salt}) {
var hmacSha256 = crypto.Hmac(crypto.sha256, salt);
var prk = hmacSha256.convert(ikm).bytes;
return Uint8List.fromList(prk);
List<int> hkdfExtract({required List<int> ikm, required List<int> salt}) {
final u8salt = Uint8List.fromList(salt);
final u8ikm = Uint8List.fromList(ikm);
final hmacSha256 = HMac(SHA256Digest(), 64)..init(KeyParameter(u8salt));
return hmacSha256.process(u8ikm);
}

Uint8List hkdfExpand({
required Uint8List prk,
required Uint8List info,
List<int> hkdfExpand({
required List<int> prk,
required List<int> info,
required int length,
}) {
var hashLen = 32;
int n = (length + hashLen - 1) ~/ hashLen;
var okm = <int>[];
var previous = <int>[];
final u8prk = Uint8List.fromList(prk);

for (var i = 1; i <= n; i++) {
var hmacSha256 = crypto.Hmac(crypto.sha256, prk);
var data = <int>[
final hmacSha256 = HMac(SHA256Digest(), 64)..init(KeyParameter(u8prk));
var data = Uint8List.fromList([
...previous,
...info,
i,
];
previous = hmacSha256.convert(data).bytes;
]);
previous = hmacSha256.process(data);
okm.addAll(previous);
}
return Uint8List.fromList(okm.sublist(0, length));
}

Uint8List unpad(Uint8List padded) {
List<int> unpad(List<int> padded) {
int unpaddedLen = (padded[0] << 8) + padded[1];
if (unpaddedLen == 0 || unpaddedLen > padded.length - 2) {
throw Exception('Invalid padding');
}
return padded.sublist(2, 2 + unpaddedLen);
}

Uint8List calculateMac(Uint8List key, Uint8List nonce, Uint8List ciphertext) {
var hmacSha256 = crypto.Hmac(crypto.sha256, key);
var mac = hmacSha256.convert([...nonce, ...ciphertext]).bytes;
return Uint8List.fromList(mac);
List<int> calculateMac(List<int> key, List<int> nonce, List<int> ciphertext) {
final u8key = Uint8List.fromList(key);
final hmacSha256 = HMac(SHA256Digest(), 64)..init(KeyParameter(u8key));
return hmacSha256.process(Uint8List.fromList([...nonce, ...ciphertext]));
}

Map<String, dynamic> parsePayload(String payload) {
Expand All @@ -148,15 +158,15 @@ Map<String, dynamic> parsePayload(String payload) {
throw Exception('Invalid payload size');
}

Uint8List data = base64.decode(payload);
final data = base64.decode(payload);

if (data[0] != 0x02) {
throw Exception('Unsupported version');
}

Uint8List nonce = data.sublist(1, 33);
Uint8List mac = data.sublist(data.length - 32);
Uint8List ciphertext = data.sublist(33, data.length - 32);
final nonce = data.sublist(1, 33);
final mac = data.sublist(data.length - 32);
final ciphertext = data.sublist(33, data.length - 32);

return {
'nonce': nonce,
Expand All @@ -166,8 +176,12 @@ Map<String, dynamic> parsePayload(String payload) {
}

void verifyMac(
Uint8List hmacKey, Uint8List nonce, Uint8List ciphertext, Uint8List mac) {
Uint8List calculatedMac = calculateMac(hmacKey, nonce, ciphertext);
List<int> hmacKey,
List<int> nonce,
List<int> ciphertext,
List<int> mac,
) {
final calculatedMac = calculateMac(hmacKey, nonce, ciphertext);
if (!const ListEquality().equals(calculatedMac, mac)) {
throw Exception('Invalid MAC');
}
Expand Down
5 changes: 3 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ dependencies:
convert: ^3.1.1
pointycastle: ^3.7.3
bech32: ^0.2.2


elliptic: ^0.3.11
collection: ^1.19.1
cryptography: ^2.7.0 # TODO: Unmaintained replace by pointycastle
Loading

0 comments on commit 95f8ded

Please sign in to comment.