Skip to content

Commit 3a4d315

Browse files
committed
feat: CoreKeyChainStorage 개발
1 parent 74ec777 commit 3a4d315

8 files changed

Lines changed: 183 additions & 1 deletion

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// CoreKeyChainStorageInterface.swift
3+
// CoreKeyChainStorageInterface
4+
//
5+
// Created by 김동준 on 6/29/26
6+
//
7+
8+
import Foundation
9+
10+
public protocol CoreKeyChainStorageInterface {
11+
func save<T: Encodable>(key: String, value: T) throws
12+
func read<T: Decodable>(key: String) throws -> T
13+
func update<T: Encodable>(key: String, value: T) throws
14+
func delete(key: String) throws
15+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// KeyChainStorageError.swift
3+
// CoreKeyChainStorageInterface
4+
//
5+
// Created by 김동준 on 6/29/26
6+
//
7+
8+
import Foundation
9+
10+
public enum KeyChainStorageError: Error {
11+
case unExpectedStatus(OSStatus)
12+
case encodingFailed
13+
case decodingFailed
14+
case noMatchKeyError
15+
16+
public var description: String {
17+
switch self {
18+
case .encodingFailed:
19+
"""
20+
🛑 KeyChainStorageError: Encoding failed
21+
"""
22+
case .decodingFailed:
23+
"""
24+
🛑 KeyChainStorageError: Decoding failed
25+
"""
26+
case .noMatchKeyError:
27+
"""
28+
🛑 KeyChainStorageError: No Match Key Error
29+
"""
30+
case .unExpectedStatus(let status):
31+
"""
32+
🛑 KeyChainStorageError: UnExpectedStatus
33+
- 타입: \(type(of: status))
34+
- 설명: \(status.description)
35+
"""
36+
}
37+
}
38+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import ProjectDescription
2+
import ProjectDescriptionHelpers
3+
4+
let project = Project.module(
5+
moduleType: .MicroFeature(.CoreKeyChainStorage),
6+
hasDemo: false
7+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//
2+
// CoreKeyChainStorage.swift
3+
// CoreKeyChainStorage
4+
//
5+
// Created by 김동준 on 6/29/26
6+
//
7+
8+
import Foundation
9+
import Security
10+
import CoreKeyChainStorageInterface
11+
12+
public struct CoreKeyChainStorage: CoreKeyChainStorageInterface {
13+
public init() {}
14+
15+
public func save<T: Encodable>(key: String, value: T) throws {
16+
let data: Data
17+
do {
18+
data = try JSONEncoder().encode(value)
19+
} catch {
20+
let error = KeyChainStorageError.encodingFailed
21+
throw error
22+
}
23+
24+
var query = baseQuery(key)
25+
query[kSecValueData as String] = data
26+
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
27+
28+
let status = SecItemAdd(query as CFDictionary, nil)
29+
switch status {
30+
case errSecSuccess:
31+
return
32+
case errSecDuplicateItem:
33+
try update(key: key, value: value)
34+
default:
35+
throw KeyChainStorageError.unExpectedStatus(status)
36+
}
37+
}
38+
39+
public func read<T: Decodable>(key: String) throws -> T {
40+
var query = baseQuery(key)
41+
query[kSecReturnData as String] = true
42+
query[kSecMatchLimit as String] = kSecMatchLimitOne
43+
44+
var dataTypeRef: CFTypeRef?
45+
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
46+
47+
guard status == errSecSuccess else {
48+
if status == errSecItemNotFound {
49+
let error = KeyChainStorageError.noMatchKeyError
50+
throw error
51+
}
52+
53+
let error = KeyChainStorageError.unExpectedStatus(status)
54+
throw error
55+
}
56+
57+
guard let retrievedData = dataTypeRef as? Data else {
58+
let error = KeyChainStorageError.decodingFailed
59+
throw error
60+
}
61+
62+
do {
63+
return try JSONDecoder().decode(T.self, from: retrievedData)
64+
} catch {
65+
let error = KeyChainStorageError.decodingFailed
66+
throw error
67+
}
68+
}
69+
70+
public func update<T: Encodable>(key: String, value: T) throws {
71+
let data: Data
72+
do {
73+
data = try JSONEncoder().encode(value)
74+
} catch {
75+
let error = KeyChainStorageError.encodingFailed
76+
throw error
77+
}
78+
79+
let attributes = [
80+
kSecValueData as String: data,
81+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
82+
] as [String: Any]
83+
84+
let status = SecItemUpdate(baseQuery(key) as CFDictionary, attributes as CFDictionary)
85+
guard status == errSecSuccess else {
86+
throw KeyChainStorageError.unExpectedStatus(status)
87+
}
88+
}
89+
90+
public func delete(key: String) throws {
91+
let query = baseQuery(key)
92+
let status = SecItemDelete(query as CFDictionary)
93+
94+
switch status {
95+
case errSecSuccess, errSecItemNotFound:
96+
return
97+
default:
98+
throw KeyChainStorageError.unExpectedStatus(status)
99+
}
100+
}
101+
}
102+
103+
private extension CoreKeyChainStorage {
104+
func baseQuery(_ key: String) -> [String: Any] {
105+
[
106+
kSecClass as String: kSecClassGenericPassword,
107+
kSecAttrAccount as String: key
108+
]
109+
}
110+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import CoreKeyChainStorageInterface
2+
3+
public struct CoreKeyChainStorageTesting {
4+
public init() {}
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import XCTest
2+
@testable import CoreKeyChainStorage
3+
4+
final class CoreKeyChainStorageTests: XCTestCase {}

Tuist/ProjectDescriptionHelpers/Dependency/DependencyInfo.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public let dependencyInfo: DependencyInfo = DependencyInfo(
4848
.module(.MicroFeature(.SignIn)),
4949
.module(.MicroFeature(.OnBoarding)),
5050
.module(.MicroFeature(.SignUpDone)),
51-
.external(.Swinject)
51+
.external(.Swinject),
52+
.module(.MicroFeature(.CoreKeyChainStorage))
5253
],
5354
.Root: [
5455
.microFeature(.Splash),

Tuist/ProjectDescriptionHelpers/Module/Module.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public enum ExternalModule {
2525
}
2626

2727
public enum MicroFeatureModule {
28+
case CoreKeyChainStorage
2829
case SignUpDone
2930
case MyPage
3031
case Challenge
@@ -59,6 +60,7 @@ public enum MicroFeatureModule {
5960
case .Challenge: "Projects/Feature/Challenge"
6061
case .MyPage: "Projects/Feature/MyPage"
6162
case .SignUpDone: "Projects/Feature/SignUpDone"
63+
case .CoreKeyChainStorage: "Projects/Core/CoreKeyChainStorage"
6264
}
6365
}
6466
}

0 commit comments

Comments
 (0)