Skip to content

Commit

Permalink
Change NIP-04 direct message and encryption naming to avoid ambiguity…
Browse files Browse the repository at this point in the history
… with NIP-17
  • Loading branch information
tyiu committed May 21, 2024
1 parent 4277478 commit 32325aa
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 97 deletions.
2 changes: 1 addition & 1 deletion Sources/NostrSDK/EventCreating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ enum EventCreatingError: Error {
case invalidInput
}

public protocol EventCreating: DirectMessageEncrypting, RelayURLValidating {}
public protocol EventCreating: NIP04DirectMessageEncrypting, RelayURLValidating {}
79 changes: 40 additions & 39 deletions Sources/NostrSDK/EventKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha

/// This kind of event should have a recipient pubkey tag.
///
/// See [NIP-04 - Direct Messages](https://github.com/nostr-protocol/nips/blob/master/04.md)
case directMessage
/// See [NIP-04 - Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
/// > Warning: Deprecated in favor of [NIP-17 - Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/04.md).
case nip04EncryptedDirectMessage

/// This kind of event indicates that the author requests that the events in the included
/// tags should be deleted.
Expand Down Expand Up @@ -110,7 +111,7 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha
.setMetadata,
.textNote,
.followList,
.directMessage,
.nip04EncryptedDirectMessage,
.deletion,
.repost,
.reaction,
Expand All @@ -137,48 +138,48 @@ public enum EventKind: RawRepresentable, CaseIterable, Codable, Equatable, Hasha

public var rawValue: RawValue {
switch self {
case .setMetadata: return 0
case .textNote: return 1
case .followList: return 3
case .directMessage: return 4
case .deletion: return 5
case .repost: return 6
case .reaction: return 7
case .genericRepost: return 16
case .report: return 1984
case .muteList: return 10000
case .bookmarksList: return 10003
case .authentication: return 22242
case .longformContent: return 30023
case .dateBasedCalendarEvent: return 31922
case .timeBasedCalendarEvent: return 31923
case .calendar: return 31924
case .calendarEventRSVP: return 31925
case let .unknown(value): return value
case .setMetadata: return 0
case .textNote: return 1
case .followList: return 3
case .nip04EncryptedDirectMessage: return 4
case .deletion: return 5
case .repost: return 6
case .reaction: return 7
case .genericRepost: return 16
case .report: return 1984
case .muteList: return 10000
case .bookmarksList: return 10003
case .authentication: return 22242
case .longformContent: return 30023
case .dateBasedCalendarEvent: return 31922
case .timeBasedCalendarEvent: return 31923
case .calendar: return 31924
case .calendarEventRSVP: return 31925
case let .unknown(value): return value
}
}

