Skip to content

Commit 6458ba7

Browse files
authored
[NL-66]: 기기 기반 고유값 발급, 저장 로직 구현 (#35)
* [NL-66]: KeyChain 로직 구현 * [NL-66]: 기기 기반 고유값 발급, 저장 로직 구현
1 parent c8bf70c commit 6458ba7

File tree

5 files changed

+196
-2
lines changed

5 files changed

+196
-2
lines changed

App/Sources/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Auth
12
import DIInjector
23
import Lib
34
import Onboarding
@@ -11,7 +12,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
1112
_ application: UIApplication,
1213
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1314
) -> Bool {
14-
15+
DeviceUUIDManager.setup()
1516
registRouter()
1617

1718
return true
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// DeviceUUIDManager.swift
3+
// Auth
4+
//
5+
// Created by ttozzi on 8/15/25.
6+
//
7+
8+
import Foundation
9+
import Lib
10+
11+
public final class DeviceUUIDManager {
12+
13+
private enum Constant {
14+
static let uuidKey = "device-uuid"
15+
}
16+
17+
public static let shared = DeviceUUIDManager()
18+
public var deviceUUID: String {
19+
do {
20+
return try KeyChainService.getString(forKey: Constant.uuidKey)
21+
} catch {
22+
let newUUID = UUID().uuidString
23+
try? KeyChainService.set(newUUID, forKey: Constant.uuidKey)
24+
return newUUID
25+
}
26+
}
27+
28+
private init() {}
29+
30+
func regenerateUUID() -> String {
31+
let newUUID = UUID().uuidString
32+
try? KeyChainService.set(newUUID, forKey: Constant.uuidKey)
33+
return newUUID
34+
}
35+
36+
func deleteUUID() {
37+
try? KeyChainService.remove(forKey: Constant.uuidKey)
38+
}
39+
}
40+
41+
public extension DeviceUUIDManager {
42+
static func setup() {
43+
_ = shared.deviceUUID
44+
}
45+
}

Common/Auth/Sources/empty.swift

Whitespace-only changes.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//
2+
// KeyChainService.swift
3+
// Auth
4+
//
5+
// Created by ttozzi on 8/15/25.
6+
//
7+
8+
import Foundation
9+
import Security
10+
11+
public final class KeyChainService {
12+
13+
enum KeychainError: Error {
14+
case duplicateEntry
15+
case unknown(OSStatus)
16+
case itemNotFound
17+
case invalidItemFormat
18+
case unhandledError(status: OSStatus)
19+
}
20+
21+
private static let service = "com.hanbang.satto"
22+
23+
private static func set(_ data: Data, forKey key: String) throws {
24+
let query: [String: Any] = [
25+
kSecClass as String: kSecClassGenericPassword,
26+
kSecAttrService as String: service,
27+
kSecAttrAccount as String: key
28+
]
29+
30+
let attributes: [String: Any] = [
31+
kSecValueData as String: data
32+
]
33+
34+
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
35+
36+
switch status {
37+
case errSecSuccess:
38+
return
39+
case errSecItemNotFound:
40+
try add(data, forKey: key)
41+
default:
42+
throw KeychainError.unhandledError(status: status)
43+
}
44+
}
45+
46+
private static func get(forKey key: String) throws -> Data {
47+
let query: [String: Any] = [
48+
kSecClass as String: kSecClassGenericPassword,
49+
kSecAttrService as String: service,
50+
kSecAttrAccount as String: key,
51+
kSecReturnData as String: true,
52+
kSecMatchLimit as String: kSecMatchLimitOne
53+
]
54+
55+
var result: AnyObject?
56+
let status = SecItemCopyMatching(query as CFDictionary, &result)
57+
58+
guard status == errSecSuccess else {
59+
if status == errSecItemNotFound {
60+
throw KeychainError.itemNotFound
61+
}
62+
throw KeychainError.unhandledError(status: status)
63+
}
64+
65+
guard let data = result as? Data else {
66+
throw KeychainError.invalidItemFormat
67+
}
68+
69+
return data
70+
}
71+
72+
public static func remove(forKey key: String) throws {
73+
let query: [String: Any] = [
74+
kSecClass as String: kSecClassGenericPassword,
75+
kSecAttrService as String: service,
76+
kSecAttrAccount as String: key
77+
]
78+
79+
let status = SecItemDelete(query as CFDictionary)
80+
81+
guard status == errSecSuccess || status == errSecItemNotFound else {
82+
throw KeychainError.unhandledError(status: status)
83+
}
84+
}
85+
86+
static func hasKey(_ key: String) -> Bool {
87+
do {
88+
_ = try get(forKey: key)
89+
return true
90+
} catch {
91+
return false
92+
}
93+
}
94+
95+
private static func add(_ data: Data, forKey key: String) throws {
96+
let query: [String: Any] = [
97+
kSecClass as String: kSecClassGenericPassword,
98+
kSecAttrService as String: service,
99+
kSecAttrAccount as String: key,
100+
kSecValueData as String: data
101+
]
102+
103+
let status = SecItemAdd(query as CFDictionary, nil)
104+
105+
switch status {
106+
case errSecSuccess:
107+
return
108+
case errSecDuplicateItem:
109+
throw KeychainError.duplicateEntry
110+
default:
111+
throw KeychainError.unhandledError(status: status)
112+
}
113+
}
114+
}
115+
116+
public extension KeyChainService {
117+
static func set(_ string: String, forKey key: String) throws {
118+
guard let data = string.data(using: .utf8) else {
119+
throw KeychainError.invalidItemFormat
120+
}
121+
try set(data, forKey: key)
122+
}
123+
124+
static func getString(forKey key: String) throws -> String {
125+
let data = try get(forKey: key)
126+
guard let string = String(data: data, encoding: .utf8) else {
127+
throw KeychainError.invalidItemFormat
128+
}
129+
return string
130+
}
131+
}
132+
133+
public extension KeyChainService {
134+
static func set<T: Codable>(_ object: T, forKey key: String) throws {
135+
let data = try JSONEncoder().encode(object)
136+
try set(data, forKey: key)
137+
}
138+
139+
static func get<T: Codable>(_ type: T.Type, forKey key: String) throws -> T {
140+
let data = try get(forKey: key)
141+
return try JSONDecoder().decode(type, from: data)
142+
}
143+
}

Common/Project.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ struct CommonLayer: Layer {
2424
]
2525
)
2626
),
27-
.createTarget(name: "Auth"),
27+
.createTarget(
28+
name: "Auth",
29+
dependencies: [
30+
.target(name: "Lib"),
31+
]
32+
),
2833
.createTarget(
2934
name: "Base",
3035
dependencies: [

0 commit comments

Comments
 (0)