Skip to content

Commit

Permalink
xcreds login window
Browse files Browse the repository at this point in the history
  • Loading branch information
twocanoes committed Jul 16, 2022
1 parent 2e3114e commit 03e929f
Show file tree
Hide file tree
Showing 37 changed files with 2,125 additions and 1,236 deletions.
22 changes: 22 additions & 0 deletions Helper+URLDecode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Helper+URLDecode.swift
// XCreds
//
// Created by Timothy Perfitt on 7/13/22.
//

import Foundation

func base64UrlDecode(value: String) -> Data? {
var base64 = value.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8))
let requiredLenght = 4 * ceil(length/4)
let paddingLenght = requiredLenght - length
if paddingLenght > 0 {
let padding = "".padding(toLength: Int(paddingLenght), withPad: "=", startingAt: 0)
base64 += padding
}

return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
}
46 changes: 40 additions & 6 deletions XCreds/KeychainUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ class KeychainUtil {

func findPassword(_ name: String) throws -> String {

TCSLogWithMark("Finding Password")
myErr = SecKeychainFindGenericPassword(nil, UInt32(serviceName.count), serviceName, UInt32(name.count), name, &passLength, &passPtr, &myKeychainItem)

if myErr == OSStatus(errSecSuccess) {
let password = NSString(bytes: passPtr!, length: Int(passLength), encoding: String.Encoding.utf8.rawValue)
TCSLogWithMark("Password found")
return password as! String
} else {
TCSLogWithMark("Password not found")
throw KeychainError.noStoredPassword
}
}
Expand All @@ -68,16 +71,34 @@ class KeychainUtil {
return myErr
}

func updatePassword(_ name: String, pass: String) -> Bool {
func updatePassword(_ name: String, pass: String, shouldUpdateACL:Bool=false, keychainPassword:String?=nil ) -> Bool {
if (try? findPassword(name)) != nil {
deletePassword()
TCSLogWithMark("Deleting password")
let _ = deletePassword()
}
TCSLogWithMark("setting new password")

myErr = setPassword(name, pass: pass)
if myErr == OSStatus(errSecSuccess) {
return true
} else {
if myErr != OSStatus(errSecSuccess) {
TCSLogWithMark("setting new password FAILURE")
return false
}
TCSLogWithMark("setting new password success")

if shouldUpdateACL==true {
if let keychainPassword = keychainPassword {
TCSLogWithMark("Updating ACL for \(name)")

updateACL(password:keychainPassword)
}
else {
TCSLogWithMark("ERROR Updating ACL")

return false
}

}
return true
}

// delete the password from the keychain
Expand Down Expand Up @@ -131,6 +152,19 @@ class KeychainUtil {
secApps.append(trust!)
}
}
if FileManager.default.fileExists(atPath: "/System/Library/Frameworks/Security.framework/Versions/A/MachServices/authorizationhost.bundle/Contents/XPCServices/authorizationhosthelper.x86_64.xpc", isDirectory: nil) {
err = SecTrustedApplicationCreateFromPath("/System/Library/Frameworks/Security.framework/Versions/A/MachServices/authorizationhost.bundle/Contents/XPCServices/authorizationhosthelper.x86_64.xpc", &trust)
if err == 0 {
secApps.append(trust!)
}
}
if FileManager.default.fileExists(atPath: "/System/Library/Frameworks/Security.framework/Versions/A/MachServices/authorizationhost.bundle/Contents/XPCServices/authorizationhosthelper.arm64.xpc", isDirectory: nil) {
err = SecTrustedApplicationCreateFromPath("/System/Library/Frameworks/Security.framework/Versions/A/MachServices/authorizationhost.bundle/Contents/XPCServices/authorizationhosthelper.arm64.xpc", &trust)
if err == 0 {
secApps.append(trust!)
}
}

for acl in myACLs as! Array<SecACL> {
SecACLCopyContents(acl, &appList, &desc, &prompt)
let authArray = SecACLCopyAuthorizations(acl)
Expand Down Expand Up @@ -160,7 +194,7 @@ class KeychainUtil {
} catch {
TCSLogWithMark("No teamid in ACLAuthorizationPartitionID.")
}
let teamIds = [ "teamid:UXP6YEHSPW" ]
let teamIds = [ "apple:", "teamid:UXP6YEHSPW" ]

propertyListObject["Partitions"] = teamIds

Expand Down
12 changes: 8 additions & 4 deletions XCreds/MainController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MainController: NSObject {
let _ = localPassword()
NotificationCenter.default.addObserver(forName: Notification.Name("TCSTokensUpdated"), object: nil, queue: nil) { notification in
//now we set the password.

Mark()
DispatchQueue.main.async {
mainMenu.webView?.window?.close()

Expand All @@ -38,7 +38,11 @@ class MainController: NSObject {
return
}
if tokens.refreshToken.count>0 {
mainMenu.statusBarItem.button?.image=NSImage(named: "xcreds menu icon check")
Mark()
DispatchQueue.main.async {
mainMenu.statusBarItem.button?.image=NSImage(named: "xcreds menu icon check")
}

}
let localPassword = self.localPassword()
if (localPassword != tokens.password){
Expand Down Expand Up @@ -88,7 +92,7 @@ class MainController: NSObject {

}
}
if TokenManager.shared.saveTokensToKeychain(tokens: tokens) == false {
if TokenManager.shared.saveTokensToKeychain(tokens: tokens, setACL: true, password:tokens.password ) == false {
TCSLogWithMark("error saving tokens to keychain")
}
ScheduleManager.shared.startCredentialCheck()
Expand Down Expand Up @@ -126,7 +130,7 @@ class MainController: NSObject {
let isPasswordValid = PasswordUtils.verifyCurrentUserPassword(password:localPassword )
if isPasswordValid==true {
passwordWindowController.window?.close()
let err = keychainUtil.updatePassword(PrefKeys.password.rawValue, pass: localPassword)
let err = keychainUtil.updatePassword(PrefKeys.password.rawValue, pass: localPassword, shouldUpdateACL: true)
if err == false {
return nil
}
Expand Down
45 changes: 45 additions & 0 deletions XCreds/PasswordUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,51 @@ class PasswordUtils: NSObject {
}
return true
}
public class func doesUserHomeExist(_ name: String) throws -> Bool {
// first get the user record

os_log("Checking for existing home directory", log: noLoMechlog, type: .debug)
var records = [ODRecord]()
let odsession = ODSession.default()
do {
let node = try ODNode.init(session: odsession, type: ODNodeType(kODNodeTypeLocalNodes))
let query = try ODQuery.init(node: node, forRecordTypes: kODRecordTypeUsers, attribute: kODAttributeTypeRecordName, matchType: ODMatchType(kODMatchEqualTo), queryValues: name, returnAttributes: kODAttributeTypeAllAttributes, maximumResults: 0)
records = try query.resultsAllowingPartial(false) as! [ODRecord]
} catch {
let errorText = error.localizedDescription
os_log("ODError while trying to check for local user: %{public}@", log: noLoMechlog, type: .error, errorText)
return true
}

os_log("Record search returned", log: noLoMechlog, type: .info)

if records.isEmpty {
os_log("No user found to delete, success!", log: noLoMechlog, type: .debug)
return true
} else if records.count > 1 {
os_log("Multiple users found, failing local user removal", log: noLoMechlog, type: .info)
return false
}

if let homePaths = records.first?.value(forKey: kODAttributeTypeNFSHomeDirectory) as? [String] {

os_log("Home path found", log: noLoMechlog, type: .info)

let fm = FileManager.default

if let homePath = homePaths.first {
if fm.fileExists(atPath: homePath) {
os_log("Home is: %{public}@", log: noLoMechlog, type: .info, homePath)
return true

} else {
return false
}
}
}
return false
}


/// Checks a local username and password to see if they are valid.
///
Expand Down
160 changes: 159 additions & 1 deletion XCreds/PrefKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,163 @@
import Foundation

enum PrefKeys: String {
case clientID, clientSecret, password="local password",discoveryURL, redirectURI, scopes, accessToken, idToken, refreshToken, tokenEndpoint, expirationDate, invalidToken, refreshRateHours, showDebug, verifyPassword, shouldShowQuitMenu, shouldShowPreferencesOnStart, shouldSetGoogleAccessTypeToOffline, passwordChangeURL, shouldShowAboutMenu
case clientID, clientSecret, password="local password",discoveryURL, redirectURI, scopes, accessToken, idToken, refreshToken, tokenEndpoint, expirationDate, invalidToken, refreshRateHours, showDebug, verifyPassword, shouldShowQuitMenu, shouldShowPreferencesOnStart, shouldSetGoogleAccessTypeToOffline, passwordChangeURL, shouldShowAboutMenu, username
}
func getManagedPreference(key: Preferences) -> Any? {

return nil
}
enum Preferences: String {
/// The desired AD domain as a `String`.
case ADDomain
/// Allows appending of other domains at the loginwindow. Set as a `Bool` to allow any, or as an Array of Strings to whitelist
case AdditionalADDomains
/// list of domains to show in the domain pull down
case AdditionalADDomainList
/// add user's NT domain name as an alias to newly created accounts
case AliasNTName
/// add user's UPN as an alias to newly created accounts
case AliasUPN
/// Allow network select button on login window
case AllowNetworkSelection
/// Allow network text
case AllowNetworkText
/// A filesystem path to a background image as a `String`.
case BackgroundImage
/// An image to display as the background image as a Base64 encoded `String`.
case BackgroundImageData
/// The alpha value of the background image as an `Int`.
case BackgroundImageAlpha
/// Should new users be created as local administrators? Set as a `Bool`.
case CreateAdminUser
/// List of groups that should have its members created as local administrators. Set as an Array of Strings of the group name.
case CreateAdminIfGroupMember
/// Should existing mobile accounts be converted into plain local accounts? Set as a `Bool`.
case CustomNoMADLocation
/// If defined it specifies the custom location of the application to be given access to the keychain item. Set as a `String`
case DemobilizeUsers
/// Should we always have a password already set up before demobilizing
case DemobilizeForcePasswordCheck
/// Should we preserve the AltSecurityIdentities OD attribute during demobilization
case DemobilizeSaveAltSecurityIdentities
/// Dissallow local auth, and always do network authentication
case DenyLocal
/// Users to allow locally when DenyLocal is on
case DenyLocalExcluded
/// List of groups that should have it's members allowed to sign in. Set as an Array of Strings of the group name
case DenyLoginUnlessGroupMember
/// Defines which system inforation should be showed by default. Set as `String`.
case DefaultSystemInformation
/// Should FDE be enabled at first login on APFS disks? Set as a `Bool`.
case EnableFDE
/// Should the PRK be saved to disk for the MDM Escrow Service to collect? Set as a `Bool`.
case EnableFDERecoveryKey
// Specify a custom path for the recovery key
case EnableFDERecoveryKeyPath
// Should we rotate the PRK
case EnableFDERekey
/// Path for where the EULA acceptance info goes
case EULAPath
/// Text for EULA as a `String`.
case EULAText
/// Headline for EULA as a `String`.
case EULATitle
/// Subhead for EULA as a `String`.
case EULASubTitle
/// Allow for guest accounts
case GuestUser
/// the accounts to allow as an array of strings
case GuestUserAccounts
/// where to put the guest user password
case GuestUserAccountPasswordPath
/// First name for the guest user
case GuestUserFirst
/// Last name for the guest user
case GuestUserLast
/// Ignore sites in AD. This is a compatibility measure for AD installs that have issues with sites. Set as a `Bool`.
case IgnoreSites
/// Adds a NoMAD entry into the keychain. `Bool` value.
case KeychainAddNoMAD
/// Should NoLo create a Keychain if it doesn't exist. `Bool` value.
case KeychainCreate
/// Should NoLo reset the Keychain if the login pass doesn't match. `Bool` value.
case KeychainReset
/// Force LDAP lookups to use SSL connections. Requires certificate trust be established. Set as a `Bool`.
case LDAPOverSSL
/// Force specific LDAP servers instead of finding them via DNS
case LDAPServers
/// Fallback to local auth if the network is not available
case LocalFallback
/// A filesystem path to an image to display on the login screen as a `String`.
case LoginLogo
/// Alpha value for the login logo
case LoginLogoAlpha
/// A Base64 encoded string of an image to display on the login screen.
case LoginLogoData
/// Should NoLo display a macOS-style login screen instead of a window? Set as a `Bool`,
case LoginScreen
/// If the create User mech should manage the SecureTokens with a service account
case ManageSecureTokens
/// If Notify should add additional logging
case NotifyLogStyle
/// NT Domain to AD domain mappings
case NTtoADDomainMappings
/// should we migrate users?
case Migrate
/// should we hide users when we migrate?
case MigrateUsersHide
/// If the powercontrol options should be disabled in the SignIn UI
case PowerControlDisabled
/// should we recursively looku groups at login
case RecursiveGroupLookup
/// Path to script to run, currently only one script path can be used, if you want to run this multiple times, keep the logic in your script
case ScriptPath
/// Arguments for the script, if any
case ScriptArgs
/// Should NoMAD Login enable all users that login with with a secure token as a `Bool`
case SecureTokenManagementEnableOnlyAdminUsers
/// Path of the icon to be used for the Secure Token management user as `String`
case SecureTokenManagementIconPath
/// Should NoMAD Login only enable the first admin user that login with with a secure token as a `Bool`
case SecureTokenManagementOnlyEnableFirstUser
/// Full Name of the Secure Token Management user as a `String`
case SecureTokenManagementFullName
/// The UID to use for the Management Account as a `Int` or `String`
case SecureTokenManagementUID
/// The location to save and read the Secure Token management password as a `String`
case SecureTokenManagementPasswordLocation
/// Length fo the SecureToken Management User's password as an `Int`
case SecureTokenManagementPasswordLength
/// Username to use to for the securetoken management account as a `String`
case SecureTokenManagementUsername
/// Tool to use for UID numbers
case UIDTool
/// Use the CN from AD as the full name
case UseCNForFullName
/// A string to show as the placeholder in the Username textfield
case UseCNForFullNameFallback
/// Uses the CN as the fullname on the account when the givenName and sn fields are blank
case UsernameFieldPlaceholder
/// A filesystem path to an image to set the user profile image to as a `String`
case UserProfileImage

case NormalWindowLevel
//UserInput bits

case UserInputOutputPath
case UserInputUI
case UserInputLogo
case UserInputTitle
case UserInputMainText

//Messages

case MessagePasswordSync // what to show when the password needs to sync

//Password update keys

case PasswordOverwriteSilent // will silently update user password to new one
case PasswordOverwriteOptional // allow the user to stomp on the password if interested

}

0 comments on commit 03e929f

Please sign in to comment.