/// The ``NostrEvent`` subclass associated with the kind.
public var classForKind: NostrEvent.Type {
switch self {
case .setMetadata: return SetMetadataEvent.self
case .textNote: return TextNoteEvent.self
case .followList: return FollowListEvent.self
case .directMessage: return DirectMessageEvent.self
case .deletion: return DeletionEvent.self
case .repost: return TextNoteRepostEvent.self
case .reaction: return ReactionEvent.self
case .genericRepost: return GenericRepostEvent.self
case .report: return ReportEvent.self
case .muteList: return MuteListEvent.self
case .bookmarksList: return BookmarksListEvent.self
case .authentication: return AuthenticationEvent.self
case .longformContent: return LongformContentEvent.self
case .dateBasedCalendarEvent: return DateBasedCalendarEvent.self
case .timeBasedCalendarEvent: return TimeBasedCalendarEvent.self
case .calendar: return CalendarListEvent.self
case .calendarEventRSVP: return CalendarEventRSVP.self
case .unknown: return NostrEvent.self
case .setMetadata: return SetMetadataEvent.self
case .textNote: return TextNoteEvent.self
case .followList: return FollowListEvent.self
case .nip04EncryptedDirectMessage: return NIP04EncryptedDirectMessageEvent.self
case .deletion: return DeletionEvent.self
case .repost: return TextNoteRepostEvent.self
case .reaction: return ReactionEvent.self
case .genericRepost: return GenericRepostEvent.self
case .report: return ReportEvent.self
case .muteList: return MuteListEvent.self
case .bookmarksList: return BookmarksListEvent.self
case .authentication: return AuthenticationEvent.self
case .longformContent: return LongformContentEvent.self
case .dateBasedCalendarEvent: return DateBasedCalendarEvent.self
case .timeBasedCalendarEvent: return TimeBasedCalendarEvent.self
case .calendar: return CalendarListEvent.self
case .calendarEventRSVP: return CalendarEventRSVP.self
case .unknown: return NostrEvent.self
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/NostrSDK/Events/BookmarksListEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ public extension EventCreating {
let rawPrivateTags = privateTags.map { $0.raw }
if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags),
let unencryptedContent = String(data: unencryptedData, encoding: .utf8) {
encryptedContent = try encrypt(content: unencryptedContent,
privateKey: keypair.privateKey,
publicKey: keypair.publicKey)
encryptedContent = try nip04Encrypt(content: unencryptedContent,
privateKey: keypair.privateKey,
publicKey: keypair.publicKey)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/NostrSDK/Events/MuteListEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ public extension EventCreating {
let rawPrivateTags = privateTags.map { $0.raw }
if let unencryptedData = try? JSONSerialization.data(withJSONObject: rawPrivateTags),
let unencryptedContent = String(data: unencryptedData, encoding: .utf8) {
encryptedContent = try encrypt(content: unencryptedContent,
privateKey: keypair.privateKey,
publicKey: keypair.publicKey)
encryptedContent = try nip04Encrypt(content: unencryptedContent,
privateKey: keypair.privateKey,
publicKey: keypair.publicKey)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DirectMessageEvent.swift
//
// NIP04EncryptedDirectMessageEvent.swift
//
//
// Created by Joel Klabo on 8/10/23.
//
Expand All @@ -9,8 +9,9 @@ import Foundation

/// An event that contains an encrypted message.
///
/// > Note: [NIP-04 Specification](https://github.com/nostr-protocol/nips/blob/master/04.md)
public final class DirectMessageEvent: NostrEvent, DirectMessageEncrypting {
/// > Note: [NIP-04 - Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
/// > Warning: Deprecated in favor of [NIP-17 - Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/04.md).
public final class NIP04EncryptedDirectMessageEvent: NostrEvent, NIP04DirectMessageEncrypting {

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
Expand All @@ -22,7 +23,7 @@ public final class DirectMessageEvent: NostrEvent, DirectMessageEncrypting {
}

init(content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws {
try super.init(kind: .directMessage, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
try super.init(kind: .nip04EncryptedDirectMessage, content: content, tags: tags, createdAt: createdAt, signedBy: keypair)
}

/// Returns decrypted content from Event given a `privateKey`
Expand All @@ -32,29 +33,29 @@ public final class DirectMessageEvent: NostrEvent, DirectMessageEncrypting {
}

guard let recipientPublicKeyHex = recipient?.value, let recipientPublicKey = PublicKey(hex: recipientPublicKeyHex) else {
throw DirectMessageEncryptingError.pubkeyInvalid
throw NIP04DirectMessageEncryptingError.pubkeyInvalid
}

return try decrypt(encryptedContent: content, privateKey: privateKey, publicKey: recipientPublicKey)
return try nip04Decrypt(encryptedContent: content, privateKey: privateKey, publicKey: recipientPublicKey)
}
}

public extension EventCreating {

/// Creates a ``DirectMessageEvent`` (kind 4) and signs it with the provided ``Keypair``.
/// Creates a ``NIP04EncryptedDirectMessageEvent`` (kind 4) and signs it with the provided ``Keypair``.
/// - Parameters:
/// - content: The content of the text note.
/// - toRecipient: The PublicKey of the recipient.
/// - keypair: The Keypair to sign with.
/// - Returns: The signed ``DirectMessageEvent``.
/// - Returns: The signed ``NIP04EncryptedDirectMessageEvent``.
///
/// See [NIP-04 - Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
func directMessage(withContent content: String, toRecipient pubkey: PublicKey, signedBy keypair: Keypair) throws -> DirectMessageEvent {
guard let encryptedMessage = try? encrypt(content: content, privateKey: keypair.privateKey, publicKey: pubkey) else {
/// See [NIP-04 - Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
func nip04EncryptedDirectMessage(withContent content: String, toRecipient pubkey: PublicKey, signedBy keypair: Keypair) throws -> NIP04EncryptedDirectMessageEvent {
guard let encryptedMessage = try? nip04Encrypt(content: content, privateKey: keypair.privateKey, publicKey: pubkey) else {
throw EventCreatingError.invalidInput
}

let recipientTag = Tag.pubkey(pubkey.hex)
return try DirectMessageEvent(content: encryptedMessage, tags: [recipientTag], signedBy: keypair)
return try NIP04EncryptedDirectMessageEvent(content: encryptedMessage, tags: [recipientTag], signedBy: keypair)
}
}
4 changes: 2 additions & 2 deletions Sources/NostrSDK/Events/Tags/PrivateTagInterpreting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public protocol PrivateTagInterpreting: DirectMessageEncrypting {}
public protocol PrivateTagInterpreting: NIP04DirectMessageEncrypting {}
public extension PrivateTagInterpreting {

/// The private tags encrypted in the content of the event.
Expand All @@ -16,7 +16,7 @@ public extension PrivateTagInterpreting {
/// - Parameter keypair: The keypair to use to decrypt the content.
/// - Returns: The private tags.
func privateTags(from content: String, withName tagName: TagName? = nil, using keypair: Keypair) -> [Tag] {
guard let decryptedContent = try? decrypt(encryptedContent: content, privateKey: keypair.privateKey, publicKey: keypair.publicKey),
guard let decryptedContent = try? nip04Decrypt(encryptedContent: content, privateKey: keypair.privateKey, publicKey: keypair.publicKey),
let jsonData = decryptedContent.data(using: .utf8) else {
return []
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DirectMessageEncrypting.swift
//
// NIP04DirectMessageEncrypting.swift
//
//
// Created by Joel Klabo on 8/10/23.
//
Expand All @@ -10,16 +10,16 @@ import secp256k1
import CommonCrypto
import CryptoKit

public enum DirectMessageEncryptingError: Error {
public enum NIP04DirectMessageEncryptingError: Error {
case pubkeyInvalid
case unsuccessfulExponentiation
case encryptionError
case decryptionError
case missingValue
}

public protocol DirectMessageEncrypting {}
public extension DirectMessageEncrypting {
public protocol NIP04DirectMessageEncrypting {}
public extension NIP04DirectMessageEncrypting {

/// Produces a `String` containing `content` that has been encrypted using a sender's `privateKey` and a recipient's `publicKey`.
/// This function can `throw` in the case of a failure to create a shared secret, a failure to successfully encrypt, or an invalid `publicKey`.
Expand All @@ -29,14 +29,14 @@ public extension DirectMessageEncrypting {
/// - privateKey: The private key of the sender.
/// - publicKey: The public key of the intended recipient.
/// - Returns: Encrypted content.
func encrypt(content: String, privateKey: PrivateKey, publicKey: PublicKey) throws -> String {
func nip04Encrypt(content: String, privateKey: PrivateKey, publicKey: PublicKey) throws -> String {

let sharedSecret = try getSharedSecret(privateKey: privateKey, recipient: publicKey)

let iv = Data.randomBytes(count: 16).bytes
let utf8Content = Data(content.utf8).bytes
guard let encryptedMessage = AESEncrypt(data: utf8Content, iv: iv, sharedSecret: sharedSecret) else {
throw DirectMessageEncryptingError.encryptionError
throw NIP04DirectMessageEncryptingError.encryptionError
}

return encodeDMBase64(content: encryptedMessage.bytes, iv: iv)
Expand All @@ -50,32 +50,32 @@ public extension DirectMessageEncrypting {
/// - privateKey: The private key of the receiver.
/// - publicKey: The public key of the sender.
/// - Returns: The un-encrypted message.
func decrypt(encryptedContent message: String, privateKey: PrivateKey, publicKey: PublicKey) throws -> String {
func nip04Decrypt(encryptedContent message: String, privateKey: PrivateKey, publicKey: PublicKey) throws -> String {
guard let sharedSecret = try? getSharedSecret(privateKey: privateKey, recipient: publicKey) else {
throw EventCreatingError.invalidInput
}

let sections = Array(message.split(separator: "?"))

if sections.count != 2 {
throw DirectMessageEncryptingError.decryptionError
throw NIP04DirectMessageEncryptingError.decryptionError
}

guard let encryptedContent = sections.first,
let encryptedContentData = Data(base64Encoded: String(encryptedContent)) else {
throw DirectMessageEncryptingError.decryptionError
throw NIP04DirectMessageEncryptingError.decryptionError
}

guard let ivContent = sections.last else {
throw DirectMessageEncryptingError.decryptionError
throw NIP04DirectMessageEncryptingError.decryptionError
}

let ivContentTrimmed = ivContent.dropFirst(3)

guard let ivContentData = Data(base64Encoded: String(ivContentTrimmed)),
let decryptedContentData = AESDecrypt(data: encryptedContentData.bytes, iv: ivContentData.bytes, sharedSecret: sharedSecret),
let decryptedMessage = String(data: decryptedContentData, encoding: .utf8) else {
throw DirectMessageEncryptingError.decryptionError
throw NIP04DirectMessageEncryptingError.decryptionError
}

return decryptedMessage
Expand All @@ -98,7 +98,7 @@ public extension DirectMessageEncrypting {
private func parsePublicKey(from bytes: [UInt8]) throws -> secp256k1_pubkey {
var recipientPublicKey = secp256k1_pubkey()
guard secp256k1_ec_pubkey_parse(secp256k1.Context.rawRepresentation, &recipientPublicKey, bytes, bytes.count) != 0 else {
throw DirectMessageEncryptingError.pubkeyInvalid
throw NIP04DirectMessageEncryptingError.pubkeyInvalid
}
return recipientPublicKey
}
Expand All @@ -110,7 +110,7 @@ public extension DirectMessageEncrypting {
memcpy(output, x32, 32)
return 1
}, nil) != 0 else {
throw DirectMessageEncryptingError.unsuccessfulExponentiation
throw NIP04DirectMessageEncryptingError.unsuccessfulExponentiation
}
return sharedSecret
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
//
// DirectMessageEventTests.swift
//
// NIP04EncryptedDirectMessageEventTests.swift
//
//
// Created by Terry Yiu on 4/14/24.
//

@testable import NostrSDK
import XCTest

final class DirectMessageEventTests: XCTestCase, EventCreating, EventVerifying, FixtureLoading {
final class NIP04EncryptedDirectMessageEventTests: XCTestCase, EventCreating, EventVerifying, FixtureLoading {

func testCreateDirectMessageEvent() throws {
func testCreateNIP04EncryptedDirectMessageEvent() throws {
let content = "Secret message."
let recipientPubKey = Keypair.test.publicKey
let recipientTag = Tag.pubkey(recipientPubKey.hex)

let event = try directMessage(withContent: content, toRecipient: recipientPubKey, signedBy: Keypair.test)
let event = try nip04EncryptedDirectMessage(withContent: content, toRecipient: recipientPubKey, signedBy: Keypair.test)

// Content should contain "?iv=" if encrypted
XCTAssert(event.content.contains("?iv="))
Expand All @@ -30,14 +30,14 @@ final class DirectMessageEventTests: XCTestCase, EventCreating, EventVerifying,
try verifyEvent(event)
}

func testDecodeDirectMessage() throws {
let event: DirectMessageEvent = try decodeFixture(filename: "dm")
func testDecodeNIP04EncryptedDirectMessage() throws {
let event: NIP04EncryptedDirectMessageEvent = try decodeFixture(filename: "nip04_encrypted_direct_message")

XCTAssertEqual(event.content, "+0V/p6oNtFXAlWVzDTx6wg==?iv=L6gDJ8ei4k1t3lUNgYAahw==")
XCTAssertEqual(event.id, "a606649e4995a12226902bd38573c21b04732c0835e415d09be6fbe93879b666")
XCTAssertEqual(event.pubkey, "9947f9659dd80c3682402b612f5447e28249997fb3709500c32a585eb0977340")
XCTAssertEqual(event.createdAt, 1691768179)
XCTAssertEqual(event.kind, .directMessage)
XCTAssertEqual(event.kind, .nip04EncryptedDirectMessage)

let expectedTags: [Tag] = [
.pubkey("9947f9659dd80c3682402b612f5447e28249997fb3709500c32a585eb0977340")
Expand Down
File renamed without changes.
Loading

0 comments on commit 32325aa

Please sign in to comment.