damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

NIP44v2EncryptionTests.swift (16602B)


      1 //
      2 //  NIP44v2EncryptionTests.swift
      3 //  damus
      4 //
      5 //  Based on NIP44v2EncryptingTests.swift, taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
      6 //
      7 //    MIT License
      8 //
      9 //    Copyright (c) 2023 Nostr SDK
     10 //
     11 //    Permission is hereby granted, free of charge, to any person obtaining a copy
     12 //    of this software and associated documentation files (the "Software"), to deal
     13 //    in the Software without restriction, including without limitation the rights
     14 //    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     15 //    copies of the Software, and to permit persons to whom the Software is
     16 //    furnished to do so, subject to the following conditions:
     17 //
     18 //    The above copyright notice and this permission notice shall be included in all
     19 //    copies or substantial portions of the Software.
     20 //
     21 //    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     22 //    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     23 //    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     24 //    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     25 //    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     26 //    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     27 //    SOFTWARE.
     28 //
     29 //
     30 //  Adapted by Daniel D’Aquino for damus on 2025-02-10.
     31 //
     32 import XCTest
     33 import CryptoKit
     34 @testable import damus
     35 
     36 final class NIP44v2EncryptingTests: XCTestCase {
     37 
     38     private lazy var vectors: NIP44Vectors = try! decodeFixture(filename: "nip44.vectors")  // swiftlint:disable:this force_try
     39 
     40     /// Calculate the conversation key from secret key, sec1, and public key, pub2.
     41     func testValidConversationKey() throws {
     42         let conversationKeyVectors = try XCTUnwrap(vectors.v2.valid.getConversationKey)
     43 
     44         try conversationKeyVectors.forEach { vector in
     45             let expectedConversationKey = try XCTUnwrap(vector.conversationKey)
     46             let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
     47             let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
     48             let conversationKeyBytes = try NIP44v2Encryption.conversationKey(
     49                 privateKeyA: privateKeyA,
     50                 publicKeyB: publicKeyB
     51             ).bytes
     52             let conversationKey = Data(conversationKeyBytes).hexString
     53             XCTAssertEqual(conversationKey, expectedConversationKey)
     54         }
     55     }
     56 
     57     /// Calculate ChaCha key, ChaCha nonce, and HMAC key from conversation key and nonce.
     58     func testValidMessageKeys() throws {
     59         let messageKeyVectors = try XCTUnwrap(vectors.v2.valid.getMessageKeys)
     60         let conversationKey = messageKeyVectors.conversationKey
     61         let conversationKeyBytes = try XCTUnwrap(conversationKey.hexDecoded?.bytes)
     62         let keys = messageKeyVectors.keys
     63 
     64         try keys.forEach { vector in
     65             let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
     66             let messageKeys = try NIP44v2Encryption.messageKeys(conversationKey: conversationKeyBytes, nonce: nonce)
     67             XCTAssertEqual(messageKeys.chaChaKey.hexString, vector.chaChaKey)
     68             XCTAssertEqual(messageKeys.chaChaNonce.hexString, vector.chaChaNonce)
     69             XCTAssertEqual(messageKeys.hmacKey.hexString, vector.hmacKey)
     70         }
     71     }
     72 
     73     /// Take unpadded length (first value), calculate padded length (second value).
     74     func testValidCalculatePaddedLength() throws {
     75         let calculatePaddedLengthVectors = try XCTUnwrap(vectors.v2.valid.calculatePaddedLength)
     76         try calculatePaddedLengthVectors.forEach { vector in
     77             XCTAssertEqual(vector.count, 2)
     78             let paddedLength = try NIP44v2Encryption.calculatePaddedLength(vector[0])
     79             XCTAssertEqual(paddedLength, vector[1])
     80         }
     81     }
     82 
     83     /// Emulate real conversation with a hardcoded nonce.
     84     /// Calculate pub2 from sec2, verify conversation key from (sec1, pub2), encrypt, verify payload.
     85     /// Then calculate pub1 from sec1, verify conversation key from (sec2, pub1), decrypt, verify plaintext.
     86     func testValidEncryptDecrypt() throws {
     87         let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
     88         try encryptDecryptVectors.forEach { vector in
     89             let sec1 = vector.sec1
     90             let sec2 = vector.sec2
     91             let expectedConversationKey = vector.conversationKey
     92             let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
     93             let plaintext = vector.plaintext
     94             let payload = vector.payload
     95 
     96             let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
     97             let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
     98             let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
     99             let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
    100 
    101             // Conversation key from sec1 and pub2.
    102             let conversationKey1Bytes = try NIP44v2Encryption.conversationKey(
    103                 privateKeyA: keypair1.privkey,
    104                 publicKeyB: keypair2.pubkey
    105             ).bytes
    106             XCTAssertEqual(expectedConversationKey, Data(conversationKey1Bytes).hexString)
    107 
    108             // Verify payload.
    109             let ciphertext = try NIP44v2Encryption.encrypt(
    110                 plaintext: plaintext,
    111                 conversationKey: conversationKey1Bytes,
    112                 nonce: nonce
    113             )
    114             XCTAssertEqual(payload, ciphertext)
    115 
    116             // Conversation key from sec2 and pub1.
    117             let conversationKey2Bytes = try NIP44v2Encryption.conversationKey(
    118                 privateKeyA: keypair2.privkey,
    119                 publicKeyB: keypair1.pubkey
    120             ).bytes
    121             XCTAssertEqual(expectedConversationKey, Data(conversationKey2Bytes).hexString)
    122 
    123             // Verify that decrypted data equals the plaintext that we started off with.
    124             let decrypted = try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey2Bytes)
    125             XCTAssertEqual(decrypted, plaintext)
    126         }
    127     }
    128 
    129     /// Same as previous step, but instead of a full plaintext and payload, their checksum is provided.
    130     func testValidEncryptDecryptLongMessage() throws {
    131         let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecryptLongMessage)
    132         try encryptDecryptVectors.forEach { vector in
    133             let conversationKey = vector.conversationKey
    134             let conversationKeyData = try XCTUnwrap(conversationKey.hexDecoded)
    135             let conversationKeyBytes = conversationKeyData.bytes
    136 
    137             let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
    138             let expectedPlaintextSHA256 = vector.plaintextSHA256
    139 
    140             let plaintext = String(repeating: vector.pattern, count: vector.repeatCount)
    141             let plaintextData = try XCTUnwrap(plaintext.data(using: .utf8))
    142             let plaintextSHA256 = plaintextData.sha256()
    143 
    144             XCTAssertEqual(plaintextSHA256.hexString, expectedPlaintextSHA256)
    145 
    146             let payloadSHA256 = vector.payloadSHA256
    147 
    148             let ciphertext = try NIP44v2Encryption.encrypt(
    149                 plaintext: plaintext,
    150                 conversationKey: conversationKeyBytes,
    151                 nonce: nonce
    152             )
    153             let ciphertextData = try XCTUnwrap(ciphertext.data(using: .utf8))
    154             let ciphertextSHA256 = ciphertextData.sha256().hexString
    155             XCTAssertEqual(ciphertextSHA256, payloadSHA256)
    156 
    157             let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, conversationKey: conversationKeyBytes)
    158             XCTAssertEqual(decrypted, plaintext)
    159         }
    160     }
    161 
    162     /// Emulate real conversation with only the public encrypt and decrypt functions,
    163     /// where the nonce used for encryption is a cryptographically secure pseudorandom generated series of bytes.
    164     func testValidEncryptDecryptRandomNonce() throws {
    165         let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
    166         try encryptDecryptVectors.forEach { vector in
    167             let sec1 = vector.sec1
    168             let sec2 = vector.sec2
    169             let plaintext = vector.plaintext
    170 
    171             let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
    172             let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
    173             
    174             let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
    175             let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
    176 
    177             // Encrypt plaintext with user A's private key and user B's public key.
    178             let ciphertext = try NIP44v2Encryption.encrypt(
    179                 plaintext: plaintext,
    180                 privateKeyA: keypair1.privkey,
    181                 publicKeyB: keypair2.pubkey
    182             )
    183 
    184             // Decrypt ciphertext with user B's private key and user A's public key.
    185             let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, privateKeyA: keypair2.privkey, publicKeyB: keypair1.pubkey)
    186             XCTAssertEqual(decrypted, plaintext)
    187         }
    188     }
    189 
    190     /// Encrypting a plaintext message that is not at a minimum of 1 byte and maximum of 65535 bytes must throw an error.
    191     func testInvalidEncryptMessageLengths() throws {
    192         let encryptMessageLengthsVectors = try XCTUnwrap(vectors.v2.invalid.encryptMessageLengths)
    193         try encryptMessageLengthsVectors.forEach { length in
    194             let randomBytes = Data.secureRandomBytes(count: 32)
    195             XCTAssertThrowsError(try NIP44v2Encryption.encrypt(plaintext: String(repeating: "a", count: length), conversationKey: randomBytes))
    196         }
    197     }
    198 
    199     /// Calculating conversation key must throw an error.
    200     func testInvalidConversationKey() throws {
    201         let conversationKeyVectors = try XCTUnwrap(vectors.v2.invalid.getConversationKey)
    202 
    203         try conversationKeyVectors.forEach { vector in
    204             let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
    205             let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
    206             XCTAssertThrowsError(try NIP44v2Encryption.conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB), vector.note ?? "")
    207         }
    208     }
    209 
    210     /// Decrypting message content must throw an error
    211     func testInvalidDecrypt() throws {
    212         let decryptVectors = try XCTUnwrap(vectors.v2.invalid.decrypt)
    213         try decryptVectors.forEach { vector in
    214             let conversationKey = try XCTUnwrap(vector.conversationKey.hexDecoded).bytes
    215             let payload = vector.payload
    216             XCTAssertThrowsError(try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey), vector.note)
    217         }
    218     }
    219 
    220 }
    221 
    222 
    223 struct NIP44Vectors: Decodable {
    224     let v2: NIP44VectorsV2
    225 
    226     private enum CodingKeys: String, CodingKey {
    227         case v2
    228     }
    229 }
    230 
    231 struct NIP44VectorsV2: Decodable {
    232     let valid: NIP44VectorsV2Valid
    233     let invalid: NIP44VectorsV2Invalid
    234 
    235     private enum CodingKeys: String, CodingKey {
    236         case valid
    237         case invalid
    238     }
    239 }
    240 
    241 struct NIP44VectorsV2Valid: Decodable {
    242     let getConversationKey: [NIP44VectorsV2GetConversationKey]
    243     let getMessageKeys: NIP44VectorsV2GetMessageKeys
    244     let calculatePaddedLength: [[Int]]
    245     let encryptDecrypt: [NIP44VectorsV2EncryptDecrypt]
    246     let encryptDecryptLongMessage: [NIP44VectorsV2EncryptDecryptLongMessage]
    247 
    248     private enum CodingKeys: String, CodingKey {
    249         case getConversationKey = "get_conversation_key"
    250         case getMessageKeys = "get_message_keys"
    251         case calculatePaddedLength = "calc_padded_len"
    252         case encryptDecrypt = "encrypt_decrypt"
    253         case encryptDecryptLongMessage = "encrypt_decrypt_long_msg"
    254     }
    255 }
    256 
    257 struct NIP44VectorsV2Invalid: Decodable {
    258     let encryptMessageLengths: [Int]
    259     let getConversationKey: [NIP44VectorsV2GetConversationKey]
    260     let decrypt: [NIP44VectorsDecrypt]
    261 
    262     private enum CodingKeys: String, CodingKey {
    263         case encryptMessageLengths = "encrypt_msg_lengths"
    264         case getConversationKey = "get_conversation_key"
    265         case decrypt
    266     }
    267 }
    268 
    269 struct NIP44VectorsDecrypt: Decodable {
    270     let conversationKey: String
    271     let nonce: String
    272     let plaintext: String
    273     let payload: String
    274     let note: String
    275 
    276     private enum CodingKeys: String, CodingKey {
    277         case conversationKey = "conversation_key"
    278         case nonce
    279         case plaintext
    280         case payload
    281         case note
    282     }
    283 }
    284 
    285 struct NIP44VectorsV2GetConversationKey: Decodable {
    286     let sec1: String
    287     let pub2: String
    288     let conversationKey: String?
    289     let note: String?
    290 
    291     private enum CodingKeys: String, CodingKey {
    292         case sec1
    293         case pub2
    294         case conversationKey = "conversation_key"
    295         case note
    296     }
    297 }
    298 
    299 struct NIP44VectorsV2GetMessageKeys: Decodable {
    300     let conversationKey: String
    301     let keys: [NIP44VectorsV2MessageKeys]
    302 
    303     private enum CodingKeys: String, CodingKey {
    304         case conversationKey = "conversation_key"
    305         case keys
    306     }
    307 }
    308 
    309 struct NIP44VectorsV2MessageKeys: Decodable {
    310     let nonce: String
    311     let chaChaKey: String
    312     let chaChaNonce: String
    313     let hmacKey: String
    314 
    315     private enum CodingKeys: String, CodingKey {
    316         case nonce
    317         case chaChaKey = "chacha_key"
    318         case chaChaNonce = "chacha_nonce"
    319         case hmacKey = "hmac_key"
    320     }
    321 }
    322 
    323 struct NIP44VectorsV2EncryptDecrypt: Decodable {
    324     let sec1: String
    325     let sec2: String
    326     let conversationKey: String
    327     let nonce: String
    328     let plaintext: String
    329     let payload: String
    330 
    331     private enum CodingKeys: String, CodingKey {
    332         case sec1
    333         case sec2
    334         case conversationKey = "conversation_key"
    335         case nonce
    336         case plaintext
    337         case payload
    338     }
    339 }
    340 
    341 struct NIP44VectorsV2EncryptDecryptLongMessage: Decodable {
    342     let conversationKey: String
    343     let nonce: String
    344     let pattern: String
    345     let repeatCount: Int
    346     let plaintextSHA256: String
    347     let payloadSHA256: String
    348 
    349     private enum CodingKeys: String, CodingKey {
    350         case conversationKey = "conversation_key"
    351         case nonce
    352         case pattern
    353         case repeatCount = "repeat"
    354         case plaintextSHA256 = "plaintext_sha256"
    355         case payloadSHA256 = "payload_sha256"
    356     }
    357 }
    358 
    359 fileprivate extension Data {
    360     var hexString: String {
    361         let hexDigits = Array("0123456789abcdef".utf16)
    362         var hexChars = [UTF16.CodeUnit]()
    363         hexChars.reserveCapacity(bytes.count * 2)
    364         
    365         for byte in self {
    366             let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
    367             hexChars.append(hexDigits[index1])
    368             hexChars.append(hexDigits[index2])
    369         }
    370         
    371         return String(utf16CodeUnits: hexChars, count: hexChars.count)
    372     }
    373 }
    374 
    375 extension String {
    376     var hexDecoded: Data? {
    377         guard self.count.isMultiple(of: 2) else { return nil }
    378         
    379         // https://stackoverflow.com/a/62517446/982195
    380         let stringArray = Array(self)
    381         var data = Data()
    382         for i in stride(from: 0, to: count, by: 2) {
    383             let pair = String(stringArray[i]) + String(stringArray[i + 1])
    384             if let byteNum = UInt8(pair, radix: 16) {
    385                 let byte = Data([byteNum])
    386                 data.append(byte)
    387             } else {
    388                 return nil
    389             }
    390         }
    391         return data
    392     }
    393 }
    394 
    395 extension NIP44v2EncryptingTests {
    396     func loadFixtureString(_ filename: String) throws -> String? {
    397         let data = try self.loadFixtureData(filename)
    398 
    399         guard let originalString = String(data: data, encoding: .utf8) else {
    400             throw FixtureLoadingError.decodingError
    401         }
    402 
    403         let trimmedString = originalString.filter { !"\n\t\r".contains($0) }
    404         return trimmedString
    405     }
    406     
    407     func loadFixtureData(_ filename: String) throws -> Data {
    408         guard let bundleData = try? readBundleFile(name: filename, ext: "json") else {
    409             throw FixtureLoadingError.missingFile
    410         }
    411         return bundleData
    412     }
    413 
    414     func decodeFixture<T: Decodable>(filename: String) throws -> T {
    415         let data = try self.loadFixtureData(filename)
    416         return try JSONDecoder().decode(T.self, from: data)
    417     }
    418     
    419     func readBundleFile(name: String, ext: String) throws -> Data {
    420         let bundle = Bundle(for: type(of: self))
    421         guard let fileURL = bundle.url(forResource: name, withExtension: ext) else {
    422             throw CocoaError(.fileReadNoSuchFile)
    423         }
    424         
    425         return try Data(contentsOf: fileURL)
    426     }
    427     
    428     enum FixtureLoadingError: Error {
    429         case missingFile
    430         case decodingError
    431     }
    432 }