// // NostrEvent.swift // damus // // Created by William Casarin on 2022-04-11. // import Foundation import CommonCrypto import secp256k1 import secp256k1_implementation import CryptoKit import NaturalLanguage enum ValidationResult: Decodable { case unknown case ok case bad_id case bad_sig } /* class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable { // TODO: memory mapped db events private var note_data: UnsafeMutablePointer init(data: UnsafeMutablePointer) { self.note_data = data } var id: [UInt8] { let buffer = UnsafeBufferPointer(start: ndb_note_id(note_data), count: 32) return Array(buffer) } var content: String { String(cString: ndb_note_content(self.note_data)) } var sig: [UInt8] { let buffer = UnsafeBufferPointer(start: ndb_note_signature(note_data), count: 64) return Array(buffer) } var tags: TagIterator let id: String let content: String let sig: String let tags: Tags //var boosted_by: String? // cached field for pow calc //var pow: Int? // custom flags for internal use //var flags: Int = 0 let pubkey: String let created_at: UInt32 let kind: UInt32 // cached stuff private var _event_refs: [EventRef]? = nil var decrypted_content: String? = nil private var _blocks: Blocks? = nil private lazy var inner_event: NostrEventOld? = { return event_from_json(dat: self.content) }() static func == (lhs: NostrEventOld, rhs: NostrEventOld) -> Bool { return lhs.id == rhs.id } static func < (lhs: NostrEventOld, rhs: NostrEventOld) -> Bool { return lhs.created_at < rhs.created_at } func hash(into hasher: inout Hasher) { hasher.combine(id) } private enum CodingKeys: String, CodingKey { case id, sig, tags, pubkey, created_at, kind, content } static func owned_from_json(json: String) -> NostrEventOld? { let decoder = JSONDecoder() guard let dat = json.data(using: .utf8) else { return nil } guard let ev = try? decoder.decode(NostrEventOld.self, from: dat) else { return nil } return ev } init?(content: String, keypair: Keypair, kind: UInt32 = 1, tags: [[String]] = [], createdAt: UInt32 = UInt32(Date().timeIntervalSince1970)) { self.content = content self.pubkey = keypair.pubkey self.kind = kind self.tags = tags self.created_at = createdAt if let privkey = keypair.privkey { self.id = hex_encode(calculate_event_id(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content)) self.sig = sign_id(privkey: privkey, id: self.id) } else { self.id = "" self.sig = "" } } } extension NostrEventOld { var is_textlike: Bool { return kind == 1 || kind == 42 || kind == 30023 } var too_big: Bool { return known_kind != .longform && self.content.utf8.count > 16000 } var should_show_event: Bool { return !too_big } func blocks(_ privkey: String?) -> Blocks { if let bs = _blocks { return bs } let blocks = get_blocks(content: self.get_content(privkey)) self._blocks = blocks return blocks } func get_blocks(content: String) -> Blocks { return parse_note_content(content: content, tags: self.tags) } func get_inner_event(cache: EventCache) -> NostrEventOld? { guard self.known_kind == .boost else { return nil } if self.content == "", let ref = self.referenced_ids.first { return cache.lookup(ref.ref_id.string()) } return self.inner_event } func event_refs(_ privkey: String?) -> [EventRef] { if let rs = _event_refs { return rs } let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags) self._event_refs = refs return refs } func decrypted(privkey: String?) -> String? { if let decrypted_content = decrypted_content { return decrypted_content } guard let key = privkey else { return nil } guard let our_pubkey = privkey_to_pubkey(privkey: key) else { return nil } var pubkey = self.pubkey // This is our DM, we need to use the pubkey of the person we're talking to instead if our_pubkey == pubkey { guard let refkey = self.referenced_pubkeys.first else { return nil } pubkey = refkey.ref_id } let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64) self.decrypted_content = dec return dec } func get_content(_ privkey: String?) -> String { if known_kind == .dm { return decrypted(privkey: privkey) ?? "*failed to decrypt content*" } return content } var description: String { return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) content '\(content)' }" } var known_kind: NostrKind? { return NostrKind.init(rawValue: kind) } private func get_referenced_ids(key: String) -> [ReferencedId] { return damus.get_referenced_ids(tags: self.tags, key: key) } public func direct_replies(_ privkey: String?) -> [ReferencedId] { return event_refs(privkey).reduce(into: []) { acc, evref in if let direct_reply = evref.is_direct_reply { acc.append(direct_reply) } } } public func thread_id(privkey: String?) -> String { for ref in event_refs(privkey) { if let thread_id = ref.is_thread_id { return thread_id.ref_id } } return self.id } public func last_refid() -> ReferencedId? { var mlast: Int? = nil var i: Int = 0 for tag in tags { if tag.count >= 2 && tag[0] == "e" { mlast = i } i += 1 } guard let last = mlast else { return nil } return tag_to_refid(tags[last]) } public func references(id: String, key: AsciiCharacter) -> Bool { for tag in tags { if tag.count >= 2 && tag[0].matches_char(key) { if tag[1] == id { return true } } } return false } func is_reply(_ privkey: String?) -> Bool { return event_is_reply(self.event_refs(privkey)) } func note_language(_ privkey: String?) -> String? { // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer. let originalBlocks = blocks(privkey).blocks let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ") // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate. let languageRecognizer = NLLanguageRecognizer() languageRecognizer.processString(originalOnlyText) guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else { return nil } // Remove the variant component and just take the language part as translation services typically only supports the variant-less language. // Moreover, speakers of one variant can generally understand other variants. return localeToLanguage(locale) } public var referenced_ids: [ReferencedId] { return get_referenced_ids(key: "e") } public var referenced_pubkeys: [ReferencedId] { return get_referenced_ids(key: "p") } public var referenced_hashtags: [ReferencedId] { return get_referenced_ids(key: "t") } var age: TimeInterval { let event_date = Date(timeIntervalSince1970: TimeInterval(created_at)) return Date.now.timeIntervalSince(event_date) } } */ func sign_id(privkey: String, id: String) -> String { let priv_key_bytes = try! privkey.bytes let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes) // Extra params for custom signing var aux_rand = random_bytes(count: 64).bytes var digest = try! id.bytes // API allows for signing variable length messages let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand) return hex_encode(signature.rawRepresentation) } func decode_nostr_event(txt: String) -> NostrResponse? { return NostrResponse.owned_from_json(json: txt) } func encode_json(_ val: T) -> String? { let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) } } func decode_nostr_event_json(json: String) -> NostrEvent? { return NostrEvent.owned_from_json(json: json) } /* func decode_nostr_event_json(json: String) -> NostrEvent? { guard let json_str = json.cString(using: .utf8) else { return nil } // Allocate a double pointer (pointer to pointer) for ndb_note var notePtr: UnsafeMutablePointer? = nil // Create the buffer var buf = [Int8](repeating: 0, count: 2<<18) // Call the C function let result = withUnsafeMutablePointer(to: ¬ePtr) { (ptr) -> Int32 in return ndb_note_from_json(json_str, Int32(json_str.count), ptr, &buf, Int32(buf.count)) } guard result == 0, let note = notePtr?.pointee else { return nil } return .init(data: note) } */ func decode_json(_ val: String) -> T? { return try? JSONDecoder().decode(T.self, from: Data(val.utf8)) } func decode_data(_ data: Data) -> T? { let decoder = JSONDecoder() do { return try decoder.decode(T.self, from: data) } catch { print("decode_data failed for \(T.self): \(error)") } return nil } func event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> String { let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let str_data = try! encoder.encode(content) let content = String(decoding: str_data, as: UTF8.self) let tags_encoder = JSONEncoder() tags_encoder.outputFormatting = .withoutEscapingSlashes let tags_data = try! tags_encoder.encode(tags) let tags = String(decoding: tags_data, as: UTF8.self) return "[0,\"\(pubkey.hex())\",\(created_at),\(kind),\(tags),\(content)]" } func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data { let target = event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) return target.data(using: .utf8)! } func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> NoteId { let commitment = calculate_event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content) return NoteId(sha256(commitment)) } func sha256(_ data: Data) -> Data { var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) } return Data(hash) } func hexchar(_ val: UInt8) -> UInt8 { if val < 10 { return 48 + val; } if val < 16 { return 97 + val - 10; } assertionFailure("impossiburu") return 0 } func random_bytes(count: Int) -> Data { var bytes = [Int8](repeating: 0, count: count) guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else { fatalError("can't copy secure random data") } return Data(bytes: bytes, count: count) } func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? { var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag }) tags.append(["e", boosted.id.hex(), "", "root"]) tags.append(["p", boosted.pubkey.hex()]) let content = event_to_json(ev: boosted) return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags) } func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? { var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in guard tag.count >= 2, (tag[0].matches_char("e") || tag[0].matches_char("p")) else { return } ts.append(tag.strings()) } tags.append(["e", liked.id.hex()]) tags.append(["p", liked.pubkey.hex()]) return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags) } func generate_private_keypair(our_privkey: Privkey, id: NoteId, created_at: UInt32) -> FullKeypair? { let to_hash = our_privkey.hex() + id.hex() + String(created_at) guard let dat = to_hash.data(using: .utf8) else { return nil } let privkey_bytes = sha256(dat) let privkey = Privkey(privkey_bytes) guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } return FullKeypair(pubkey: pubkey, privkey: privkey) } func uniq(_ xs: [T]) -> [T] { var s = Set() var ys: [T] = [] for x in xs { if s.contains(x) { continue } s.insert(x) ys.append(x) } return ys } func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] { var ids: [RefId] = from.referenced_ids.first.map({ ref in [ .event(ref) ] }) ?? [] let pks = from.referenced_pubkeys.reduce(into: [RefId]()) { rs, pk in if pk == our_pubkey { return } rs.append(.pubkey(pk)) } ids.append(.event(from.id)) ids.append(contentsOf: uniq(pks)) if from.pubkey != our_pubkey { ids.append(.pubkey(from.pubkey)) } return ids } func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] { var ids: [RefId] = [.quote(from.id.quote_id)] if from.pubkey != our_pubkey { ids.append(.pubkey(from.pubkey)) } return ids } func event_from_json(dat: String) -> NostrEvent? { return NostrEvent.owned_from_json(json: dat) } func event_to_json(ev: NostrEvent) -> String { let encoder = JSONEncoder() guard let res = try? encoder.encode(ev) else { return "{}" } guard let str = String(data: res, encoding: .utf8) else { return "{}" } return str } func decrypt_dm(_ privkey: Privkey?, pubkey: Pubkey, content: String, encoding: EncEncoding) -> String? { guard let privkey = privkey else { return nil } guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else { return nil } guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else { return nil } guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else { return nil } return String(data: dat, encoding: .utf8) } func decrypt_note(our_privkey: Privkey, their_pubkey: Pubkey, enc_note: String, encoding: EncEncoding) -> NostrEvent? { guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else { return nil } return decode_nostr_event_json(json: dec) } func get_shared_secret(privkey: Privkey, pubkey: Pubkey) -> [UInt8]? { let privkey_bytes = privkey.bytes var pk_bytes = pubkey.bytes pk_bytes.insert(2, at: 0) var publicKey = secp256k1_pubkey() var shared_secret = [UInt8](repeating: 0, count: 32) var ok = secp256k1_ec_pubkey_parse( secp256k1.Context.raw, &publicKey, pk_bytes, pk_bytes.count) != 0 if !ok { return nil } ok = secp256k1_ecdh( secp256k1.Context.raw, &shared_secret, &publicKey, privkey_bytes, {(output,x32,_,_) in memcpy(output,x32,32) return 1 }, nil) != 0 if !ok { return nil } return shared_secret } enum EncEncoding { case base64 case bech32 } struct DirectMessageBase64 { let content: [UInt8] let iv: [UInt8] } func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String { let content_bech32 = bech32_encode(hrp: "pzap", content) let iv_bech32 = bech32_encode(hrp: "iv", iv) return content_bech32 + "_" + iv_bech32 } func decode_dm_bech32(_ all: String) -> DirectMessageBase64? { let parts = all.split(separator: "_") guard parts.count == 2 else { return nil } let content_bech32 = String(parts[0]) let iv_bech32 = String(parts[1]) guard let content_tup = try? bech32_decode(content_bech32) else { return nil } guard let iv_tup = try? bech32_decode(iv_bech32) else { return nil } guard content_tup.hrp == "pzap" else { return nil } guard iv_tup.hrp == "iv" else { return nil } return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes) } func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String { let content_b64 = base64_encode(content) let iv_b64 = base64_encode(iv) return content_b64 + "?iv=" + iv_b64 } func decode_dm_base64(_ all: String) -> DirectMessageBase64? { let splits = Array(all.split(separator: "?")) if splits.count != 2 { return nil } guard let content = base64_decode(String(splits[0])) else { return nil } var sec = String(splits[1]) if !sec.hasPrefix("iv=") { return nil } sec = String(sec.dropFirst(3)) guard let iv = base64_decode(sec) else { return nil } return DirectMessageBase64(content: content, iv: iv) } func base64_encode(_ content: [UInt8]) -> String { return Data(content).base64EncodedString() } func base64_decode(_ content: String) -> [UInt8]? { guard let dat = Data(base64Encoded: content) else { return nil } return dat.bytes } func aes_decrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { return aes_operation(operation: CCOperation(kCCDecrypt), data: data, iv: iv, shared_sec: shared_sec) } func aes_encrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { return aes_operation(operation: CCOperation(kCCEncrypt), data: data, iv: iv, shared_sec: shared_sec) } func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { let data_len = data.count let bsize = kCCBlockSizeAES128 let len = Int(data_len) + bsize var decrypted_data = [UInt8](repeating: 0, count: len) let key_length = size_t(kCCKeySizeAES256) if shared_sec.count != key_length { assert(false, "unexpected shared_sec len: \(shared_sec.count) != 32") return nil } let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES128) let options: CCOptions = UInt32(kCCOptionPKCS7Padding) var num_bytes_decrypted :size_t = 0 let status = CCCrypt(operation, /*op:*/ algorithm, /*alg:*/ options, /*options:*/ shared_sec, /*key:*/ key_length, /*keyLength:*/ iv, /*iv:*/ data, /*dataIn:*/ data_len, /*dataInLength:*/ &decrypted_data,/*dataOut:*/ len,/*dataOutAvailable:*/ &num_bytes_decrypted/*dataOutMoved:*/ ) if UInt32(status) != UInt32(kCCSuccess) { return nil } return Data(bytes: decrypted_data, count: num_bytes_decrypted) } func validate_event(ev: NostrEvent) -> ValidationResult { let id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags.strings(), content: ev.content) if id != ev.id { return .bad_id } let ctx = secp256k1.Context.raw var xonly_pubkey = secp256k1_xonly_pubkey.init() var ev_pubkey = ev.pubkey.id.bytes var ok = secp256k1_xonly_pubkey_parse(ctx, &xonly_pubkey, &ev_pubkey) != 0 if !ok { return .bad_sig } var sig = ev.sig.data.bytes var idbytes = id.id.bytes ok = secp256k1_schnorrsig_verify(ctx, &sig, &idbytes, 32, &xonly_pubkey) > 0 return ok ? .ok : .bad_sig } func first_eref_mention(ev: NostrEvent, keypair: Keypair) -> Mention? { let blocks = ev.blocks(keypair).blocks.filter { block in guard case .mention(let mention) = block else { return false } switch mention.ref { case .note, .nevent: return true default: return false } } /// MARK: - Preview if let firstBlock = blocks.first, case .mention(let mention) = firstBlock { switch mention.ref { case .note(let note_id): return .note(note_id) case .nevent(let nevent): return .note(nevent.noteid) default: return nil } } return nil } func separate_invoices(ev: NostrEvent, keypair: Keypair) -> [Invoice]? { let invoiceBlocks: [Invoice] = ev.blocks(keypair).blocks.reduce(into: []) { invoices, block in guard case .invoice(let invoice) = block else { return } invoices.append(invoice) } return invoiceBlocks.isEmpty ? nil : invoiceBlocks } /** Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji. If the known kind is not a `NostrKind.like`, it will return `nil`. If the event content is an empty string or `+`, it will map that to a heart ❤️ emoji. If the event content is a "-", it will map that to a dislike 👎 emoji. Otherwise, it will return the event content at face value without transforming it. */ func to_reaction_emoji(ev: NostrEvent) -> String? { guard ev.known_kind == NostrKind.like else { return nil } switch ev.content { case "", "+": return "❤️" case "-": return "👎" default: return ev.content } } extension NostrEvent { /// The mutelist for a given event /// /// If the event is not a mutelist it will return `nil`. var mute_list: Set? { if (self.kind == NostrKind.list_deprecated.rawValue && self.referenced_params.contains(where: { p in p.param.matches_str("mute") })) || self.kind == NostrKind.mute_list.rawValue { return Set(self.referenced_mute_items) } else { return nil } } }