Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storing credentials in Keychain #209

Merged
merged 5 commits into from
Jan 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion BuildaKit/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public class Logging {

public class func setup(persistence: Persistence, alsoIntoFile: Bool) {

let path = persistence.fileURLWithName("Builda.log", intention: .Writing, isDirectory: false)
let path = persistence
.fileURLWithName("Logs", intention: .Writing, isDirectory: true)
.URLByAppendingPathComponent("Builda.log", isDirectory: false)

var loggers = [Logger]()

Expand Down
2 changes: 1 addition & 1 deletion BuildaKit/NetworkUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class NetworkUtils {

public class func checkAvailabilityOfGitHubWithCurrentSettingsOfProject(project: Project, completion: (success: Bool, error: ErrorType?) -> ()) {

let token = project.config.value.githubToken
let token = project.config.value.serverAuthentication!
//TODO: have project spit out Set<SourceServerOption>

let options: Set<SourceServerOption> = [.Token(token)]
Expand Down
12 changes: 11 additions & 1 deletion BuildaKit/Persistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ public class Persistence {
_ = try? self.fileManager.copyItemAtURL(url, toURL: writeUrl)
}

public func copyFileToFolder(fileName: String, folder: String) {

let url = self.fileURLWithName(fileName, intention: .Reading, isDirectory: false)
let writeUrl = self
.fileURLWithName(folder, intention: .Writing, isDirectory: true)
.URLByAppendingPathComponent(fileName, isDirectory: false)

_ = try? self.fileManager.copyItemAtURL(url, toURL: writeUrl)
}

public func createFolderIfNotExists(url: NSURL) {

let fm = self.fileManager
Expand All @@ -244,7 +254,7 @@ public class Persistence {
case WritingNoCreateFolder
}

private func folderForIntention(intention: PersistenceIntention) -> NSURL {
func folderForIntention(intention: PersistenceIntention) -> NSURL {
switch intention {
case .Reading:
return self.readingFolder
Expand Down
118 changes: 114 additions & 4 deletions BuildaKit/PersistenceMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import BuildaUtils
import XcodeServerSDK

public protocol MigratorType {
init(persistence: Persistence)
Expand Down Expand Up @@ -47,7 +48,8 @@ public class CompositeMigrator: MigratorType {
public required init(persistence: Persistence) {
self.childMigrators = [
Migrator_v0_v1(persistence: persistence),
Migrator_v1_v2(persistence: persistence)
Migrator_v1_v2(persistence: persistence),
Migrator_v2_v3(persistence: persistence)
]
}

Expand All @@ -56,7 +58,9 @@ public class CompositeMigrator: MigratorType {
}

public func attemptMigration() throws {
try self.childMigrators.forEach { try $0.attemptMigration() }
try self.childMigrators
.filter { $0.isMigrationRequired() }
.forEach { try $0.attemptMigration() }
}
}

Expand Down Expand Up @@ -107,8 +111,6 @@ class Migrator_v0_v1: MigratorType {

/*
- ServerConfigs.json: each server now has an id


- Config.json: persistence_version: 1 -> 2
*/
class Migrator_v1_v2: MigratorType {
Expand Down Expand Up @@ -271,3 +273,111 @@ class Migrator_v1_v2: MigratorType {
self.persistence.saveDictionary("Config.json", item: mutableConfig)
}
}

/*
- ServerConfigs.json: password moved to the keychain
- Projects.json: github_token -> server_authentication, ssh_passphrase moved to keychain
- move any .log files to a separate folder called 'Logs'
*/
class Migrator_v2_v3: MigratorType {

internal var persistence: Persistence
required init(persistence: Persistence) {
self.persistence = persistence
}

func isMigrationRequired() -> Bool {

return self.persistenceVersion() == 2
}

func attemptMigration() throws {

let pers = self.persistence

//migrate
self.migrateProjectAuthentication()
self.migrateServerAuthentication()
self.migrateLogs()

//copy the rest
pers.copyFileToWriteLocation("Syncers.json", isDirectory: false)
pers.copyFileToWriteLocation("BuildTemplates", isDirectory: true)
pers.copyFileToWriteLocation("Triggers", isDirectory: true)

let config = self.config()
let mutableConfig = config.mutableCopy() as! NSMutableDictionary
mutableConfig[kPersistenceVersion] = 3

//save the updated config
pers.saveDictionary("Config.json", item: mutableConfig)
}

func migrateProjectAuthentication() {

let pers = self.persistence
let projects = pers.loadArrayOfDictionariesFromFile("Projects.json") ?? []
let mutableProjects = projects.map { $0.mutableCopy() as! NSMutableDictionary }

let renamedAuth = mutableProjects.map {
(d: NSMutableDictionary) -> NSDictionary in

let id = d.stringForKey("id")
let token = d.stringForKey("github_token")
let passphrase = d.optionalStringForKey("ssh_passphrase")
d.removeObjectForKey("github_token")
d.removeObjectForKey("ssh_passphrase")

let tokenKeychain = SecurePersistence.sourceServerTokenKeychain()
tokenKeychain.writeIfNeeded(id, value: token)

let passphraseKeychain = SecurePersistence.sourceServerPassphraseKeychain()
passphraseKeychain.writeIfNeeded(id, value: passphrase)

precondition(tokenKeychain.read(id) == token, "Saved token must match")
precondition(passphraseKeychain.read(id) == passphrase, "Saved passphrase must match")

return d
}

pers.saveArray("Projects.json", items: renamedAuth)
}

func migrateServerAuthentication() {

let pers = self.persistence
let servers = pers.loadArrayOfDictionariesFromFile("ServerConfigs.json") ?? []
let mutableServers = servers.map { $0.mutableCopy() as! NSMutableDictionary }

let withoutPasswords = mutableServers.map {
(d: NSMutableDictionary) -> NSDictionary in

let password = d.stringForKey("password")
let key = (try! XcodeServerConfig(json: d)).keychainKey()

let keychain = SecurePersistence.xcodeServerPasswordKeychain()
keychain.writeIfNeeded(key, value: password)

d.removeObjectForKey("password")

precondition(keychain.read(key) == password, "Saved password must match")

return d
}

pers.saveArray("ServerConfigs.json", items: withoutPasswords)
}

func migrateLogs() {

let pers = self.persistence
(pers.filesInFolder(pers.folderForIntention(.Reading)) ?? [])
.map { $0.lastPathComponent ?? "" }
.filter { $0.hasSuffix("log") }
.forEach {
pers.copyFileToFolder($0, folder: "Logs")
pers.deleteFile($0)
}
}
}

13 changes: 4 additions & 9 deletions BuildaKit/ProjectConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ public struct ProjectConfig {

public let id: RefType
public var url: String
public var githubToken: String
public var privateSSHKeyPath: String
public var publicSSHKeyPath: String
public var sshPassphrase: String?

public var sshPassphrase: String? //loaded from the keychain
public var serverAuthentication: String? //loaded from the keychain

//creates a new default ProjectConfig
public init() {
self.id = Ref.new()
self.url = ""
self.githubToken = ""
self.serverAuthentication = ""
self.privateSSHKeyPath = ""
self.publicSSHKeyPath = ""
self.sshPassphrase = nil
Expand All @@ -36,10 +37,8 @@ public struct ProjectConfig {
private struct Keys {

static let URL = "url"
static let GitHubToken = "github_token"
static let PrivateSSHKeyPath = "ssh_private_key_url"
static let PublicSSHKeyPath = "ssh_public_key_url"
static let SSHPassphrase = "ssh_passphrase"
static let Id = "id"
}

Expand All @@ -50,22 +49,18 @@ extension ProjectConfig: JSONSerializable {
let json = NSMutableDictionary()

json[Keys.URL] = self.url
json[Keys.GitHubToken] = self.githubToken
json[Keys.PrivateSSHKeyPath] = self.privateSSHKeyPath
json[Keys.PublicSSHKeyPath] = self.publicSSHKeyPath
json[Keys.Id] = self.id
json.optionallyAddValueForKey(self.sshPassphrase, key: "ssh_passphrase")
return json
}

public init(json: NSDictionary) throws {

self.url = try json.get(Keys.URL)
self.githubToken = try json.get(Keys.GitHubToken)
self.privateSSHKeyPath = try json.get(Keys.PrivateSSHKeyPath)
self.publicSSHKeyPath = try json.get(Keys.PublicSSHKeyPath)
self.id = try json.get(Keys.Id)
self.sshPassphrase = try json.getOptionally(Keys.SSHPassphrase)
}
}

96 changes: 96 additions & 0 deletions BuildaKit/SecurePersistence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// SecurePersistence.swift
// Buildasaur
//
// Created by Honza Dvorsky on 1/22/16.
// Copyright © 2016 Honza Dvorsky. All rights reserved.
//

import Foundation
import KeychainAccess
import XcodeServerSDK
import SwiftSafe

final class SecurePersistence {

#if TESTING
typealias Keychain = NSMutableDictionary
#endif

static let Prefix = "com.honzadvorsky.buildasaur"

private let keychain: Keychain
private let safe: Safe

private init(keychain: Keychain, safe: Safe = EREW()) {
self.keychain = keychain
self.safe = safe
}

static func xcodeServerPasswordKeychain() -> SecurePersistence {
return self.keychain("\(Prefix).xcs.password")
}

static func sourceServerTokenKeychain() -> SecurePersistence {
return self.keychain("\(Prefix).source_server.oauth_tokens")
}

static func sourceServerPassphraseKeychain() -> SecurePersistence {
return self.keychain("\(Prefix).source_server.passphrase")
}

static private func keychain(service: String) -> SecurePersistence {
#if TESTING
let keychain = NSMutableDictionary()
#else
let keychain = Keychain(service: service)
#endif
return self.init(keychain: keychain)
}

func read(key: String) -> String? {
var val: String?
self.safe.read {
#if TESTING
val = self.keychain[key] as? String
#else
val = self.keychain[key]
#endif
}
return val
}

func writeIfNeeded(key: String, value: String?) {
self.safe.write {
self.updateIfNeeded(key, value: value)
}
}

private func updateIfNeeded(key: String, value: String?) {
#if TESTING
let existing = self.keychain[key] as? String
#else
let existing = self.keychain[key]
#endif
if existing != value {
self.keychain[key] = value
}
}
}

public protocol KeychainSaveable {
func keychainKey() -> String
}

extension XcodeServerConfig: KeychainSaveable {
public func keychainKey() -> String {
return "\(self.host):\(self.user ?? "")"
}
}

extension ProjectConfig: KeychainSaveable {
public func keychainKey() -> String {
return self.id
}
}

Loading