Remove rust-nostr dependency
This commit removes rust-nostr dependency, and replaces the NIP-44 logic with a new NIP-44 module based on the Swift NostrSDK implementation. The decision to move away from rust-nostr and the Swift NostrSDK was made for the following reasons: 1. `rust-nostr` caused the app size to double 2. We only need NIP44 functionality, and we don't need to bring everything else 3. The Swift NostrSDK caused conflicts around the secp256k1 dependency that is hard to address 4. The way we do things in the codebase is far different from the Swift NostrSDK, and we optimize it for use with NostrDB. Bringing it an outside library causes significant complexity in integration with NostrDB, and would effectively cause the codebase to be split into two different ways of achieving the same results. Therefore it is cleaner if we stick to our own Nostr structures and functions and focus on maintaining them. However, the library CryptoSwift was added as a dependency, to bring in ChaCha20 which is not supported by CryptoKit (CryptoKit supports the ChaCha20-Poly1305 cipher, but NIP-44 uses ChaCha20 with HMAC-SHA256 instead) Closes: https://github.com/damus-io/damus/issues/2849 Changelog-Changed: Made internal changes to reduce the app binary size Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
@@ -0,0 +1,432 @@
|
||||
//
|
||||
// NIP44v2EncryptionTests.swift
|
||||
// damus
|
||||
//
|
||||
// Based on NIP44v2EncryptingTests.swift, taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
//
|
||||
// Adapted by Daniel D’Aquino for damus on 2025-02-10.
|
||||
//
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
@testable import damus
|
||||
|
||||
final class NIP44v2EncryptingTests: XCTestCase {
|
||||
|
||||
private lazy var vectors: NIP44Vectors = try! decodeFixture(filename: "nip44.vectors") // swiftlint:disable:this force_try
|
||||
|
||||
/// Calculate the conversation key from secret key, sec1, and public key, pub2.
|
||||
func testValidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.valid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let expectedConversationKey = try XCTUnwrap(vector.conversationKey)
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
let conversationKeyBytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: privateKeyA,
|
||||
publicKeyB: publicKeyB
|
||||
).bytes
|
||||
let conversationKey = Data(conversationKeyBytes).hexString
|
||||
XCTAssertEqual(conversationKey, expectedConversationKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate ChaCha key, ChaCha nonce, and HMAC key from conversation key and nonce.
|
||||
func testValidMessageKeys() throws {
|
||||
let messageKeyVectors = try XCTUnwrap(vectors.v2.valid.getMessageKeys)
|
||||
let conversationKey = messageKeyVectors.conversationKey
|
||||
let conversationKeyBytes = try XCTUnwrap(conversationKey.hexDecoded?.bytes)
|
||||
let keys = messageKeyVectors.keys
|
||||
|
||||
try keys.forEach { vector in
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let messageKeys = try NIP44v2Encryption.messageKeys(conversationKey: conversationKeyBytes, nonce: nonce)
|
||||
XCTAssertEqual(messageKeys.chaChaKey.hexString, vector.chaChaKey)
|
||||
XCTAssertEqual(messageKeys.chaChaNonce.hexString, vector.chaChaNonce)
|
||||
XCTAssertEqual(messageKeys.hmacKey.hexString, vector.hmacKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take unpadded length (first value), calculate padded length (second value).
|
||||
func testValidCalculatePaddedLength() throws {
|
||||
let calculatePaddedLengthVectors = try XCTUnwrap(vectors.v2.valid.calculatePaddedLength)
|
||||
try calculatePaddedLengthVectors.forEach { vector in
|
||||
XCTAssertEqual(vector.count, 2)
|
||||
let paddedLength = try NIP44v2Encryption.calculatePaddedLength(vector[0])
|
||||
XCTAssertEqual(paddedLength, vector[1])
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with a hardcoded nonce.
|
||||
/// Calculate pub2 from sec2, verify conversation key from (sec1, pub2), encrypt, verify payload.
|
||||
/// Then calculate pub1 from sec1, verify conversation key from (sec2, pub1), decrypt, verify plaintext.
|
||||
func testValidEncryptDecrypt() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let expectedConversationKey = vector.conversationKey
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let plaintext = vector.plaintext
|
||||
let payload = vector.payload
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Conversation key from sec1 and pub2.
|
||||
let conversationKey1Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey1Bytes).hexString)
|
||||
|
||||
// Verify payload.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKey1Bytes,
|
||||
nonce: nonce
|
||||
)
|
||||
XCTAssertEqual(payload, ciphertext)
|
||||
|
||||
// Conversation key from sec2 and pub1.
|
||||
let conversationKey2Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair2.privkey,
|
||||
publicKeyB: keypair1.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey2Bytes).hexString)
|
||||
|
||||
// Verify that decrypted data equals the plaintext that we started off with.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey2Bytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as previous step, but instead of a full plaintext and payload, their checksum is provided.
|
||||
func testValidEncryptDecryptLongMessage() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecryptLongMessage)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let conversationKey = vector.conversationKey
|
||||
let conversationKeyData = try XCTUnwrap(conversationKey.hexDecoded)
|
||||
let conversationKeyBytes = conversationKeyData.bytes
|
||||
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let expectedPlaintextSHA256 = vector.plaintextSHA256
|
||||
|
||||
let plaintext = String(repeating: vector.pattern, count: vector.repeatCount)
|
||||
let plaintextData = try XCTUnwrap(plaintext.data(using: .utf8))
|
||||
let plaintextSHA256 = plaintextData.sha256()
|
||||
|
||||
XCTAssertEqual(plaintextSHA256.hexString, expectedPlaintextSHA256)
|
||||
|
||||
let payloadSHA256 = vector.payloadSHA256
|
||||
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKeyBytes,
|
||||
nonce: nonce
|
||||
)
|
||||
let ciphertextData = try XCTUnwrap(ciphertext.data(using: .utf8))
|
||||
let ciphertextSHA256 = ciphertextData.sha256().hexString
|
||||
XCTAssertEqual(ciphertextSHA256, payloadSHA256)
|
||||
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, conversationKey: conversationKeyBytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with only the public encrypt and decrypt functions,
|
||||
/// where the nonce used for encryption is a cryptographically secure pseudorandom generated series of bytes.
|
||||
func testValidEncryptDecryptRandomNonce() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let plaintext = vector.plaintext
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Encrypt plaintext with user A's private key and user B's public key.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
)
|
||||
|
||||
// Decrypt ciphertext with user B's private key and user A's public key.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, privateKeyA: keypair2.privkey, publicKeyB: keypair1.pubkey)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypting a plaintext message that is not at a minimum of 1 byte and maximum of 65535 bytes must throw an error.
|
||||
func testInvalidEncryptMessageLengths() throws {
|
||||
let encryptMessageLengthsVectors = try XCTUnwrap(vectors.v2.invalid.encryptMessageLengths)
|
||||
try encryptMessageLengthsVectors.forEach { length in
|
||||
let randomBytes = Data.secureRandomBytes(count: 32)
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.encrypt(plaintext: String(repeating: "a", count: length), conversationKey: randomBytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculating conversation key must throw an error.
|
||||
func testInvalidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.invalid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB), vector.note ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypting message content must throw an error
|
||||
func testInvalidDecrypt() throws {
|
||||
let decryptVectors = try XCTUnwrap(vectors.v2.invalid.decrypt)
|
||||
try decryptVectors.forEach { vector in
|
||||
let conversationKey = try XCTUnwrap(vector.conversationKey.hexDecoded).bytes
|
||||
let payload = vector.payload
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey), vector.note)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct NIP44Vectors: Decodable {
|
||||
let v2: NIP44VectorsV2
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case v2
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2: Decodable {
|
||||
let valid: NIP44VectorsV2Valid
|
||||
let invalid: NIP44VectorsV2Invalid
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case valid
|
||||
case invalid
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Valid: Decodable {
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let getMessageKeys: NIP44VectorsV2GetMessageKeys
|
||||
let calculatePaddedLength: [[Int]]
|
||||
let encryptDecrypt: [NIP44VectorsV2EncryptDecrypt]
|
||||
let encryptDecryptLongMessage: [NIP44VectorsV2EncryptDecryptLongMessage]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case getMessageKeys = "get_message_keys"
|
||||
case calculatePaddedLength = "calc_padded_len"
|
||||
case encryptDecrypt = "encrypt_decrypt"
|
||||
case encryptDecryptLongMessage = "encrypt_decrypt_long_msg"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Invalid: Decodable {
|
||||
let encryptMessageLengths: [Int]
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let decrypt: [NIP44VectorsDecrypt]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case encryptMessageLengths = "encrypt_msg_lengths"
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case decrypt
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsDecrypt: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
let note: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetConversationKey: Decodable {
|
||||
let sec1: String
|
||||
let pub2: String
|
||||
let conversationKey: String?
|
||||
let note: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case pub2
|
||||
case conversationKey = "conversation_key"
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetMessageKeys: Decodable {
|
||||
let conversationKey: String
|
||||
let keys: [NIP44VectorsV2MessageKeys]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case keys
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2MessageKeys: Decodable {
|
||||
let nonce: String
|
||||
let chaChaKey: String
|
||||
let chaChaNonce: String
|
||||
let hmacKey: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case nonce
|
||||
case chaChaKey = "chacha_key"
|
||||
case chaChaNonce = "chacha_nonce"
|
||||
case hmacKey = "hmac_key"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecrypt: Decodable {
|
||||
let sec1: String
|
||||
let sec2: String
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case sec2
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecryptLongMessage: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let pattern: String
|
||||
let repeatCount: Int
|
||||
let plaintextSHA256: String
|
||||
let payloadSHA256: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case pattern
|
||||
case repeatCount = "repeat"
|
||||
case plaintextSHA256 = "plaintext_sha256"
|
||||
case payloadSHA256 = "payload_sha256"
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Data {
|
||||
var hexString: String {
|
||||
let hexDigits = Array("0123456789abcdef".utf16)
|
||||
var hexChars = [UTF16.CodeUnit]()
|
||||
hexChars.reserveCapacity(bytes.count * 2)
|
||||
|
||||
for byte in self {
|
||||
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
|
||||
hexChars.append(hexDigits[index1])
|
||||
hexChars.append(hexDigits[index2])
|
||||
}
|
||||
|
||||
return String(utf16CodeUnits: hexChars, count: hexChars.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var hexDecoded: Data? {
|
||||
guard self.count.isMultiple(of: 2) else { return nil }
|
||||
|
||||
// https://stackoverflow.com/a/62517446/982195
|
||||
let stringArray = Array(self)
|
||||
var data = Data()
|
||||
for i in stride(from: 0, to: count, by: 2) {
|
||||
let pair = String(stringArray[i]) + String(stringArray[i + 1])
|
||||
if let byteNum = UInt8(pair, radix: 16) {
|
||||
let byte = Data([byteNum])
|
||||
data.append(byte)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP44v2EncryptingTests {
|
||||
func loadFixtureString(_ filename: String) throws -> String? {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
|
||||
guard let originalString = String(data: data, encoding: .utf8) else {
|
||||
throw FixtureLoadingError.decodingError
|
||||
}
|
||||
|
||||
let trimmedString = originalString.filter { !"\n\t\r".contains($0) }
|
||||
return trimmedString
|
||||
}
|
||||
|
||||
func loadFixtureData(_ filename: String) throws -> Data {
|
||||
guard let bundleData = try? readBundleFile(name: filename, ext: "json") else {
|
||||
throw FixtureLoadingError.missingFile
|
||||
}
|
||||
return bundleData
|
||||
}
|
||||
|
||||
func decodeFixture<T: Decodable>(filename: String) throws -> T {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
func readBundleFile(name: String, ext: String) throws -> Data {
|
||||
let bundle = Bundle(for: type(of: self))
|
||||
guard let fileURL = bundle.url(forResource: name, withExtension: ext) else {
|
||||
throw CocoaError(.fileReadNoSuchFile)
|
||||
}
|
||||
|
||||
return try Data(contentsOf: fileURL)
|
||||
}
|
||||
|
||||
enum FixtureLoadingError: Error {
|
||||
case missingFile
|
||||
case decodingError
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user