krz/keycask

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

Commit 84a94244b1

84a94244b1abd781e3377d837d448882e2280309

parent: 26c3860391

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-18 01:22 UTC

Create the vault temp file exclusively

Layout: unified · split

Sources/keycask/AtomicFile.swift +2 −1
@@ -49,7 +49,8 @@ enum AtomicFile {
49 } 49 }
50 #else 50 #else
51 private static func writePrivate(_ data: Data, to url: URL) throws { 51 private static func writePrivate(_ data: Data, to url: URL) throws {
52 let fd = open(url.path, O_WRONLY | O_CREAT | O_TRUNC, 0o600) 52 _ = unlink(url.path)
53 let fd = open(url.path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0o600)
53 guard fd >= 0 else { 54 guard fd >= 0 else {
54 throw KeycaskError.io("open \(url.path): \(String(cString: strerror(errno)))") 55 throw KeycaskError.io("open \(url.path): \(String(cString: strerror(errno)))")
55 } 56 }
Tests/KeycaskCLITests/InitTests.swift +30
@@ -65,6 +65,36 @@ import Testing
65 let mode = (attrs[.posixPermissions] as! NSNumber).intValue & 0o777 65 let mode = (attrs[.posixPermissions] as! NSNumber).intValue & 0o777
66 #expect(mode == 0o600) 66 #expect(mode == 0o600)
67 } 67 }
68
69 @Test func preExistingTempFileDoesNotWeakenPermissions() throws {
70 let cli = try CLI()
71 let temp = cli.dir.appendingPathComponent("vault.kc.tmp")
72 FileManager.default.createFile(
73 atPath: temp.path, contents: Data(), attributes: [.posixPermissions: 0o644])
74
75 let r = try cli.run(["init"])
76 #expect(r.status == 0)
77 let attrs = try FileManager.default.attributesOfItem(atPath: cli.vault.path)
78 let mode = (attrs[.posixPermissions] as! NSNumber).intValue & 0o777
79 #expect(mode == 0o600)
80 #expect(!FileManager.default.fileExists(atPath: temp.path))
81 }
82
83 @Test func symlinkedTempFileIsNotFollowed() throws {
84 let cli = try CLI()
85 let victim = cli.dir.appendingPathComponent("victim.txt")
86 FileManager.default.createFile(atPath: victim.path, contents: Data())
87 let temp = cli.dir.appendingPathComponent("vault.kc.tmp")
88 try FileManager.default.createSymbolicLink(at: temp, withDestinationURL: victim)
89
90 let r = try cli.run(["init"])
91 #expect(r.status == 0)
92 let victimAttrs = try FileManager.default.attributesOfItem(atPath: victim.path)
93 #expect((victimAttrs[.size] as! NSNumber).intValue == 0)
94 let attrs = try FileManager.default.attributesOfItem(atPath: cli.vault.path)
95 #expect(attrs[.type] as? FileAttributeType == .typeRegular)
96 #expect((attrs[.posixPermissions] as! NSNumber).intValue & 0o777 == 0o600)
97 }
68 #endif 98 #endif
69 99
70 @Test func noTempFileLeftBehind() throws { 100 @Test func noTempFileLeftBehind() throws {