Skip to content

Instantly share code, notes, and snippets.

@sharplet
Created April 25, 2020 23:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sharplet/affaf7b8183261f5c5ee3f780a7d55ad to your computer and use it in GitHub Desktop.
Save sharplet/affaf7b8183261f5c5ee3f780a7d55ad to your computer and use it in GitHub Desktop.
A lightweight keychain wrapper for querying the keychain databse
// Copyright (c) 2019–20 Adam Sharp and thoughtbot, inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
struct KeychainItem {
var account: String
var service: String
#if !os(macOS)
var accessibility: CFString
#endif
var `class`: CFString = kSecClassGenericPassword
var exists: Bool {
let query = makeQuery()
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData] = false
query[kSecUseAuthenticationUI] = kSecUseAuthenticationUIFail
let status = SecItemCopyMatching(query, nil)
return status != errSecItemNotFound
}
var isAccessibleWithoutInteraction: Bool {
let query = makeQuery()
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData] = false
query[kSecUseAuthenticationUI] = kSecUseAuthenticationUIFail
let status = SecItemCopyMatching(query, nil)
return status == noErr
}
var isUserInteractionRequired: Bool {
let query = makeQuery()
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData] = false
query[kSecUseAuthenticationUI] = kSecUseAuthenticationUIFail
let status = SecItemCopyMatching(query, nil)
return status == errSecInteractionNotAllowed
}
func create(with data: Data, requireUserPresence: Bool) throws {
let attributes = makeQuery()
attributes[kSecValueData] = data
#if !os(macOS)
if requireUserPresence {
attributes[kSecAttrAccessControl] = makeAccessControl()
} else {
attributes[kSecAttrAccessible] = accessibility
}
#endif
try Security.addItem(withAttributes: attributes)
}
#if os(macOS)
func createOrUpdate(with data: Data) throws {
try createOrUpdate(with: data, requireUserPresence: false)
}
#endif
func createOrUpdate(with data: Data, requireUserPresence: Bool) throws {
do {
try create(with: data, requireUserPresence: requireUserPresence)
} catch OSError.securityDuplicateItem {
try update(with: data)
}
}
func recreate(with data: Data? = nil, requireUserPresence: Bool? = nil) throws {
guard let data = try data ?? loadData() else { return }
let requireUserPresence = requireUserPresence ?? (exists && isUserInteractionRequired)
do {
try delete()
} catch OSError.securityItemNotFound {
// no-op
}
try create(with: data, requireUserPresence: requireUserPresence)
}
func delete() throws {
try Security.deleteItem(matchingQuery: makeQuery())
}
func loadData() throws -> Data? {
try Security.loadData(forItemMatchingQuery: makeQuery())
}
func setRequiresUserPresence(_ isRequired: Bool) throws {
do {
try recreate(requireUserPresence: isRequired)
} catch OSError.securityItemNotFound {
// no-op
}
}
func update(with data: Data) throws {
try Security.updateItem(matchingQuery: makeQuery(), withAttributes: [kSecValueData: data])
}
#if !os(macOS)
private func makeAccessControl() -> SecAccessControl {
SecAccessControlCreateWithFlags(nil, accessibility, .userPresence, nil)!
}
#endif
private func makeQuery() -> NSMutableDictionary {
[
kSecAttrAccount: account,
kSecAttrService: service,
kSecClass: `class`,
]
}
}
#if canImport(Combine)
import Combine
extension KeychainItem {
func decode<T: Decodable, Decoder: TopLevelDecoder>(_: T.Type, with decoder: Decoder) throws -> T?
where Decoder.Input == Data
{
guard let data = try loadData() else { return nil }
return try decoder.decode(T.self, from: data)
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment