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 }