krz/keycask

Password manager: Swift core library, CLI for macOS/Linux/Windows, iOS/macOS app. cli password-manager swift

Tests/KeycaskCoreTests/EnvelopeTests.swift

v0.1.0
keycask/Tests/KeycaskCoreTests/EnvelopeTests.swift history · blame · raw

119 lines · 5153 bytes

  1import Crypto
  2import Foundation
  3import Testing
  4
  5@testable import KeycaskCore
  6
  7@Suite struct EnvelopeTests {
  8    // Low iteration count keeps the suite fast. Production uses Envelope.defaultIterations.
  9    let kdf = Envelope.KDFParams(
 10        name: Envelope.kdfName, iterations: 1_000, salt: Data(repeating: 7, count: 16))
 11
 12    func hex(_ key: SymmetricKey) -> String {
 13        key.withUnsafeBytes { $0.map { String(format: "%02x", $0) }.joined() }
 14    }
 15
 16    @Test func pbkdf2MatchesPublishedVectors() throws {
 17        let one = Envelope.KDFParams(name: Envelope.kdfName, iterations: 1, salt: Data("salt".utf8))
 18        #expect(
 19            hex(try Envelope.deriveKey(passphrase: "password", kdf: one))
 20                == "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b")
 21        let many = Envelope.KDFParams(
 22            name: Envelope.kdfName, iterations: 4096, salt: Data("salt".utf8))
 23        #expect(
 24            hex(try Envelope.deriveKey(passphrase: "password", kdf: many))
 25                == "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a")
 26    }
 27
 28    @Test func sealThenOpenRoundTrips() throws {
 29        let env = try Envelope.seal(Data("hello vault".utf8), passphrase: "pw", kdf: kdf)
 30        #expect(env.format == 1)
 31        #expect(env.kdf == kdf)
 32        #expect(try env.open(passphrase: "pw") == Data("hello vault".utf8))
 33    }
 34
 35    @Test func wrongPassphraseCannotDecrypt() throws {
 36        let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 37        #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "PW") }
 38    }
 39
 40    @Test func tamperedBoxCannotDecrypt() throws {
 41        var env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 42        env.box[env.box.count - 1] ^= 0x01
 43        #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "pw") }
 44    }
 45
 46    @Test func nonceIsFreshAndSaltIsKept() throws {
 47        let a = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 48        let b = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 49        #expect(a.box != b.box)
 50        #expect(a.kdf.salt == b.kdf.salt)
 51    }
 52
 53    @Test func freshParamsUseDefaults() {
 54        let p = Envelope.KDFParams.fresh()
 55        #expect(p.name == "pbkdf2-hmac-sha256")
 56        #expect(p.iterations == 600_000)
 57        #expect(p.salt.count == 16)
 58        #expect(p.salt != Envelope.KDFParams.fresh().salt)
 59    }
 60
 61    @Test func encodedShapeMatchesTheSpec() throws {
 62        let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 63        let json = try JSONSerialization.jsonObject(with: env.encoded()) as! [String: Any]
 64        #expect(json["format"] as? Int == 1)
 65        let k = json["kdf"] as! [String: Any]
 66        #expect(k["name"] as? String == "pbkdf2-hmac-sha256")
 67        #expect(k["iterations"] as? Int == 1_000)
 68        #expect(Data(base64Encoded: k["salt"] as! String) == kdf.salt)
 69        #expect(Data(base64Encoded: json["box"] as! String) == env.box)
 70        #expect(try Envelope(parsing: env.encoded()) == env)
 71    }
 72
 73    @Test func malformedInputsAreCorrupt() throws {
 74        #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("not json".utf8)) }
 75        #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("{\"format\":1}".utf8)) }
 76
 77        var wrongFormat = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 78        wrongFormat.format = 2
 79        #expect(throws: KeycaskError.corrupt("unsupported format 2")) {
 80            try wrongFormat.open(passphrase: "pw")
 81        }
 82
 83        var wrongKDF = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 84        wrongKDF.kdf.name = "argon2id"
 85        #expect(throws: KeycaskError.corrupt("unsupported kdf argon2id")) {
 86            try wrongKDF.open(passphrase: "pw")
 87        }
 88
 89        var shortBox = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 90        shortBox.box = Data([1, 2, 3])
 91        #expect(throws: KeycaskError.corrupt("box too short")) {
 92            try shortBox.open(passphrase: "pw")
 93        }
 94    }
 95
 96    @Test func outOfRangeIterationsAreCorrupt() throws {
 97        var env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
 98        env.kdf.iterations = -1
 99        #expect(throws: KeycaskError.corrupt("bad iteration count -1")) {
100            try env.open(passphrase: "pw")
101        }
102        env.kdf.iterations = 0
103        #expect(throws: KeycaskError.corrupt("bad iteration count 0")) {
104            try env.open(passphrase: "pw")
105        }
106        let tooMany = Envelope.KDFParams(
107            name: Envelope.kdfName, iterations: Int(UInt32.max) + 1, salt: kdf.salt)
108        #expect(throws: KeycaskError.corrupt("bad iteration count \(Int(UInt32.max) + 1)")) {
109            try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: tooMany)
110        }
111    }
112
113    @Test func passphraseIsNFCNormalized() throws {
114        let composed = "caf\u{00E9}"
115        let decomposed = "cafe\u{0301}"
116        let env = try Envelope.seal(Data("x".utf8), passphrase: composed, kdf: kdf)
117        #expect(try env.open(passphrase: decomposed) == Data("x".utf8))
118    }
119}