Skip to content

Commit

Permalink
Merge pull request #209 from czechboy0/hd/keychain
Browse files Browse the repository at this point in the history
[WIP] Storing credentials in Keychain
  • Loading branch information
czechboy0 committed Jan 23, 2016
2 parents 70542e7 + 1def561 commit 4536208
Show file tree
Hide file tree
Showing 34 changed files with 775 additions and 227 deletions.
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

0 comments on commit 4536208

Please sign in to comment.