mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Move modules to Modules folder.
This commit is contained in:
5
Modules/Secrets/.gitignore
vendored
Normal file
5
Modules/Secrets/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
23
Modules/Secrets/Package.swift
Normal file
23
Modules/Secrets/Package.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
// swift-tools-version:6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Secrets",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "Secrets",
|
||||
type: .dynamic,
|
||||
targets: ["Secrets"]
|
||||
)
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Secrets",
|
||||
dependencies: [],
|
||||
exclude: ["SecretKey.swift.gyb"]
|
||||
)
|
||||
]
|
||||
)
|
||||
3
Modules/Secrets/README.md
Normal file
3
Modules/Secrets/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Secrets
|
||||
|
||||
A description of this package.
|
||||
37
Modules/Secrets/Sources/Secrets/Credentials.swift
Normal file
37
Modules/Secrets/Sources/Secrets/Credentials.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Credentials.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 12/9/17.
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum CredentialsError: Error, Sendable {
|
||||
case incompleteCredentials
|
||||
case unhandledError(status: OSStatus)
|
||||
}
|
||||
|
||||
public enum CredentialsType: String, Sendable {
|
||||
case basic = "password"
|
||||
case newsBlurBasic = "newsBlurBasic"
|
||||
case newsBlurSessionID = "newsBlurSessionId"
|
||||
case readerBasic = "readerBasic"
|
||||
case readerAPIKey = "readerAPIKey"
|
||||
case oauthAccessToken = "oauthAccessToken"
|
||||
case oauthAccessTokenSecret = "oauthAccessTokenSecret"
|
||||
case oauthRefreshToken = "oauthRefreshToken"
|
||||
}
|
||||
|
||||
public struct Credentials: Equatable, Sendable {
|
||||
public let type: CredentialsType
|
||||
public let username: String
|
||||
public let secret: String
|
||||
|
||||
public init(type: CredentialsType, username: String, secret: String) {
|
||||
self.type = type
|
||||
self.username = username
|
||||
self.secret = secret
|
||||
}
|
||||
}
|
||||
124
Modules/Secrets/Sources/Secrets/CredentialsManager.swift
Normal file
124
Modules/Secrets/Sources/Secrets/CredentialsManager.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// CredentialsManager.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Maurice Parker on 5/5/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CredentialsManager {
|
||||
|
||||
private static let keychainGroup: String? = {
|
||||
guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String else {
|
||||
return nil
|
||||
}
|
||||
let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String
|
||||
let appGroupSuffix = appGroup.suffix(appGroup.count - 6)
|
||||
return "\(appIdentifierPrefix)\(appGroupSuffix)"
|
||||
}()
|
||||
|
||||
public static func storeCredentials(_ credentials: Credentials, server: String) throws {
|
||||
|
||||
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
||||
kSecAttrAccount as String: credentials.username,
|
||||
kSecAttrServer as String: server]
|
||||
|
||||
if credentials.type != .basic {
|
||||
query[kSecAttrSecurityDomain as String] = credentials.type.rawValue
|
||||
}
|
||||
|
||||
if let securityGroup = keychainGroup {
|
||||
query[kSecAttrAccessGroup as String] = securityGroup
|
||||
}
|
||||
|
||||
let secretData = credentials.secret.data(using: String.Encoding.utf8)!
|
||||
query[kSecValueData as String] = secretData
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return
|
||||
case errSecDuplicateItem:
|
||||
break
|
||||
default:
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
var deleteQuery = query
|
||||
deleteQuery.removeValue(forKey: kSecAttrAccessible as String)
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let addStatus = SecItemAdd(query as CFDictionary, nil)
|
||||
if addStatus != errSecSuccess {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static func retrieveCredentials(type: CredentialsType, server: String, username: String) throws -> Credentials? {
|
||||
|
||||
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true]
|
||||
|
||||
if type != .basic {
|
||||
query[kSecAttrSecurityDomain as String] = type.rawValue
|
||||
}
|
||||
|
||||
if let securityGroup = keychainGroup {
|
||||
query[kSecAttrAccessGroup as String] = securityGroup
|
||||
}
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
guard status != errSecItemNotFound else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard status == errSecSuccess else {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
guard let existingItem = item as? [String : Any],
|
||||
let secretData = existingItem[kSecValueData as String] as? Data,
|
||||
let secret = String(data: secretData, encoding: String.Encoding.utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Credentials(type: type, username: username, secret: secret)
|
||||
|
||||
}
|
||||
|
||||
public static func removeCredentials(type: CredentialsType, server: String, username: String) throws {
|
||||
|
||||
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: username,
|
||||
kSecAttrServer as String: server,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true]
|
||||
|
||||
if type != .basic {
|
||||
query[kSecAttrSecurityDomain as String] = type.rawValue
|
||||
}
|
||||
|
||||
if let securityGroup = keychainGroup {
|
||||
query[kSecAttrAccessGroup as String] = securityGroup
|
||||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
49
Modules/Secrets/Sources/Secrets/SecretKey.swift.gyb
Normal file
49
Modules/Secrets/Sources/Secrets/SecretKey.swift.gyb
Normal file
@@ -0,0 +1,49 @@
|
||||
// Generated by SecretKey.swift.gyb
|
||||
%{
|
||||
import os
|
||||
|
||||
secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
|
||||
def chunks(seq, size):
|
||||
return (seq[i:(i + size)] for i in range(0, len(seq), size))
|
||||
|
||||
def encode(string, salt):
|
||||
bytes_ = string.encode("UTF-8")
|
||||
return [bytes_[i] ^ salt[i % len(salt)] for i in range(0, len(bytes_))]
|
||||
|
||||
def snake_to_camel(snake_str):
|
||||
components = snake_str.split('_')
|
||||
components = [components[0].lower()] + [x.title() if x != 'ID' else x for x in components[1:]]
|
||||
camel_case_str = ''.join(components)
|
||||
return camel_case_str
|
||||
|
||||
salt = [byte for byte in os.urandom(64)]
|
||||
}%
|
||||
import Foundation
|
||||
|
||||
public struct SecretKey {
|
||||
% for secret in secrets:
|
||||
|
||||
public static let ${snake_to_camel(secret)}: String = {
|
||||
let encoded: [UInt8] = [
|
||||
% for chunk in chunks(encode(os.environ.get(secret) or "", salt), 8):
|
||||
${"".join(["0x%02x, " % byte for byte in chunk])}
|
||||
% end
|
||||
]
|
||||
|
||||
return decode(encoded)
|
||||
}()
|
||||
% end
|
||||
}
|
||||
|
||||
private let salt: [UInt8] = [
|
||||
% for chunk in chunks(salt, 8):
|
||||
${"".join(["0x%02x, " % byte for byte in chunk])}
|
||||
% end
|
||||
]
|
||||
|
||||
private func decode(_ encoded: [UInt8]) -> String {
|
||||
String(decoding: encoded.enumerated().map { (offset, element) in
|
||||
element ^ salt[offset % salt.count]
|
||||
}, as: UTF8.self)
|
||||
}
|
||||
Reference in New Issue
Block a user