1071 lines
29 KiB
Swift
1071 lines
29 KiB
Swift
//
|
|
// 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
|
|
|
|
var is_bad: Bool {
|
|
return self == .bad_id || self == .bad_sig
|
|
}
|
|
}
|
|
|
|
struct OtherEvent {
|
|
let event_id: String
|
|
let relay_url: String
|
|
}
|
|
|
|
struct KeyEvent {
|
|
let key: String
|
|
let relay_url: String
|
|
}
|
|
|
|
struct ReferencedId: Identifiable, Hashable, Equatable {
|
|
let ref_id: String
|
|
let relay_id: String?
|
|
let key: String
|
|
|
|
var id: String {
|
|
return ref_id
|
|
}
|
|
|
|
static func q(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
|
return ReferencedId(ref_id: id, relay_id: relay_id, key: "q")
|
|
}
|
|
|
|
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
|
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
|
|
}
|
|
|
|
static func p(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
|
return ReferencedId(ref_id: id, relay_id: relay_id, key: "p")
|
|
}
|
|
}
|
|
|
|
struct EventId: Identifiable, CustomStringConvertible {
|
|
let id: String
|
|
|
|
var description: String {
|
|
id
|
|
}
|
|
}
|
|
|
|
class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable {
|
|
static func == (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
|
|
return lhs.id == rhs.id
|
|
}
|
|
|
|
static func < (lhs: NostrEvent, rhs: NostrEvent) -> Bool {
|
|
return lhs.created_at < rhs.created_at
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
|
|
var id: String
|
|
var sig: String
|
|
var tags: [[String]]
|
|
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: Int64
|
|
let kind: Int
|
|
let content: String
|
|
|
|
var is_textlike: Bool {
|
|
return kind == 1 || kind == 42
|
|
}
|
|
|
|
var too_big: Bool {
|
|
return self.content.utf8.count > 16000
|
|
}
|
|
|
|
var should_show_event: Bool {
|
|
return !too_big
|
|
}
|
|
|
|
var is_valid_id: Bool {
|
|
return calculate_event_id(ev: self) == self.id
|
|
}
|
|
|
|
private var _blocks: [Block]? = nil
|
|
func blocks(_ privkey: String?) -> [Block] {
|
|
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) -> [Block] {
|
|
return parse_mentions(content: content, tags: self.tags)
|
|
}
|
|
|
|
private lazy var inner_event: NostrEvent? = {
|
|
return event_from_json(dat: self.content)
|
|
}()
|
|
|
|
func get_inner_event(cache: EventCache) -> NostrEvent? {
|
|
guard self.known_kind == .boost else {
|
|
return nil
|
|
}
|
|
|
|
if self.content == "", let ref = self.referenced_ids.first {
|
|
return cache.lookup(ref.ref_id)
|
|
}
|
|
|
|
return self.inner_event
|
|
}
|
|
|
|
private var _event_refs: [EventRef]? = nil
|
|
func event_refs(_ privkey: String?) -> [EventRef] {
|
|
if let rs = _event_refs {
|
|
return rs
|
|
}
|
|
let refs = interpret_event_refs(blocks: self.blocks(privkey), tags: self.tags)
|
|
self._event_refs = refs
|
|
return refs
|
|
}
|
|
|
|
var decrypted_content: String? = nil
|
|
|
|
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
|
|
|
|
/*
|
|
switch validity {
|
|
case .ok:
|
|
return content
|
|
case .bad_id:
|
|
return content + "\n\n*WARNING: invalid note id, could be forged!*"
|
|
case .bad_sig:
|
|
return content + "\n\n*WARNING: invalid signature, could be forged!*"
|
|
}
|
|
*/
|
|
}
|
|
|
|
var description: String {
|
|
//let p = pow.map { String($0) } ?? "?"
|
|
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) content '\(content)' }"
|
|
}
|
|
|
|
var known_kind: NostrKind? {
|
|
return NostrKind.init(rawValue: kind)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, sig, tags, pubkey, created_at, kind, content
|
|
}
|
|
|
|
private func get_referenced_ids(key: String) -> [ReferencedId] {
|
|
return damus.get_referenced_ids(tags: self.tags, key: key)
|
|
}
|
|
|
|
public func is_root_event() -> Bool {
|
|
for tag in tags {
|
|
if tag.count >= 1 && tag[0] == "e" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
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: String) -> Bool {
|
|
for tag in tags {
|
|
if tag.count >= 2 && tag[0] == key {
|
|
if tag[1] == id {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func is_reply(_ privkey: String?) -> Bool {
|
|
return event_is_reply(self, privkey: 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)
|
|
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 func count_ids() -> Int {
|
|
return count_refs("e")
|
|
}
|
|
|
|
public func count_refs(_ type: String) -> Int {
|
|
var count: Int = 0
|
|
for tag in tags {
|
|
if tag.count >= 2 && tag[0] == "e" {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
public var referenced_pubkeys: [ReferencedId] {
|
|
return get_referenced_ids(key: "p")
|
|
}
|
|
|
|
/// Make a local event
|
|
public static func local(content: String, pubkey: String) -> NostrEvent {
|
|
let ev = NostrEvent(content: content, pubkey: pubkey)
|
|
ev.flags |= 1
|
|
return ev
|
|
}
|
|
|
|
public var is_local: Bool {
|
|
return (self.flags & 1) != 0
|
|
}
|
|
|
|
init(id: String = "", content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
|
|
self.id = id
|
|
self.sig = ""
|
|
|
|
self.content = content
|
|
self.pubkey = pubkey
|
|
self.kind = kind
|
|
self.tags = tags
|
|
self.created_at = createdAt
|
|
}
|
|
|
|
init(from: NostrEvent, content: String? = nil) {
|
|
self.id = from.id
|
|
self.sig = from.sig
|
|
|
|
self.content = content ?? from.content
|
|
self.pubkey = from.pubkey
|
|
self.kind = from.kind
|
|
self.tags = from.tags
|
|
self.created_at = from.created_at
|
|
}
|
|
|
|
func calculate_id() {
|
|
self.id = calculate_event_id(ev: self)
|
|
//self.pow = count_hash_leading_zero_bits(self.id)
|
|
}
|
|
|
|
// TODO: timeout
|
|
/*
|
|
func mine_id(pow: Int, done: @escaping (String) -> ()) {
|
|
let nonce_ind = self.ensure_nonce_tag()
|
|
let nonce: Int64 = 0
|
|
|
|
DispatchQueue.global(qos: .background).async {
|
|
while
|
|
}
|
|
}
|
|
*/
|
|
|
|
private func ensure_nonce_tag() -> Int {
|
|
for (i, tags) in self.tags.enumerated() {
|
|
for tag in tags {
|
|
if tags.count == 2 && tag == "nonce" {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
|
|
self.tags.append(["nonce", "0"])
|
|
return self.tags.count - 1
|
|
}
|
|
|
|
func sign(privkey: String) {
|
|
self.sig = sign_event(privkey: privkey, ev: self)
|
|
}
|
|
}
|
|
|
|
func sign_event(privkey: String, ev: NostrEvent) -> 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)
|
|
var digest = try! ev.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 decode_data(Data(txt.utf8))
|
|
}
|
|
|
|
func encode_json<T: Encodable>(_ 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 decode_json(json)
|
|
}
|
|
|
|
func decode_json<T: Decodable>(_ val: String) -> T? {
|
|
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
|
}
|
|
|
|
func decode_data<T: Decodable>(_ 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(ev: NostrEvent, tags: String) -> String {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .withoutEscapingSlashes
|
|
let str_data = try! encoder.encode(ev.content)
|
|
let content = String(decoding: str_data, as: UTF8.self)
|
|
let commit = "[0,\"\(ev.pubkey)\",\(ev.created_at),\(ev.kind),\(tags),\(content)]"
|
|
//print("COMMIT", commit)
|
|
return commit
|
|
}
|
|
|
|
func calculate_event_commitment(ev: NostrEvent) -> Data {
|
|
let tags_encoder = JSONEncoder()
|
|
tags_encoder.outputFormatting = .withoutEscapingSlashes
|
|
let tags_data = try! tags_encoder.encode(ev.tags)
|
|
let tags = String(decoding: tags_data, as: UTF8.self)
|
|
|
|
let target = event_commitment(ev: ev, tags: tags)
|
|
let target_data = target.data(using: .utf8)!
|
|
return target_data
|
|
}
|
|
|
|
func calculate_event_id(ev: NostrEvent) -> String {
|
|
let commitment = calculate_event_commitment(ev: ev)
|
|
let hash = sha256(commitment)
|
|
|
|
return hex_encode(hash)
|
|
}
|
|
|
|
|
|
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 hex_encode(_ data: Data) -> String {
|
|
var str = ""
|
|
for c in data {
|
|
let c1 = hexchar(c >> 4)
|
|
let c2 = hexchar(c & 0xF)
|
|
|
|
str.append(Character(Unicode.Scalar(c1)))
|
|
str.append(Character(Unicode.Scalar(c2)))
|
|
}
|
|
return str
|
|
}
|
|
|
|
|
|
|
|
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 refid_to_tag(_ ref: ReferencedId) -> [String] {
|
|
var tag = [ref.key, ref.ref_id]
|
|
if let relay_id = ref.relay_id {
|
|
tag.append(relay_id)
|
|
}
|
|
return tag
|
|
}
|
|
|
|
func tag_to_refid(_ tag: [String]) -> ReferencedId? {
|
|
if tag.count == 0 {
|
|
return nil
|
|
}
|
|
if tag.count == 1 {
|
|
return nil
|
|
}
|
|
|
|
var relay_id: String? = nil
|
|
if tag.count > 2 {
|
|
relay_id = tag[2]
|
|
}
|
|
|
|
return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
|
|
}
|
|
|
|
func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] {
|
|
return tags.reduce(into: []) { (acc, tag) in
|
|
if tag.count >= 2 && tag[0] == key {
|
|
var relay_id: String? = nil
|
|
if tag.count >= 3 {
|
|
relay_id = tag[2]
|
|
}
|
|
acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
|
|
}
|
|
}
|
|
}
|
|
|
|
func get_referenced_id_set(tags: [[String]], key: String) -> Set<ReferencedId> {
|
|
return tags.reduce(into: Set()) { (acc, tag) in
|
|
if tag.count >= 2 && tag[0] == key {
|
|
var relay_id: String? = nil
|
|
if tag.count >= 3 {
|
|
relay_id = tag[2]
|
|
}
|
|
acc.insert(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
|
|
}
|
|
}
|
|
}
|
|
|
|
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
|
guard let privkey = keypair.privkey else {
|
|
return nil
|
|
}
|
|
|
|
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
|
let rw_relay_info = RelayInfo(read: true, write: true)
|
|
var relays: [String: RelayInfo] = [:]
|
|
|
|
for relay in bootstrap_relays {
|
|
relays[relay] = rw_relay_info
|
|
}
|
|
|
|
let relay_json = encode_json(relays)!
|
|
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
|
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
|
|
let tags = [
|
|
["p", damus_pubkey],
|
|
["p", jb55_pubkey],
|
|
["p", keypair.pubkey] // you're a friend of yourself!
|
|
]
|
|
let ev = NostrEvent(content: relay_json,
|
|
pubkey: keypair.pubkey,
|
|
kind: NostrKind.contacts.rawValue,
|
|
tags: tags)
|
|
ev.calculate_id()
|
|
ev.sign(privkey: privkey)
|
|
return ev
|
|
}
|
|
|
|
func make_metadata_event(keypair: Keypair, metadata: Profile) -> NostrEvent? {
|
|
guard let privkey = keypair.privkey else {
|
|
return nil
|
|
}
|
|
|
|
let metadata_json = encode_json(metadata)!
|
|
let ev = NostrEvent(content: metadata_json,
|
|
pubkey: keypair.pubkey,
|
|
kind: NostrKind.metadata.rawValue,
|
|
tags: [])
|
|
|
|
ev.calculate_id()
|
|
ev.sign(privkey: privkey)
|
|
return ev
|
|
}
|
|
|
|
func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> NostrEvent {
|
|
var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
|
|
|
tags.append(["e", boosted.id, "", "root"])
|
|
tags.append(["p", boosted.pubkey])
|
|
|
|
let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags)
|
|
ev.calculate_id()
|
|
ev.sign(privkey: privkey)
|
|
return ev
|
|
}
|
|
|
|
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent {
|
|
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
|
tags.append(["e", liked.id])
|
|
tags.append(["p", liked.pubkey])
|
|
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags)
|
|
ev.calculate_id()
|
|
ev.sign(privkey: privkey)
|
|
|
|
return ev
|
|
}
|
|
|
|
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
|
switch target {
|
|
case .profile(let pk):
|
|
return [["p", pk]]
|
|
case .note(let note_target):
|
|
return [["e", note_target.note_id], ["p", note_target.author]]
|
|
}
|
|
}
|
|
|
|
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
|
|
// target tags must be the same as zap request target tags
|
|
let tags = zap_target_to_tags(target)
|
|
|
|
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
|
|
note.id = calculate_event_id(ev: note)
|
|
note.sig = sign_event(privkey: identity.privkey, ev: note)
|
|
|
|
guard let note_json = encode_json(note) else {
|
|
return nil
|
|
}
|
|
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
|
}
|
|
|
|
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
|
|
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
|
|
return nil
|
|
}
|
|
|
|
let enc_note = anon_tag[1]
|
|
|
|
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
|
|
|
|
// check to see if the private note was from us
|
|
if note == nil {
|
|
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
|
|
return nil
|
|
}
|
|
// use our private keypair and their pubkey to get the shared secret
|
|
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
|
|
}
|
|
|
|
guard let note else {
|
|
return nil
|
|
}
|
|
|
|
guard note.kind == 9733 else {
|
|
return nil
|
|
}
|
|
|
|
let zr_etag = zapreq.referenced_ids.first
|
|
let note_etag = note.referenced_ids.first
|
|
|
|
guard zr_etag == note_etag else {
|
|
return nil
|
|
}
|
|
|
|
let zr_ptag = zapreq.referenced_pubkeys.first
|
|
let note_ptag = note.referenced_pubkeys.first
|
|
|
|
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
|
|
return nil
|
|
}
|
|
|
|
guard validate_event(ev: note) == .ok else {
|
|
return nil
|
|
}
|
|
|
|
return note
|
|
}
|
|
|
|
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
|
|
let to_hash = our_privkey + id + String(created_at)
|
|
guard let dat = to_hash.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
let privkey_bytes = sha256(dat)
|
|
let privkey = hex_encode(privkey_bytes)
|
|
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
|
|
return nil
|
|
}
|
|
|
|
return FullKeypair(pubkey: pubkey, privkey: privkey)
|
|
}
|
|
|
|
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
|
|
var tags = zap_target_to_tags(target)
|
|
var relay_tag = ["relays"]
|
|
relay_tag.append(contentsOf: relays.map { $0.url.id })
|
|
tags.append(relay_tag)
|
|
|
|
var kp = keypair
|
|
|
|
let now = Int64(Date().timeIntervalSince1970)
|
|
|
|
var message = content
|
|
switch zap_type {
|
|
case .pub:
|
|
break
|
|
case .non_zap:
|
|
break
|
|
case .anon:
|
|
tags.append(["anon"])
|
|
kp = generate_new_keypair().to_full()!
|
|
case .priv:
|
|
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
|
|
return nil
|
|
}
|
|
kp = priv_kp
|
|
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
|
|
return nil
|
|
}
|
|
tags.append(["anon", privreq])
|
|
message = ""
|
|
}
|
|
|
|
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
|
|
ev.id = calculate_event_id(ev: ev)
|
|
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
|
|
return ev
|
|
}
|
|
|
|
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
|
var s = Set<T>()
|
|
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: String, from: NostrEvent) -> [ReferencedId] {
|
|
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
|
|
|
ids.append(.e(from.id))
|
|
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
|
|
if from.pubkey != our_pubkey {
|
|
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
|
var ids: [ReferencedId] = [.q(from.id)]
|
|
if from.pubkey != our_pubkey {
|
|
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func event_from_json(dat: String) -> NostrEvent? {
|
|
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
|
|
}
|
|
|
|
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: String?, pubkey: String, 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: String, their_pubkey: String, 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: String, pubkey: String) -> [UInt8]? {
|
|
guard let privkey_bytes = try? privkey.bytes else {
|
|
return nil
|
|
}
|
|
guard var pk_bytes = try? pubkey.bytes else {
|
|
return nil
|
|
}
|
|
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
|
|
}
|
|
|
|
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 raw_id = sha256(calculate_event_commitment(ev: ev))
|
|
let id = hex_encode(raw_id)
|
|
|
|
if id != ev.id {
|
|
return .bad_id
|
|
}
|
|
|
|
// TODO: implement verify
|
|
guard var sig64 = hex_decode(ev.sig)?.bytes else {
|
|
return .bad_sig
|
|
}
|
|
|
|
guard var ev_pubkey = hex_decode(ev.pubkey)?.bytes else {
|
|
return .bad_sig
|
|
}
|
|
|
|
let ctx = secp256k1.Context.raw
|
|
var xonly_pubkey = secp256k1_xonly_pubkey.init()
|
|
var ok = secp256k1_xonly_pubkey_parse(ctx, &xonly_pubkey, &ev_pubkey) != 0
|
|
if !ok {
|
|
return .bad_sig
|
|
}
|
|
var raw_id_bytes = raw_id.bytes
|
|
|
|
ok = secp256k1_schnorrsig_verify(ctx, &sig64, &raw_id_bytes, raw_id.count, &xonly_pubkey) > 0
|
|
return ok ? .ok : .bad_sig
|
|
}
|
|
|
|
func last_etag(tags: [[String]]) -> String? {
|
|
var e: String? = nil
|
|
for tag in tags {
|
|
if tag.count >= 2 && tag[0] == "e" {
|
|
e = tag[1]
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
func inner_event_or_self(ev: NostrEvent, cache: EventCache) -> NostrEvent {
|
|
guard let inner_ev = ev.get_inner_event(cache: cache) else {
|
|
return ev
|
|
}
|
|
|
|
return inner_ev
|
|
}
|
|
|
|
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
|
let blocks = ev.blocks(privkey).filter { block in
|
|
guard case .mention(let mention) = block else {
|
|
return false
|
|
}
|
|
|
|
guard case .event = mention.type else {
|
|
return false
|
|
}
|
|
|
|
if mention.ref.key != "e" {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/// MARK: - Preview
|
|
if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" {
|
|
return mention
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
extension [ReferencedId] {
|
|
var pRefs: [ReferencedId] {
|
|
get {
|
|
Set(self).filter { ref in
|
|
ref.key == "p"
|
|
}
|
|
}
|
|
}
|
|
|
|
var eRefs: [ReferencedId] {
|
|
get {
|
|
self.filter { ref in
|
|
ref.key == "e"
|
|
}
|
|
}
|
|
}
|
|
}
|