diff --git a/FWPM prebuilt binary/firmware_password_manager.zip b/FWPM prebuilt binary/firmware_password_manager.zip new file mode 100644 index 0000000..bc80d2e Binary files /dev/null and b/FWPM prebuilt binary/firmware_password_manager.zip differ diff --git a/example scripts/JSS EA for FWPM.py b/JSS integration/JSS EA for FWPM.py similarity index 100% rename from example scripts/JSS EA for FWPM.py rename to JSS integration/JSS EA for FWPM.py diff --git a/JSS integration/JSS FWPM controller script.py b/JSS integration/JSS FWPM controller script.py new file mode 100755 index 0000000..31bea64 --- /dev/null +++ b/JSS integration/JSS FWPM controller script.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +Script to remotely control and configure firmware password manager. +""" +# Copyright (c) 2020 University of Utah Student Computing Labs. ################ +# All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appears in all copies and +# that both that copyright notice and this permission notice appear +# in supporting documentation, and that the name of The University +# of Utah not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. This software is supplied as is without expressed or +# implied warranties of any kind. +################################################################################ + +# FWPM_control.py ############################################################## +# +# +# +# 0.8.0 2019.11.13 Complete rewrite. FWPM built with pyinstall and kept +# on client machines, Control script holds keylist and +# config file, writes files and launches FWPM. tjm +# +# 1.0.0 2020.01.23 Initial public release. tjm +# +################################################################################ + +from __future__ import division +from __future__ import print_function +import base64 +import logging +import inspect +import subprocess + +__version__ = '1.0' + +KEYFILE = { + 'previous': ["oneOldPassword", "AnotherPasswordWeUsed"], + 'new': "myNewFWPW!" +} + +FWPM_CONFIG = { + 'flags': { + 'use_fwpw': True, + 'management_string_type': 'hash', + 'custom_string': '', + 'use_reboot_on_exit': False, + 'path_to_fw_tool': '', + 'use_test_mode': False, + }, + + 'keyfile': { + 'path': '/tmp/current_fwpw.txt', + 'use_obfuscation': True, + }, + + 'logging': { + 'use_logging': True, + 'log_path': '/var/log/fwpm_controller.log', + }, + + 'slack': { + 'use_slack': True, + 'slack_identifier': 'hostname', + + 'slack_info_url': 'https://hooks.slack.com/services/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', + 'slack_error_url': 'https://hooks.slack.com/services/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', + } +} + + +def prepare_keyfile(logger, cleartext): + """ + Convert keyfile into format FWPM expects and obfuscate results. + """ + if FWPM_CONFIG['logging']['use_logging']: + logger.info("%s: activated" % inspect.stack()[0][3]) + + obfuscated_string = "" + sanity_check_new = False + sanity_check_previous = False + + # sanity check cleartext! + logger.info("sanity check new.") + if not isinstance(cleartext['new'], str): + logger.critical("New password improperly defined.") + sanity_check_new = False + else: + sanity_check_new = True + + logger.info("sanity check previous.") + if len(cleartext['previous']) <= 1: + logger.critical("No previous password defined.") + sanity_check_previous = False + else: + sanity_check_previous = True + + if not sanity_check_new or not sanity_check_previous: + logger.critical("sanity check failure.") + return None + else: + logger.info("Sanity check successful.") + + encoded_comment = base64.b64encode('old'.encode('utf-8')) + + for item in cleartext['previous']: + tmp_item = base64.b64encode(item.encode('utf-8')) + tmp_string = '#'.encode('utf-8') + encoded_comment + ':'.encode('utf-8') + tmp_item + ','.encode('utf-8') + obfuscated_string = obfuscated_string + tmp_string.decode('utf-8') + + tmp_item = base64.b64encode(cleartext['new'].encode('utf-8')) + tmp_string = base64.b64encode('new'.encode('utf-8')) + ':'.encode('utf-8') + tmp_item + obfuscated_string += str(tmp_string.decode('utf-8')) + + return base64.b64encode(obfuscated_string.encode('utf-8')) + + +def main(): + """ + This should not be blank. + """ + + if FWPM_CONFIG['logging']['use_logging']: + logging.basicConfig(filename=FWPM_CONFIG['logging']['log_path'], level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + logger = logging.getLogger(__name__) + + logger.info("fwpm controller launched.") + logger.info("fwpm controller version {}".format(__version__)) + + configuraton_file_path = '/tmp/cfg.cfg' + + obfuscated_text = prepare_keyfile(logger, KEYFILE) + + if obfuscated_text is None: + logger.critical("Exiting on sanity check failure.") + quit() + + with open(FWPM_CONFIG['keyfile']['path'], 'w') as output_file: + output_file.write((obfuscated_text.decode('utf-8'))) + + with open(configuraton_file_path, 'w') as writer: + for k in FWPM_CONFIG: + writer.write("[" + k + "]" + "\n") + for k2 in FWPM_CONFIG[k]: + writer.write(k2 + ": " + str(FWPM_CONFIG[k][k2]) + "\n") + + # launch fwpm + logger.info("launching fwpm.") + + try: + _ = subprocess.check_output(["/usr/local/sbin/firmware_password_manager", "-c", configuraton_file_path]) + except Exception as exception_message: + print(exception_message) + logger.critical(exception_message) + + + +if __name__ == '__main__': + main() diff --git a/README.md b/README.md index 7e9df8f..3b7ac61 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Firmware Password Manager 2.1.5 + + +# Firmware Password Manager 2.5 A Python script to help Macintosh administrators manage the firmware passwords of their computers. @@ -15,18 +17,35 @@ A Python script to help Macintosh administrators manage the firmware passwords o * [How FWPM keeps track of the current password](#how-fwpm-keeps-track-of-the-current-password) * [Usage](#usage) * [Options](#options) + * [The configuration file](#) + * [Flags](#flags) + * [Keyfile](#keyfile) + * [Logging](#logging) + * [Slack](#slack) * [The keyfile](#the-keyfile) * [Security](#security) - * [Example](#example) * [Slack integration](#slack-integration) - * [nvram](#nvram) + * [nvram string](#nvram) * [firmwarepasswd](#firmwarepasswd) - * [Error messages](#common-error-messages) - * [Example scripts](#example) - * [JAMF JSS extention attribute](#jamf-jss-extention-attribute) - * [Running Firmware Password Manager remotely](#remote) - * [New features in version 2.1](#new) -* [Notes](#notes) + * [Common error messages](#common-error-messages) + * [JAMF Integration](#jamf-integration) + * [JAMF FWPM installation policy](#jamf-fwpm-installation-script) + * [JAMF FWPM controller script](#jamf-fwpm-controller-script) + * [Editing the keylist variables](#editing-the-keylist-variables) + * [Editing the configuration file variables](#editing-the-configuration-file-variables) + * [JAMF JSS extention attribute](#jamf-jss-extention-attribute) + * [Skeleton Key](#skeleton-key) + * [Using Skeleton Key](#using-skeleton-key) + * [Current State](#current-state) + * [Location](#location) + * [Retrieve from JSS Script](#retrieve-from-jss-script) + * [Fetch from Remote Volume](#fetch-from-remote-volume) + * [Retrieve from Local Volume](#retrieve-from-local-volume) + * [Enter Firmware Password](#enter-firmware-password) + * [Keyfile Hash](#keyfile-hash) + * [Status Messages](#status-messages) + * [Rebuilding the binary](#rebuilding-the-binary) + * [Notes](#notes) * [Update History](#update-history) ## Download @@ -39,17 +58,17 @@ If you have any comments, questions, or other input, either [file an issue](../. ## System Requirements -* Python 2.7+ (which you can download [here](https://www.python.org/download/)) +* Python 3.7+ (which you can download [here](https://www.python.org/download/)) * Pexpect 3.3+ (which you can download [here](https://github.com/pexpect/pexpect)) -* [Management Tools](https://github.com/univ-of-utah-marriott-library-apple/management_tools) - version 1.6.0 or greater +* Pyinstaller (which you can download [here](https://www.pyinstaller.org/)) ## Install -Place the script in a root-executable location. We use `/usr/local/sbin/`. Make sure it is executable. +Place the script (or binary) in a root-executable location. We use `/usr/local/sbin/`. Make sure it is executable. ## Uninstall -Remove the script. +Remove the script/binary/application. ## Purpose @@ -58,7 +77,7 @@ In a nutshell, the firmware password in Apple computers prevents non-privileged The firmware password is one of three interlocking methods used to secure Apple computers. The other two are: using strong passwords on user accounts and FileVault to apply full disk encryption (FDE). Strong account passwords are always the first line of defense. FDE effectively scrambles the information written a storage device and renders it unreadable by unauthorized persons. Using all three methods can make a computer unusable should it be lost or stolen. -Depending on the model of computer, removing the firmware password can be easy or incredibly difficult, please refer to the [Notes](#notes) section for more information about removing the password. +Depending on the age of computer, removing the firmware password can be easy or incredibly difficult, please refer to the [Notes](#notes) section for more information about removing the password. ### Firmware Password Manager When I began this project there wasn't a solution available for actively managing firmware passwords, other than the "set-it-and-forget-it" method. This approach seems error-prone and difficult to maintain beyond more than a small handful of machines. My solution centers on maintaining a single list of current and formerly used passwords that I call the keyfile. This approach allows the administrator to easily bring any number of machines up to the current password, and identify those whose firmware passwords aren't in the master list and need additional maintenance. @@ -78,7 +97,7 @@ A hash can be thought of as the finger print of a file. The goal of a hash funct ## Usage ``` -firmware_password_manager.py [-h -v] [-# -r -m -n] [-k /path/to/keyfile -o] [-s -i] [-t] +firmware_password_manager.py [-h -v] [-c /path/to/configfile] ``` @@ -89,34 +108,99 @@ firmware_password_manager.py [-h -v] [-# -r -m -n] [-k /path/to/keyfile -o] [-s --------|--------- `-h`, `--help` | Prints help information and quits. `-v`, `--version` | Prints version information and quits. -`-r`, `--remove` | Remove the current firmware password. -`-m`, `--management` | Define management string in nvram. -`-#`, `--hash` | Record hash of keyfile in nvram entry. FWPW 1.0 behavior. Default choice. -`-n`, `--nostring` | Do not set management string. -`-k`, `--keyfile` | Provide the path to the keyfile. -`-t`, `--testmode` | More verbose logging, will not delete keyfile. -`-s`, `--slack` | Sends important messages to Slack. -`-o`, `--obfuscate` | Accepts a plist containing the obfuscated keyfile. -`-b`, `--reboot` | Reboots the computer after the script completes successfully. -`-i`, `--identifier` | Set slack identifier. `IP`, `MAC`, `computername`, `hostname`, `serial` - -Certain flags are mutually exclusive. The script will refuse to run if more than one of the following flags are used: `-remove` `-management` `-hash` `-nostring` +`-c CONFIGFILE`, `--configfile CONFIGFILE` | Specifies the location of the required configuration file. + +### The configuration file + +Here is the default configuration file included with FWPM: + +``` +[flags] +use_firmwarepassword: yes +management_string_type: hash +custom_string: +use_reboot_on_exit: + +[keyfile] +path: /YourVolume/this/location/example_keyfile.txt +use_obfuscation: No +remote_type: smb +server_path: server_path +username: user_name +password: pass_word + +[logging] +use_logging: true +log_path: /tmp + +[slack] +use_slack: true +slack_identifier: hostname + +slack_info_url: https://hooks.slack.com/services/aaa/bbb/ccc +slack_info_channel: #fwpw_manager_info +slack_info_bot_name: FWPM informational message + +slack_error_url: https://hooks.slack.com/services/xxx/yyy/zzz +slack_error_channel: #fwpw_manager_errors +slack_error_bot_name: FWPM error message +``` + +The configuration file is broken up into sections roughly approximating the command line flags used in the previous versions. + + + +#### Flags + +Name | Type|Purpose +--------|---------|--------- +use_firmwarepassword|Boolean|Turn FW password on or off. +management_string_type|String|Select the type of management string to use. If blank, keyfile has will be used. +custom_string|String|If you elect to use a custom string, enter it here. +use_reboot_on_exit|Boolean|If you want FWPM to force an immediate reboot on success. + + +#### Keyfile +Name | Type|Purpose +--------|---------|--------- +path| String |Path of the keyfile to be used. +use_obfuscation| Boolean |Use keyfile obfuscation. +remote_type| String |The remote server address to be used. +server_path| String |The path of the keyfile on the server. +username| String |User with adequate privileges on the server to access the keyfile. +password| String |Password for the user. + +#### Logging +Name | Type|Purpose +--------|---------|--------- +use_logging|Boolean|Create a log file for FWPM. +log_path|String|Path of the log file. + +#### Slack +Name | Type|Purpose +--------|---------|--------- +use_slack|Boolean|Use Slack messaging. +slack_identifier|String|Method of identifying the message sender on Slack. +|| +slack_info_url|String|Slack URL for informational messages. +|| +slack_error_url|String|Slack URL for error messages. If not defined messages will be directed to the info channel. + + ### The keyfile The script works with a text document I call the keyfile. It contains the new password, as well as any previously used passwords. Having previously used passwords available allows the script to update machines that may have been missed during previous runs of the script. -The script requires a specific format for the keyfile. Each lines contains the following: a note string, a colon, and a password string. No additional colons. I assume the newest passwords will be at the end of the file, and the script will try those first. Only the `new` note has a special meaning, others are ignored. +The script requires a specific format for the keyfile. Each line contains the following: a note string, a colon, and a password string. I assume the newest passwords will be at the end of the file, and the script will try those first. **Only the `new` note has a special meaning, others are ignored.** Here is the keyfile format: Notes | Purpose --------|--------- new|the new password to be installed. -note|any other note strings are ignored. -#new|a hash mark will cause the note to be ignored. - - +note|any other note strings cause the password to be treated as previously used. +#new|a hash mark will cause the password to be treated as a previously used entry. Here's an example keyfile: ``` @@ -131,24 +215,15 @@ Version 1 made use of the `current` note to designate what was thought to be cur The keyfile contains, of course, incredibly sensitive information. When the script successfully completes or encounters an error, it attempts to securely delete the keyfile from the disk. -### Example - -``` -firmware_password_manager.py -s -k path/to/keyfile -# -``` -This command will place a hash in nvram, slack informational messages and use the keyfile at `path/to/keyfile`. - ### Slack integration -We make heavy use of Slack in our office. The `-s` `--slack` flag directs FWPM to send informational messages to a slack team. I have integrated the slack functionality of our management_tools library into FWPM. You simply need to add the URL and channel information for your Slack group to the script. - -Please see Slack's documentation for additional configuration options: https://api.slack.com/incoming-webhooks +We make heavy use of Slack in our office. The `use_slack` option directs FWPM to send informational messages to a slack channel. You simply need to add the URL and channel information for your Slack group to the script. Please see Slack's documentation for additional configuration options: https://api.slack.com/incoming-webhooks -This image shows example messages in Slack: +This image shows example FWPM messages in Slack: ![ScreenShot](img/slack_example.png) -Version 2.1.1 adds additional configuration options for Slack. The `-i` `--identifer` flag allows you to select how machines are identified in Slack messages. This feature request was issue #2. These flags are mutually exclusive. +The `slack_identifier` option allows you to select how machines are identified in Slack messages. This feature request was issue #2. These flags are mutually exclusive. String|Purpose -------|----------- @@ -160,13 +235,13 @@ serial|The machines serial number is used. If an error occurs discovering the pr ### nvram string -To make the most of FWPM, we suggest using the `-#``--hash` flag to store the hash of the keyfile used to create the current firmware password. This allows you to use a variety of tools to remotely check the status of the firmware password on a machine. Using this flag the script will create an SHA-2 hash of the new keyfile and store it in non-volitile RAM (nvram) when the password is changed. The hash can then be accessed locally through the terminal or remotely with SSH, ARD or other tool. +To make the most of FWPM, we suggest using the `management_string_type` flag to store the hash of the keyfile used to create the current firmware password. This allows you to use a variety of tools to remotely check the status of the firmware password on a machine. Using this flag the script will create an SHA-2 hash of the new keyfile and store it in non-volitile RAM (nvram) when the password is changed. The hash can then be accessed locally through the terminal or remotely with SSH, ARD or other tool. -The `-m``--management` flag allows you to define any string to place in nvram. You could record the date the password was changed last or a cryptic hint to help you remember the password in the future (not recommended). +The `custom_string` flag allows you to define any string to place in nvram. You could record the date the password was changed last or a cryptic hint to help you remember the password in the future (not recommended). ### firmwarepasswd -Version 2 of FWPM uses Apple's `firmwarepasswd` tool to make changes to the firmware password. `firmwarepasswd` was shipped beginning with Mac OS X 10.10 "Yosemite". If you need to manage firmware passwords on OS X prior to 10.10, consider using the previous version of Firmware Password Manager. +Version 2+ of FWPM uses Apple's `firmwarepasswd` tool to make changes to the firmware password. `firmwarepasswd` was shipped beginning with Mac OS X 10.10 "Yosemite". If you need to manage firmware passwords on OS X prior to 10.10, consider using the previous version of Firmware Password Manager. ### Common error messages @@ -184,31 +259,61 @@ Current FW password not in keyfile.| This is an critical message that the keyfil nvram reported error.|This is a catchall error stating that nvram encountered an error. An error occured. Failed to modify firmware password.|This means one of the above errors likely occured. Keep reading the log to find the exact error. -## New Features in version 2.1 +## JAMF Integration -Version 2.1 adds an important new security feature. I've included a new tool: `obfuscate_keylist.py`. +FWPM was originally written to work with our unique management system. During the python rewrite, I made an effort to make FWPM independent of any specific administration philosophy and making it easier to integrate into future management solutions. I've included sample scripts for integrating FWPM into JAMF Casper, UNIX and ARD. The source is included in the example scripts folder. -``` -obfuscate_keylist.py -s original_keylist.txt -d obfuscated_keylist.plist -``` -Flag|Purpose --------|----------- --s, --source|path to source keyfile --d, --desination|path to save obfuscated keyfile --t, --testmode|verbose output --v, --version|show program's version number and exit --h, --help|show help message and exit +### JAMF FWPM installation policy -This tool takes your original keyfile and outputs a base64-encoded property list. It's important to note that obfuscation is not technically encryption. The flag for FWPM to use this form of plist is `-o, --obfuscate` +You will need a policy to ensure that the FWPM binary is installed on each of your client machines. -I've also added a reboot flag `-b, --reboot`. If this flag is selected and the script finishes successfully, it will force a reboot. +### JAMF FWPM controller script -Also new to this release is a sample script (`remotely_set_firmpwarepassword_scp_keylist.py`) showing how to remotely execute FWPM and securely copy (`scp`) your keyfile over the network. A shell script version is included for use in Apple's Remote Desktop. +The controller script directs the automated configuration and launch of FWPM. It contains the new and old firmware passwords, the logic to error check and create an obfuscated keyfile and configuration file, and launches FWPM. -## Example scripts +#### Editing the keylist variables -FWPM was originally written to work with our unique management system. During the python rewrite, I made an effort to make FWPM independent of any specific administration philosophy and making it easier to integrate into future management solutions. I've included sample scripts for integrating FWPM into JAMF Casper, UNIX and ARD. The source is included in the example scripts folder. +```python +KEYFILE = { + 'previous': ["oneOldPassword", "AnotherPasswordWeUsed"], + 'new': "myNewFWPW!" +} +``` +#### Editing the configuration file variables + +```python +FWPM_CONFIG = { + 'flags': { + 'use_fwpw': True, + 'management_string_type': 'hash', + 'custom_string': '', + 'use_reboot_on_exit': False, + 'path_to_fw_tool': '', + 'use_test_mode': False, + }, + + 'keyfile': { + 'path': '/tmp/current_fwpw.txt', + 'use_obfuscation': True, + }, + + 'logging': { + 'use_logging': True, + 'log_path': '/var/log/fwpm_controller.log', + }, + + 'slack': { + 'use_slack': True, + 'slack_identifier': 'hostname', + + 'slack_info_url': 'https://hooks.slack.com/services/aaaa/bbbbb/cccc', + 'slack_error_url': 'https://hooks.slack.com/services/dddd/eeee/ffff', + } +} +``` + +Please see the previous discussion of the configuration file for info on these values. ### JAMF JSS extention attribute @@ -228,15 +333,82 @@ Here is how the smart groups are built: ![ScreenShot](img/jss_not_current.png) -### Running Firmware Password Manager remotely +## Skeleton Key + +Skeleton Key was written to add a GUI to the firmwarepasswd command and Firmware Password Manager and give it mulitple ways to obtain the keylist file. Skeleton Key is designed to be used by technicians with limited experience or access. + +![sk_ui](img/sk_ui.png) -The awkwardly titled remotely_set_firmpwarepassword_scp_keylist.py script uses OpenSSH's Secure Copy command to copy the keyfile and execute FWPM. +### Using Skeleton Key -The equally awkward ard_wrapped_remotely_set_firmpwarepassword_scp_keylist.sh script is written to use in an Apple Remote Desktop Send UNIX command. +Upon launching Skeleton Key, you must enter a valid administrator password to proceed. -These examples are designed to be customized with the server address, account name and password to allow scp to function. Care will be required to prevent unauthorized users from examining the contents. +sk_login + +You may also see the following message, press OK to continue. + +sk_os_alert + +This is the main interface for Skeleton Key. It can be broken down into three areas: Current State, Modifiers and Status messages. + +sk_os_alert + + + +#### Current State + +The Current State area will tell you if there is a Firmware password in place, and if the keyfile you have given it was read correctly. + +no_keys + + + +yes_keys + +#### Location + +The Location area is where you will specify the location of the keyfile you wish to use. You can copy it directly from the JSS FWPM controller script, from a remote file share, from a local disk or entered directly into the application. + +##### Retrieve from JSS Script + +This option allows you to specify the JAMF server that contains the FWPM Controller script for your environment. Skelton Key will parse the script and find the keylist automatically. + +##### jamf_fetch_trimmed + +##### Fetch from Remote Volume + +This option allows you to specify a remote SMB or AFS file share where your keylist is located. Enter the complete URL, username and password. + +##### remote_fetch_trimmed + +##### Retrieve from Local Volume + +A standard MacOS X Open File dialog will appear allowing you to navigate to your keyfile. + +##### Enter Firmware Password + +This option allows you to directly enter the current or new firmware password of the machine. + +direct_entry_trimmed + +#### Keyfile hash + +When a keyfile is read successfully into memory, the hash will appear here. The hash is used in the JAMF Smart Group, to tell which machines are up to date and which need to be updated. + +hashed_keys + +#### Status Messages + +Any informational or error messages will appear in this location. + + + +### Rebuilding the binary + +I use pyinstaller to build the binary. As long as you maintain the file structure, you should be able to rebuild the binary with the following command: `pyinstaller --onefile skeleton_key.py` ## Notes + If you have forgotten the firmware password for a machine your available options depend upon the age of the machine. Only Apple Retail Stores or Apple Authorized Service Providers can unlock the following Mac models when protected by a firmware password: @@ -266,6 +438,7 @@ Thank you to macmule for ]] [[-m -p ] -o ] +# +# -c Check whether password is enabled. +# Sets return status of 0 if set, 1 otherwise. +# -d Delete current password/mode. +# Requires current password on some machines. +# -p Set password. +# Requires current password on some machines. +# -m Set security mode. +# Requires current password on some machines. +# Mode can be either "full" or "command". +# Full mode requires entry of the password on +# every boot, command mode only requires entry +# of the password if the boot picker is invoked +# to select a different boot device. +# +# When enabling the Firmware Password for the first +# time, both the password and mode must be provided. +# Once the firmware password has been enabled, providing +# the mode or password alone will change that parameter +# only. +# +# -o Old password. +# Only required on certain machines to disable +# or change password or mode. Optional, if not +# provided the tool will prompt for the password. +# ################################################################################ # # imports -# from sys import exit +from argparse import RawTextHelpFormatter import argparse +import base64 +import configparser +import hashlib +import inspect +import json +import logging import os +import platform +import plistlib import re import socket import subprocess -import plistlib -import base64 +import sys + import pexpect -from management_tools import loggers -from management_tools.slack import IncomingWebhooksSender as IWS +import requests -# -# functions -def secure_delete_keyfile(logger, args, error_bot, local_identifier): +class FWPM_Object(object): """ - attempts to securely delete the keyfile with medium overwrite and zeroing settings + This should not be blank. """ + def __init__(self, args, logger, master_version): + """ + This should not be blank. + """ + self.args = args + self.logger = logger + self.master_version = master_version + + self.srp_path = None + self.fwpwd_path = None + self.config_options = {} + self.local_identifier = None + self.passwords_raw = None + self.fwpw_managed_string = None + self.new_password = None + self.other_password_list = [] + self.current_fwpw_state = False + self.current_fwpm_hash = None + + self.clean_exit = False + self.read_config = False + self.read_keyfile = False + self.modify_fwpw = False + self.modify_nvram = False + self.matching_hashes = False + self.matching_passwords = False + + self.configuration_path = None + + self.system_version = platform.mac_ver()[0].split(".") + + self.srp_check() + self.fwpwd_check() + + if self.fwpwd_path: + self.current_fwpw_state = self.fwpwd_current_state() + elif self.srp_path: + self.current_fwpw_state = self.srp_current_state() + + self.injest_config() + if self.config_options["slack"]["use_slack"]: + self.slack_optionator() + + self.injest_keyfile() + + self.hash_current_state() + self.hash_incoming() - logger.info("Deleting keyfile") - if args.testmode: - logger.info("Test mode, keyfile not deleted.") - return - try: - deleted_keyfile = subprocess.call(["/bin/rm", "-Pf", args.keyfile]) - logger.info("keyfile deleted successfuly.") - except: - logger.critical("Issue with attempt to remove keyfile.") + # + # What if the string isn't a hash?!? + if (self.current_fwpm_hash == self.fwpw_managed_string) and self.config_options["flags"]["management_string_type"] == 'hash': + self.matching_hashes = True + self.master_control() -# is this really needed? - if os.path.exists(args.keyfile): - logger.critical("Failure to remove keyfile.") - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "An error occured. Failed to remove keyfile.") - else: - logger.info("Keyfile removed.") - return + def master_control(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + if self.current_fwpm_hash == self.fwpw_managed_string: + if self.logger: + self.logger.info("Hashes match. No change required.") -def main(): - """ - Master Control Function. - """ + else: + if self.logger: + self.logger.info("Hashes DO NOT match. Change required.") - # - # require root to run. - if os.geteuid(): - print "Must be root to run script." - exit(2) + if self.fwpwd_path: + self.fwpwd_change() + self.secure_delete() - # - # parse option definitions - parser = argparse.ArgumentParser( - description='Manages the firmware password on Apple Computers.') - # - # required, mutually exclusive commands - prime_group = parser.add_argument_group('Required management settings', - 'Choosing one of these options is required \ - to run FWPM. They tell FWPM how you \ - want to manage the firmware password.') - subprime = prime_group.add_mutually_exclusive_group(required=True) - subprime.add_argument('-r', '--remove', action="store_true", - default=False, help='Remove the firmware password') - subprime.add_argument('-m', '--management', default=None, - help='Set a custom nvram management string') - subprime.add_argument('-#', '--hash', action="store_true", default=False, - help='Set nvram string to hash of keyfile') - subprime.add_argument('-n', '--nostring', action="store_true", default=False, - help='Do not set an nvram management string') - - keyfile_group = parser.add_argument_group('Keyfile options', 'The keyfile is \ - required to use FWPM. These options \ - allow you to set the location and \ - format of the keyfile.') - keyfile_group.add_argument('-k', '--keyfile', help='Set the path to your keyfile', - required=True) - keyfile_group.add_argument('-o', '--obfuscated', action="store_true", default=False, - help='Tell FWPM your keylist is an obfuscated plist.') - - slack_group = parser.add_argument_group('Slack integration', - 'FWPM allows you to send informational \ - and error messages to your Slack team. \ - Additionally you can select different \ - methods of identifiying clients.') - slack_group.add_argument('-s', '--slack', action="store_true", - default=False, help='Send important messages to Slack.') - slack_group.add_argument('-i', '--identifier', default=None, - choices=['IP', 'hostname', 'MAC', 'computername', 'serial'], - required=False, help='Set slack identifier.') - - parser.add_argument('-b', '--reboot', action="store_true", default=False, - help='Reboots the computer after the script completes successfully.') - parser.add_argument('-t', '--testmode', action="store_true", default=False, - help='Test mode. Verbose logging, will not delete keyfile.') - parser.add_argument('-v', '--version', action='version', version='%(prog)s 2.1.5') + elif self.srp_path: + self.srp_change() + self.secure_delete() - args = parser.parse_args() + else: + print("No FW tool found.") + quit() - if args.testmode: - print args + # + # nvram maintenance + # + self.nvram_manager() - # - # Open log file - logger = loggers.file_logger(name='FWPW_Manager2') - logger.info("Running Firmware Password Manager 2") + # + # some kind of post action reporting. + # handle reboot flag here? + # + self.exit_manager() - # - # set up slack channel(s) - slack_info_url = 'your FWPM slack info URL' - slack_info_channel = '#your FWPM slack info channel' - info_bot = IWS(slack_info_url, bot_name="FWPM informational message", channel=slack_info_channel) - - slack_error_url = 'your FWPM slack error URL' - slack_error_channel = '#your FWPM slack error channel' - error_bot = IWS(slack_error_url, bot_name="FWPM error message", channel=slack_error_channel) - - local_identifier = None - - if args.slack: - full_ioreg = subprocess.check_output(['ioreg', '-l']) - serial_number_raw = re.findall(r'\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) - serial_number = serial_number_raw[0] - if args.testmode: - print "Serial number: %r" % serial_number - - if args.identifier == 'IP' or args.identifier == 'MAC' or args.identifier == 'hostname': - processed_device_list = [] - - # Get ordered list of network devices - base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]) - network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) - for device in network_device_list: - device_name = device[0] - port_name = device[1] - try: - device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]) - mac_address = re.findall(r'ether (.*) \n', device_info_raw) - if args.testmode: - print "%r" % mac_address - ether_address = re.findall(r'inet (.*) netmask', device_info_raw) - if args.testmode: - print "%r" % ether_address - processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) - except: - pass + def hash_current_state(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) - if len(processed_device_list) > 0: - logger.info("1 or more active IP addresses. Choosing primary.") - if args.testmode: - print processed_device_list - if args.identifier == 'IP': - local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + existing_keyfile_hash = None + if self.logger: + self.logger.info("Checking existing hash.") - if args.identifier == 'MAC': - local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + try: + existing_keyfile_hash_raw = subprocess.check_output(["/usr/sbin/nvram", "-p"]).decode('utf-8') + existing_keyfile_hash_raw = existing_keyfile_hash_raw.split('\n') + for item in existing_keyfile_hash_raw: + if "fwpw-hash" in item: + existing_keyfile_hash = item + else: + self.current_fwpm_hash = None - if args.identifier == 'hostname': - try: - local_identifier = socket.getfqdn() - except: - logger.error("error discovering hostname info.") - local_identifier = serial_number + self.current_fwpm_hash = existing_keyfile_hash.split("\t")[1] - elif len(processed_device_list) == 0: - logger.error("error discovering IP info.") - local_identifier = serial_number + if self.args.testmode: + print("Existing hash: %s" % self.current_fwpm_hash) - elif args.identifier == 'computername': - try: - cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) - local_identifier = cname_identifier_raw.split('\n')[0] - logger.info("Computername: " + local_identifier) - except: - logger.info("error discovering computername.") - local_identifier = serial_number - elif args.identifier == 'serial': - local_identifier = serial_number - logger.info("Serial number: " + local_identifier) - else: - logger.info("bad or no identifier flag, defaulting to serial number.") - local_identifier = serial_number + except: + pass - if args.testmode: - print "Local identifier: %r" % local_identifier + def hash_incoming(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) - # - # keyfile checks - path_to_keyfile_exists = os.path.exists(args.keyfile) + if self.logger: + self.logger.info("Checking incoming hash.") + + if self.config_options["flags"]["management_string_type"] == "custom": + # + # ?!?!?!?!?!?!? + # + self.fwpw_managed_string = self.config_options["flags"]["management_string_type"] - if not path_to_keyfile_exists: - logger.critical(args.keyfile + " does not exist. Exiting.") - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry_sign:\n" + "Keyfile does not exist.") - exit(2) + elif self.config_options["flags"]["management_string_type"] == "hash": - # - # generate hash from incoming keyfile - logger.info("Checking incoming hash.") - if args.management: - fwpw_managed_string = args.management - elif args.hash: - incoming_hash = subprocess.check_output(["/usr/bin/openssl", "dgst", "-sha256", args.keyfile]) - incoming_hash = incoming_hash.rstrip('\r\n') - fwpw_managed_string = incoming_hash.split(" ")[1] - # prepend '2:' to denote hash created with v2 of script, will force a password change from v1 - fwpw_managed_string = '2:' + fwpw_managed_string - else: - fwpw_managed_string = None + hashed_key = hashlib.new('sha256') + # hashed_key.update(self.passwords_raw.encode('utf-8')) - if args.testmode: - print "Incoming hash: %s" % fwpw_managed_string + hashed_key.update(self.new_password.encode('utf-8')) - # - # compare incoming hash with current nvram hash - existing_keyfile_hash = None - logger.info("Checking existing hash.") - if not args.remove: - try: - existing_keyfile_hash = subprocess.check_output(["/usr/sbin/nvram", "fwpw-hash"]) - existing_keyfile_hash = existing_keyfile_hash.rstrip('\r\n') - existing_keyfile_hash = existing_keyfile_hash.split("\t")[1] - - if args.testmode: - print "Existing hash: %s" % existing_keyfile_hash - - except Exception as this_exception: - logger.warning("nvram failed with " + str(this_exception) + " (fwpw probably not set).") - - if existing_keyfile_hash == fwpw_managed_string: - if args.hash: - logger.info("Hashes match. Exiting.") - if args.slack: - info_bot.send_message("_*" + local_identifier + "*_ :white_check_mark:\n" + "Hashes match. No Change.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.reboot: - logger.info("Reboot not required, canceling.") - # return a value? - exit(0) - else: - logger.info("Management strings match. Continuing.") + for entry in sorted(self.other_password_list): + hashed_key.update(entry.encode('utf-8')) - # - # firmwarepasswd tool checks - new_fw_tool_path = '/usr/sbin/firmwarepasswd' - new_fw_tool_exists = os.path.exists(new_fw_tool_path) + self.fwpw_managed_string = hashed_key.hexdigest() - if not new_fw_tool_exists: - logger.critical("No Firmware password tool available. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "No Firmware password tool available.") - exit(1) + # prepend '2:' to denote hash created with v2 of script, will force a password change from v1 + self.fwpw_managed_string = '2:' + self.fwpw_managed_string - # - # checking for existing fw password - logger.info("Checking for existing firmware password") - - existing_fw_pw = subprocess.check_output([new_fw_tool_path, "-check"]) - logger.info("New tools says " + existing_fw_pw) - if 'No' in existing_fw_pw: - if args.remove: - logger.critical("Asked to delete, no password set. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Asked to Delete, no password set.") + else: + self.fwpw_managed_string = None + + if self.args.testmode: + print("Incoming hash: %s" % self.fwpw_managed_string) + + def secure_delete(self): + """ + attempts to securely delete the keyfile with medium overwrite and zeroing settings + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if self.logger: + self.logger.info("Deleting keyfile") + + use_srm = bool(os.path.exists("/usr/bin/srm")) + + if self.args.testmode: + if self.logger: + self.logger.info("Test mode, keyfile not deleted.") + return + + if use_srm: try: - modify_nvram = subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) - logger.info("nvram entry pruned.") - except: - logger.warning("nvram reported error attempting to remove hash. Hash may not have existed.") - exit(1) + subprocess.call(["/usr/bin/srm", "-mz", self.config_options["keyfile"]["path"]]) + if self.logger: + self.logger.info("keyfile deleted successfuly.") + except Exception as exception_message: + if self.logger: + self.logger.critical("Issue with attempt to remove keyfile. %s" % exception_message) else: - logger.info("No firmware password set.") - existing_password = False - elif 'Yes' in existing_fw_pw: - logger.info("Existing firmware password set.") - existing_password = True - else: - logger.critical("Firmwarepasswd bad response at -check. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Firmwarepasswd bad response at -check.") - exit(1) - - logger.info("Reading password file") - - if args.obfuscated: - # - # unobfuscate plist - logger.info("Reading plist") - passwords = [] - try: - input_plist = plistlib.readPlist(args.keyfile) - except: - logger.critical("Error reading plist. Exiting.") - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Error reading plist.") - exit(1) - content_raw = input_plist["data"] - - content_raw = base64.b64decode(content_raw) - content_raw = content_raw.split(",") - content_raw = [x for x in content_raw if x] - - for item in content_raw: - label, pword = item.split(':') - pword = base64.b64decode(pword) try: - commented = label.split('#')[1] - commented = base64.b64decode(commented) - is_commented = True - except: - is_commented = False + deleted_keyfile = subprocess.call(["/bin/rm", "-Pf", self.config_options["keyfile"]["path"]]) + print("return: %r" % deleted_keyfile) + if self.logger: + self.logger.info("keyfile deleted successfuly.") + except Exception as exception_message: + if self.logger: + self.logger.critical("Issue with attempt to remove keyfile. %s" % exception_message) + + # is this really needed? + if os.path.exists(self.config_options["keyfile"]["path"]): + if self.logger: + self.logger.critical("Failure to remove keyfile.") + else: + if self.logger: + self.logger.info("Keyfile removed.") + return - if is_commented: - output_string = "#" + commented + ":"+pword - else: - output_string = label + ":"+pword - passwords.append(output_string) + def injest_config(self): + """ + attempts to consume and format configuration file + """ - else: - # - # read keyfile - logger.info("Reading plain text") + # handle parsing errors in cfg?!? + + # where to handle looking for cfg in specific locations?!? + + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) try: - with open(args.keyfile, "r") as keyfile: - passwords = keyfile.read().splitlines() - except: - logger.critical("Error reading keyfile. Exiting.") - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Error reading keyfile.") - exit(1) + if os.path.exists(self.args.configfile): + # firmware_password_manager_cfg_v2.5b8.py:434: DeprecationWarning: The SafeConfigParser class has been renamed to ConfigParser in Python 3.2. This alias will be removed in future versions. Use ConfigParser directly instead. + config = configparser.ConfigParser(allow_no_value=True) + config.read(self.args.configfile) + + self.config_options["flags"] = {} + self.config_options["keyfile"] = {} + self.config_options["logging"] = {} + self.config_options["slack"] = {} + self.config_options["os"] = {} + self.config_options["fwpm"] = {} + + for section in ["flags", "keyfile", "logging", "slack"]: + for item in config.options(section): + if "use_" in item: + try: + self.config_options[section][item] = config.getboolean(section, item) + except: + self.config_options[section][item] = False + elif "path" in item: + self.config_options[section][item] = config.get(section, item) + else: + self.config_options[section][item] = config.get(section, item) + + if self.args.testmode: + print("Configuration file variables:") + for key, value in self.config_options.items(): + print(key) + for sub_key, sub_value in value.items(): + print("\t%s %r" % (sub_key, sub_value)) + else: + if self.logger: + self.logger.critical("Issue locating configuration file, exiting.") + sys.exit() + except Exception as exception_message: + if self.logger: + self.logger.critical("Issue reading configuration file, exiting. %s" % exception_message) + sys.exit() + + self.read_config = True + + def sanity_check(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + def srp_check(self): + """ + full setregproptool support later, if ever. + """ + + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if os.path.exists('/usr/local/bin/setregproptool'): + self.srp_path = '/usr/local/bin/setregproptool' + elif os.path.exists(os.path.dirname(os.path.abspath(__file__)) + '/setregproptool'): + self.srp_path = os.path.dirname(os.path.abspath(__file__)) + '/setregproptool' + else: + print("SRP #3a") - logger.info("Closed password file") + if self.logger: + self.logger.info("SRP path: %s" % self.srp_path) - new_password = None - other_password_list = [] + def srp_current_state(self): + """ + full setregproptool support later, if ever. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) - # - # parse data from keyfile and build list of passwords - for entry in passwords: try: - key, value = entry.split(":", 1) - except Exception as this_exception: - logger.critical("Malformed keyfile, key:value format required. " + this_exception + ". Quitting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.") - exit(1) - - if args.testmode: - logger.info(key + ":" + value) - - if key.lower() == 'new': - if new_password is not None: - logger.critical("Malformed keyfile, multiple new keys. Quitting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.") - exit(1) + existing_fw_pw = subprocess.call([self.srp_path, "-c"]) + if self.logger: + self.logger.info("srp says %r" % existing_fw_pw) + + if existing_fw_pw: + return False + # it's weird, I know. Blame Apple. else: - new_password = value - other_password_list.append(value) + return True + + except: + if self.logger: + self.logger.info("ERROR srp says %r" % existing_fw_pw) + return False + +# +# # E:451,15: Undefined variable 'CalledProcessError' (undefined-variable) +# except CalledProcessError: +# if self.logger: +# self.logger.info("ERROR srp says %r" % existing_fw_pw) +# return False + + def srp_change(self): + """ + full setregproptool support later, if ever. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + print("Using srp tool!") + + print("%r" % self.current_fwpw_state) + + def fwpwd_check(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if os.path.exists('/usr/sbin/firmwarepasswd'): + self.fwpwd_path = '/usr/sbin/firmwarepasswd' else: - other_password_list.append(value) + print("FWPWD #2b") - logger.info("Sanity checking password file contents") + if self.logger: + self.logger.info("FWPWD path: %s" % self.fwpwd_path) - if new_password is None and not args.remove: - logger.critical("Malformed keyfile, no \'new\' key. Quitting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.") - exit(1) + def fwpwd_current_state(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) - exit_normal = False - known_current_password = False + existing_fw_pw = subprocess.check_output([self.fwpwd_path, "-check"]) - # - # if a password is set, attempt to discover it using keyfile - if existing_password: - logger.info("Verifying current FW password") - new_fw_tool_cmd = [new_fw_tool_path, '-verify'] - logger.info(' '.join(new_fw_tool_cmd)) + # R:484, 8: The if statement can be replaced with 'return bool(test)' (simplifiable-if-statement) +# return bool('Yes' in existing_fw_pw) - for keyfile_index in reversed(xrange(len(other_password_list))): - child = pexpect.spawn(' '.join(new_fw_tool_cmd)) - child.expect('Enter password:') + if b'Yes' in existing_fw_pw: + return True + else: + return False - child.sendline(other_password_list[keyfile_index]) - result = child.expect(['Correct', 'Incorrect']) + def fwpwd_change(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) - if result == 0: - # - # correct password, exit loop - current_password = other_password_list[keyfile_index] - known_current_password = True - break - else: - # - # wrong password, keep going - continue + known_current_password = False + current_password = '' - # - # We've discovered the currently set firmware password - if known_current_password: + # is this really needed?!? + new_fw_tool_cmd = [self.fwpwd_path, '-verify'] - # - # Deleting firmware password - if args.remove: - logger.info("Deleting FW password") + if self.current_fwpw_state: + if self.logger: + self.logger.info("Verifying current FW password") - new_fw_tool_cmd = [new_fw_tool_path, '-delete'] - logger.info(' '.join(new_fw_tool_cmd)) + for index in reversed(range(len(self.other_password_list))): child = pexpect.spawn(' '.join(new_fw_tool_cmd)) child.expect('Enter password:') - child.sendline(current_password) - result = child.expect(['removed', 'incorrect']) + child.sendline(self.other_password_list[index]) + result = child.expect(['Correct', 'Incorrect']) if result == 0: # - # password accepted, log result and exit - logger.info("Finished. Password should be removed. Restart required. [" + (keyfile_index + 1) + "]") - exit_normal = True + # correct password, exit loop + current_password = self.other_password_list[index] + known_current_password = True + break else: - logger.critical("Asked to delete, current password not accepted. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Asked to delete, current password not accepted.") - exit(1) + # + # wrong password, keep going + continue # - # Current and new password are identical - elif current_password == new_password: - logger.info("Match, no change required. Exiting.") - exit_normal = True + # We've discovered the currently set firmware password + if known_current_password: + + # + # Deleting firmware password + if not self.config_options["flags"]["use_fwpw"]: + if self.logger: + self.logger.info("Deleting FW password") + + new_fw_tool_cmd = [self.fwpwd_path, '-delete'] + if self.logger: + self.logger.info(' '.join(new_fw_tool_cmd)) + + child = pexpect.spawn(' '.join(new_fw_tool_cmd)) + child.expect('Enter password:') + + child.sendline(current_password) + result = child.expect(['removed', 'incorrect']) + + if result == 0: + # + # password accepted, log result and exit + if self.logger: + self.logger.info("Finished. Password should be removed. Restart required. [%i]" % (index + 1)) + self.clean_exit = True + else: + if self.logger: + self.logger.critical("Asked to delete, current password not accepted. Exiting.") +# secure_delete_keyfile(logger, args, config_options) + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Asked to delete, current password not accepted.", '', 'error') +# self.error_bot.send_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Asked to delete, current password not accepted.") + sys.exit(1) + + # + # Current and new password are identical + # + # + # WAIT. How (is/would) this possible, clearly the hashes don't match!!! What if they aren't using hashes? + # + # + elif current_password == self.new_password: + self.matching_passwords = True + self.clean_exit = True + + # + # Change current firmware password to new password + else: + if self.logger: + self.logger.info("Updating FW password") + + new_fw_tool_cmd = [self.fwpwd_path, '-setpasswd'] + if self.logger: + self.logger.info(' '.join(new_fw_tool_cmd)) + + child = pexpect.spawn(' '.join(new_fw_tool_cmd)) + + result = child.expect('Enter password:') + if result == 0: + pass + else: + if self.logger: + self.logger.error("bad response from firmwarepasswd. Exiting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') + sys.exit(1) + child.sendline(current_password) + + result = child.expect('Enter new password:') + if result == 0: + pass + else: + if self.logger: + self.logger.error("bad response from firmwarepasswd. Exiting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') + sys.exit(1) + child.sendline(self.new_password) + + result = child.expect('Re-enter new password:') + if result == 0: + pass + else: + if self.logger: + self.logger.error("bad response from firmwarepasswd. Exiting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') + sys.exit(1) + child.sendline(self.new_password) + + child.expect(pexpect.EOF) + child.close() + + if self.logger: + self.logger.info("Updated FW Password.") + self.clean_exit = True # - # Change current firmware password to new password + # Unable to match current password with contents of keyfile else: - logger.info("Updating FW password") + if self.logger: + self.logger.critical("Current FW password not in keyfile. Quitting.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Current FW password not in keyfile.", '', 'error') + self.secure_delete() + sys.exit(1) - new_fw_tool_cmd = [new_fw_tool_path, '-setpasswd'] - logger.info(' '.join(new_fw_tool_cmd)) + # + # No current firmware password, setting it + else: - child = pexpect.spawn(' '.join(new_fw_tool_cmd)) + new_fw_tool_cmd = [self.fwpwd_path, '-setpasswd'] + if self.logger: + self.logger.info(' '.join(new_fw_tool_cmd)) - result = child.expect('Enter password:') - if result == 0: - pass - else: - logger.error("bad response from firmwarepasswd. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.") - exit(1) - child.sendline(current_password) - - result = child.expect('Enter new password:') - if result == 0: - pass + child = pexpect.spawn(' '.join(new_fw_tool_cmd)) + + result = child.expect('Enter new password:') + print(child.before) + if result == 0: + pass + else: + if self.logger: + self.logger.error("bad response from firmwarepasswd. Exiting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') + sys.exit(1) + child.sendline(self.new_password) + + result = child.expect('Re-enter new password:') + if result == 0: + pass + else: + if self.logger: + self.logger.error("bad response from firmwarepasswd. Exiting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') + sys.exit(1) + child.sendline(self.new_password) + + child.expect(pexpect.EOF) + child.close() + + if self.logger: + self.logger.info("Added FW Password.") + self.clean_exit = True + + def slack_optionator(self): + """ + + ip, mac, hostname + computername + serial + + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if self.verify_network(): + try: + full_ioreg = subprocess.check_output(['ioreg', '-l']).decode('utf-8') + serial_number_raw = re.findall('\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) + serial_number = serial_number_raw[0] + if self.args.testmode: + print("Serial number: %r" % serial_number) + + if self.config_options["slack"]["slack_identifier"].lower() == 'ip' or self.config_options["slack"]["slack_identifier"].lower() == 'mac' or self.config_options["slack"]["slack_identifier"].lower() == 'hostname': + processed_device_list = [] + + # Get ordered list of network devices + base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]).decode('utf-8') + network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) + ether_up_list = subprocess.check_output(["/sbin/ifconfig", "-au", "ether"]).decode('utf-8') + for device in network_device_list: + device_name = device[0] + port_name = device[1] + try: + if self.args.testmode: + print(device_name, port_name) + + if port_name in ether_up_list: + device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]).decode('utf-8') + mac_address = re.findall('ether (.*) \n', device_info_raw) + if self.args.testmode: + print("%r" % mac_address) + ether_address = re.findall('inet (.*) netmask', device_info_raw) + if self.args.testmode: + print("%r" % ether_address) + if len(ether_address) and len(mac_address): + processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) + except Exception as this_exception: + print(this_exception) + + if processed_device_list: + if self.logger: + self.logger.info("1 or more active IP addresses. Choosing primary.") + if self.args.testmode: + print("Processed devices: ", processed_device_list) + + if self.config_options["slack"]["slack_identifier"].lower() == 'ip': + self.local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + elif self.config_options["slack"]["slack_identifier"].lower() == 'mac': + self.local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + elif self.config_options["slack"]["slack_identifier"].lower() == 'hostname': + try: + self.local_identifier = socket.getfqdn() + except: + if self.logger: + self.logger.error("error discovering hostname info.") + self.local_identifier = serial_number + + else: + if self.logger: + self.logger.error("error discovering IP info.") + self.local_identifier = serial_number + + elif self.config_options["slack"]["slack_identifier"].lower() == 'computername': + try: + cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) + self.local_identifier = cname_identifier_raw.split('\n')[0] + if self.logger: + self.logger.info("Computername: %r" % self.local_identifier) + except: + if self.logger: + self.logger.info("error discovering computername.") + self.local_identifier = serial_number + elif self.config_options["slack"]["slack_identifier"].lower() == 'serial': + self.local_identifier = serial_number + if self.logger: + self.logger.info("Serial number: %r" % self.local_identifier) else: - logger.error("bad response from firmwarepasswd. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.") - exit(1) - child.sendline(new_password) - - result = child.expect('Re-enter new password:') - if result == 0: - pass + if self.logger: + self.logger.info("bad or no identifier flag, defaulting to serial number.") + self.local_identifier = serial_number + + if self.args.testmode: + print("Local identifier: %r" % self.local_identifier) + + except Exception as this_exception: + print(this_exception) + self.config_options["slack"]["use_slack"] = False + else: + self.config_options["slack"]["use_slack"] = False + if self.logger: + self.logger.info("No network detected.") + + def slack_message(self, message, icon, type): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + slack_info_channel = False + slack_error_channel = False + + if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_info_url"]: + slack_info_channel = True + + if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_error_url"]: + slack_error_channel = True + + if slack_error_channel and type == 'error': + slack_url = self.config_options["slack"]["slack_error_url"] + elif slack_info_channel: + slack_url = self.config_options["slack"]["slack_info_url"] + else: + return + + payload = {'text': message, 'username': 'FWPM ' + self.master_version, 'icon_emoji': ':key:'} + + response = requests.post(slack_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) + + self.logger.info('Response: ' + str(response.text)) + self.logger.info('Response code: ' + str(response.status_code)) + + def reboot_exit(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + def injest_keyfile(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + path_to_keyfile_exists = os.path.exists(self.config_options["keyfile"]["path"]) + + if not path_to_keyfile_exists: + if self.logger: + self.logger.critical("%r does not exist. Exiting." % self.config_options["keyfile"]["path"]) + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Keyfile does not exist.", '', 'error') + sys.exit(2) + + if self.logger: + self.logger.info("Reading password file") + + if self.config_options["keyfile"]["use_obfuscation"]: + # + # unobfuscate plist + if self.logger: + self.logger.info("Reading plist") + passwords = [] + if "plist" in self.config_options["keyfile"]["path"]: + try: + keyfile_plist = plistlib.readPlist(self.config_options["keyfile"]["path"]) + content_raw = keyfile_plist["data"] + except: + if self.logger: + self.logger.critical("Error reading plist. Exiting.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading plist.", '', 'error') + sys.exit(1) + + else: + try: + with open(self.config_options["keyfile"]["path"], 'r') as reader: + content_raw = reader.read() + except: + if self.logger: + self.logger.critical("Error reading plist. Exiting.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading plist.", '', 'error') + sys.exit(1) + + content_raw = base64.b64decode(content_raw) + content_raw = content_raw.decode('utf-8').split(",") + content_raw = [x for x in content_raw if x] + + output_string = "" + for item in content_raw: + label, pword = item.split(':') + pword = base64.b64decode(pword) + try: + commented = label.split('#')[1] + commented = base64.b64decode(commented) + is_commented = True + except: + is_commented = False + + if is_commented: + output_string = "#" + commented.decode('utf-8') + ":" + pword.decode('utf-8') + passwords.append(output_string) + else: - logger.error("bad response from firmwarepasswd. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.") - exit(1) - child.sendline(new_password) + uncommented = base64.b64decode(label) + output_string = uncommented.decode('utf-8') + ":" + pword.decode('utf-8') + passwords.append(output_string) + + else: + # + # read keyfile + if self.logger: + self.logger.info("Reading plain text") - child.expect(pexpect.EOF) - child.close() + try: + with open(self.config_options["keyfile"]["path"], "r") as keyfile: + self.passwords_raw = keyfile.read() + + passwords = self.passwords_raw.splitlines() + + except: + if self.logger: + self.logger.critical("Error reading keyfile. Exiting.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading keyfile.", '', 'error') + sys.exit(1) - logger.info("Updated FW Password.") - exit_normal = True + if self.logger: + self.logger.info("Closed password file") + + # new_password = None + # other_password_list = [] # - # Unable to match current password with contents of keyfile - else: - logger.critical("Current FW password not in keyfile. Quitting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Current FW password not in keyfile.") - exit(1) + # parse data from keyfile and build list of passwords + for entry in passwords: + try: + key, value = entry.split(":", 1) + except Exception as this_exception: + if self.logger: + self.logger.critical("Malformed keyfile, key:value format required. %r. Quitting." % this_exception) + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') + sys.exit(1) + + if key.lower() == 'new': + if self.new_password is not None: + if self.logger: + self.logger.critical("Malformed keyfile, multiple new keys. Quitting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') + sys.exit(1) + else: + self.new_password = value + self.other_password_list.append(value) + else: + self.other_password_list.append(value) - # - # No current firmware password, setting it - else: - new_fw_tool_cmd = [new_fw_tool_path, '-setpasswd'] - logger.info(' '.join(new_fw_tool_cmd)) + if self.logger: + self.logger.info("Sanity checking password file contents") + + if self.new_password is None and self.config_options["flags"]["use_fwpw"]: + if self.logger: + self.logger.critical("Malformed keyfile, no \'new\' key. Quitting.") + self.secure_delete() + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') + sys.exit(1) - child = pexpect.spawn(' '.join(new_fw_tool_cmd)) + self.read_keyfile = True - result = child.expect('Enter new password:') - print child.before - if result == 0: + try: + self.other_password_list.remove(self.new_password) + except: pass + + def nvram_manager(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if self.clean_exit: + if not self.config_options["flags"]["use_fwpw"]: + try: + subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) + if self.logger: + self.logger.info("nvram entry pruned.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :unlock:\n" + "FWPW and nvram entry removed.", '', 'info') + # + # Should we return here? + # + except Exception as exception_message: + if self.logger: + self.logger.warning("nvram reported error attempting to remove hash. Exiting. %s" % exception_message) + # + # Slack? + # + sys.exit(1) + + if self.config_options["flags"]["management_string_type"] == "None": + try: + # ? + # existing_keyfile_hash = subprocess.check_output(["/usr/sbin/nvram", "fwpw-hash"]) + try: + subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) + if self.logger: + self.logger.info("nvram entry pruned.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.", '', 'info') + except Exception as exception_message: + if self.logger: + self.logger.warning("nvram reported error attempting to remove hash. Exiting. %s" % exception_message) + sys.exit(1) + except: + # assuming hash doesn't exist. + if self.logger: + self.logger.info("Assuming nvram entry doesn't exist.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.", '', 'info') + + elif self.config_options["flags"]["management_string_type"] == "custom" or self.config_options["flags"]["management_string_type"] == "hash": + if self.matching_hashes: + if self.matching_passwords: + if self.logger: + self.logger.info("Hashes and Passwords match. No changes needed.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :white_check_mark::white_check_mark:\n" + "FWPM hashes and FW passwords match.", '', 'info') + else: + if self.logger: + self.logger.info("Hashes match, password modified.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :white_check_mark::heavy_exclamation_mark:\n" + "FWPM hashes and FW passwords match.", '', 'info') + else: + try: + subprocess.call(["/usr/sbin/nvram", "fwpw-hash=" + self.fwpw_managed_string]) + if self.logger: + self.logger.info("nvram modified.") + except Exception as exception_message: + if self.logger: + self.logger.warning("nvram modification failed. nvram reported error. %s" % exception_message) + # + # slack error message? + # + sys.exit(1) + + if self.matching_passwords: + if self.logger: + self.logger.info("Hash mismatch, Passwords match. Correcting hash.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :heavy_exclamation_mark: :white_check_mark:\n" + "Hash mismatch, Passwords match. Correcting hash.", '', 'info') + else: + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW and hash updated.", '', 'info') + else: - logger.error("bad response from firmwarepasswd. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.") - exit(1) - child.sendline(new_password) - - result = child.expect('Re-enter new password:') - if result == 0: - pass + if self.logger: + self.logger.critical("An error occured. Failed to modify firmware password.") + if self.config_options["slack"]["use_slack"]: + self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "An error occured. Failed to modify firmware password.", '', 'error') + sys.exit(1) + + def exit_manager(self): + """ + This should not be blank. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + # + # check the new booleans, etc to find out what we accomplished... + # + + # self.clean_exit = False + # + # self.read_config = False + # self.read_keyfile = False + # self.modify_fwpw = False + # self.modify_nvram = False + # + + if self.config_options["flags"]["use_reboot_on_exit"]: + if self.args.testmode: + if self.logger: + self.logger.info("Test mode, cancelling reboot.") + else: + if self.logger: + self.logger.warning("Normal completion. Rebooting.") + os.system('reboot') else: - logger.error("bad response from firmwarepasswd. Exiting.") - secure_delete_keyfile(logger, args, error_bot, local_identifier) - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.") - exit(1) - child.sendline(new_password) + if self.logger: + self.logger.info("FWPM exiting normally.") + sys.exit(0) + + def verify_network(self): + """ + Host: 8.8.8.8 (google-public-dns-a.google.com) + OpenPort: 53/tcp + Service: domain (DNS/TCP) + """ - child.expect(pexpect.EOF) - child.close() + try: + _ = requests.get("https://8.8.8.8", timeout=3) + return True + except requests.ConnectionError as exception_message: + print(exception_message) + return False + + +def main(): + """ + This should not be blank. + """ + master_version = "2.5" + + logo = """ + /_ _/ /_ _/ University of Utah + _/ _/ Marriott Library + _/ _/ Mac Group + _/ _/ https://apple.lib.utah.edu/ + _/_/ https://github.com/univ-of-utah-marriott-library-apple - logger.info("Added FW Password.") - exit_normal = True + + """ + desc = "Manages the firmware password on Apple Macintosh computers." # - # Delete keyfile securely. - secure_delete_keyfile(logger, args, error_bot, local_identifier) + # require root to run. + if os.geteuid(): + print("Must be root to run script.") + sys.exit(2) # - # closing reports - logger.info("Closing out.") + # parse option definitions + parser = argparse.ArgumentParser(description=logo+desc, formatter_class=RawTextHelpFormatter) # - # No errors detected during run. - # nvram modifications, reporting, and exits - if exit_normal: - if args.remove: - try: - modify_nvram = subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) - logger.info("nvram entry pruned.") - if args.slack: - info_bot.send_message("_*" + local_identifier + "*_ :unlock:\n" + "FWPW removed.") - except: - logger.warning("nvram reported error attempting to remove hash. Hash may not have existed.") - exit(1) + # required, mutually exclusive commands + prime_group = parser.add_argument_group('Required management settings', 'Choosing one of these options is required to run FWPM. They tell FWPM how you want to manage the firmware password.') + subprime = prime_group.add_mutually_exclusive_group(required=True) + subprime.add_argument('-c', '--configfile', help='Read configuration file') - if args.nostring: - try: - existing_keyfile_hash = subprocess.check_output(["/usr/sbin/nvram", "fwpw-hash"]) - try: - modify_nvram = subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) - logger.info("nvram entry pruned.") - if args.slack: - info_bot.send_message("_*" + local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.") - except: - logger.warning("nvram reported error attempting to remove hash. Exiting.") - exit(1) - except: - # assuming hash doesn't exist. - logger.info("Assuming nvram entry doesn't exist.") - if args.slack: - info_bot.send_message("_*" + local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.") + parser.add_argument('-b', '--reboot', action="store_true", default=False, help='Reboots the computer after the script completes successfully.') + parser.add_argument('-t', '--testmode', action="store_true", default=False, help='Test mode. Verbose logging, will not delete keyfile.') + parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + master_version) - if args.management or args.hash: - try: - modify_nvram = subprocess.call(["/usr/sbin/nvram", "fwpw-hash="+fwpw_managed_string]) - logger.info("nvram modified.") - if args.slack: - info_bot.send_message("_*" + local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.") - except: - logger.warning("nvram modification failed. nvram reported error.") - exit(1) + args = parser.parse_args() - if args.reboot: - logger.warning("Normal completion. Rebooting.") - os.system('reboot') - else: - exit(0) + if args.testmode: + print(args) # - # Errors detected during run. - else: - logger.critical("An error occured. Failed to modify firmware password.") - if args.slack: - error_bot.send_message("_*" + local_identifier + "*_ :no_entry:\n" + "An error occured. Failed to modify firmware password.") - exit(1) + # Open log file + try: + log_path = '/var/log/' + 'FWPW_Manager_' + master_version + logging.basicConfig(filename=log_path, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + logger = logging.getLogger(__name__) + logger.info("Running Firmware Password Manager " + master_version) + except: + logger = None + + FWPM_Object(args, logger, master_version) if __name__ == '__main__': diff --git a/img/direct_entry_trimmed.png b/img/direct_entry_trimmed.png new file mode 100644 index 0000000..b04b3f3 Binary files /dev/null and b/img/direct_entry_trimmed.png differ diff --git a/img/hashed_keys.png b/img/hashed_keys.png new file mode 100644 index 0000000..77b8772 Binary files /dev/null and b/img/hashed_keys.png differ diff --git a/img/jamf_fetch_trimmed.png b/img/jamf_fetch_trimmed.png new file mode 100644 index 0000000..0f8bd00 Binary files /dev/null and b/img/jamf_fetch_trimmed.png differ diff --git a/img/no_keys.png b/img/no_keys.png new file mode 100644 index 0000000..c106f67 Binary files /dev/null and b/img/no_keys.png differ diff --git a/img/remote_fetch_trimmed.png b/img/remote_fetch_trimmed.png new file mode 100644 index 0000000..9881311 Binary files /dev/null and b/img/remote_fetch_trimmed.png differ diff --git a/img/sk_help.png b/img/sk_help.png new file mode 100644 index 0000000..c2771bc Binary files /dev/null and b/img/sk_help.png differ diff --git a/img/sk_login.png b/img/sk_login.png new file mode 100644 index 0000000..dc8194a Binary files /dev/null and b/img/sk_login.png differ diff --git a/img/sk_os_alert.png b/img/sk_os_alert.png new file mode 100644 index 0000000..bc9e929 Binary files /dev/null and b/img/sk_os_alert.png differ diff --git a/img/sk_ui.png b/img/sk_ui.png new file mode 100644 index 0000000..becd32e Binary files /dev/null and b/img/sk_ui.png differ diff --git a/img/yes_keys.png b/img/yes_keys.png new file mode 100644 index 0000000..38bed6c Binary files /dev/null and b/img/yes_keys.png differ diff --git a/setup.py b/setup.py index 4a42277..a3f3820 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ setup( name='Firmware Password Manager', - version='2.1.5', + version='2.5', url='https://github.com/univ-of-utah-marriott-library-apple/firmware_password_manager', author='Todd McDaniel, Marriott Library Client Platform Services', author_email='mlib-its-mac-github@lists.utah.edu', description=('A Python script to help Macintosh administrators manage the firmware ', 'passwords of their computers.'), license='MIT', - scripts=['firmware_password_manager.py', 'obfuscate_keylist.py'], + scripts=['firmware_password_manager.py'], classifiers=[ 'Development Status :: 5 - Stable', 'Environment :: Console', diff --git a/skeleton key/Skeleton_Key.py b/skeleton key/Skeleton_Key.py new file mode 100755 index 0000000..90e56cd --- /dev/null +++ b/skeleton key/Skeleton_Key.py @@ -0,0 +1,1508 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +Skeleton Key +""" + +# Copyright (c) 2020 University of Utah Student Computing Labs. ################ +# All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appears in all copies and +# that both that copyright notice and this permission notice appear +# in supporting documentation, and that the name of The University +# of Utah not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. This software is supplied as is without expressed or +# implied warranties of any kind. +################################################################################ + +# skeleton_key.py ################################################# +# +# A Python Tk application to set/unset the firmware password. +# +# +# 0.1.0 2017.04.13 Initial build. tjm +# 0.2.0 2017.05.04 single pane. tjm +# 1.0.0 2020.01.22 Initial release, +# JAMF and Slack integration, +# hash generation, reading config file. tjm +# +################################################################################ + +# notes: ####################################################################### +# +# sudo /usr/local/bin/pyinstaller --onefile Skeleton_Key.spec +# +# +# +# +################################################################################ + +from __future__ import division +from __future__ import print_function +import base64 +import ConfigParser +import hashlib +import inspect +import json +import os +import platform +import plistlib +import pwd +import re +import socket +import subprocess +import sys +import tkFileDialog +import tkSimpleDialog +import ttk +from Tkinter import Tk, N, E, S, W, StringVar, IntVar, PhotoImage, HORIZONTAL +import logging +import pexpect +import requests + +try: + import mount_shares_better as msb +except: + pass + + +class SinglePane(object): + """ + Load keys, generate hashes, toggle fwpw + """ + + def __init__(self, root, logger, admin_password, fwpw_status, master_version): + """ + Initialize object and variables + """ + + self.logger = logger + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + self.root = root + self.root.title("Skeleton Key " + master_version) + + self.logo = '''\ + R0lGODlhWAJRAPcAAAEAAAgHBwwLCxAPDxQTExgXFxwcHCEfHyYlJSgnJywsLD0nJzItLTY2Njg3 + Nzw8PF4dHGMcG24cGnIdG1QgH18gH0EmJkgpKEA/P1glJEJCQkhHR0tLS1BPT1NSUlhXV1lZWWFf + X3NcXGRjY2hnZ2xra3Bvb3ppaXRzc3h2d318fJcAAJsAAp8AC4obGJ4dGKAADaEAFKQAGqceGbAf + GqUhHqwhHrQhHL4hG6cEIqgEI6cKI6kNJqkOKKkQJ6sWK6sYLKwbMJgsKpw0MpI0M6UlIqEoJaEs + KqolIqMxL68lNLEpOaE1M6E5N6M+PbMwPp9AP7QyQrc8SZ1DQpxLSplHRp5ZWKJDQqFLSrpBTaFQ + T79OWKVZWIBuboB+fop4eKxraqdnZql6ebN+fbJ2dcNXX8JXYcRcZsZeacdjbshmb8hmcM92freA + f896gnKBgISEhIyMjIiGh5OTk5ycnJiWlpCJibuMi6Cfn76enqyJiaSjo6inp6qqqrCvr7+trbS0 + tLi3t7m5ubm3uNODiteOltiPlsSZmNeRltiRltqUmtqYncCMi+KboMSkpMqqqt+mqt6ho8C/v8q6 + uti7u+Cnq+GqruuiqeOusuSxs+K5vMTExMjHx8zMzN/GxtDPz9/Jyc/Q0NLS0tjX19zc3O3KzOjH + yO/P0eTV1e/R0+Pa2u/e3vDR0/Lc3fTe4N/h4OTk5Orl5erq6unn6PPt7vjt7vT09Pr29/////f4 + +Pnu8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAABYAlEA + AAj+AHEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKFAlLkBwQGho00ABCjiBY + I2PKnEmzps2bOHPq3MmzJ8FZdRoEAEAAgYKjChAQABCAwRyYPqNKnUq1qtWrWLPmhFUiQIADChgg + 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 + 5T53utfd7nfHe971jouAAAA7 + ''' + + self.open_lock_icon = '''\ + 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 + By988Hf0cVFAADs= + ''' + + self.closed_lock_icon = '''\ + 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 + ''' + + self.key_icon = '''\ + R0lGODlhMAAwAPcAAAABAAkFAgQLBg0IAwwNCxMMBQwQCBgRBhEVChQYCxsYChISEhwcHCIXCSUb + CyseCzgVBC8hDDEiDTgnDz0tEiYmJi0tLTQ0NDs7O0YdB00iCVYmC1wqC0o1FFY7Flg9F1k/GGsx + DlxBGWBDGmZJHGlNHm5QHnRRH0RAOnVUIXpVIEVFRUxMTFxZVlxcXGJiYmpqanFuZHNzc3l5eZNG + FaVQGINbI4NhJo1jJpJmJ5BnKJVrKZ5sKbVuJ6N1LbJyKrl2K7B5L7J9ML5/MchiHsN8LPN3JPl8 + JrSBMryDMr+MNcyALtWDLsGFM8mNNtWWOtSYOtibPP+BJ/+FKP+JKv+SLeOMMuyPM+GcPO+cOueh + PuyiPvGjPqqbeu2mQPOnQPSqQfqtQvGzRP60Rf+5R/i3SPe8SP69Sf/ES//LTv/RT//RUf/cVP/q + Wf/0Xf/udIWFhYiIiJKSkqWlpKysrLOzs7u7u83EkP/0nMnDp8XFxcvLy9/f3+Tk5Ovr6/Xv6PX1 + 9Pr6+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIIALAAAAAAwADAA + AAj+AAUJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3MixY0Y6L1asgKHHo8IZAABI+ODBAYAF + dEwW9FMBAA4uZNCsQRNGBAAYMgUCYqBAyxoyY9BgGWKmDQ8AcYKyKCAGDZgwaEikBHDDTQ4Ae0zu + AeAEDRcwaHYACEFkAwAhbB4A9eiCAhoxXMaQEZChypQqEBCw0UGgJEdABHSkAXN1DAAOVYxU0SBA + DRQAcjqOdXKGMdoUAGocoQHAhpotAaJyHPukM+MxZShs9YAGzeXMHPkASGKV8ZekQkzwTuoDQJ2O + gQyQ6O0bq5reaDqsCNQRDw8erj1r54ImCoCYHLv+YKn9Rft2NGQcXOjogixzNGfCeCazZssDAn44 + sgDQowoYMfKp0ERttYVhAwAM5LfRfkBQccUZtY3hgAATePBBBwUAMAMgHK0AQBFUZIGGEx6o8AYg + e8ThggszzKHgRisIAGIWEJ4AwAV/BDUQBggsEWIZaKmAgY47EgDiFmiUUQYbJqxHpAUIMBHiGV5g + MYYQP+kISAUJWEGFF2ngAEAAGbqgZQUGXPEjGiMwMIccc+jYxwUJqLnFGWOsAYKTRNIBQINedIak + BxYQKVAdAPyAJxgRjAkACoYKgigSanQWQAx92NFHpIgGAd8XANwR6UCIHuCAAwcAkMeoAvUBxwsU + MswwwwubsmrrrbjmquuuvPbKUUAAOw== + ''' + + self.balloon_icon = '''\ + R0lGODlhMAAwAPejAAAAAAICAgMDAwQEBAUFBQgICAkJCQoKCgsLCwwMDA0NDQ8PDxAQEBERERQU + FBUVFRYWFhgYGBsbGx0dHR4eHh8fHyEhISMjIyUlJSYmJicnJygoKCsrKy0tLTAwMDIyMjMzMzQ0 + NDc3Nzg4ODo6Ojw8PD09PT8/P0JCQkdHR0hISEtLS0xMTE1NTU9PT1BQUFFRUVJSUlRUVFVVVVZW + VldXV1lZWVpaWltbW11dXV9fX2BgYGFhYWJiYmVlZWZmZmhoaGlpaWpqamtra21tbW5ubm9vb3Jy + cnNzc3Z2dnh4eHt7e3x8fH5+fn9/f4CAgIKCgoODg4SEhIaGhoyMjI2NjY6Ojo+Pj5CQkJSUlJaW + lpeXl5mZmZubm52dnZ6enqGhoaKioqOjo6SkpKenp6mpqaqqqqurq6ysrK2tra6urq+vr7CwsLGx + sbOzs7S0tLa2tre3t7m5ubu7u7y8vL29vb+/v8HBwcLCwsPDw8TExMXFxcbGxsnJycrKysvLy83N + zc/Pz9DQ0NLS0tTU1NXV1djY2NnZ2dra2tvb29zc3N/f3+Dg4OHh4ePj4+fn5+jo6Onp6erq6uzs + 7O3t7e/v7/Dw8PHx8fLy8vPz8/T09Pb29vf39/n5+fr6+vv7+/z8/P39/f7+/v///wAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKQALAAAAAAwADAA + AAj+AEkJHEiwoMGDCBMqXMiwocOHECNKnEiRVKAvVbr4qTgRlBscCgCINCAjTSeODvVUIMDji59D + gML8OBBBDsqFUQDsqDSqp89RmoQASHITIRYAYX4q7bkGgJOiBeEAELO0ahsAZaAO3MCjqtcjETZp + VQPgkdeqmBqI0YrDx1mvRlYYyuIFEcUyHhgA+PK26hkAASYsAACBDMRNIQD84GOoU9+lmvpACuXp + ERMANBx+ytBh0ePPPu8AMNLQRgRKoFOPmgNgo8JFAPKoVt3iRiOFVCh4mp0azAMvCmG45Q1aUAAp + ClEgIQ4akIApCn2kYP55i4S1Ca9Got63RJFLCy3+POF+9guARAzHACBDfmkaANAbOtnbvuefFQCa + QJwhpP6kABcYEpEOQdQ3ygpFSKSECAYO8YJEdAAwSH0q+DCRBle0BxsaEy1RgFncdYFBRR/k8Jko + opxFSEWEELDDYzw08IdPfXxQgBCQoHTHACYg4tUiKmggAwBbjOIFABv0AIEDe6D0CAkA+JBGIZZY + YkgbQADwASOkxJGJIgBY4dMQGhQFRw0IACCAAAAUAMMbBTEBwk+NAMAGVJzgAQYYdmRyUAw9/JRJ + AlxoxdAVAUASGgCFGLoQKBWwgEgldUSQAymPmCGJowg1koFIALhAiiVpRoAJpwgNsoYjAhHRwigb + J0CBKkNOjCAIB1rMyhALANig66/ABitsUQEBADs= + ''' + + self.shield_icon = '''\ + R0lGODlhMAAwAPcAAAEAAAwBAQAGCAALDQsLCxIAABgAAAAPEgAQEwQdHhQUFB0dHSQBASwBADIB + AD0OBxQsLRMjJQ8wMRsyNRk1OCQkJCsrKzAvLjAwLyUyNDMzMzg3Nz08PEQDAFwKBlULCFcGA08U + DVEUDVoXDmMIA2sKBXANCHQUD34RDGYbEXwmGiZCRjpRUkRDQ09OTkpcXFNTUlhYV1tbWmBgX0Rl + Zk9lZlJgYVJsbllzdGRkY2pqamJzdXJxcHV6enx7e4gTD44XEpMZEqojHLkkHZYuIYwxI5czJaU6 + K8sjHs4nIc0qI9knIdIvJ9ovJ9EvKNIwJ9MyKtwxKdg9Mu0sJuQzK+s1LPM3Lfg3Lvc4Lvs4L/87 + MbdCM8pMO99JPOBNPt9WRORUQ+dYRupaR+xdSfVSQ/9WRfVdSfxbSe5iTfRgTPxiTf9mUf9rVP9v + WP9yWnaAgXyLjYOEg4yLioCSk4uRkYSYmJSTk5ubm5SdnZqjpJ2oqKSkpKOop6mpp6eqqqurq6ez + srOzs7O9vb69vcC/v77Dw8PEw8jGxsnIx8vLytHQz9TU09jW1dvZ19zc3ODf3t/g3uPj4+rn5uvp + 5+rq6fDv7vTz8/7+/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAJgALAAAAAAwADAA + AAj+ADEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGA06GmRHR4wWLVzkkBNokaWMBxfFqQCAQAUO + LmTI+IhBAQAFOQahFLjIBYAKOghZquRokdFFjihZUqkBgIU/Ge847XPJUqEdEw4A2ArgAAQbgChd + 6glAxkU7AHRcutRjwFYHKYoQAYKChAOuLyBd+gOgRUVHAHJcOoQAwAguatawaVNmipXHVJSY2Brn + UiAAdijqWDAYAIMvbNaYESNmzJgnTFJHoZKFigcAPS7JqECRQ4xLEQqEFhNmDGkxo1GnTk1FCwgA + kezQnhjDRSIARNiEKU3dNBopqYUziQxAUBwNFH3+cDgEwAib3+jHeFEyPDuVId1n+J3Ix4IiBCnO + oxeDxkuS9kygZkUQABTSgg4UIRLAIhN0oB96aITxH4BMXFHCAIdYcAdFjwAwyA0NrOEbdaT1x95w + AnYggSIEHELRJQrQAQcAaoy23xhpSMEeagIycAMhADxSUQwx5AEAYvvxFwaFqwEwhxwKXFKRHRaQ + p8KDpPmW3ZZWCAEAHzGYVdEiACSyggNYjtFfEk4EOJxxAySyQB8WWRLAHXPQaONvY0iRhHZMWFEA + DeQ5cpEMOZB3hHQQjgGFE5AqwZ0eciyA0SAEODIBmjei8egTkGrxmiMcxIHRJQTcYWQYavhmWp/4 + EzIRRRUA4EBeJBnpoAElAqTQxnT8dcFmak5oAQQAi+iK0nOHzCiGGtP5xqOsVgTAgiQB5LETBy1c + AoAIbowxHRqnpabFCcj5oMBJKJGHCB4AbPGrdX5SwQQANlgiQGY7YdKCBZdkAIAZa4SBhrCsMTCA + bFH2iwlgd3jbABtsrNeEFh+USSZUDmMSBwCVkBeCG2QscS4AdFxSwXwdY7IAwHwA8MAZosImGwCU + tMwTALddFgADAFT2MZ06C9RHWmNBkEAgl6DlQ9EEocVDVZFcIhWCUEfNsyWX+JBW1gb9QYAGLQQN + 9kGRcGBBIme37fbbcMct99xQBwQAOw== + ''' + + self.double_exclamation_icon = '''\ + R0lGODlhKAAoAPcAAJcbD5QbEZkcEL4cDq0fE50lGZ8pHaAqHrQhFLsiFbYmGpguJZoyKpk4MMId + D8EeEMQkFsslF8onGMIoG84oGdUpGtwrGtEwH+MtHOouHPEtGuwwHvAxHsYuIcgzJcs3KdQyItk3 + KN87Kt0+MO80IOM/LvE2IfQ4I8pAM8pGOs1LP9RCNd9DNNVGONlLPeRBMKBQSrVTS61eV7peValo + Y8FTSc1XTN9TRNRWSd1WStdbTs1bUNleUvJbR9ZnXd9mWuBqXvFpWvhwW9FsY9hsYdtxZ9N0a9l/ + dut1Zut2aOt6bPp3Yvl8aOF8cseEftCEfd2FfeOCePGEeLmGgsiGgdmHgNyLg9CPiN2TjN6cleGN + heuKgOWUjOOZkuqbkvSclN63tNW4ttm/veimoOOqpeurpeavquyxq+i1sO67tuS9uu2+uu3DvvnE + v9/KyOHEwezGw+7JxfDHw/HLx/zKxPzPyfLRzfzRzPLV0vPZ1/Xc2v3b2O/k4/Pm5fjm5P7o5vbt + 7Pnt6/bw7/bx8Pvz8vz49/36+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIcALAAAAAAoACgA + AAj+AA8JHEiwoMGDCBMqXMiwocOHECNKnAhxT5s6dO7cqVPnTsaChuhk1MiRIx0/DL/0kMKkpUsh + UgoWCuKy5pIgbRh6wWDCBAkTG06c4BDF4A0THIAOFXrDDsM0IjRoyEA1w1QvBoFsmFp1KhCUC+3k + IJGBhIYNGzhgOGMwSlq0GbZmiGKIoZ8fZami3YAhjkEue/dy2MClIaEmGOJmwIC2Qh6DZRhXjYuh + jEMuGMiSSEwiRCCDbCxQJUvVAhuHY0RjSMzYxcE8F0iojnsBj8PQm1cn5nEQEIgNGUSLBvG5oZ0K + unUXOWioRWnRGVrUbejnQnIMFrQg1IE9uY6Hhkb+YLdgAXsXhER0l7dABCIO8t0roEEIZf16KBB9 + kIdfAQ7CLhXsZ0EF5z0ERYAVIFeBHgiZEeB+FZgBERYIBugBIQjBgSB5FPj3EBkVVoBDQnhIMGCA + ETz2kBoJtrhDQoB4cGIFHgACUYktUmCEQim0WEEKEfnhQY5WKLSDjy9CZAgKM2ahEBE+thfRDAEc + UIAAaih0RQAGHADAFRIRMoghhBAyHUKFCGKIIYIUQtGbEemxBRJKWKZQIF4kgYQXxT00BwslgADC + C00kRIgOL4BQQQk5YPgQDxUMICkEFWR5kBYVPPDAAA5UoJ1DfkwAgQQQQJBABEUeZEOprEKQJHVT + CrRa6hMI1SBrBDFAFAMEFCQAQQQEgIEQFQhQUGoECTgBkRsBIEAqATLAyAACpSLAgI3LwrAAA1Oc + 2RsNDDBAA1gR9SHGG94iNIgYYggC57sFBQQAOw== + ''' + + self.master_version = master_version + self.admin_password = admin_password + self.fwpw_status = fwpw_status + self.hashed_key = None + self.obfuscated_keys = None + self.obfuscated_string = None + self.cleared_keys = None + self.postinstall_script = None + self.plaintext_keys = None + self.key_item = '' + self.keys_loaded = False + self.key_source = "" + self.state_button_state = "disabled" + self.hash_button_state = "disabled" + + self.previous_keys = [] + self.current_key = '' + + self.config_options = {} + self.injest_config() + + self.remote_username = StringVar() + self.remote_password = StringVar() + self.remote_hostname = StringVar() + + self.jamf_username = StringVar() + self.jamf_password = StringVar() + self.jamf_hostname = StringVar() + + self.jamf_hostname.set("https://jamf.pro.server:8443") + self.jamf_username.set("") + self.jamf_password.set("") + + self.hashed_results = StringVar() + self.fwpm_package_dest = StringVar() + self.signing_cert = StringVar() + self.keyfile_loc = StringVar() + self.status_string = StringVar() + self.fwpm_package_dest.set("/") +# self.status_string.set(u'\U0001F923'.encode('utf-8')) + self.status_string.set("Ready.") + self.fwpw_enable = IntVar() + self.fwpw_enable.set(0) + self.reboot_enable = IntVar() + self.reboot_enable.set(0) + self.include_config = IntVar() + self.include_config.set(0) + + self.use_slack = IntVar() + self.use_slack.set(0) + self.slack_identifier = StringVar() + self.slack_url = StringVar() + self.slack_info_url = StringVar() + self.slack_info_channel = StringVar() + self.slack_info_bot = StringVar() + self.slack_error_url = StringVar() + self.slack_error_channel = StringVar() + self.slack_error_bot = StringVar() + self.state_string = StringVar() + self.state_string.set('Firmware password is ' + self.fwpw_status) + self.keys_loaded_string = StringVar() + self.keys_loaded_string.set('No keys in memory') + self.logger.info(self.state_string.get()) + self.logger.info(self.keys_loaded_string.get()) + + if self.config_options: + if self.config_options['slack']['slack_info_url']: + self.slack_info_url.set(self.config_options['slack']['slack_info_url']) + + if self.config_options['slack']['slack_info_bot_name']: + self.slack_info_bot.set(self.config_options['slack']['slack_info_bot_name']) + + if self.config_options['slack']['slack_info_channel']: + self.slack_info_channel.set(self.config_options['slack']['slack_info_channel']) + + if self.config_options['slack']['slack_error_url']: + self.slack_error_url.set(self.config_options['slack']['slack_error_url']) + + if self.config_options['slack']['slack_error_bot_name']: + self.slack_error_bot.set(self.config_options['slack']['slack_error_bot_name']) + + if self.config_options['slack']['slack_error_channel']: + self.slack_error_channel.set(self.config_options['slack']['slack_error_channel']) + + if self.config_options['slack']['slack_identifier']: + self.slack_identifier.set(self.config_options['slack']['slack_identifier']) + + if self.config_options['slack']['use_slack']: + # translate into 0/1 for false/true + self.use_slack.set(1) + self.slack_optionator() + else: + self.use_slack.set(0) + + if self.config_options['keyfile']['path']: + self.keyfile_loc.set(self.config_options['keyfile']['path']) + + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + self.root.geometry("604x500") + + self.logo_photoimage = PhotoImage(data=self.logo) + self.closed_lock_icon_photoimage = PhotoImage(data=self.closed_lock_icon) + self.open_lock_icon_photoimage = PhotoImage(data=self.open_lock_icon) + self.key_icon_photoimage = PhotoImage(data=self.key_icon) + self.balloon_icon_photoimage = PhotoImage(data=self.balloon_icon) + self.shield_icon_photoimage = PhotoImage(data=self.shield_icon) + self.double_exclamation_icon_photoimage = PhotoImage(data=self.double_exclamation_icon) + + self.superframe = ttk.Frame(self.root, width=604, height=525) + self.superframe.grid(column=0, row=0, sticky=(N, W, E, S)) + + self.logoframe = ttk.Frame(self.superframe, width=604, height=90) + self.logoframe.grid(column=0, row=0, sticky=(N, W, E, S)) + + self.logoframe.grid_rowconfigure(0, weight=1) + self.logoframe.grid_rowconfigure(2, weight=1) + self.logoframe.grid_columnconfigure(0, weight=1) + self.logoframe.grid_columnconfigure(2, weight=1) + + self.logo_label = ttk.Label(self.logoframe) + self.logo_label['image'] = self.logo_photoimage + self.logo_label.grid(column=1, row=1, sticky=(N, S, E, W)) + + self.stateframe = ttk.Frame(self.superframe, width=604, height=30) + self.stateframe.grid(column=0, row=1, sticky=(N, W, E, S)) + self.stateframe.grid_columnconfigure(0, weight=1) + self.stateframe.grid_rowconfigure(0, weight=1) + self.stateframe.grid_columnconfigure(2, weight=1) + self.stateframe.grid_rowconfigure(2, weight=1) + + self.lock_label = ttk.Label(self.stateframe) + if self.fwpw_status == 'On': + self.lock_label['image'] = self.closed_lock_icon_photoimage + else: + self.lock_label['image'] = self.open_lock_icon_photoimage + self.lock_label.grid(column=0, row=1, sticky=(E)) + + self.state_label = ttk.Label(self.stateframe, textvariable=self.state_string, font=("Helvetica", 24)) + self.state_label.grid(column=1, row=1, columnspan=1, sticky=(W)) + + self.keys_label = ttk.Label(self.stateframe) + self.keys_label['image'] = self.balloon_icon_photoimage + self.keys_label.grid(column=0, row=2, sticky=(E)) + + self.keys_loaded_label = ttk.Label(self.stateframe, textvariable=self.keys_loaded_string, font=("Helvetica", 24)) + self.keys_loaded_label.grid(column=1, row=2, columnspan=1, sticky=(W)) + + ttk.Separator(self.stateframe, orient=HORIZONTAL).grid(row=10, columnspan=3, sticky=(E, W), pady=0) + + self.navframe = ttk.Frame(self.superframe, width=604, height=30) + self.navframe.grid(column=0, row=10, sticky=N) + + self.master_pane() + + def master_pane(self): + """ + The home pane. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + self.logger.info("%s" % inspect.stack()[1][3]) + + self.mainframe = ttk.Frame(self.superframe, width=604, height=510) + self.mainframe.grid(column=0, row=2, sticky=(N, W, E, S)) + + self.mainframe.grid_rowconfigure(0, weight=1) + self.mainframe.grid_rowconfigure(5, weight=1) + self.mainframe.grid_columnconfigure(0, weight=1) + self.mainframe.grid_columnconfigure(2, weight=1) + + self.change_state_btn = ttk.Button(self.mainframe, width=20, text="Change State", command=self.change_state) + self.change_state_btn.grid(column=0, row=80, pady=4, columnspan=3) + self.change_state_btn.configure(state=self.state_button_state) + + self.info_status_label = ttk.Label(self.mainframe, text='Location of keyfile:') + self.info_status_label.grid(column=0, row=90, pady=8, columnspan=3) + + ttk.Button(self.mainframe, width=20, text="Retrieve from JSS Script", command=self.jss_pane).grid(column=0, row=100, pady=4, columnspan=3) + + ttk.Button(self.mainframe, width=20, text="Fetch from Remote Volume", command=self.remote_nav_pane).grid(column=0, row=200, pady=4, columnspan=3) + ttk.Button(self.mainframe, width=20, text="Retrieve from Local Volume", command=self.local_nav_pane).grid(column=0, row=300, pady=4, columnspan=3) + ttk.Button(self.mainframe, width=20, text="Enter Firmware Password", command=self.direct_entry_pane).grid(column=0, row=320, pady=4, columnspan=3) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=400, columnspan=3, sticky=(E, W), pady=8) + + hash_display = ttk.Entry(self.mainframe, width=58, textvariable=self.hashed_results) + hash_display.grid(column=0, row=450, columnspan=4) + + self.hash_btn = ttk.Button(self.mainframe, width=20, text="Copy hash to clipboard", command=self.copy_hash) + self.hash_btn.grid(column=0, row=500, pady=4, columnspan=3) + self.hash_btn.configure(state=self.hash_button_state) + + ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=700, columnspan=3, sticky=(E, W), pady=8) + + self.status_label = ttk.Label(self.mainframe, textvariable=self.status_string) + self.status_label.grid(column=0, row=2100, sticky=W, columnspan=2) + + ttk.Button(self.mainframe, text="Quit", width=6, command=self.root.destroy).grid(column=2, row=2100, sticky=E) + + def jss_pane(self): + """ + JAMF server interaction pane. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + self.mainframe.grid_remove() + + try: + if self.config_options["keyfile"]["remote_type"] == 'jamf': + if self.config_options["keyfile"]["server_path"]: + self.jamf_hostname.set(self.config_options["keyfile"]["server_path"]) + + if self.config_options["keyfile"]["username"]: + self.jamf_username.set(self.config_options["keyfile"]["username"]) + + if self.config_options["keyfile"]["password"]: + self.jamf_password.set(self.config_options["keyfile"]["password"]) + except: + pass + + self.jss_frame = ttk.Frame(self.superframe, width=604, height=510) + self.jss_frame.grid(column=0, row=2, sticky=(N, W, E, S)) + + self.jss_frame.grid_columnconfigure(0, weight=1) + self.jss_frame.grid_columnconfigure(1, weight=1) + self.jss_frame.grid_columnconfigure(2, weight=1) + self.jss_frame.grid_columnconfigure(3, weight=1) + + # 52? + beam_a = ttk.Button(self.jss_frame, width=5) + beam_a.grid(column=0, row=0, sticky=W) + beam_b = ttk.Button(self.jss_frame, width=20) + beam_b.grid(column=1, row=0, sticky=W) + beam_c = ttk.Button(self.jss_frame, width=10) + beam_c.grid(column=2, row=0, sticky=W) + beam_d = ttk.Button(self.jss_frame, width=16) + beam_d.grid(column=3, row=0, sticky=W) + beam_a.grid_remove() + beam_b.grid_remove() + beam_c.grid_remove() + beam_d.grid_remove() + + ttk.Label(self.jss_frame, text="Download keys from Jamf Pro FWPM script:").grid(column=0, row=100, columnspan=4, sticky=(E, W)) + # ttk.Separator(self.hash_frame, orient=HORIZONTAL).grid(row=120, columnspan=50, sticky=(E, W)) + + ttk.Label(self.jss_frame, text="Server:").grid(column=0, row=150, sticky=E) + hname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_hostname) + hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) + + ttk.Label(self.jss_frame, text="Username:").grid(column=0, row=200, sticky=E) + uname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_username) + uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) + + ttk.Label(self.jss_frame, text="Password:").grid(column=0, row=250, sticky=E) + pword_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_password, show="*") + pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) + + ttk.Button(self.jss_frame, text="Find Script", width=15, default='active', command=self.search_jss).grid(column=1, row=300, columnspan=2, pady=12) + + ttk.Separator(self.jss_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) + + ttk.Button(self.jss_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) + ttk.Button(self.jss_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) + + def direct_entry_pane(self): + """ + Directly enter fwpw. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + self.current_key = tkSimpleDialog.askstring("FW Password", "Enter firmware password:", show='*', parent=self.root) + + if self.current_key: + self.keys_loaded = True + self.calculate_hash() + self.status_string.set('Keys loaded successfully.') + self.keys_label['image'] = self.key_icon_photoimage + self.keys_loaded_string.set('Keys in memory.') + self.change_state_btn.configure(state="normal") + + else: + self.flush_keys() + self.status_string.set('Blank password entered.') + self.logger.error('Direct enter blank password.') + self.change_state_btn.configure(state="disabled") + + def remote_nav_pane(self): + """ + Connect to server and select keyfile. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + self.mainframe.grid_remove() + + try: + if self.config_options["keyfile"]["remote_type"] == 'smb': + if self.config_options["keyfile"]["server_path"]: + self.remote_hostname.set(self.config_options["keyfile"]["server_path"]) + + if self.config_options["keyfile"]["username"]: + self.remote_username.set(self.config_options["keyfile"]["username"]) + + if self.config_options["keyfile"]["password"]: + self.remote_password.set(self.config_options["keyfile"]["password"]) + except: + pass + + self.remote_nav_frame = ttk.Frame(self.superframe, width=604, height=510) + self.remote_nav_frame.grid(column=0, row=2, sticky=(N, W, E, S)) + + self.remote_nav_frame.grid_columnconfigure(0, weight=1) + self.remote_nav_frame.grid_columnconfigure(1, weight=1) + self.remote_nav_frame.grid_columnconfigure(2, weight=1) + self.remote_nav_frame.grid_columnconfigure(3, weight=1) + + ttk.Label(self.remote_nav_frame, text="Read keyfile from remote server: (ie smb://...)").grid(column=0, row=100, columnspan=4, sticky=(E, W)) + + ttk.Label(self.remote_nav_frame, text="Server path:").grid(column=0, row=150, sticky=E) + hname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_hostname) + hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) + + ttk.Label(self.remote_nav_frame, text="Username:").grid(column=0, row=200, sticky=E) + uname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_username) + uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) + + ttk.Label(self.remote_nav_frame, text="Password:").grid(column=0, row=250, sticky=E) + pword_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_password, show="*") + pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) + + ttk.Button(self.remote_nav_frame, text="Read keyfile", width=15, default='active', command=self.read_remote).grid(column=1, row=300, columnspan=2, pady=12) + + ttk.Separator(self.remote_nav_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) + + ttk.Button(self.remote_nav_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) + ttk.Button(self.remote_nav_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) + + def local_nav_pane(self): + """ + Select keyfile from local volume. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) + + if self.key_item: + self.status_string.set('Object found.') + self.handle_key_item() + else: + self.status_string.set('No object selected.') + + def search_jss(self): + """ + Search the JAMF server for FWPM Control script, strip out and categorize keyfile. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + try: + jss_search_url = self.jamf_hostname.get() + '/JSSResource/scripts' + headers = {'Accept': 'application/json', } + response = requests.get(url=jss_search_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) + + script_list = response.json() + + except requests.exceptions.HTTPError as this_error: + self.logger.error("http error %s: %s\n" % (response.status_code, this_error)) + + if response.status_code == 400: + self.logger.error("HTTP code {}: {}".format(response.status_code, "Request error.")) + elif response.status_code == 401: + self.logger.error("HTTP code {}: {}".format(response.status_code, "Authorization error.")) + elif response.status_code == 403: + self.logger.error("HTTP code {}: {}".format(response.status_code, "Permissions error.")) + elif response.status_code == 404: + self.logger.error("HTTP code {}: {}".format(response.status_code, "Resource not found.")) + + + for item in script_list['scripts']: + if 'FWPM Control' in item['name']: + target_id = item['id'] + + script_url = self.jamf_hostname.get() + '/JSSResource/scripts/id/' + str(target_id) + headers = {'Accept': 'application/json', } + response = requests.get(url=script_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) + + response_json = response.json() + + if response.status_code != 200: + self.logger.info("%i returned." % response.code) + return + + working_output = response_json['script']['script_contents'].split('\n') + self.previous_keys = [] + + for line in working_output: + if "'previous':" in line and '#' not in line: + try: + contents = re.findall(r'\s*\'previous\': \[(.*)\]', line) + if contents: + in_contents = contents[0].split(', ') + in_contents = [i for i in in_contents if i] + for item in in_contents: + subitem = item.split('"') + subitem = [i for i in subitem if i] + subitem = [i for i in subitem if i != ','] + + if subitem: + self.previous_keys.append(subitem[0]) + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + elif "'new':" in line and '#' not in line: + try: + contents = re.findall(r'\s*\'new\': (.*)', line) + if contents: + if len(contents) == 1: + contents = contents[0] + else: + quit() + subitem = contents.split('"') + subitem = [i for i in subitem if i] + # self.current_key = subitem[0] + self.current_key = subitem[0] + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + try: + self.calculate_hash() + self.status_string.set('Keys loaded successfully.') + self.keys_loaded_string.set('Keys copied to memory.') +# self.hash_button_state +# self.change_state_btn.configure(state="normal") + except Exception as exception_message: + self.logger.error(exception_message) + self.flush_keys() +# self.change_state_btn.configure(state="disabled") + + def local_fetch(self): + """ + Popup simple local navigation dialog. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) + + if self.key_item: + self.status_string.set('Object found.') + self.handle_key_item() + else: + self.status_string.set('No object selected.') + + def read_remote(self): + """ + Handle server connection, keyfile selection and remote volume dismount. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + tmp_directory = "/tmp/sk/mount" + if not os.path.exists(tmp_directory): + os.makedirs(tmp_directory) + + try: + self.logger.info("%s: %s" % (inspect.stack()[0][3], "Mounting")) + msb.mount_share_at_path_with_credentials(self.remote_hostname.get(), tmp_directory, self.remote_username.get(), self.remote_password.get()) + + self.key_item = tkFileDialog.askopenfilename(initialdir=tmp_directory, title="Remote object", message="Select remote object:", parent=self.root) + + self.logger.info("%s: %s" % (inspect.stack()[0][3], self.key_item)) + + if self.key_item: + self.status_string.set('Object found.') + self.handle_key_item() + else: + self.status_string.set('No object selected.') + + self.logger.info("%s: %s" % (inspect.stack()[0][3], "Dismounting")) + umount_results = subprocess.check_output(["/usr/sbin/diskutil", "unmount", tmp_directory]) + self.logger.info(umount_results) + + except Exception as exception_message: + self.logger.error(exception_message) + + def calculate_hash(self): + """ + Builds hash identical to FWPM binary. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + try: + hashed_key = hashlib.new('sha256') + hashed_key.update(self.current_key) + + for entry in sorted(self.previous_keys): + hashed_key.update(entry) + + fwpw_managed_string = hashed_key.hexdigest() + self.hashed_results.set(fwpw_managed_string) + + self.keys_label['image'] = self.key_icon_photoimage + + self.hash_btn.configure(state='normal') + self.change_state_btn.configure(state='normal') + + self.state_button_state = 'normal' + self.hash_button_state = 'normal' + + except Exception as exception_message: + self.logger.error(exception_message) + + self.flush_keys() + + def copy_hash(self): + """ + Provides single button to copy hash to clipboard. + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + os.system("echo '%s' | /usr/bin/pbcopy" % self.hashed_results.get()) + + def handle_key_item(self): + """ + attempts to open and parse selected keyfile + + plain text + obfuscated + + inside dmg --someday + inside encrypted dmg --someday + """ + + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + self.flush_keys() + + if os.path.exists(self.key_item): + item_filename = self.key_item.split('/')[-1] + item_extension = item_filename.split('.')[-1] + + if item_extension == 'plist': + passwords = [] + try: + keyfile_plist = plistlib.readPlist(self.key_item) + + content_raw = keyfile_plist["data"] + content_raw = base64.b64decode(content_raw) + content_raw = content_raw.split(",") + content_raw = [x for x in content_raw if x] + + for item in content_raw: + label, pword = item.split(':') + pword = base64.b64decode(pword) + + if label == 'new': + self.current_key = pword + else: + self.previous_keys.append(pword) + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + return + + elif item_extension == 'txt': + try: + with open(self.key_item, "r") as keyfile: + passwords = keyfile.read().splitlines() + + for item in passwords: + label, pword = item.split(':') + + if label == 'new': + self.current_key = pword + else: + self.previous_keys.append(pword) + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + return + + else: + self.logger.error("%s: Error parsing keyfile." % (inspect.stack()[0][3])) + + self.calculate_hash() + + else: + # print('no key item') + # print(self.key_item) + pass + + def injest_config(self): + """ + attempts to consume and format configuration file + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + running_pathname = os.path.abspath(os.path.dirname(sys.argv[0])) + self.logger.info("%s: Application pathname: %s" % (inspect.stack()[0][3], running_pathname)) + + config_name = '/fwpm_config.ini' + config_path = '' + + if os.path.exists(pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name): + config_path = pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name + else: + if ".app/Contents" in running_pathname: + running_root = '/'.join(running_pathname.split('/')[0:-3]) + self.logger.info("%s: Application root folder: %s" % (inspect.stack()[0][3], running_root)) + + if os.path.exists(running_root + config_name): + config_path = running_root + config_name + else: + if os.path.exists(running_pathname + config_name): + config_path = running_pathname + config_name + + if not config_path: + return + + self.logger.info("Configuration file: %s" % config_path) + if not os.access(config_path, os.R_OK): + self.logger.critical("Unable to access config file, check privileges.") + return + + config = ConfigParser.SafeConfigParser(allow_no_value=True) + config.read(config_path) + + self.config_options["flags"] = {} + self.config_options["keyfile"] = {} + self.config_options["logging"] = {} + self.config_options["slack"] = {} + + for section in ["flags", "keyfile", "logging", "slack"]: + for item in config.options(section): + if item.startswith("use_"): +# if "use_" in item: + try: + self.config_options[section][item] = config.getboolean(section, item) + except Exception as exception_message: + self.config_options[section][item] = False + self.logger.error("%s: Invalid/Blank value: %s:%s. [%s]" % (inspect.stack()[0][3], section, item, exception_message)) + elif "path" in item: + self.config_options[section][item] = config.get(section, item) + else: + self.config_options[section][item] = config.get(section, item) + + self.logger.info("Configuration file variables:") + for key, value in self.config_options.items(): + self.logger.info(key) + for sub_key, sub_value in value.items(): + self.logger.info("\t%s %r" % (sub_key, sub_value)) + + def change_state(self): + """ + Handles toggling of FWPW + """ + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + current_password = '' + known_current_password = False + + new_fw_tool_path = '/usr/sbin/firmwarepasswd' + new_fw_tool_exists = os.path.exists(new_fw_tool_path) + + if not new_fw_tool_exists: + self.logger.critical("firmwarepasswd tool not found.") + + full_keylist = self.previous_keys + full_keylist.append(self.current_key) + + if self.fwpw_status == 'On': + self.status_string.set('Attempting to find current password...') + new_fw_tool_cmd = [new_fw_tool_path, '-verify'] + self.logger.info(' '.join(new_fw_tool_cmd)) + + for index in reversed(xrange(len(full_keylist))): + + try: + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -verify']) + + exit_condition = False + while not exit_condition: + result = child.expect(['Password:', 'password:', 'Correct', 'Incorrect', pexpect.EOF, pexpect.TIMEOUT]) + + if result == 0: + child.sendline(self.admin_password) + elif result == 1: + child.sendline(full_keylist[index]) + elif result == 2: + current_password = full_keylist[index] + known_current_password = True + self.status_string.set('local password found.') + self.logger.info('local password found.') + break + elif result == 3: + # self.logger.info('#3.') + break + elif result == 4: + # self.logger.info('#4.') + break + elif result == 5: + # self.logger.info('#5.') + break + else: + self.logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) + return + + if known_current_password: + + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -delete']) + result = child.expect('Password:') + + if result == 0: + child.sendline(self.admin_password) + + result = child.expect('password:') + if result == 0: + child.sendline(current_password) + + result = child.expect(['NOTE', 'ERROR']) + if result == 0: + self.logger.info('Off.') + elif result == 1: + self.logger.info('PW incorrect.') + else: + self.logger.info('Error turning off.') + + try: + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram -d fwpw-hash']) + result = child.expect('Password:') + + if result == 0: + child.sendline(self.admin_password) + + result = child.expect('') + if result == 0: + self.logger.info('removed nvram.') + elif result == 1: + self.logger.info('nvrmam oops 1') + else: + self.logger.info('nvram oops 2') + + else: + pass + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + self.lock_label['image'] = self.double_exclamation_icon_photoimage + self.state_string.set('FW password removed, reboot!') + self.slack_message("_*" + self.local_identifier + "*_ :unlock:\n" + "FWPW and nvram entry removed.", '', 'info') + + break + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + else: # self.fwpw_status == 'Off' + + # ~/Box Sync/working stuff @ box/FWPM/skeleton key 4:48pm root@t-mcdaniel-mac-laptop #170 ]firmwarepasswd -setpasswd + # Setting Firmware Password + # Enter password: + # Enter new password: + # Re-enter new password: + # ERROR | setPasswdFromCommandLine | Unable to verify password + # ERROR | main | Exiting with error: 4 + + self.logger.info("Setting FW password") + + self.logger.info("Using %s" % self.current_key) + if not self.current_key: + self.logger.error('Blank key.') + return + + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -setpasswd']) + result = child.expect('Password:') + + if result == 0: + child.sendline(self.admin_password) + + result = child.expect('password:') + if result == 0: + child.sendline(self.current_key) + else: + pass + + result = child.expect('new password:') + if result == 0: + child.sendline(self.current_key) + + result = child.expect(['NOTE', 'ERROR']) + if result == 0: + self.logger.info('On.') + elif result == 1: + self.logger.info('PW incorrect.') + else: + self.logger.info('Error turning off.') + + try: + + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram fwpw-hash=2:' + self.hashed_results.get()]) + result = child.expect('Password:') + + if result == 0: + child.sendline(self.admin_password) + + result = child.expect('') + if result == 0: + self.logger.info('added nvram.') + elif result == 1: + self.logger.info('nvrmam oops 3') + else: + self.logger.info('nvram oops 4') + + else: + pass + + except Exception as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + self.status_string.set('Password activated. Reboot!') + self.state_string.set('FW password activated, reboot!') + self.lock_label['image'] = self.shield_icon_photoimage + self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW and hash updated.", '', 'info') + + def flush_keys(self): + """ + Erase loaded keys, reset UI. + """ + # "secure" erase keys + # update label + # deactivate button(s) + # change icon + + self.previous_keys = [] + self.current_key = '' + + self.keys_label['image'] = self.balloon_icon_photoimage + self.keys_loaded_string.set('No keys in memory') + self.hashed_results.set('') + + self.state_button_state = 'disabled' + self.hash_button_state = 'disabled' + + self.hash_btn.configure(state='disabled') + self.change_state_btn.configure(state='disabled') + + def slack_message(self, message, icon, msg_type): + """ + Sends slack messages. + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + slack_info_channel = False + slack_error_channel = False + + if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_info_url"]: + slack_info_channel = True + + if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_error_url"]: + slack_error_channel = True + + if slack_error_channel and msg_type == 'error': + slack_url = self.config_options["slack"]["slack_error_url"] + elif slack_info_channel: + slack_url = self.config_options["slack"]["slack_info_url"] + else: + return + + payload = {'text': message, 'username': 'Skeleton Key ' + self.master_version, 'icon_emoji': ':old_key:'} + + response = requests.post(slack_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) + + self.logger.info('Response: ' + str(response.text)) + self.logger.info('Response code: ' + str(response.status_code)) + + def slack_optionator(self): + """ + Builds the local identifier string per configuration file. + + + ip, mac, hostname + computername + serial + + """ + if self.logger: + self.logger.info("%s: activated" % inspect.stack()[0][3]) + + if self.verify_network(): + try: + full_ioreg = subprocess.check_output(['ioreg', '-l']).decode('utf-8') + serial_number_raw = re.findall('\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) + serial_number = serial_number_raw[0] + + if self.config_options["slack"]["slack_identifier"].lower() == 'ip' or self.config_options["slack"]["slack_identifier"].lower() == 'mac' or self.config_options["slack"]["slack_identifier"].lower() == 'hostname': + processed_device_list = [] + + # Get ordered list of network devices + base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]).decode('utf-8') + network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) + ether_up_list = subprocess.check_output(["/sbin/ifconfig", "-au", "ether"]).decode('utf-8') + for device in network_device_list: + device_name = device[0] + port_name = device[1] + try: + if port_name in ether_up_list: + device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]).decode('utf-8') + mac_address = re.findall('ether (.*) \n', device_info_raw) + ether_address = re.findall('inet (.*) netmask', device_info_raw) + +# if len(ether_address) and len(mac_address): + if ether_address and mac_address: + processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) + except Exception as this_exception: + self.logger.error("error discovering device info. [%s]" % this_exception) + + + if processed_device_list: + if self.logger: + self.logger.info("1 or more active IP addresses. Choosing primary.") + + if self.config_options["slack"]["slack_identifier"].lower() == 'ip': + self.local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + elif self.config_options["slack"]["slack_identifier"].lower() == 'mac': + self.local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" + elif self.config_options["slack"]["slack_identifier"].lower() == 'hostname': + try: + self.local_identifier = socket.getfqdn() + except Exception as exception_message: + if self.logger: + self.logger.error("error discovering hostname. [%s]" % exception_message) + self.local_identifier = serial_number + + else: + if self.logger: + self.logger.error("error discovering IP info.") + self.local_identifier = serial_number + + elif self.config_options["slack"]["slack_identifier"].lower() == 'computername': + try: + cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) + self.local_identifier = cname_identifier_raw.split('\n')[0] + if self.logger: + self.logger.info("Computername: %r" % self.local_identifier) + except Exception as exception_message: + if self.logger: + self.logger.info("error discovering computername. [%s]" % exception_message) + self.local_identifier = serial_number + elif self.config_options["slack"]["slack_identifier"].lower() == 'serial': + self.local_identifier = serial_number + if self.logger: + self.logger.info("Serial number: %r" % self.local_identifier) + else: + if self.logger: + self.logger.info("bad or no identifier flag, defaulting to serial number.") + self.local_identifier = serial_number + + except Exception as this_exception: + self.logger.error("error verifying network. [%s]" % exception_message) + self.config_options["slack"]["use_slack"] = False + else: + self.config_options["slack"]["use_slack"] = False + if self.logger: + self.logger.info("No network detected.") + + def verify_network(self): + """ + Verifies network availability. + + Host: 8.8.8.8 (google-public-dns-a.google.com) + OpenPort: 53/tcp + Service: domain (DNS/TCP) + """ + + try: + _ = requests.get("https://dns.google.com", timeout=3) + # _ = requests.get("https://8.8.8.8", timeout=3) + return True + except requests.ConnectionError as exception_message: + self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + return False + + +def login(root, logger): + """ + aquire admin password + """ + logger.info("%s: activated" % inspect.stack()[0][3]) + + try: + root.withdraw() + + if platform.system() == 'Darwin': + tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' + script = tmpl.format(os.getpid()) + _ = subprocess.check_call(['/usr/bin/osascript', '-e', script]) + + password = tkSimpleDialog.askstring("Password", "Enter admin password:", show='*', parent=root) + + if not password: + logger.error("%s: Canceled login." % (inspect.stack()[0][3])) + return + + cmd_output = [] + try: + child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -check']) + + exit_condition = False + while not exit_condition: + result = child.expect(['WARNING:', '\n\nPass', 'Password:', 'attempts', pexpect.EOF, pexpect.TIMEOUT]) + + cmd_output.append(child.before) + cmd_output.append(child.after) + if result == 0: + continue + elif result == 1: + child.sendline(password) + elif result == 2: + child.sendline(password) + elif result == 3: + logger.error("%s: Incorrect admin password." % (inspect.stack()[0][3])) + sys.exit() + elif result == 4: + exit_condition = True + elif result == 5: + exit_condition = True + else: + logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) + return + except Exception as exception_message: + logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) + + # + # begin parsing out useful content + checked_output = [] + for value in cmd_output: + if isinstance(value, basestring): + if "System\r\nAdministrator." in value: + pass + elif "WARNING" in value: + pass + elif "Improper" in value: + pass + elif value == '\r\n': + pass + elif not value: + pass + elif value == "Password:": + pass + else: + checked_output.append(value) + + for item in checked_output: + if 'Enabled' in item: + if 'Yes' in item: + logger.info("Yes. %r" % item) + return password, 'On' + else: + logger.info("No. %r" % item) + return password, 'Off' + else: + sys.exit() + + except ValueError: + logger.error("%s: Error here." % (inspect.stack()[0][3])) + return + + sys.exit() + + +def main(): + """ + Entry into script. + """ + master_version = "1.0" + + logging.basicConfig(filename='/tmp/skeleton_key_v' + master_version + '.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + logger = logging.getLogger(__name__) + logger.info("Running Skeleton Key " + master_version) + + root = Tk() + + try: + admin_password, fwpw_status = login(root, logger) + except Exception as exception_message: + logger.error("%s: Error logging in. [%s]" % (inspect.stack()[0][3], exception_message)) + sys.exit(0) + + root.deiconify() + SinglePane(root, logger, admin_password, fwpw_status, master_version) + + root.mainloop() + + +if __name__ == '__main__': + main() diff --git a/skeleton key/sk_icon.icns b/skeleton key/sk_icon.icns new file mode 100644 index 0000000..f40b578 Binary files /dev/null and b/skeleton key/sk_icon.icns differ diff --git a/skeleton key/skeleton key prebuilt binary/Skeleton Key.zip b/skeleton key/skeleton key prebuilt binary/Skeleton Key.zip new file mode 100644 index 0000000..ddb4727 Binary files /dev/null and b/skeleton key/skeleton key prebuilt binary/Skeleton Key.zip differ