diff --git a/MyPassCore/Sources/MyPassCore/Models/ProtectedString.swift b/MyPassCore/Sources/MyPassCore/Models/ProtectedString.swift new file mode 100644 index 0000000..dd0d19a --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/ProtectedString.swift @@ -0,0 +1,32 @@ +import Foundation + +public struct ProtectedString: Equatable { + private let obfuscated: [UInt8] + private let key: [UInt8] + public let isProtected: Bool + + public init(_ value: String, isProtected: Bool = true) { + self.isProtected = isProtected + let bytes = Array(value.utf8) + let k = (0 ..< bytes.count).map { _ in UInt8.random(in: 0 ... 255) } + self.key = k + self.obfuscated = zip(bytes, k).map { $0 ^ $1 } + } + + public func reveal() -> String { + let bytes = zip(obfuscated, key).map { $0 ^ $1 } + return String(bytes: bytes, encoding: .utf8) ?? "" + } + + public static func == (lhs: ProtectedString, rhs: ProtectedString) -> Bool { + lhs.reveal() == rhs.reveal() && lhs.isProtected == rhs.isProtected + } +} + +extension ProtectedString: CustomStringConvertible { + public var description: String { isProtected ? "***" : reveal() } +} + +extension ProtectedString: CustomDebugStringConvertible { + public var debugDescription: String { "ProtectedString(isProtected: \(isProtected))" } +} diff --git a/MyPassCore/Tests/MyPassCoreTests/ProtectedStringTests.swift b/MyPassCore/Tests/MyPassCoreTests/ProtectedStringTests.swift new file mode 100644 index 0000000..4eeb392 --- /dev/null +++ b/MyPassCore/Tests/MyPassCoreTests/ProtectedStringTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import MyPassCore + +final class ProtectedStringTests: XCTestCase { + func test_reveal_returnsOriginalValue() { + let ps = ProtectedString("hunter2", isProtected: true) + XCTAssertEqual(ps.reveal(), "hunter2") + } + + func test_description_isRedactedWhenProtected() { + let ps = ProtectedString("secret", isProtected: true) + XCTAssertEqual("\(ps)", "***") + } + + func test_description_isPlainWhenNotProtected() { + let ps = ProtectedString("visible", isProtected: false) + XCTAssertEqual("\(ps)", "visible") + } + + func test_equality_matchesByRevealedValue() { + let a = ProtectedString("abc", isProtected: true) + let b = ProtectedString("abc", isProtected: true) + XCTAssertEqual(a, b) + } + + func test_debugDescription_neverRevealSecret() { + let ps = ProtectedString("topsecret", isProtected: true) + XCTAssertFalse(ps.debugDescription.contains("topsecret")) + } +}