├── .gitignore ├── FWPM prebuilt binary └── firmware_password_manager.zip ├── JSS integration ├── JSS EA for FWPM.py └── JSS FWPM controller script.py ├── LICENSE ├── README.md ├── additional resources ├── example_keyfile.txt └── obfuscate_keylist.py ├── firmware_password_manager.py ├── img ├── direct_entry_trimmed.png ├── hash_image.png ├── hashed_keys.png ├── jamf_fetch_trimmed.png ├── jss_ea.png ├── jss_not_current.png ├── jss_smart_group.png ├── no_keys.png ├── remote_fetch_trimmed.png ├── sk_help.png ├── sk_login.png ├── sk_os_alert.png ├── sk_ui.png ├── slack_example.png └── yes_keys.png ├── setup.py └── skeleton key ├── Skeleton_Key.py ├── sk_icon.icns └── skeleton key prebuilt binary └── Skeleton Key.zip /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Custom 60 | original 61 | original/* 62 | -------------------------------------------------------------------------------- /FWPM prebuilt binary/firmware_password_manager.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/FWPM prebuilt binary/firmware_password_manager.zip -------------------------------------------------------------------------------- /JSS integration/JSS EA for FWPM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import subprocess 4 | 5 | fwpw_hash_raw = subprocess.check_output(["/usr/sbin/nvram", "fwpw-hash"]) 6 | fwpw_hash_raw = fwpw_hash_raw.split('\n')[0] 7 | fwpw_hash = fwpw_hash_raw.split('\t')[1] 8 | fwpw_version = fwpw_hash.split(':')[0] 9 | 10 | if fwpw_version == "2": 11 | print ""+fwpw_hash+"" 12 | else: 13 | print "Bad" 14 | -------------------------------------------------------------------------------- /JSS integration/JSS FWPM controller script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Script to remotely control and configure firmware password manager. 5 | """ 6 | # Copyright (c) 2020 University of Utah Student Computing Labs. ################ 7 | # All Rights Reserved. 8 | # 9 | # Permission to use, copy, modify, and distribute this software and 10 | # its documentation for any purpose and without fee is hereby granted, 11 | # provided that the above copyright notice appears in all copies and 12 | # that both that copyright notice and this permission notice appear 13 | # in supporting documentation, and that the name of The University 14 | # of Utah not be used in advertising or publicity pertaining to 15 | # distribution of the software without specific, written prior 16 | # permission. This software is supplied as is without expressed or 17 | # implied warranties of any kind. 18 | ################################################################################ 19 | 20 | # FWPM_control.py ############################################################## 21 | # 22 | # 23 | # 24 | # 0.8.0 2019.11.13 Complete rewrite. FWPM built with pyinstall and kept 25 | # on client machines, Control script holds keylist and 26 | # config file, writes files and launches FWPM. tjm 27 | # 28 | # 1.0.0 2020.01.23 Initial public release. tjm 29 | # 30 | ################################################################################ 31 | 32 | from __future__ import division 33 | from __future__ import print_function 34 | import base64 35 | import logging 36 | import inspect 37 | import subprocess 38 | 39 | __version__ = '1.0' 40 | 41 | KEYFILE = { 42 | 'previous': ["oneOldPassword", "AnotherPasswordWeUsed"], 43 | 'new': "myNewFWPW!" 44 | } 45 | 46 | FWPM_CONFIG = { 47 | 'flags': { 48 | 'use_fwpw': True, 49 | 'management_string_type': 'hash', 50 | 'custom_string': '', 51 | 'use_reboot_on_exit': False, 52 | 'path_to_fw_tool': '', 53 | 'use_test_mode': False, 54 | }, 55 | 56 | 'keyfile': { 57 | 'path': '/tmp/current_fwpw.txt', 58 | 'use_obfuscation': True, 59 | }, 60 | 61 | 'logging': { 62 | 'use_logging': True, 63 | 'log_path': '/var/log/fwpm_controller.log', 64 | }, 65 | 66 | 'slack': { 67 | 'use_slack': True, 68 | 'slack_identifier': 'hostname', 69 | 70 | 'slack_info_url': 'https://hooks.slack.com/services/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', 71 | 'slack_error_url': 'https://hooks.slack.com/services/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', 72 | } 73 | } 74 | 75 | 76 | def prepare_keyfile(logger, cleartext): 77 | """ 78 | Convert keyfile into format FWPM expects and obfuscate results. 79 | """ 80 | if FWPM_CONFIG['logging']['use_logging']: 81 | logger.info("%s: activated" % inspect.stack()[0][3]) 82 | 83 | obfuscated_string = "" 84 | sanity_check_new = False 85 | sanity_check_previous = False 86 | 87 | # sanity check cleartext! 88 | logger.info("sanity check new.") 89 | if not isinstance(cleartext['new'], str): 90 | logger.critical("New password improperly defined.") 91 | sanity_check_new = False 92 | else: 93 | sanity_check_new = True 94 | 95 | logger.info("sanity check previous.") 96 | if len(cleartext['previous']) <= 1: 97 | logger.critical("No previous password defined.") 98 | sanity_check_previous = False 99 | else: 100 | sanity_check_previous = True 101 | 102 | if not sanity_check_new or not sanity_check_previous: 103 | logger.critical("sanity check failure.") 104 | return None 105 | else: 106 | logger.info("Sanity check successful.") 107 | 108 | encoded_comment = base64.b64encode('old'.encode('utf-8')) 109 | 110 | for item in cleartext['previous']: 111 | tmp_item = base64.b64encode(item.encode('utf-8')) 112 | tmp_string = '#'.encode('utf-8') + encoded_comment + ':'.encode('utf-8') + tmp_item + ','.encode('utf-8') 113 | obfuscated_string = obfuscated_string + tmp_string.decode('utf-8') 114 | 115 | tmp_item = base64.b64encode(cleartext['new'].encode('utf-8')) 116 | tmp_string = base64.b64encode('new'.encode('utf-8')) + ':'.encode('utf-8') + tmp_item 117 | obfuscated_string += str(tmp_string.decode('utf-8')) 118 | 119 | return base64.b64encode(obfuscated_string.encode('utf-8')) 120 | 121 | 122 | def main(): 123 | """ 124 | This should not be blank. 125 | """ 126 | 127 | if FWPM_CONFIG['logging']['use_logging']: 128 | logging.basicConfig(filename=FWPM_CONFIG['logging']['log_path'], level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 129 | logger = logging.getLogger(__name__) 130 | 131 | logger.info("fwpm controller launched.") 132 | logger.info("fwpm controller version {}".format(__version__)) 133 | 134 | configuraton_file_path = '/tmp/cfg.cfg' 135 | 136 | obfuscated_text = prepare_keyfile(logger, KEYFILE) 137 | 138 | if obfuscated_text is None: 139 | logger.critical("Exiting on sanity check failure.") 140 | quit() 141 | 142 | with open(FWPM_CONFIG['keyfile']['path'], 'w') as output_file: 143 | output_file.write((obfuscated_text.decode('utf-8'))) 144 | 145 | with open(configuraton_file_path, 'w') as writer: 146 | for k in FWPM_CONFIG: 147 | writer.write("[" + k + "]" + "\n") 148 | for k2 in FWPM_CONFIG[k]: 149 | writer.write(k2 + ": " + str(FWPM_CONFIG[k][k2]) + "\n") 150 | 151 | # launch fwpm 152 | logger.info("launching fwpm.") 153 | 154 | try: 155 | _ = subprocess.check_output(["/usr/local/sbin/firmware_password_manager", "-c", configuraton_file_path]) 156 | except Exception as exception_message: 157 | print(exception_message) 158 | logger.critical(exception_message) 159 | 160 | 161 | 162 | if __name__ == '__main__': 163 | main() 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 University of Utah, Marriott Library, Client Platform Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Firmware Password Manager 2.5 4 | 5 | A Python script to help Macintosh administrators manage the firmware passwords of their computers. 6 | 7 | ## Contents 8 | 9 | * [Download](#download) - get the .dmg 10 | * [Contact](#contact) 11 | * [System Requirements](#system-requirements) 12 | * [Install](#install) 13 | * [Uninstall](#uninstall) 14 | * [Purpose](#purpose) 15 | * [Why set the Firmware Password?](#why-set-the-firmware-password) 16 | * [Firmware Password Manager](#firmware-password-manager) 17 | * [How FWPM keeps track of the current password](#how-fwpm-keeps-track-of-the-current-password) 18 | * [Usage](#usage) 19 | * [Options](#options) 20 | * [The configuration file](#the-configuration-file) 21 | * [Flags](#flags) 22 | * [Keyfile](#keyfile) 23 | * [Logging](#logging) 24 | * [Slack](#slack) 25 | * [The keyfile](#the-keyfile) 26 | * [Security](#security) 27 | * [Slack integration](#slack-integration) 28 | * [nvram string](#nvram) 29 | * [firmwarepasswd](#firmwarepasswd) 30 | * [Common error messages](#common-error-messages) 31 | * [JAMF Integration](#jamf-integration) 32 | * [JAMF FWPM installation policy](#jamf-fwpm-installation-script) 33 | * [JAMF FWPM controller script](#jamf-fwpm-controller-script) 34 | * [Editing the keylist variables](#editing-the-keylist-variables) 35 | * [Editing the configuration file variables](#editing-the-configuration-file-variables) 36 | * [JAMF JSS extention attribute](#jamf-jss-extention-attribute) 37 | * [Skeleton Key](#skeleton-key) 38 | * [Using Skeleton Key](#using-skeleton-key) 39 | * [Current State](#current-state) 40 | * [Location](#location) 41 | * [Retrieve from JSS Script](#retrieve-from-jss-script) 42 | * [Fetch from Remote Volume](#fetch-from-remote-volume) 43 | * [Retrieve from Local Volume](#retrieve-from-local-volume) 44 | * [Enter Firmware Password](#enter-firmware-password) 45 | * [Keyfile Hash](#keyfile-hash) 46 | * [Status Messages](#status-messages) 47 | * [Rebuilding the binary](#rebuilding-the-binary) 48 | * [Notes](#notes) 49 | * [Update History](#update-history) 50 | 51 | ## Download 52 | 53 | [Download the latest installer here!](../../releases/) 54 | 55 | ## Contact 56 | 57 | If you have any comments, questions, or other input, either [file an issue](../../issues) or [send us an email](mailto:mlib-its-mac-github@lists.utah.edu). Thanks! 58 | 59 | ## System Requirements 60 | 61 | * Python 3.7+ (which you can download [here](https://www.python.org/download/)) 62 | * Pexpect 3.3+ (which you can download [here](https://github.com/pexpect/pexpect)) 63 | * Pyinstaller (which you can download [here](https://www.pyinstaller.org/)) *Only needed if you wish to rebuild either binary.* 64 | 65 | ## Install 66 | 67 | Place the script (or binary) in a root-executable location. We use `/usr/local/sbin/`. Make sure it is executable. 68 | 69 | ## Uninstall 70 | 71 | Remove the script/binary/application. 72 | 73 | ## Purpose 74 | 75 | ### Why set the Firmware Password? 76 | In a nutshell, the firmware password in Apple computers prevents non-privileged users from booting from a foreign device. 77 | 78 | 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. 79 | 80 | 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. 81 | 82 | ### Firmware Password Manager 83 | 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. 84 | 85 | `firmware_password_manager.py` will use your keyfile to set a firmware password on a machine with no existing firmware password, attempt to change the existing firmware password to your new password or remove the current password. The script is best used when it can be installed and left on the machine for future use. This allows the admin to then create an installer package containing the keyfile and a postflight action to run FWPM. Or the admin could create a launchagent to run FWPM at every boot 86 | 87 | Version 2 represents a complete rewrite of Firmware Password Manager (FWPM). The previous version, a shell script, always felt brittle to me. The new version is written in Python. I also focused on utilizing `firmwarepasswd`, rather than the outdated `setregproptool`. 88 | 89 | ### How FWPM keeps track of the current password 90 | 91 | When FWPM successfully sets or changes the firmware password it computes a hash based on the contents of the keyfile and stores the results in nvram. When FWPM is run again on a client it will compare the hash of the current keyfile and the hash stored on the machine, if they are different it will signal the need to change the firmware password to a new value. 92 | 93 | A hash can be thought of as the finger print of a file. The goal of a hash function is that no two files will share the same hash value. FWPM uses SHA-256 to generate the hash, or the SHA-2 hash function at a length of 256 bits (32 bytes). Rather than simply hashing the password itself, FWPM hashes the entire keyfile for additional security. 94 | 95 | ![ScreenShot](img/hash_image.png) 96 | 97 | ## Usage 98 | 99 | ``` 100 | firmware_password_manager [-h -v] [-c /path/to/configfile] 101 | ``` 102 | 103 | 104 | 105 | ### Options 106 | 107 | Flag | Purpose 108 | --------|--------- 109 | `-h`, `--help` | Prints help information and quits. 110 | `-v`, `--version` | Prints version information and quits. 111 | `-c CONFIGFILE`, `--configfile CONFIGFILE` | Specifies the location of the required configuration file. 112 | 113 | ### The configuration file 114 | 115 | Here is the default configuration file included with FWPM: 116 | 117 | ``` 118 | [flags] 119 | use_firmwarepassword: yes 120 | management_string_type: hash 121 | custom_string: 122 | use_reboot_on_exit: 123 | 124 | [keyfile] 125 | path: /YourVolume/this/location/example_keyfile.txt 126 | use_obfuscation: No 127 | remote_type: smb 128 | server_path: server_path 129 | username: user_name 130 | password: pass_word 131 | 132 | [logging] 133 | use_logging: true 134 | log_path: /tmp 135 | 136 | [slack] 137 | use_slack: true 138 | slack_identifier: hostname 139 | 140 | slack_info_url: https://hooks.slack.com/services/aaa/bbb/ccc 141 | slack_info_channel: #fwpw_manager_info 142 | slack_info_bot_name: FWPM informational message 143 | 144 | slack_error_url: https://hooks.slack.com/services/xxx/yyy/zzz 145 | slack_error_channel: #fwpw_manager_errors 146 | slack_error_bot_name: FWPM error message 147 | ``` 148 | 149 | The configuration file is broken up into sections roughly approximating the command line flags used in the previous versions. 150 | 151 | 152 | 153 | #### Flags 154 | 155 | Name | Type|Purpose 156 | --------|---------|--------- 157 | use_firmwarepassword|Boolean|Turn FW password on or off. 158 | management_string_type|String|Select the type of management string to use: hash or custom_string. Hash will be used if no value is present. custom_string will use the value in the following variable. 159 | custom_string|String|If you elect to use custom_string, enter it here. 160 | use_reboot_on_exit|Boolean|If you want FWPM to force an immediate reboot on success. 161 | 162 | 163 | #### Keyfile 164 | Name | Type|Purpose 165 | --------|---------|--------- 166 | path| String |Path of the keyfile to be used. 167 | use_obfuscation| Boolean |Use keyfile obfuscation. 168 | remote_type| String |The remote server address to be used. 169 | server_path| String |The path of the keyfile on the server. 170 | username| String |User with adequate privileges on the server to access the keyfile. 171 | password| String |Password for the user. 172 | 173 | #### Logging 174 | Name | Type|Purpose 175 | --------|---------|--------- 176 | use_logging|Boolean|Create a log file for FWPM. 177 | log_path|String|Path of the log file. 178 | 179 | #### Slack 180 | Name | Type|Purpose 181 | --------|---------|--------- 182 | use_slack|Boolean|Use Slack messaging. 183 | slack_identifier|String|Method of identifying the message sender on Slack. 184 | || 185 | slack_info_url|String|Slack URL for informational messages. 186 | || 187 | slack_error_url|String|Slack URL for error messages. If not defined messages will be directed to the info channel. 188 | 189 | 190 | 191 | ### The keyfile 192 | 193 | 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. 194 | 195 | 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.** 196 | 197 | Here is the keyfile format: 198 | 199 | Notes | Purpose 200 | --------|--------- 201 | new|the new password to be installed. 202 | note|any other note strings cause the password to be treated as previously used. 203 | #new|a hash mark will cause the password to be treated as a previously used entry. 204 | 205 | Here's an example keyfile: 206 | ``` 207 | previous:mGoBlue 208 | other:brownCow 209 | #new:short3rPasswd 210 | new:goUtes 211 | ``` 212 | Version 1 made use of the `current` note to designate what was thought to be current password. Version 2 discovers the current password on its own and will ignore this note. 213 | 214 | ### Security 215 | 216 | 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. 217 | 218 | ### Slack integration 219 | 220 | 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 221 | 222 | This image shows example FWPM messages in Slack: 223 | 224 | ![ScreenShot](img/slack_example.png) 225 | 226 | 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. 227 | 228 | String|Purpose 229 | -------|----------- 230 | IP|The IP address of the machine is used. Previous default. 231 | MAC|The MAC address of the current device is used. 232 | computername|The computername is used. 233 | hostname|The fully qualified domain name is used. 234 | serial|The machines serial number is used. If an error occurs discovering the previous methods, FWPM will fall back to this method. 235 | 236 | ### nvram string 237 | 238 | 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. 239 | 240 | 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). 241 | 242 | ### firmwarepasswd 243 | 244 | 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. 245 | 246 | ### Common error messages 247 | 248 | message|description 249 | -------|----------- 250 | Keyfile does not exist.| The script was not able to find the keyfile define by the user, check the path again. 251 | No Firmware password tool available.|The script was unable to find the firmwarepasswd tool. Check that it has not been moved. 252 | Asked to delete, no password set.|The user selected the -r/--remove flag to remove the firmware password, but no firmware password is set. 253 | Malformed keyfile key:value format required.|The keyfile is not properly formatted. Follow the instructions above. 254 | Multiple new keys.|multiple passwords are defined as new in the keyfile, you will need to comment or rename the additional new keys. 255 | No `new` key.|No password in the keyfile has the `new` note, you will need to properly identify the password you wish the use. 256 | Asked to delete, current password not accepted.|For this error to appear something very odd and unexpected has happened, contact the author. 257 | Bad response from firmwarepasswd.|This is a catchall error stating that firmwarepasswd encountered an error. 258 | Current FW password not in keyfile.| This is an critical message that the keyfile does not contain the current password. 259 | nvram reported error.|This is a catchall error stating that nvram encountered an error. 260 | 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. 261 | 262 | ## JAMF Integration 263 | 264 | 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. 265 | 266 | ### JAMF FWPM installation policy 267 | 268 | You will need a policy to ensure that the FWPM binary is installed on each of your client machines. 269 | 270 | ### JAMF FWPM controller script 271 | 272 | 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. 273 | 274 | #### Editing the keylist variables 275 | 276 | ```python 277 | KEYFILE = { 278 | 'previous': ["oneOldPassword", "AnotherPasswordWeUsed"], 279 | 'new': "myNewFWPW!" 280 | } 281 | ``` 282 | 283 | #### Editing the configuration file variables 284 | 285 | ```python 286 | FWPM_CONFIG = { 287 | 'flags': { 288 | 'use_fwpw': True, 289 | 'management_string_type': 'hash', 290 | 'custom_string': '', 291 | 'use_reboot_on_exit': False, 292 | 'path_to_fw_tool': '', 293 | 'use_test_mode': False, 294 | }, 295 | 296 | 'keyfile': { 297 | 'path': '/tmp/current_fwpw.txt', 298 | 'use_obfuscation': True, 299 | }, 300 | 301 | 'logging': { 302 | 'use_logging': True, 303 | 'log_path': '/var/log/fwpm_controller.log', 304 | }, 305 | 306 | 'slack': { 307 | 'use_slack': True, 308 | 'slack_identifier': 'hostname', 309 | 310 | 'slack_info_url': 'https://hooks.slack.com/services/aaaa/bbbbb/cccc', 311 | 'slack_error_url': 'https://hooks.slack.com/services/dddd/eeee/ffff', 312 | } 313 | } 314 | ``` 315 | 316 | Please see the previous discussion of the configuration file for info on these values. 317 | 318 | ### JAMF JSS extention attribute 319 | 320 | We can leverage the nvram string and smart groups in JAMF Casper to automate the distribution of an updated keyfile package and direct clients to change their firmware passwords. We do this by defining an extension attribute (EA) in the JSS. We've included the script we run in the repository for FWPM 2.0. 321 | 322 | The EA script runs during recon and pushes the hash up to the JSS. We then define a smart group that contains any machine not sharing the same hash as the current keyfile. This makes it possible to apply a policy directing those machines to download the new keyfile package and run FWPM. 323 | 324 | The following image shows the EA page in the JSS: 325 | 326 | ![ScreenShot](img/jss_ea.png) 327 | 328 | This image shows the two possible smart group built using the EA: 329 | 330 | ![ScreenShot](img/jss_smart_group.png) 331 | 332 | Here is how the smart groups are built: 333 | 334 | ![ScreenShot](img/jss_not_current.png) 335 | 336 | ## Skeleton Key 337 | 338 | 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. 339 | 340 | ![sk_ui](img/sk_ui.png) 341 | 342 | ### Using Skeleton Key 343 | 344 | Upon launching Skeleton Key, you must enter a valid administrator password to proceed. 345 | 346 | sk_login 347 | 348 | You may also see the following message, press OK to continue. 349 | 350 | sk_os_alert 351 | 352 | This is the main interface for Skeleton Key. It can be broken down into three areas: Current State, Modifiers and Status messages. 353 | 354 | sk_os_alert 355 | 356 | 357 | 358 | #### Current State 359 | 360 | 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. 361 | 362 | no_keys 363 | 364 | 365 | 366 | yes_keys 367 | 368 | #### Location 369 | 370 | 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. 371 | 372 | ##### Retrieve from JSS Script 373 | 374 | 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. 375 | 376 | ##### jamf_fetch_trimmed 377 | 378 | ##### Fetch from Remote Volume 379 | 380 | 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. 381 | 382 | ##### remote_fetch_trimmed 383 | 384 | ##### Retrieve from Local Volume 385 | 386 | A standard MacOS X Open File dialog will appear allowing you to navigate to your keyfile. 387 | 388 | ##### Enter Firmware Password 389 | 390 | This option allows you to directly enter the current or new firmware password of the machine. 391 | 392 | direct_entry_trimmed 393 | 394 | #### Keyfile hash 395 | 396 | 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. 397 | 398 | hashed_keys 399 | 400 | #### Status Messages 401 | 402 | Any informational or error messages will appear in this location. 403 | 404 | 405 | 406 | ### Rebuilding the binary 407 | 408 | 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` 409 | 410 | ## Notes 411 | 412 | If you have forgotten the firmware password for a machine your available options depend upon the age of the machine. 413 | 414 | Only Apple Retail Stores or Apple Authorized Service Providers can unlock the following Mac models when protected by a firmware password: 415 | 416 | • iMac (Mid 2011 and later) 417 | • iMac Pro (2017) 418 | • MacBook (Retina, 12-inch, Early 2015 and later) 419 | • MacBook Air (Late 2010 and later) 420 | • MacBook Pro (Early 2011 and later) 421 | • Mac mini (Mid 2011 and later) 422 | • Mac Pro (Late 2013) 423 | 424 | If you can't remember your firmware password or passcode, schedule an in-person service appointment with an Apple Store or Apple Authorized Service Provider. Bring your Mac to the appointment, and bring your original receipt or invoice as proof of purchase. 425 | 426 | If you have an earlier machine, it's much easier: 427 | 428 | 1. Shutdown the machine. Remove the battery, if possible. 429 | 2. Change the configuration of RAM by removing a module. 430 | 3. Restart the machine and zap the PRAM 3 times. (Hold down Option, Command, p and r after you press the power botton, and wait for three restarts) 431 | 4. Shut the machine down and remove the battery, if possible. 432 | 5. Reinstall the RAM module. 433 | 6. Restart and the firmware password should be removed. 434 | 435 | Thank you to macmule for , which helped me get things working in version 1. 436 | 437 | ## Update History 438 | 439 | Date | Version | Notes 440 | -------|-----------|------- 441 | 2020.01.23 | 2.5 | Removed flags, uses configuration file, removed management_tools, ported to python3. Added Skeleton Key and JAMF Controller script. 442 | 2019.10.23 | 2.1.5 | Corrected issue reporting no existing firmware password. 443 | 2017.10.23 | 2.1.4 | Using rm -P for secure delete, added additional alerting, additional pylint cleanup. 444 | 2016.03.16 | 2.1.2 | Cleaned up argparse, removed obsolete flag logic. 445 | 2016.03.16 | 2.1.1 | Slack identifier flag, logic clarifications. 446 | 2016.03.07 | 2.1.0 | Obfuscation, reboot flag, bug fixes 447 | 2015.11.05 | 2.0.0 | Python rewrite, Docs rewritten 448 | 2015.02.25 | 1.0.1 | Added use of firmwarepasswd on 10.10 449 | 2014.08.20 | 1.0.0 | Initial version. 450 | -------------------------------------------------------------------------------- /additional resources/example_keyfile.txt: -------------------------------------------------------------------------------- 1 | comment:previousPasswd 2 | #new:considered_an_old_password 3 | new:everything0ld1sNewAgain 4 | -------------------------------------------------------------------------------- /additional resources/obfuscate_keylist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright (c) 2016 University of Utah Student Computing Labs. ################ 4 | # All Rights Reserved. 5 | # 6 | # Permission to use, copy, modify, and distribute this software and 7 | # its documentation for any purpose and without fee is hereby granted, 8 | # provided that the above copyright notice appears in all copies and 9 | # that both that copyright notice and this permission notice appear 10 | # in supporting documentation, and that the name of The University 11 | # of Utah not be used in advertising or publicity pertaining to 12 | # distribution of the software without specific, written prior 13 | # permission. This software is supplied as is without expressed or 14 | # implied warranties of any kind. 15 | ################################################################################ 16 | 17 | # obfuscate_keylist.py ######################################################### 18 | # 19 | # A Python script to help obfuscate a plain text keyfile. 20 | # 21 | # 22 | # 1.0.0 2016.03.07 initial release. tjm 23 | # 24 | ################################################################################ 25 | 26 | # notes: ####################################################################### 27 | # 28 | # 29 | ################################################################################ 30 | 31 | import base64 32 | import plistlib 33 | import argparse 34 | import os 35 | import sys 36 | 37 | def main(): 38 | # 39 | # parse option definitions 40 | parser = argparse.ArgumentParser(description='Obfuscate plain text keyfile to base64-encoded plist.') 41 | parser.add_argument('-s', '--source', help='Set path to source keyfile', required=True) 42 | parser.add_argument('-d', '--destination', help='Set path to save obfuscated keyfile', required=True) 43 | parser.add_argument('-t', '--testmode', action="store_true", default=False, help='Test mode, verbose output.') 44 | parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0.0') 45 | args = parser.parse_args() 46 | 47 | if args.testmode: 48 | print "Source file : %s" % args.source 49 | print "Destination file: %s\n" % args.destination 50 | 51 | 52 | obfuscated = [] 53 | unobfuscated_string = '' 54 | obfuscated_string = '' 55 | has_new_label = False 56 | 57 | if os.path.exists(args.destination): 58 | continue_choice = False 59 | continue_entry = raw_input("Destination file \"%s\" already exists, Continue? [yN]:" % args.destination) 60 | while not continue_choice: 61 | if continue_entry is "n" or continue_entry is "N" or continue_entry is "": 62 | print "Exiting." 63 | sys.exit(1) 64 | elif continue_entry is "y" or continue_entry is "Y": 65 | break 66 | else: 67 | continue_entry = raw_input("Invalid entry. Destination file \"%s\" already exists, Continue? [yN]:" % args.destination) 68 | 69 | try: 70 | tmp_file = open(args.source) 71 | content_raw = tmp_file.read() 72 | tmp_file.close() 73 | except IOError: 74 | print "%s not found. Exiting." % args.source 75 | sys.exit(1) 76 | except Exception as e: 77 | print "Unknown error [%s]. Exiting." % e 78 | sys.exit(1) 79 | 80 | content_raw = content_raw.split("\n") 81 | content_raw = [x for x in content_raw if x] 82 | 83 | if args.testmode: 84 | print "plain text: \n%s\n" % content_raw 85 | 86 | for x in content_raw: 87 | label, pword = x.split(':') 88 | 89 | if label.lower() == 'new': 90 | if has_new_label: 91 | print "ERROR. Keylist has multiple \'new\' labels and is not valid. Exiting." 92 | sys.exit(1) 93 | else: 94 | has_new_label = True 95 | 96 | if args.testmode: 97 | print "entry : %r, %r, %r" % (label, pword, has_new_label) 98 | pword = base64.b64encode(pword) 99 | try: 100 | commented = label.split('#')[1] 101 | commented = base64.b64encode(commented) 102 | is_commented = True 103 | except: 104 | is_commented = False 105 | 106 | if is_commented: 107 | output_string = "#"+commented+":"+pword 108 | else: 109 | output_string = label+":"+pword 110 | unobfuscated_string = unobfuscated_string + output_string + "," 111 | obfuscated.append(output_string) 112 | if args.testmode: 113 | print "obfuscated: %s" % (output_string) 114 | 115 | if not has_new_label: 116 | print "ERROR. Keylist has no \'new\' label and is not valid. Exiting." 117 | sys.exit(1) 118 | 119 | pl = dict( 120 | data = base64.b64encode(unobfuscated_string) 121 | ) 122 | 123 | if args.testmode: 124 | print "\nplist entry: \n%s\n" % pl 125 | 126 | try: 127 | plistlib.writePlist(pl, args.destination) 128 | except Exception as e: 129 | print "Unknown error [%s]. Exiting." % e 130 | sys.exit(1) 131 | 132 | if args.testmode: 133 | print "%s created. Exiting." % args.destination 134 | 135 | 136 | # end code here. 137 | if __name__ == '__main__': 138 | main() 139 | -------------------------------------------------------------------------------- /firmware_password_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This should not be blank. 5 | """ 6 | 7 | # Copyright (c) 2020 University of Utah Student Computing Labs. ################ 8 | # All Rights Reserved. 9 | # 10 | # Permission to use, copy, modify, and distribute this software and 11 | # its documentation for any purpose and without fee is hereby granted, 12 | # provided that the above copyright notice appears in all copies and 13 | # that both that copyright notice and this permission notice appear 14 | # in supporting documentation, and that the name of The University 15 | # of Utah not be used in advertising or publicity pertaining to 16 | # distribution of the software without specific, written prior 17 | # permission. This software is supplied as is without expressed or 18 | # implied warranties of any kind. 19 | ################################################################################ 20 | 21 | # firmware_password_manager.py ################################################# 22 | # 23 | # A Python script to help Macintosh administrators manage the firmware passwords 24 | # of their computers. 25 | # 26 | # 27 | # 2.0.0 2015.11.05 Initial python rewrite. tjm 28 | # 29 | # 2.1.0 2016.03.07 "Now with spinning rims" 30 | # bug fixes, obfuscation features, 31 | # additional tools and examples. tjm 32 | # 33 | # 2.1.1 2016.03.16 slack identifier customization, 34 | # logic clarifications. tjm 35 | # 36 | # 2.1.2 2016.03.16 cleaned up argparse. tjm 37 | # 38 | # 2.1.3 2016.04.04 remove obsolete flag logic. tjm 39 | # 40 | # 2.1.4 2017.10.23 using rm -P for secure delete, 41 | # added additional alerting, additional pylint cleanup. tjm 42 | # 43 | # 2.5.0 2017.11.14 removed flags, uses configuration file, 44 | # reintroduced setregproptool functionality, 45 | # removed management_tools, ported to 46 | # python3, added testing fuctionality. tjm 47 | # 48 | # 2.5.0 2020.01.23 2.5 actually finished and committed. tjm 49 | # 50 | # 51 | # 52 | # keyfile format: 53 | # 54 | # | comment:passwords <-- comments are ignored, except for new. 55 | # | new:newpassword <-- the new password to be installed. 56 | # 57 | ################################################################################ 58 | 59 | # notes: ####################################################################### 60 | # 61 | # ./firmware_password_manager_cfg_v2.5b3.py -c private.INI -t 62 | # 63 | # 64 | # sudo pyinstaller --onefile firmware_password_manager.py 65 | # 66 | # 67 | # 68 | ################################################################################ 69 | 70 | # external tool documentation ################################################## 71 | # 72 | # firmwarepasswd v 1.0 73 | # Copyright (C) 2014 Apple Inc. All Rights Reserved. 74 | # 75 | # 76 | # Usage: firmwarepasswd [OPTION] 77 | # 78 | # ? Show usage 79 | # -h Show usage 80 | # -setpasswd Set a firmware password. You will be promted for passwords as needed. 81 | # NOTE: if this is the first password set, and no mode is 82 | # in place, the mode will automatically be set to "command" 83 | # -setmode [mode] Set mode to: 84 | # "command" - password required to change boot disk 85 | # "full" - password required on all startups 86 | # NOTE: cannot set a mode without having set a password 87 | # -mode Prints out the current mode setting 88 | # -check Prints out whether there is / isn't a firmware password is set 89 | # -delete Delete current firmware password and mode setting 90 | # -verify Verify current firmware password 91 | # -unlockseed Generates a firmware password recovery key 92 | # NOTE: Machine must be stable for this command to generate 93 | # a valid seed. No pending changes that need a restart. 94 | # NOTE: Seed is only valid until the next time a firmware password 95 | # command occurs. 96 | # 97 | # 98 | # 99 | # setregproptool v 2.0 (9) Aug 24 2013 100 | # Copyright (C) 2001-2010 Apple Inc. 101 | # All Rights Reserved. 102 | # 103 | # Usage: setregproptool [-c] [-d [-o ]] [[-m -p ] -o ] 104 | # 105 | # -c Check whether password is enabled. 106 | # Sets return status of 0 if set, 1 otherwise. 107 | # -d Delete current password/mode. 108 | # Requires current password on some machines. 109 | # -p Set password. 110 | # Requires current password on some machines. 111 | # -m Set security mode. 112 | # Requires current password on some machines. 113 | # Mode can be either "full" or "command". 114 | # Full mode requires entry of the password on 115 | # every boot, command mode only requires entry 116 | # of the password if the boot picker is invoked 117 | # to select a different boot device. 118 | # 119 | # When enabling the Firmware Password for the first 120 | # time, both the password and mode must be provided. 121 | # Once the firmware password has been enabled, providing 122 | # the mode or password alone will change that parameter 123 | # only. 124 | # 125 | # -o Old password. 126 | # Only required on certain machines to disable 127 | # or change password or mode. Optional, if not 128 | # provided the tool will prompt for the password. 129 | # 130 | ################################################################################ 131 | 132 | # 133 | # imports 134 | from argparse import RawTextHelpFormatter 135 | import argparse 136 | import base64 137 | import configparser 138 | import hashlib 139 | import inspect 140 | import json 141 | import logging 142 | import os 143 | import platform 144 | import plistlib 145 | import re 146 | import socket 147 | import subprocess 148 | import sys 149 | 150 | import pexpect 151 | import requests 152 | 153 | 154 | class FWPM_Object(object): 155 | """ 156 | This should not be blank. 157 | """ 158 | def __init__(self, args, logger, master_version): 159 | """ 160 | This should not be blank. 161 | """ 162 | self.args = args 163 | self.logger = logger 164 | self.master_version = master_version 165 | 166 | self.srp_path = None 167 | self.fwpwd_path = None 168 | self.config_options = {} 169 | self.local_identifier = None 170 | self.passwords_raw = None 171 | self.fwpw_managed_string = None 172 | self.new_password = None 173 | self.other_password_list = [] 174 | self.current_fwpw_state = False 175 | self.current_fwpm_hash = None 176 | 177 | self.clean_exit = False 178 | self.read_config = False 179 | self.read_keyfile = False 180 | self.modify_fwpw = False 181 | self.modify_nvram = False 182 | self.matching_hashes = False 183 | self.matching_passwords = False 184 | 185 | self.configuration_path = None 186 | 187 | self.system_version = platform.mac_ver()[0].split(".") 188 | 189 | self.srp_check() 190 | self.fwpwd_check() 191 | 192 | if self.fwpwd_path: 193 | self.current_fwpw_state = self.fwpwd_current_state() 194 | elif self.srp_path: 195 | self.current_fwpw_state = self.srp_current_state() 196 | 197 | self.injest_config() 198 | if self.config_options["slack"]["use_slack"]: 199 | self.slack_optionator() 200 | 201 | self.injest_keyfile() 202 | 203 | self.hash_current_state() 204 | self.hash_incoming() 205 | 206 | # 207 | # What if the string isn't a hash?!? 208 | if (self.current_fwpm_hash == self.fwpw_managed_string) and self.config_options["flags"]["management_string_type"] == 'hash': 209 | self.matching_hashes = True 210 | 211 | self.master_control() 212 | 213 | def master_control(self): 214 | """ 215 | This should not be blank. 216 | """ 217 | if self.logger: 218 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 219 | 220 | if self.current_fwpm_hash == self.fwpw_managed_string: 221 | if self.logger: 222 | self.logger.info("Hashes match. No change required.") 223 | 224 | else: 225 | if self.logger: 226 | self.logger.info("Hashes DO NOT match. Change required.") 227 | 228 | if self.fwpwd_path: 229 | self.fwpwd_change() 230 | self.secure_delete() 231 | 232 | elif self.srp_path: 233 | self.srp_change() 234 | self.secure_delete() 235 | 236 | else: 237 | print("No FW tool found.") 238 | quit() 239 | 240 | # 241 | # nvram maintenance 242 | # 243 | self.nvram_manager() 244 | 245 | # 246 | # some kind of post action reporting. 247 | # handle reboot flag here? 248 | # 249 | self.exit_manager() 250 | 251 | def hash_current_state(self): 252 | """ 253 | This should not be blank. 254 | """ 255 | if self.logger: 256 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 257 | 258 | existing_keyfile_hash = None 259 | if self.logger: 260 | self.logger.info("Checking existing hash.") 261 | 262 | try: 263 | existing_keyfile_hash_raw = subprocess.check_output(["/usr/sbin/nvram", "-p"]).decode('utf-8') 264 | existing_keyfile_hash_raw = existing_keyfile_hash_raw.split('\n') 265 | for item in existing_keyfile_hash_raw: 266 | if "fwpw-hash" in item: 267 | existing_keyfile_hash = item 268 | else: 269 | self.current_fwpm_hash = None 270 | 271 | self.current_fwpm_hash = existing_keyfile_hash.split("\t")[1] 272 | 273 | if self.args.testmode: 274 | print("Existing hash: %s" % self.current_fwpm_hash) 275 | 276 | except: 277 | pass 278 | 279 | def hash_incoming(self): 280 | """ 281 | This should not be blank. 282 | """ 283 | if self.logger: 284 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 285 | 286 | if self.logger: 287 | self.logger.info("Checking incoming hash.") 288 | 289 | if self.config_options["flags"]["management_string_type"] == "custom": 290 | # 291 | # ?!?!?!?!?!?!? 292 | # 293 | self.fwpw_managed_string = self.config_options["flags"]["management_string_type"] 294 | 295 | elif self.config_options["flags"]["management_string_type"] == "hash": 296 | 297 | hashed_key = hashlib.new('sha256') 298 | # hashed_key.update(self.passwords_raw.encode('utf-8')) 299 | 300 | hashed_key.update(self.new_password.encode('utf-8')) 301 | 302 | for entry in sorted(self.other_password_list): 303 | hashed_key.update(entry.encode('utf-8')) 304 | 305 | self.fwpw_managed_string = hashed_key.hexdigest() 306 | 307 | # prepend '2:' to denote hash created with v2 of script, will force a password change from v1 308 | self.fwpw_managed_string = '2:' + self.fwpw_managed_string 309 | 310 | else: 311 | self.fwpw_managed_string = None 312 | 313 | if self.args.testmode: 314 | print("Incoming hash: %s" % self.fwpw_managed_string) 315 | 316 | def secure_delete(self): 317 | """ 318 | attempts to securely delete the keyfile with medium overwrite and zeroing settings 319 | """ 320 | if self.logger: 321 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 322 | 323 | if self.logger: 324 | self.logger.info("Deleting keyfile") 325 | 326 | use_srm = bool(os.path.exists("/usr/bin/srm")) 327 | 328 | if self.args.testmode: 329 | if self.logger: 330 | self.logger.info("Test mode, keyfile not deleted.") 331 | return 332 | 333 | if use_srm: 334 | try: 335 | subprocess.call(["/usr/bin/srm", "-mz", self.config_options["keyfile"]["path"]]) 336 | if self.logger: 337 | self.logger.info("keyfile deleted successfuly.") 338 | except Exception as exception_message: 339 | if self.logger: 340 | self.logger.critical("Issue with attempt to remove keyfile. %s" % exception_message) 341 | else: 342 | try: 343 | deleted_keyfile = subprocess.call(["/bin/rm", "-Pf", self.config_options["keyfile"]["path"]]) 344 | print("return: %r" % deleted_keyfile) 345 | if self.logger: 346 | self.logger.info("keyfile deleted successfuly.") 347 | except Exception as exception_message: 348 | if self.logger: 349 | self.logger.critical("Issue with attempt to remove keyfile. %s" % exception_message) 350 | 351 | # is this really needed? 352 | if os.path.exists(self.config_options["keyfile"]["path"]): 353 | if self.logger: 354 | self.logger.critical("Failure to remove keyfile.") 355 | else: 356 | if self.logger: 357 | self.logger.info("Keyfile removed.") 358 | return 359 | 360 | def injest_config(self): 361 | """ 362 | attempts to consume and format configuration file 363 | """ 364 | 365 | # handle parsing errors in cfg?!? 366 | 367 | # where to handle looking for cfg in specific locations?!? 368 | 369 | if self.logger: 370 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 371 | 372 | try: 373 | if os.path.exists(self.args.configfile): 374 | # 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. 375 | config = configparser.ConfigParser(allow_no_value=True) 376 | config.read(self.args.configfile) 377 | 378 | self.config_options["flags"] = {} 379 | self.config_options["keyfile"] = {} 380 | self.config_options["logging"] = {} 381 | self.config_options["slack"] = {} 382 | self.config_options["os"] = {} 383 | self.config_options["fwpm"] = {} 384 | 385 | for section in ["flags", "keyfile", "logging", "slack"]: 386 | for item in config.options(section): 387 | if "use_" in item: 388 | try: 389 | self.config_options[section][item] = config.getboolean(section, item) 390 | except: 391 | self.config_options[section][item] = False 392 | elif "path" in item: 393 | self.config_options[section][item] = config.get(section, item) 394 | else: 395 | self.config_options[section][item] = config.get(section, item) 396 | 397 | if self.args.testmode: 398 | print("Configuration file variables:") 399 | for key, value in self.config_options.items(): 400 | print(key) 401 | for sub_key, sub_value in value.items(): 402 | print("\t%s %r" % (sub_key, sub_value)) 403 | else: 404 | if self.logger: 405 | self.logger.critical("Issue locating configuration file, exiting.") 406 | sys.exit() 407 | except Exception as exception_message: 408 | if self.logger: 409 | self.logger.critical("Issue reading configuration file, exiting. %s" % exception_message) 410 | sys.exit() 411 | 412 | self.read_config = True 413 | 414 | def sanity_check(self): 415 | """ 416 | This should not be blank. 417 | """ 418 | if self.logger: 419 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 420 | 421 | def srp_check(self): 422 | """ 423 | full setregproptool support later, if ever. 424 | """ 425 | 426 | if self.logger: 427 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 428 | 429 | if os.path.exists('/usr/local/bin/setregproptool'): 430 | self.srp_path = '/usr/local/bin/setregproptool' 431 | elif os.path.exists(os.path.dirname(os.path.abspath(__file__)) + '/setregproptool'): 432 | self.srp_path = os.path.dirname(os.path.abspath(__file__)) + '/setregproptool' 433 | else: 434 | print("SRP #3a") 435 | 436 | if self.logger: 437 | self.logger.info("SRP path: %s" % self.srp_path) 438 | 439 | def srp_current_state(self): 440 | """ 441 | full setregproptool support later, if ever. 442 | """ 443 | if self.logger: 444 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 445 | 446 | try: 447 | existing_fw_pw = subprocess.call([self.srp_path, "-c"]) 448 | if self.logger: 449 | self.logger.info("srp says %r" % existing_fw_pw) 450 | 451 | if existing_fw_pw: 452 | return False 453 | # it's weird, I know. Blame Apple. 454 | else: 455 | return True 456 | 457 | except: 458 | if self.logger: 459 | self.logger.info("ERROR srp says %r" % existing_fw_pw) 460 | return False 461 | 462 | # 463 | # # E:451,15: Undefined variable 'CalledProcessError' (undefined-variable) 464 | # except CalledProcessError: 465 | # if self.logger: 466 | # self.logger.info("ERROR srp says %r" % existing_fw_pw) 467 | # return False 468 | 469 | def srp_change(self): 470 | """ 471 | full setregproptool support later, if ever. 472 | """ 473 | if self.logger: 474 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 475 | print("Using srp tool!") 476 | 477 | print("%r" % self.current_fwpw_state) 478 | 479 | def fwpwd_check(self): 480 | """ 481 | This should not be blank. 482 | """ 483 | if self.logger: 484 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 485 | 486 | if os.path.exists('/usr/sbin/firmwarepasswd'): 487 | self.fwpwd_path = '/usr/sbin/firmwarepasswd' 488 | else: 489 | print("FWPWD #2b") 490 | 491 | if self.logger: 492 | self.logger.info("FWPWD path: %s" % self.fwpwd_path) 493 | 494 | def fwpwd_current_state(self): 495 | """ 496 | This should not be blank. 497 | """ 498 | if self.logger: 499 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 500 | 501 | existing_fw_pw = subprocess.check_output([self.fwpwd_path, "-check"]) 502 | 503 | # R:484, 8: The if statement can be replaced with 'return bool(test)' (simplifiable-if-statement) 504 | # return bool('Yes' in existing_fw_pw) 505 | 506 | if b'Yes' in existing_fw_pw: 507 | return True 508 | else: 509 | return False 510 | 511 | def fwpwd_change(self): 512 | """ 513 | This should not be blank. 514 | """ 515 | if self.logger: 516 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 517 | 518 | known_current_password = False 519 | current_password = '' 520 | 521 | # is this really needed?!? 522 | new_fw_tool_cmd = [self.fwpwd_path, '-verify'] 523 | 524 | if self.current_fwpw_state: 525 | if self.logger: 526 | self.logger.info("Verifying current FW password") 527 | 528 | for index in reversed(range(len(self.other_password_list))): 529 | 530 | child = pexpect.spawn(' '.join(new_fw_tool_cmd)) 531 | child.expect('Enter password:') 532 | 533 | child.sendline(self.other_password_list[index]) 534 | result = child.expect(['Correct', 'Incorrect']) 535 | 536 | if result == 0: 537 | # 538 | # correct password, exit loop 539 | current_password = self.other_password_list[index] 540 | known_current_password = True 541 | break 542 | else: 543 | # 544 | # wrong password, keep going 545 | continue 546 | 547 | # 548 | # We've discovered the currently set firmware password 549 | if known_current_password: 550 | 551 | # 552 | # Deleting firmware password 553 | if not self.config_options["flags"]["use_fwpw"]: 554 | if self.logger: 555 | self.logger.info("Deleting FW password") 556 | 557 | new_fw_tool_cmd = [self.fwpwd_path, '-delete'] 558 | if self.logger: 559 | self.logger.info(' '.join(new_fw_tool_cmd)) 560 | 561 | child = pexpect.spawn(' '.join(new_fw_tool_cmd)) 562 | child.expect('Enter password:') 563 | 564 | child.sendline(current_password) 565 | result = child.expect(['removed', 'incorrect']) 566 | 567 | if result == 0: 568 | # 569 | # password accepted, log result and exit 570 | if self.logger: 571 | self.logger.info("Finished. Password should be removed. Restart required. [%i]" % (index + 1)) 572 | self.clean_exit = True 573 | else: 574 | if self.logger: 575 | self.logger.critical("Asked to delete, current password not accepted. Exiting.") 576 | # secure_delete_keyfile(logger, args, config_options) 577 | if self.config_options["slack"]["use_slack"]: 578 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Asked to delete, current password not accepted.", '', 'error') 579 | # self.error_bot.send_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Asked to delete, current password not accepted.") 580 | sys.exit(1) 581 | 582 | # 583 | # Current and new password are identical 584 | # 585 | # 586 | # WAIT. How (is/would) this possible, clearly the hashes don't match!!! What if they aren't using hashes? 587 | # 588 | # 589 | elif current_password == self.new_password: 590 | self.matching_passwords = True 591 | self.clean_exit = True 592 | 593 | # 594 | # Change current firmware password to new password 595 | else: 596 | if self.logger: 597 | self.logger.info("Updating FW password") 598 | 599 | new_fw_tool_cmd = [self.fwpwd_path, '-setpasswd'] 600 | if self.logger: 601 | self.logger.info(' '.join(new_fw_tool_cmd)) 602 | 603 | child = pexpect.spawn(' '.join(new_fw_tool_cmd)) 604 | 605 | result = child.expect('Enter password:') 606 | if result == 0: 607 | pass 608 | else: 609 | if self.logger: 610 | self.logger.error("bad response from firmwarepasswd. Exiting.") 611 | self.secure_delete() 612 | if self.config_options["slack"]["use_slack"]: 613 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') 614 | sys.exit(1) 615 | child.sendline(current_password) 616 | 617 | result = child.expect('Enter new password:') 618 | if result == 0: 619 | pass 620 | else: 621 | if self.logger: 622 | self.logger.error("bad response from firmwarepasswd. Exiting.") 623 | self.secure_delete() 624 | if self.config_options["slack"]["use_slack"]: 625 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') 626 | sys.exit(1) 627 | child.sendline(self.new_password) 628 | 629 | result = child.expect('Re-enter new password:') 630 | if result == 0: 631 | pass 632 | else: 633 | if self.logger: 634 | self.logger.error("bad response from firmwarepasswd. Exiting.") 635 | self.secure_delete() 636 | if self.config_options["slack"]["use_slack"]: 637 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') 638 | sys.exit(1) 639 | child.sendline(self.new_password) 640 | 641 | child.expect(pexpect.EOF) 642 | child.close() 643 | 644 | if self.logger: 645 | self.logger.info("Updated FW Password.") 646 | self.clean_exit = True 647 | 648 | # 649 | # Unable to match current password with contents of keyfile 650 | else: 651 | if self.logger: 652 | self.logger.critical("Current FW password not in keyfile. Quitting.") 653 | if self.config_options["slack"]["use_slack"]: 654 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Current FW password not in keyfile.", '', 'error') 655 | self.secure_delete() 656 | sys.exit(1) 657 | 658 | # 659 | # No current firmware password, setting it 660 | else: 661 | 662 | new_fw_tool_cmd = [self.fwpwd_path, '-setpasswd'] 663 | if self.logger: 664 | self.logger.info(' '.join(new_fw_tool_cmd)) 665 | 666 | child = pexpect.spawn(' '.join(new_fw_tool_cmd)) 667 | 668 | result = child.expect('Enter new password:') 669 | print(child.before) 670 | if result == 0: 671 | pass 672 | else: 673 | if self.logger: 674 | self.logger.error("bad response from firmwarepasswd. Exiting.") 675 | self.secure_delete() 676 | if self.config_options["slack"]["use_slack"]: 677 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') 678 | sys.exit(1) 679 | child.sendline(self.new_password) 680 | 681 | result = child.expect('Re-enter new password:') 682 | if result == 0: 683 | pass 684 | else: 685 | if self.logger: 686 | self.logger.error("bad response from firmwarepasswd. Exiting.") 687 | self.secure_delete() 688 | if self.config_options["slack"]["use_slack"]: 689 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Bad response from firmwarepasswd.", '', 'error') 690 | sys.exit(1) 691 | child.sendline(self.new_password) 692 | 693 | child.expect(pexpect.EOF) 694 | child.close() 695 | 696 | if self.logger: 697 | self.logger.info("Added FW Password.") 698 | self.clean_exit = True 699 | 700 | def slack_optionator(self): 701 | """ 702 | 703 | ip, mac, hostname 704 | computername 705 | serial 706 | 707 | """ 708 | if self.logger: 709 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 710 | 711 | if self.verify_network(): 712 | try: 713 | full_ioreg = subprocess.check_output(['ioreg', '-l']).decode('utf-8') 714 | serial_number_raw = re.findall('\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) 715 | serial_number = serial_number_raw[0] 716 | if self.args.testmode: 717 | print("Serial number: %r" % serial_number) 718 | 719 | 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': 720 | processed_device_list = [] 721 | 722 | # Get ordered list of network devices 723 | base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]).decode('utf-8') 724 | network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) 725 | ether_up_list = subprocess.check_output(["/sbin/ifconfig", "-au", "ether"]).decode('utf-8') 726 | for device in network_device_list: 727 | device_name = device[0] 728 | port_name = device[1] 729 | try: 730 | if self.args.testmode: 731 | print(device_name, port_name) 732 | 733 | if port_name in ether_up_list: 734 | device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]).decode('utf-8') 735 | mac_address = re.findall('ether (.*) \n', device_info_raw) 736 | if self.args.testmode: 737 | print("%r" % mac_address) 738 | ether_address = re.findall('inet (.*) netmask', device_info_raw) 739 | if self.args.testmode: 740 | print("%r" % ether_address) 741 | if len(ether_address) and len(mac_address): 742 | processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) 743 | except Exception as this_exception: 744 | print(this_exception) 745 | 746 | if processed_device_list: 747 | if self.logger: 748 | self.logger.info("1 or more active IP addresses. Choosing primary.") 749 | if self.args.testmode: 750 | print("Processed devices: ", processed_device_list) 751 | 752 | if self.config_options["slack"]["slack_identifier"].lower() == 'ip': 753 | self.local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" 754 | elif self.config_options["slack"]["slack_identifier"].lower() == 'mac': 755 | self.local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" 756 | elif self.config_options["slack"]["slack_identifier"].lower() == 'hostname': 757 | try: 758 | self.local_identifier = socket.getfqdn() 759 | except: 760 | if self.logger: 761 | self.logger.error("error discovering hostname info.") 762 | self.local_identifier = serial_number 763 | 764 | else: 765 | if self.logger: 766 | self.logger.error("error discovering IP info.") 767 | self.local_identifier = serial_number 768 | 769 | elif self.config_options["slack"]["slack_identifier"].lower() == 'computername': 770 | try: 771 | cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) 772 | self.local_identifier = cname_identifier_raw.split('\n')[0] 773 | if self.logger: 774 | self.logger.info("Computername: %r" % self.local_identifier) 775 | except: 776 | if self.logger: 777 | self.logger.info("error discovering computername.") 778 | self.local_identifier = serial_number 779 | elif self.config_options["slack"]["slack_identifier"].lower() == 'serial': 780 | self.local_identifier = serial_number 781 | if self.logger: 782 | self.logger.info("Serial number: %r" % self.local_identifier) 783 | else: 784 | if self.logger: 785 | self.logger.info("bad or no identifier flag, defaulting to serial number.") 786 | self.local_identifier = serial_number 787 | 788 | if self.args.testmode: 789 | print("Local identifier: %r" % self.local_identifier) 790 | 791 | except Exception as this_exception: 792 | print(this_exception) 793 | self.config_options["slack"]["use_slack"] = False 794 | else: 795 | self.config_options["slack"]["use_slack"] = False 796 | if self.logger: 797 | self.logger.info("No network detected.") 798 | 799 | def slack_message(self, message, icon, type): 800 | """ 801 | This should not be blank. 802 | """ 803 | if self.logger: 804 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 805 | 806 | slack_info_channel = False 807 | slack_error_channel = False 808 | 809 | if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_info_url"]: 810 | slack_info_channel = True 811 | 812 | if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_error_url"]: 813 | slack_error_channel = True 814 | 815 | if slack_error_channel and type == 'error': 816 | slack_url = self.config_options["slack"]["slack_error_url"] 817 | elif slack_info_channel: 818 | slack_url = self.config_options["slack"]["slack_info_url"] 819 | else: 820 | return 821 | 822 | payload = {'text': message, 'username': 'FWPM ' + self.master_version, 'icon_emoji': ':key:'} 823 | 824 | response = requests.post(slack_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) 825 | 826 | self.logger.info('Response: ' + str(response.text)) 827 | self.logger.info('Response code: ' + str(response.status_code)) 828 | 829 | def reboot_exit(self): 830 | """ 831 | This should not be blank. 832 | """ 833 | if self.logger: 834 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 835 | 836 | def injest_keyfile(self): 837 | """ 838 | This should not be blank. 839 | """ 840 | if self.logger: 841 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 842 | 843 | path_to_keyfile_exists = os.path.exists(self.config_options["keyfile"]["path"]) 844 | 845 | if not path_to_keyfile_exists: 846 | if self.logger: 847 | self.logger.critical("%r does not exist. Exiting." % self.config_options["keyfile"]["path"]) 848 | if self.config_options["slack"]["use_slack"]: 849 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Keyfile does not exist.", '', 'error') 850 | sys.exit(2) 851 | 852 | if self.logger: 853 | self.logger.info("Reading password file") 854 | 855 | if self.config_options["keyfile"]["use_obfuscation"]: 856 | # 857 | # unobfuscate plist 858 | if self.logger: 859 | self.logger.info("Reading plist") 860 | passwords = [] 861 | if "plist" in self.config_options["keyfile"]["path"]: 862 | try: 863 | keyfile_plist = plistlib.readPlist(self.config_options["keyfile"]["path"]) 864 | content_raw = keyfile_plist["data"] 865 | except: 866 | if self.logger: 867 | self.logger.critical("Error reading plist. Exiting.") 868 | if self.config_options["slack"]["use_slack"]: 869 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading plist.", '', 'error') 870 | sys.exit(1) 871 | 872 | else: 873 | try: 874 | with open(self.config_options["keyfile"]["path"], 'r') as reader: 875 | content_raw = reader.read() 876 | except: 877 | if self.logger: 878 | self.logger.critical("Error reading plist. Exiting.") 879 | if self.config_options["slack"]["use_slack"]: 880 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading plist.", '', 'error') 881 | sys.exit(1) 882 | 883 | content_raw = base64.b64decode(content_raw) 884 | content_raw = content_raw.decode('utf-8').split(",") 885 | content_raw = [x for x in content_raw if x] 886 | 887 | output_string = "" 888 | for item in content_raw: 889 | label, pword = item.split(':') 890 | pword = base64.b64decode(pword) 891 | try: 892 | commented = label.split('#')[1] 893 | commented = base64.b64decode(commented) 894 | is_commented = True 895 | except: 896 | is_commented = False 897 | 898 | if is_commented: 899 | output_string = "#" + commented.decode('utf-8') + ":" + pword.decode('utf-8') 900 | passwords.append(output_string) 901 | 902 | else: 903 | uncommented = base64.b64decode(label) 904 | output_string = uncommented.decode('utf-8') + ":" + pword.decode('utf-8') 905 | passwords.append(output_string) 906 | 907 | else: 908 | # 909 | # read keyfile 910 | if self.logger: 911 | self.logger.info("Reading plain text") 912 | 913 | try: 914 | with open(self.config_options["keyfile"]["path"], "r") as keyfile: 915 | self.passwords_raw = keyfile.read() 916 | 917 | passwords = self.passwords_raw.splitlines() 918 | 919 | except: 920 | if self.logger: 921 | self.logger.critical("Error reading keyfile. Exiting.") 922 | if self.config_options["slack"]["use_slack"]: 923 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Error reading keyfile.", '', 'error') 924 | sys.exit(1) 925 | 926 | if self.logger: 927 | self.logger.info("Closed password file") 928 | 929 | # new_password = None 930 | # other_password_list = [] 931 | 932 | # 933 | # parse data from keyfile and build list of passwords 934 | for entry in passwords: 935 | try: 936 | key, value = entry.split(":", 1) 937 | except Exception as this_exception: 938 | if self.logger: 939 | self.logger.critical("Malformed keyfile, key:value format required. %r. Quitting." % this_exception) 940 | self.secure_delete() 941 | if self.config_options["slack"]["use_slack"]: 942 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') 943 | sys.exit(1) 944 | 945 | if key.lower() == 'new': 946 | if self.new_password is not None: 947 | if self.logger: 948 | self.logger.critical("Malformed keyfile, multiple new keys. Quitting.") 949 | self.secure_delete() 950 | if self.config_options["slack"]["use_slack"]: 951 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') 952 | sys.exit(1) 953 | else: 954 | self.new_password = value 955 | self.other_password_list.append(value) 956 | else: 957 | self.other_password_list.append(value) 958 | 959 | if self.logger: 960 | self.logger.info("Sanity checking password file contents") 961 | 962 | if self.new_password is None and self.config_options["flags"]["use_fwpw"]: 963 | if self.logger: 964 | self.logger.critical("Malformed keyfile, no \'new\' key. Quitting.") 965 | self.secure_delete() 966 | if self.config_options["slack"]["use_slack"]: 967 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "Malformed keyfile.", '', 'error') 968 | sys.exit(1) 969 | 970 | self.read_keyfile = True 971 | 972 | try: 973 | self.other_password_list.remove(self.new_password) 974 | except: 975 | pass 976 | 977 | def nvram_manager(self): 978 | """ 979 | This should not be blank. 980 | """ 981 | if self.logger: 982 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 983 | 984 | if self.clean_exit: 985 | if not self.config_options["flags"]["use_fwpw"]: 986 | try: 987 | subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) 988 | if self.logger: 989 | self.logger.info("nvram entry pruned.") 990 | if self.config_options["slack"]["use_slack"]: 991 | self.slack_message("_*" + self.local_identifier + "*_ :unlock:\n" + "FWPW and nvram entry removed.", '', 'info') 992 | # 993 | # Should we return here? 994 | # 995 | except Exception as exception_message: 996 | if self.logger: 997 | self.logger.warning("nvram reported error attempting to remove hash. Exiting. %s" % exception_message) 998 | # 999 | # Slack? 1000 | # 1001 | sys.exit(1) 1002 | 1003 | if self.config_options["flags"]["management_string_type"] == "None": 1004 | try: 1005 | # ? 1006 | # existing_keyfile_hash = subprocess.check_output(["/usr/sbin/nvram", "fwpw-hash"]) 1007 | try: 1008 | subprocess.call(["/usr/sbin/nvram", "-d", "fwpw-hash"]) 1009 | if self.logger: 1010 | self.logger.info("nvram entry pruned.") 1011 | if self.config_options["slack"]["use_slack"]: 1012 | self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.", '', 'info') 1013 | except Exception as exception_message: 1014 | if self.logger: 1015 | self.logger.warning("nvram reported error attempting to remove hash. Exiting. %s" % exception_message) 1016 | sys.exit(1) 1017 | except: 1018 | # assuming hash doesn't exist. 1019 | if self.logger: 1020 | self.logger.info("Assuming nvram entry doesn't exist.") 1021 | if self.config_options["slack"]["use_slack"]: 1022 | self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW updated.", '', 'info') 1023 | 1024 | elif self.config_options["flags"]["management_string_type"] == "custom" or self.config_options["flags"]["management_string_type"] == "hash": 1025 | if self.matching_hashes: 1026 | if self.matching_passwords: 1027 | if self.logger: 1028 | self.logger.info("Hashes and Passwords match. No changes needed.") 1029 | if self.config_options["slack"]["use_slack"]: 1030 | self.slack_message("_*" + self.local_identifier + "*_ :white_check_mark::white_check_mark:\n" + "FWPM hashes and FW passwords match.", '', 'info') 1031 | else: 1032 | if self.logger: 1033 | self.logger.info("Hashes match, password modified.") 1034 | if self.config_options["slack"]["use_slack"]: 1035 | self.slack_message("_*" + self.local_identifier + "*_ :white_check_mark::heavy_exclamation_mark:\n" + "FWPM hashes and FW passwords match.", '', 'info') 1036 | else: 1037 | try: 1038 | subprocess.call(["/usr/sbin/nvram", "fwpw-hash=" + self.fwpw_managed_string]) 1039 | if self.logger: 1040 | self.logger.info("nvram modified.") 1041 | except Exception as exception_message: 1042 | if self.logger: 1043 | self.logger.warning("nvram modification failed. nvram reported error. %s" % exception_message) 1044 | # 1045 | # slack error message? 1046 | # 1047 | sys.exit(1) 1048 | 1049 | if self.matching_passwords: 1050 | if self.logger: 1051 | self.logger.info("Hash mismatch, Passwords match. Correcting hash.") 1052 | if self.config_options["slack"]["use_slack"]: 1053 | self.slack_message("_*" + self.local_identifier + "*_ :heavy_exclamation_mark: :white_check_mark:\n" + "Hash mismatch, Passwords match. Correcting hash.", '', 'info') 1054 | else: 1055 | if self.config_options["slack"]["use_slack"]: 1056 | self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW and hash updated.", '', 'info') 1057 | 1058 | else: 1059 | if self.logger: 1060 | self.logger.critical("An error occured. Failed to modify firmware password.") 1061 | if self.config_options["slack"]["use_slack"]: 1062 | self.slack_message("_*" + self.local_identifier + "*_ :no_entry:\n" + "An error occured. Failed to modify firmware password.", '', 'error') 1063 | sys.exit(1) 1064 | 1065 | def exit_manager(self): 1066 | """ 1067 | This should not be blank. 1068 | """ 1069 | if self.logger: 1070 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 1071 | 1072 | # 1073 | # check the new booleans, etc to find out what we accomplished... 1074 | # 1075 | 1076 | # self.clean_exit = False 1077 | # 1078 | # self.read_config = False 1079 | # self.read_keyfile = False 1080 | # self.modify_fwpw = False 1081 | # self.modify_nvram = False 1082 | # 1083 | 1084 | if self.config_options["flags"]["use_reboot_on_exit"]: 1085 | if self.args.testmode: 1086 | if self.logger: 1087 | self.logger.info("Test mode, cancelling reboot.") 1088 | else: 1089 | if self.logger: 1090 | self.logger.warning("Normal completion. Rebooting.") 1091 | os.system('reboot') 1092 | else: 1093 | if self.logger: 1094 | self.logger.info("FWPM exiting normally.") 1095 | sys.exit(0) 1096 | 1097 | def verify_network(self): 1098 | """ 1099 | Host: 8.8.8.8 (google-public-dns-a.google.com) 1100 | OpenPort: 53/tcp 1101 | Service: domain (DNS/TCP) 1102 | """ 1103 | 1104 | try: 1105 | _ = requests.get("https://8.8.8.8", timeout=3) 1106 | return True 1107 | except requests.ConnectionError as exception_message: 1108 | print(exception_message) 1109 | return False 1110 | 1111 | 1112 | def main(): 1113 | """ 1114 | This should not be blank. 1115 | """ 1116 | master_version = "2.5" 1117 | 1118 | logo = """ 1119 | /_ _/ /_ _/ University of Utah 1120 | _/ _/ Marriott Library 1121 | _/ _/ Mac Group 1122 | _/ _/ https://apple.lib.utah.edu/ 1123 | _/_/ https://github.com/univ-of-utah-marriott-library-apple 1124 | 1125 | 1126 | """ 1127 | desc = "Manages the firmware password on Apple Macintosh computers." 1128 | 1129 | # 1130 | # require root to run. 1131 | if os.geteuid(): 1132 | print("Must be root to run script.") 1133 | sys.exit(2) 1134 | 1135 | # 1136 | # parse option definitions 1137 | parser = argparse.ArgumentParser(description=logo+desc, formatter_class=RawTextHelpFormatter) 1138 | 1139 | # 1140 | # required, mutually exclusive commands 1141 | 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.') 1142 | subprime = prime_group.add_mutually_exclusive_group(required=True) 1143 | subprime.add_argument('-c', '--configfile', help='Read configuration file') 1144 | 1145 | parser.add_argument('-b', '--reboot', action="store_true", default=False, help='Reboots the computer after the script completes successfully.') 1146 | parser.add_argument('-t', '--testmode', action="store_true", default=False, help='Test mode. Verbose logging, will not delete keyfile.') 1147 | parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + master_version) 1148 | 1149 | args = parser.parse_args() 1150 | 1151 | if args.testmode: 1152 | print(args) 1153 | 1154 | # 1155 | # Open log file 1156 | try: 1157 | log_path = '/var/log/' + 'FWPW_Manager_' + master_version 1158 | logging.basicConfig(filename=log_path, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 1159 | logger = logging.getLogger(__name__) 1160 | logger.info("Running Firmware Password Manager " + master_version) 1161 | except: 1162 | logger = None 1163 | 1164 | FWPM_Object(args, logger, master_version) 1165 | 1166 | 1167 | if __name__ == '__main__': 1168 | main() 1169 | -------------------------------------------------------------------------------- /img/direct_entry_trimmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/direct_entry_trimmed.png -------------------------------------------------------------------------------- /img/hash_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/hash_image.png -------------------------------------------------------------------------------- /img/hashed_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/hashed_keys.png -------------------------------------------------------------------------------- /img/jamf_fetch_trimmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/jamf_fetch_trimmed.png -------------------------------------------------------------------------------- /img/jss_ea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/jss_ea.png -------------------------------------------------------------------------------- /img/jss_not_current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/jss_not_current.png -------------------------------------------------------------------------------- /img/jss_smart_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/jss_smart_group.png -------------------------------------------------------------------------------- /img/no_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/no_keys.png -------------------------------------------------------------------------------- /img/remote_fetch_trimmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/remote_fetch_trimmed.png -------------------------------------------------------------------------------- /img/sk_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/sk_help.png -------------------------------------------------------------------------------- /img/sk_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/sk_login.png -------------------------------------------------------------------------------- /img/sk_os_alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/sk_os_alert.png -------------------------------------------------------------------------------- /img/sk_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/sk_ui.png -------------------------------------------------------------------------------- /img/slack_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/slack_example.png -------------------------------------------------------------------------------- /img/yes_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/img/yes_keys.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='Firmware Password Manager', 5 | version='2.5', 6 | url='https://github.com/univ-of-utah-marriott-library-apple/firmware_password_manager', 7 | author='Todd McDaniel, Marriott Library Client Platform Services', 8 | author_email='mlib-its-mac-github@lists.utah.edu', 9 | description=('A Python script to help Macintosh administrators manage the firmware ', 10 | 'passwords of their computers.'), 11 | license='MIT', 12 | scripts=['firmware_password_manager.py'], 13 | classifiers=[ 14 | 'Development Status :: 5 - Stable', 15 | 'Environment :: Console', 16 | 'Environment :: MacOS X', 17 | 'Intended Audience :: Information Technology', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Natural Language :: English', 20 | 'Operating System :: MacOS :: MacOS X', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2.7' 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /skeleton key/Skeleton_Key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Skeleton Key 5 | """ 6 | 7 | # Copyright (c) 2020 University of Utah Student Computing Labs. ################ 8 | # All Rights Reserved. 9 | # 10 | # Permission to use, copy, modify, and distribute this software and 11 | # its documentation for any purpose and without fee is hereby granted, 12 | # provided that the above copyright notice appears in all copies and 13 | # that both that copyright notice and this permission notice appear 14 | # in supporting documentation, and that the name of The University 15 | # of Utah not be used in advertising or publicity pertaining to 16 | # distribution of the software without specific, written prior 17 | # permission. This software is supplied as is without expressed or 18 | # implied warranties of any kind. 19 | ################################################################################ 20 | 21 | # skeleton_key.py ################################################# 22 | # 23 | # A Python Tk application to set/unset the firmware password. 24 | # 25 | # 26 | # 0.1.0 2017.04.13 Initial build. tjm 27 | # 0.2.0 2017.05.04 single pane. tjm 28 | # 1.0.0 2020.01.22 Initial release, 29 | # JAMF and Slack integration, 30 | # hash generation, reading config file. tjm 31 | # 32 | ################################################################################ 33 | 34 | # notes: ####################################################################### 35 | # 36 | # sudo /usr/local/bin/pyinstaller --onefile Skeleton_Key.spec 37 | # 38 | # 39 | # 40 | # 41 | ################################################################################ 42 | 43 | from __future__ import division 44 | from __future__ import print_function 45 | import base64 46 | import ConfigParser 47 | import hashlib 48 | import inspect 49 | import json 50 | import os 51 | import platform 52 | import plistlib 53 | import pwd 54 | import re 55 | import socket 56 | import subprocess 57 | import sys 58 | import tkFileDialog 59 | import tkSimpleDialog 60 | import ttk 61 | from Tkinter import Tk, N, E, S, W, StringVar, IntVar, PhotoImage, HORIZONTAL 62 | import logging 63 | import pexpect 64 | import requests 65 | 66 | try: 67 | import mount_shares_better as msb 68 | except: 69 | pass 70 | 71 | 72 | class SinglePane(object): 73 | """ 74 | Load keys, generate hashes, toggle fwpw 75 | """ 76 | 77 | def __init__(self, root, logger, admin_password, fwpw_status, master_version): 78 | """ 79 | Initialize object and variables 80 | """ 81 | 82 | self.logger = logger 83 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 84 | 85 | self.root = root 86 | self.root.title("Skeleton Key " + master_version) 87 | 88 | self.logo = '''\ 89 | R0lGODlhWAJRAPcAAAEAAAgHBwwLCxAPDxQTExgXFxwcHCEfHyYlJSgnJywsLD0nJzItLTY2Njg3 90 | Nzw8PF4dHGMcG24cGnIdG1QgH18gH0EmJkgpKEA/P1glJEJCQkhHR0tLS1BPT1NSUlhXV1lZWWFf 91 | X3NcXGRjY2hnZ2xra3Bvb3ppaXRzc3h2d318fJcAAJsAAp8AC4obGJ4dGKAADaEAFKQAGqceGbAf 92 | GqUhHqwhHrQhHL4hG6cEIqgEI6cKI6kNJqkOKKkQJ6sWK6sYLKwbMJgsKpw0MpI0M6UlIqEoJaEs 93 | KqolIqMxL68lNLEpOaE1M6E5N6M+PbMwPp9AP7QyQrc8SZ1DQpxLSplHRp5ZWKJDQqFLSrpBTaFQ 94 | T79OWKVZWIBuboB+fop4eKxraqdnZql6ebN+fbJ2dcNXX8JXYcRcZsZeacdjbshmb8hmcM92freA 95 | f896gnKBgISEhIyMjIiGh5OTk5ycnJiWlpCJibuMi6Cfn76enqyJiaSjo6inp6qqqrCvr7+trbS0 96 | tLi3t7m5ubm3uNODiteOltiPlsSZmNeRltiRltqUmtqYncCMi+KboMSkpMqqqt+mqt6ho8C/v8q6 97 | uti7u+Cnq+GqruuiqeOusuSxs+K5vMTExMjHx8zMzN/GxtDPz9/Jyc/Q0NLS0tjX19zc3O3KzOjH 98 | yO/P0eTV1e/R0+Pa2u/e3vDR0/Lc3fTe4N/h4OTk5Orl5erq6unn6PPt7vjt7vT09Pr29/////f4 99 | +Pnu8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 100 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 101 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 102 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAABYAlEA 103 | AAj+AHEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKFAlLkBwQGho00ABCjiBY 104 | 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 236 | 5T53utfd7nfHe971jouAAAA7 237 | ''' 238 | 239 | self.open_lock_icon = '''\ 240 | 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 269 | By988Hf0cVFAADs= 270 | ''' 271 | 272 | self.closed_lock_icon = '''\ 273 | R0lGODlhLQAqAPcAAAAAAAoKCwIAEQoNFgcFGhIMExERERMUHBwcHCMdASIaFCUcGScgAikjBikn 274 | FCwkGykoGjUqGjwvHD4wHiEhISshICwsLDMzMzw8PFVDI0FBQUtLS1RUVFNVX1paW1ZZYmBgYGtr 275 | a3Nzc3t7e4FnH4lvJ41wIJd5Kr2ZAKuJH7CRErCTGbeWLsKeAMWgAcijA82oBdSuB9ivAN61ANqz 276 | DsmmFM6qFMegGcmoHNGtEtOuHtuzFdu3H9+5HOG3AeS6Auq+AeW6DOW5FN+6Idm0MOG/Jum/NeK9 277 | OOrABfLFAfXIAvrMAfPHCfvOCf/SBvvQD//ZCu7FEuvCHPPGEPTKEvnNEP/VEf/THv/aH//gEf/k 278 | G//pHO7IL+bSJf/VJf/bJfzSLP/cKe7INv/TMf/jIv/rJf/gLP/tLP/xKv/9Lf/jMd/TcIaGhouL 279 | i5SUlJubm6SkpKurq7S0tLy8vN/Wgd/Ygt/WjN/VlP/xgP/7hf//jP/2lf/+mt/aqf/1oP/+o//z 280 | qf/8rv/9sP//uMXFxcvLy9PT09vb2//6wv//yv//1OPj4+np6fT09P7+/gAAAAAAAAAAAAAAAAAA 281 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 282 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 283 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 284 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 285 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 286 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAI8ALAAAAAAtACoA 287 | AAj+AB8JHEiwoMGDCBMqRHgIzggPEEfAObSwYkVDbTZYCMDRwoY2FC2KJGjIA4CTKFMC4GBopEg2 288 | KDGwIXSI0SFCbC6gZONyIYeTGgo5GuqoEVFHhTCc5NAT4U8AbYYagiMCBAcQIuAYGgpzZdOCbk7G 289 | cXToDQcEKhFweLPIEZyTbr4KZHSSjSNDbNCevIDBAkoEbA456spIrggAFIq2ORkA5MBFXRsbNQBA 290 | hFy0cBzJoQAAQciCh9AikGMoROevhk4ONQ3gs8FDJ0c0PFmoaRwAFhwt0uBV4c8NhuagjdP0DYAN 291 | ZJW2WbgYQyFCOt80DYv8EO/lCpsXKqQzbs+wFwz+GdoAlTkADIa4A5De0/iFQofIY0/YXHz06QDC 292 | xy+f/fwhQ93Zdp5RJrGXkHEaFKUUcSMZIgJvAXDgwQHniWChCCFceCFvB3zwwQAAaCBCSxaNoNKJ 293 | KKaI0ggvAaBCF3f0YQcddaxRRA9DCFEEFzmg0EILLrjwAgwx0NBDFysAwJNFMO2QxiCKCPLHH3qg 294 | UcaVZqBhBRJJdKnEEk5AkUUZaQyhZIs1oAEIIn7ssccaZnghpxdhRDHDnTP48AMSTFDhBRo4nMkk 295 | AGmu2eYeeHhxhZxklLGEDHjqCQQTVfwZ6JIVwVQom24mumgZYxzBBRKR7kmppYJWtNimh3paBgv+ 296 | DxTwQApJ+JCnqZUCmupCMOGgJiJuIurFFkQIgBIBN9TqQxB8+qkrprwS+murYZRhgkokLPGDnnxW 297 | WsalaJaxZrCJlnECtkvcymcTqELbXw3iciqsFlwsgFIFO5D6A67ttuiroXvw4aoRCgCwgA7pzrBv 298 | EKc+Gy7AAitKhhoSADDBFEDcqSfDuYI7KKudhnEFGWNEAEAEQmSsbsMeqyotwMKOPAZaCwhB6sod 299 | 76qQptN2KrEXGRSQQRA/aAxEt158q3NCPMOc6JxeiCEFnjj3O6gNaQiSyB988JGHGVjIWYaWkgJx 300 | NBNNWOFFGjosjdBiWAeCCNcCh4FFGF6QYQZLFXmWnUTaawc630KHJdAABBVAAMEDDjTAQAOQO57A 301 | 5JRPHnkClYk0hwYaYMD555x7Hvron4vuuedziLSIG6y37vrrsLf+huyLHBQQADs= 302 | ''' 303 | 304 | self.key_icon = '''\ 305 | R0lGODlhMAAwAPcAAAABAAkFAgQLBg0IAwwNCxMMBQwQCBgRBhEVChQYCxsYChISEhwcHCIXCSUb 306 | CyseCzgVBC8hDDEiDTgnDz0tEiYmJi0tLTQ0NDs7O0YdB00iCVYmC1wqC0o1FFY7Flg9F1k/GGsx 307 | DlxBGWBDGmZJHGlNHm5QHnRRH0RAOnVUIXpVIEVFRUxMTFxZVlxcXGJiYmpqanFuZHNzc3l5eZNG 308 | FaVQGINbI4NhJo1jJpJmJ5BnKJVrKZ5sKbVuJ6N1LbJyKrl2K7B5L7J9ML5/MchiHsN8LPN3JPl8 309 | JrSBMryDMr+MNcyALtWDLsGFM8mNNtWWOtSYOtibPP+BJ/+FKP+JKv+SLeOMMuyPM+GcPO+cOueh 310 | PuyiPvGjPqqbeu2mQPOnQPSqQfqtQvGzRP60Rf+5R/i3SPe8SP69Sf/ES//LTv/RT//RUf/cVP/q 311 | Wf/0Xf/udIWFhYiIiJKSkqWlpKysrLOzs7u7u83EkP/0nMnDp8XFxcvLy9/f3+Tk5Ovr6/Xv6PX1 312 | 9Pr6+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 313 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 314 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 315 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 316 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 317 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 318 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIIALAAAAAAwADAA 319 | AAj+AAUJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3MixY0Y6L1asgKHHo8IZAABI+ODBAYAF 320 | dEwW9FMBAA4uZNCsQRNGBAAYMgUCYqBAyxoyY9BgGWKmDQ8AcYKyKCAGDZgwaEikBHDDTQ4Ae0zu 321 | AeAEDRcwaHYACEFkAwAhbB4A9eiCAhoxXMaQEZChypQqEBCw0UGgJEdABHSkAXN1DAAOVYxU0SBA 322 | DRQAcjqOdXKGMdoUAGocoQHAhpotAaJyHPukM+MxZShs9YAGzeXMHPkASGKV8ZekQkzwTuoDQJ2O 323 | gQyQ6O0bq5reaDqsCNQRDw8erj1r54ImCoCYHLv+YKn9Rft2NGQcXOjogixzNGfCeCazZssDAn44 324 | sgDQowoYMfKp0ERttYVhAwAM5LfRfkBQccUZtY3hgAATePBBBwUAMAMgHK0AQBFUZIGGEx6o8AYg 325 | e8ThggszzKHgRisIAGIWEJ4AwAV/BDUQBggsEWIZaKmAgY47EgDiFmiUUQYbJqxHpAUIMBHiGV5g 326 | MYYQP+kISAUJWEGFF2ngAEAAGbqgZQUGXPEjGiMwMIccc+jYxwUJqLnFGWOsAYKTRNIBQINedIak 327 | BxYQKVAdAPyAJxgRjAkACoYKgigSanQWQAx92NFHpIgGAd8XANwR6UCIHuCAAwcAkMeoAvUBxwsU 328 | MswwwwubsmrrrbjmquuuvPbKUUAAOw== 329 | ''' 330 | 331 | self.balloon_icon = '''\ 332 | R0lGODlhMAAwAPejAAAAAAICAgMDAwQEBAUFBQgICAkJCQoKCgsLCwwMDA0NDQ8PDxAQEBERERQU 333 | FBUVFRYWFhgYGBsbGx0dHR4eHh8fHyEhISMjIyUlJSYmJicnJygoKCsrKy0tLTAwMDIyMjMzMzQ0 334 | NDc3Nzg4ODo6Ojw8PD09PT8/P0JCQkdHR0hISEtLS0xMTE1NTU9PT1BQUFFRUVJSUlRUVFVVVVZW 335 | VldXV1lZWVpaWltbW11dXV9fX2BgYGFhYWJiYmVlZWZmZmhoaGlpaWpqamtra21tbW5ubm9vb3Jy 336 | cnNzc3Z2dnh4eHt7e3x8fH5+fn9/f4CAgIKCgoODg4SEhIaGhoyMjI2NjY6Ojo+Pj5CQkJSUlJaW 337 | lpeXl5mZmZubm52dnZ6enqGhoaKioqOjo6SkpKenp6mpqaqqqqurq6ysrK2tra6urq+vr7CwsLGx 338 | sbOzs7S0tLa2tre3t7m5ubu7u7y8vL29vb+/v8HBwcLCwsPDw8TExMXFxcbGxsnJycrKysvLy83N 339 | zc/Pz9DQ0NLS0tTU1NXV1djY2NnZ2dra2tvb29zc3N/f3+Dg4OHh4ePj4+fn5+jo6Onp6erq6uzs 340 | 7O3t7e/v7/Dw8PHx8fLy8vPz8/T09Pb29vf39/n5+fr6+vv7+/z8/P39/f7+/v///wAAAAAAAAAA 341 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 342 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 343 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 344 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 345 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKQALAAAAAAwADAA 346 | AAj+AEkJHEiwoMGDCBMqXMiwocOHECNKnEiRVKAvVbr4qTgRlBscCgCINCAjTSeODvVUIMDji59D 347 | gML8OBBBDsqFUQDsqDSqp89RmoQASHITIRYAYX4q7bkGgJOiBeEAELO0ahsAZaAO3MCjqtcjETZp 348 | VQPgkdeqmBqI0YrDx1mvRlYYyuIFEcUyHhgA+PK26hkAASYsAACBDMRNIQD84GOoU9+lmvpACuXp 349 | ERMANBx+ytBh0ePPPu8AMNLQRgRKoFOPmgNgo8JFAPKoVt3iRiOFVCh4mp0azAMvCmG45Q1aUAAp 350 | ClEgIQ4akIApCn2kYP55i4S1Ca9Got63RJFLCy3+POF+9guARAzHACBDfmkaANAbOtnbvuefFQCa 351 | QJwhpP6kABcYEpEOQdQ3ygpFSKSECAYO8YJEdAAwSH0q+DCRBle0BxsaEy1RgFncdYFBRR/k8Jko 352 | opxFSEWEELDDYzw08IdPfXxQgBCQoHTHACYg4tUiKmggAwBbjOIFABv0AIEDe6D0CAkA+JBGIZZY 353 | YkgbQADwASOkxJGJIgBY4dMQGhQFRw0IACCAAAAUAMMbBTEBwk+NAMAGVJzgAQYYdmRyUAw9/JRJ 354 | AlxoxdAVAUASGgCFGLoQKBWwgEgldUSQAymPmCGJowg1koFIALhAiiVpRoAJpwgNsoYjAhHRwigb 355 | J0CBKkNOjCAIB1rMyhALANig66/ABitsUQEBADs= 356 | ''' 357 | 358 | self.shield_icon = '''\ 359 | R0lGODlhMAAwAPcAAAEAAAwBAQAGCAALDQsLCxIAABgAAAAPEgAQEwQdHhQUFB0dHSQBASwBADIB 360 | AD0OBxQsLRMjJQ8wMRsyNRk1OCQkJCsrKzAvLjAwLyUyNDMzMzg3Nz08PEQDAFwKBlULCFcGA08U 361 | DVEUDVoXDmMIA2sKBXANCHQUD34RDGYbEXwmGiZCRjpRUkRDQ09OTkpcXFNTUlhYV1tbWmBgX0Rl 362 | Zk9lZlJgYVJsbllzdGRkY2pqamJzdXJxcHV6enx7e4gTD44XEpMZEqojHLkkHZYuIYwxI5czJaU6 363 | K8sjHs4nIc0qI9knIdIvJ9ovJ9EvKNIwJ9MyKtwxKdg9Mu0sJuQzK+s1LPM3Lfg3Lvc4Lvs4L/87 364 | MbdCM8pMO99JPOBNPt9WRORUQ+dYRupaR+xdSfVSQ/9WRfVdSfxbSe5iTfRgTPxiTf9mUf9rVP9v 365 | WP9yWnaAgXyLjYOEg4yLioCSk4uRkYSYmJSTk5ubm5SdnZqjpJ2oqKSkpKOop6mpp6eqqqurq6ez 366 | srOzs7O9vb69vcC/v77Dw8PEw8jGxsnIx8vLytHQz9TU09jW1dvZ19zc3ODf3t/g3uPj4+rn5uvp 367 | 5+rq6fDv7vTz8/7+/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 368 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 369 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 370 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 371 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 372 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAJgALAAAAAAwADAA 373 | AAj+ADEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGA06GmRHR4wWLVzkkBNokaWMBxfFqQCAQAUO 374 | LmTI+IhBAQAFOQahFLjIBYAKOghZquRokdFFjihZUqkBgIU/Ge847XPJUqEdEw4A2ArgAAQbgChd 375 | 6glAxkU7AHRcutRjwFYHKYoQAYKChAOuLyBd+gOgRUVHAHJcOoQAwAguatawaVNmipXHVJSY2Brn 376 | UiAAdijqWDAYAIMvbNaYESNmzJgnTFJHoZKFigcAPS7JqECRQ4xLEQqEFhNmDGkxo1GnTk1FCwgA 377 | kezQnhjDRSIARNiEKU3dNBopqYUziQxAUBwNFH3+cDgEwAib3+jHeFEyPDuVId1n+J3Ix4IiBCnO 378 | oxeDxkuS9kygZkUQABTSgg4UIRLAIhN0oB96aITxH4BMXFHCAIdYcAdFjwAwyA0NrOEbdaT1x95w 379 | AnYggSIEHELRJQrQAQcAaoy23xhpSMEeagIycAMhADxSUQwx5AEAYvvxFwaFqwEwhxwKXFKRHRaQ 380 | p8KDpPmW3ZZWCAEAHzGYVdEiACSyggNYjtFfEk4EOJxxAySyQB8WWRLAHXPQaONvY0iRhHZMWFEA 381 | DeQ5cpEMOZB3hHQQjgGFE5AqwZ0eciyA0SAEODIBmjei8egTkGrxmiMcxIHRJQTcYWQYavhmWp/4 382 | EzIRRRUA4EBeJBnpoAElAqTQxnT8dcFmak5oAQQAi+iK0nOHzCiGGtP5xqOsVgTAgiQB5LETBy1c 383 | AoAIbowxHRqnpabFCcj5oMBJKJGHCB4AbPGrdX5SwQQANlgiQGY7YdKCBZdkAIAZa4SBhrCsMTCA 384 | bFH2iwlgd3jbABtsrNeEFh+USSZUDmMSBwCVkBeCG2QscS4AdFxSwXwdY7IAwHwA8MAZosImGwCU 385 | tMwTALddFgADAFT2MZ06C9RHWmNBkEAgl6DlQ9EEocVDVZFcIhWCUEfNsyWX+JBW1gb9QYAGLQQN 386 | 9kGRcGBBIme37fbbcMct99xQBwQAOw== 387 | ''' 388 | 389 | self.double_exclamation_icon = '''\ 390 | R0lGODlhKAAoAPcAAJcbD5QbEZkcEL4cDq0fE50lGZ8pHaAqHrQhFLsiFbYmGpguJZoyKpk4MMId 391 | D8EeEMQkFsslF8onGMIoG84oGdUpGtwrGtEwH+MtHOouHPEtGuwwHvAxHsYuIcgzJcs3KdQyItk3 392 | KN87Kt0+MO80IOM/LvE2IfQ4I8pAM8pGOs1LP9RCNd9DNNVGONlLPeRBMKBQSrVTS61eV7peValo 393 | Y8FTSc1XTN9TRNRWSd1WStdbTs1bUNleUvJbR9ZnXd9mWuBqXvFpWvhwW9FsY9hsYdtxZ9N0a9l/ 394 | dut1Zut2aOt6bPp3Yvl8aOF8cseEftCEfd2FfeOCePGEeLmGgsiGgdmHgNyLg9CPiN2TjN6cleGN 395 | heuKgOWUjOOZkuqbkvSclN63tNW4ttm/veimoOOqpeurpeavquyxq+i1sO67tuS9uu2+uu3DvvnE 396 | v9/KyOHEwezGw+7JxfDHw/HLx/zKxPzPyfLRzfzRzPLV0vPZ1/Xc2v3b2O/k4/Pm5fjm5P7o5vbt 397 | 7Pnt6/bw7/bx8Pvz8vz49/36+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 398 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 399 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 400 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 401 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 402 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 403 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIcALAAAAAAoACgA 404 | AAj+AA8JHEiwoMGDCBMqXMiwocOHECNKnAhxT5s6dO7cqVPnTsaChuhk1MiRIx0/DL/0kMKkpUsh 405 | UgoWCuKy5pIgbRh6wWDCBAkTG06c4BDF4A0THIAOFXrDDsM0IjRoyEA1w1QvBoFsmFp1KhCUC+3k 406 | IJGBhIYNGzhgOGMwSlq0GbZmiGKIoZ8fZami3YAhjkEue/dy2MClIaEmGOJmwIC2Qh6DZRhXjYuh 407 | jEMuGMiSSEwiRCCDbCxQJUvVAhuHY0RjSMzYxcE8F0iojnsBj8PQm1cn5nEQEIgNGUSLBvG5oZ0K 408 | unUXOWioRWnRGVrUbejnQnIMFrQg1IE9uY6Hhkb+YLdgAXsXhER0l7dABCIO8t0roEEIZf16KBB9 409 | kIdfAQ7CLhXsZ0EF5z0ERYAVIFeBHgiZEeB+FZgBERYIBugBIQjBgSB5FPj3EBkVVoBDQnhIMGCA 410 | ETz2kBoJtrhDQoB4cGIFHgACUYktUmCEQim0WEEKEfnhQY5WKLSDjy9CZAgKM2ahEBE+thfRDAEc 411 | UIAAaih0RQAGHADAFRIRMoghhBAyHUKFCGKIIYIUQtGbEemxBRJKWKZQIF4kgYQXxT00BwslgADC 412 | C00kRIgOL4BQQQk5YPgQDxUMICkEFWR5kBYVPPDAAA5UoJ1DfkwAgQQQQJBABEUeZEOprEKQJHVT 413 | CrRa6hMI1SBrBDFAFAMEFCQAQQQEgIEQFQhQUGoECTgBkRsBIEAqATLAyAACpSLAgI3LwrAAA1Oc 414 | 2RsNDDBAA1gR9SHGG94iNIgYYggC57sFBQQAOw== 415 | ''' 416 | 417 | self.master_version = master_version 418 | self.admin_password = admin_password 419 | self.fwpw_status = fwpw_status 420 | self.hashed_key = None 421 | self.obfuscated_keys = None 422 | self.obfuscated_string = None 423 | self.cleared_keys = None 424 | self.postinstall_script = None 425 | self.plaintext_keys = None 426 | self.key_item = '' 427 | self.keys_loaded = False 428 | self.key_source = "" 429 | self.state_button_state = "disabled" 430 | self.hash_button_state = "disabled" 431 | 432 | self.previous_keys = [] 433 | self.current_key = '' 434 | 435 | self.config_options = {} 436 | self.injest_config() 437 | 438 | self.remote_username = StringVar() 439 | self.remote_password = StringVar() 440 | self.remote_hostname = StringVar() 441 | 442 | self.jamf_username = StringVar() 443 | self.jamf_password = StringVar() 444 | self.jamf_hostname = StringVar() 445 | 446 | self.jamf_hostname.set("https://jamf.pro.server:8443") 447 | self.jamf_username.set("") 448 | self.jamf_password.set("") 449 | 450 | self.hashed_results = StringVar() 451 | self.fwpm_package_dest = StringVar() 452 | self.signing_cert = StringVar() 453 | self.keyfile_loc = StringVar() 454 | self.status_string = StringVar() 455 | self.fwpm_package_dest.set("/") 456 | # self.status_string.set(u'\U0001F923'.encode('utf-8')) 457 | self.status_string.set("Ready.") 458 | self.fwpw_enable = IntVar() 459 | self.fwpw_enable.set(0) 460 | self.reboot_enable = IntVar() 461 | self.reboot_enable.set(0) 462 | self.include_config = IntVar() 463 | self.include_config.set(0) 464 | 465 | self.use_slack = IntVar() 466 | self.use_slack.set(0) 467 | self.slack_identifier = StringVar() 468 | self.slack_url = StringVar() 469 | self.slack_info_url = StringVar() 470 | self.slack_info_channel = StringVar() 471 | self.slack_info_bot = StringVar() 472 | self.slack_error_url = StringVar() 473 | self.slack_error_channel = StringVar() 474 | self.slack_error_bot = StringVar() 475 | self.state_string = StringVar() 476 | self.state_string.set('Firmware password is ' + self.fwpw_status) 477 | self.keys_loaded_string = StringVar() 478 | self.keys_loaded_string.set('No keys in memory') 479 | self.logger.info(self.state_string.get()) 480 | self.logger.info(self.keys_loaded_string.get()) 481 | 482 | if self.config_options: 483 | if self.config_options['slack']['slack_info_url']: 484 | self.slack_info_url.set(self.config_options['slack']['slack_info_url']) 485 | 486 | if self.config_options['slack']['slack_info_bot_name']: 487 | self.slack_info_bot.set(self.config_options['slack']['slack_info_bot_name']) 488 | 489 | if self.config_options['slack']['slack_info_channel']: 490 | self.slack_info_channel.set(self.config_options['slack']['slack_info_channel']) 491 | 492 | if self.config_options['slack']['slack_error_url']: 493 | self.slack_error_url.set(self.config_options['slack']['slack_error_url']) 494 | 495 | if self.config_options['slack']['slack_error_bot_name']: 496 | self.slack_error_bot.set(self.config_options['slack']['slack_error_bot_name']) 497 | 498 | if self.config_options['slack']['slack_error_channel']: 499 | self.slack_error_channel.set(self.config_options['slack']['slack_error_channel']) 500 | 501 | if self.config_options['slack']['slack_identifier']: 502 | self.slack_identifier.set(self.config_options['slack']['slack_identifier']) 503 | 504 | if self.config_options['slack']['use_slack']: 505 | # translate into 0/1 for false/true 506 | self.use_slack.set(1) 507 | self.slack_optionator() 508 | else: 509 | self.use_slack.set(0) 510 | 511 | if self.config_options['keyfile']['path']: 512 | self.keyfile_loc.set(self.config_options['keyfile']['path']) 513 | 514 | self.root.columnconfigure(0, weight=1) 515 | self.root.rowconfigure(0, weight=1) 516 | self.root.geometry("604x500") 517 | 518 | self.logo_photoimage = PhotoImage(data=self.logo) 519 | self.closed_lock_icon_photoimage = PhotoImage(data=self.closed_lock_icon) 520 | self.open_lock_icon_photoimage = PhotoImage(data=self.open_lock_icon) 521 | self.key_icon_photoimage = PhotoImage(data=self.key_icon) 522 | self.balloon_icon_photoimage = PhotoImage(data=self.balloon_icon) 523 | self.shield_icon_photoimage = PhotoImage(data=self.shield_icon) 524 | self.double_exclamation_icon_photoimage = PhotoImage(data=self.double_exclamation_icon) 525 | 526 | self.superframe = ttk.Frame(self.root, width=604, height=525) 527 | self.superframe.grid(column=0, row=0, sticky=(N, W, E, S)) 528 | 529 | self.logoframe = ttk.Frame(self.superframe, width=604, height=90) 530 | self.logoframe.grid(column=0, row=0, sticky=(N, W, E, S)) 531 | 532 | self.logoframe.grid_rowconfigure(0, weight=1) 533 | self.logoframe.grid_rowconfigure(2, weight=1) 534 | self.logoframe.grid_columnconfigure(0, weight=1) 535 | self.logoframe.grid_columnconfigure(2, weight=1) 536 | 537 | self.logo_label = ttk.Label(self.logoframe) 538 | self.logo_label['image'] = self.logo_photoimage 539 | self.logo_label.grid(column=1, row=1, sticky=(N, S, E, W)) 540 | 541 | self.stateframe = ttk.Frame(self.superframe, width=604, height=30) 542 | self.stateframe.grid(column=0, row=1, sticky=(N, W, E, S)) 543 | self.stateframe.grid_columnconfigure(0, weight=1) 544 | self.stateframe.grid_rowconfigure(0, weight=1) 545 | self.stateframe.grid_columnconfigure(2, weight=1) 546 | self.stateframe.grid_rowconfigure(2, weight=1) 547 | 548 | self.lock_label = ttk.Label(self.stateframe) 549 | if self.fwpw_status == 'On': 550 | self.lock_label['image'] = self.closed_lock_icon_photoimage 551 | else: 552 | self.lock_label['image'] = self.open_lock_icon_photoimage 553 | self.lock_label.grid(column=0, row=1, sticky=(E)) 554 | 555 | self.state_label = ttk.Label(self.stateframe, textvariable=self.state_string, font=("Helvetica", 24)) 556 | self.state_label.grid(column=1, row=1, columnspan=1, sticky=(W)) 557 | 558 | self.keys_label = ttk.Label(self.stateframe) 559 | self.keys_label['image'] = self.balloon_icon_photoimage 560 | self.keys_label.grid(column=0, row=2, sticky=(E)) 561 | 562 | self.keys_loaded_label = ttk.Label(self.stateframe, textvariable=self.keys_loaded_string, font=("Helvetica", 24)) 563 | self.keys_loaded_label.grid(column=1, row=2, columnspan=1, sticky=(W)) 564 | 565 | ttk.Separator(self.stateframe, orient=HORIZONTAL).grid(row=10, columnspan=3, sticky=(E, W), pady=0) 566 | 567 | self.navframe = ttk.Frame(self.superframe, width=604, height=30) 568 | self.navframe.grid(column=0, row=10, sticky=N) 569 | 570 | self.master_pane() 571 | 572 | def master_pane(self): 573 | """ 574 | The home pane. 575 | """ 576 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 577 | self.logger.info("%s" % inspect.stack()[1][3]) 578 | 579 | self.mainframe = ttk.Frame(self.superframe, width=604, height=510) 580 | self.mainframe.grid(column=0, row=2, sticky=(N, W, E, S)) 581 | 582 | self.mainframe.grid_rowconfigure(0, weight=1) 583 | self.mainframe.grid_rowconfigure(5, weight=1) 584 | self.mainframe.grid_columnconfigure(0, weight=1) 585 | self.mainframe.grid_columnconfigure(2, weight=1) 586 | 587 | self.change_state_btn = ttk.Button(self.mainframe, width=20, text="Change State", command=self.change_state) 588 | self.change_state_btn.grid(column=0, row=80, pady=4, columnspan=3) 589 | self.change_state_btn.configure(state=self.state_button_state) 590 | 591 | self.info_status_label = ttk.Label(self.mainframe, text='Location of keyfile:') 592 | self.info_status_label.grid(column=0, row=90, pady=8, columnspan=3) 593 | 594 | ttk.Button(self.mainframe, width=20, text="Retrieve from JSS Script", command=self.jss_pane).grid(column=0, row=100, pady=4, columnspan=3) 595 | 596 | 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) 597 | 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) 598 | ttk.Button(self.mainframe, width=20, text="Enter Firmware Password", command=self.direct_entry_pane).grid(column=0, row=320, pady=4, columnspan=3) 599 | 600 | ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=400, columnspan=3, sticky=(E, W), pady=8) 601 | 602 | hash_display = ttk.Entry(self.mainframe, width=58, textvariable=self.hashed_results) 603 | hash_display.grid(column=0, row=450, columnspan=4) 604 | 605 | self.hash_btn = ttk.Button(self.mainframe, width=20, text="Copy hash to clipboard", command=self.copy_hash) 606 | self.hash_btn.grid(column=0, row=500, pady=4, columnspan=3) 607 | self.hash_btn.configure(state=self.hash_button_state) 608 | 609 | ttk.Separator(self.mainframe, orient=HORIZONTAL).grid(row=700, columnspan=3, sticky=(E, W), pady=8) 610 | 611 | self.status_label = ttk.Label(self.mainframe, textvariable=self.status_string) 612 | self.status_label.grid(column=0, row=2100, sticky=W, columnspan=2) 613 | 614 | ttk.Button(self.mainframe, text="Quit", width=6, command=self.root.destroy).grid(column=2, row=2100, sticky=E) 615 | 616 | def jss_pane(self): 617 | """ 618 | JAMF server interaction pane. 619 | """ 620 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 621 | self.mainframe.grid_remove() 622 | 623 | try: 624 | if self.config_options["keyfile"]["remote_type"] == 'jamf': 625 | if self.config_options["keyfile"]["server_path"]: 626 | self.jamf_hostname.set(self.config_options["keyfile"]["server_path"]) 627 | 628 | if self.config_options["keyfile"]["username"]: 629 | self.jamf_username.set(self.config_options["keyfile"]["username"]) 630 | 631 | if self.config_options["keyfile"]["password"]: 632 | self.jamf_password.set(self.config_options["keyfile"]["password"]) 633 | except: 634 | pass 635 | 636 | self.jss_frame = ttk.Frame(self.superframe, width=604, height=510) 637 | self.jss_frame.grid(column=0, row=2, sticky=(N, W, E, S)) 638 | 639 | self.jss_frame.grid_columnconfigure(0, weight=1) 640 | self.jss_frame.grid_columnconfigure(1, weight=1) 641 | self.jss_frame.grid_columnconfigure(2, weight=1) 642 | self.jss_frame.grid_columnconfigure(3, weight=1) 643 | 644 | # 52? 645 | beam_a = ttk.Button(self.jss_frame, width=5) 646 | beam_a.grid(column=0, row=0, sticky=W) 647 | beam_b = ttk.Button(self.jss_frame, width=20) 648 | beam_b.grid(column=1, row=0, sticky=W) 649 | beam_c = ttk.Button(self.jss_frame, width=10) 650 | beam_c.grid(column=2, row=0, sticky=W) 651 | beam_d = ttk.Button(self.jss_frame, width=16) 652 | beam_d.grid(column=3, row=0, sticky=W) 653 | beam_a.grid_remove() 654 | beam_b.grid_remove() 655 | beam_c.grid_remove() 656 | beam_d.grid_remove() 657 | 658 | ttk.Label(self.jss_frame, text="Download keys from Jamf Pro FWPM script:").grid(column=0, row=100, columnspan=4, sticky=(E, W)) 659 | # ttk.Separator(self.hash_frame, orient=HORIZONTAL).grid(row=120, columnspan=50, sticky=(E, W)) 660 | 661 | ttk.Label(self.jss_frame, text="Server:").grid(column=0, row=150, sticky=E) 662 | hname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_hostname) 663 | hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) 664 | 665 | ttk.Label(self.jss_frame, text="Username:").grid(column=0, row=200, sticky=E) 666 | uname_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_username) 667 | uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) 668 | 669 | ttk.Label(self.jss_frame, text="Password:").grid(column=0, row=250, sticky=E) 670 | pword_entry = ttk.Entry(self.jss_frame, width=30, textvariable=self.jamf_password, show="*") 671 | pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) 672 | 673 | 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) 674 | 675 | ttk.Separator(self.jss_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) 676 | 677 | ttk.Button(self.jss_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) 678 | ttk.Button(self.jss_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) 679 | 680 | def direct_entry_pane(self): 681 | """ 682 | Directly enter fwpw. 683 | """ 684 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 685 | 686 | self.current_key = tkSimpleDialog.askstring("FW Password", "Enter firmware password:", show='*', parent=self.root) 687 | 688 | if self.current_key: 689 | self.keys_loaded = True 690 | self.calculate_hash() 691 | self.status_string.set('Keys loaded successfully.') 692 | self.keys_label['image'] = self.key_icon_photoimage 693 | self.keys_loaded_string.set('Keys in memory.') 694 | self.change_state_btn.configure(state="normal") 695 | 696 | else: 697 | self.flush_keys() 698 | self.status_string.set('Blank password entered.') 699 | self.logger.error('Direct enter blank password.') 700 | self.change_state_btn.configure(state="disabled") 701 | 702 | def remote_nav_pane(self): 703 | """ 704 | Connect to server and select keyfile. 705 | """ 706 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 707 | self.mainframe.grid_remove() 708 | 709 | try: 710 | if self.config_options["keyfile"]["remote_type"] == 'smb': 711 | if self.config_options["keyfile"]["server_path"]: 712 | self.remote_hostname.set(self.config_options["keyfile"]["server_path"]) 713 | 714 | if self.config_options["keyfile"]["username"]: 715 | self.remote_username.set(self.config_options["keyfile"]["username"]) 716 | 717 | if self.config_options["keyfile"]["password"]: 718 | self.remote_password.set(self.config_options["keyfile"]["password"]) 719 | except: 720 | pass 721 | 722 | self.remote_nav_frame = ttk.Frame(self.superframe, width=604, height=510) 723 | self.remote_nav_frame.grid(column=0, row=2, sticky=(N, W, E, S)) 724 | 725 | self.remote_nav_frame.grid_columnconfigure(0, weight=1) 726 | self.remote_nav_frame.grid_columnconfigure(1, weight=1) 727 | self.remote_nav_frame.grid_columnconfigure(2, weight=1) 728 | self.remote_nav_frame.grid_columnconfigure(3, weight=1) 729 | 730 | ttk.Label(self.remote_nav_frame, text="Read keyfile from remote server: (ie smb://...)").grid(column=0, row=100, columnspan=4, sticky=(E, W)) 731 | 732 | ttk.Label(self.remote_nav_frame, text="Server path:").grid(column=0, row=150, sticky=E) 733 | hname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_hostname) 734 | hname_entry.grid(column=1, row=150, sticky=W, columnspan=2) 735 | 736 | ttk.Label(self.remote_nav_frame, text="Username:").grid(column=0, row=200, sticky=E) 737 | uname_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_username) 738 | uname_entry.grid(column=1, row=200, sticky=W, columnspan=2) 739 | 740 | ttk.Label(self.remote_nav_frame, text="Password:").grid(column=0, row=250, sticky=E) 741 | pword_entry = ttk.Entry(self.remote_nav_frame, width=30, textvariable=self.remote_password, show="*") 742 | pword_entry.grid(column=1, row=250, sticky=W, columnspan=2) 743 | 744 | 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) 745 | 746 | ttk.Separator(self.remote_nav_frame, orient=HORIZONTAL).grid(row=1000, columnspan=50, pady=12, sticky=(E, W)) 747 | 748 | ttk.Button(self.remote_nav_frame, text="Return to home", command=self.master_pane).grid(column=2, row=1100, sticky=E) 749 | ttk.Button(self.remote_nav_frame, text="Quit", width=6, command=self.root.destroy).grid(column=3, row=1100, sticky=W) 750 | 751 | def local_nav_pane(self): 752 | """ 753 | Select keyfile from local volume. 754 | """ 755 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 756 | 757 | self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) 758 | 759 | if self.key_item: 760 | self.status_string.set('Object found.') 761 | self.handle_key_item() 762 | else: 763 | self.status_string.set('No object selected.') 764 | 765 | def search_jss(self): 766 | """ 767 | Search the JAMF server for FWPM Control script, strip out and categorize keyfile. 768 | """ 769 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 770 | 771 | try: 772 | jss_search_url = self.jamf_hostname.get() + '/JSSResource/scripts' 773 | headers = {'Accept': 'application/json', } 774 | response = requests.get(url=jss_search_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) 775 | 776 | script_list = response.json() 777 | 778 | except requests.exceptions.HTTPError as this_error: 779 | self.logger.error("http error %s: %s\n" % (response.status_code, this_error)) 780 | 781 | if response.status_code == 400: 782 | self.logger.error("HTTP code {}: {}".format(response.status_code, "Request error.")) 783 | elif response.status_code == 401: 784 | self.logger.error("HTTP code {}: {}".format(response.status_code, "Authorization error.")) 785 | elif response.status_code == 403: 786 | self.logger.error("HTTP code {}: {}".format(response.status_code, "Permissions error.")) 787 | elif response.status_code == 404: 788 | self.logger.error("HTTP code {}: {}".format(response.status_code, "Resource not found.")) 789 | 790 | 791 | for item in script_list['scripts']: 792 | if 'FWPM Control' in item['name']: 793 | target_id = item['id'] 794 | 795 | script_url = self.jamf_hostname.get() + '/JSSResource/scripts/id/' + str(target_id) 796 | headers = {'Accept': 'application/json', } 797 | response = requests.get(url=script_url, headers=headers, auth=requests.auth.HTTPBasicAuth(self.jamf_username.get(), self.jamf_password.get())) 798 | 799 | response_json = response.json() 800 | 801 | if response.status_code != 200: 802 | self.logger.info("%i returned." % response.code) 803 | return 804 | 805 | working_output = response_json['script']['script_contents'].split('\n') 806 | self.previous_keys = [] 807 | 808 | for line in working_output: 809 | if "'previous':" in line and '#' not in line: 810 | try: 811 | contents = re.findall(r'\s*\'previous\': \[(.*)\]', line) 812 | if contents: 813 | in_contents = contents[0].split(', ') 814 | in_contents = [i for i in in_contents if i] 815 | for item in in_contents: 816 | subitem = item.split('"') 817 | subitem = [i for i in subitem if i] 818 | subitem = [i for i in subitem if i != ','] 819 | 820 | if subitem: 821 | self.previous_keys.append(subitem[0]) 822 | 823 | except Exception as exception_message: 824 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 825 | 826 | elif "'new':" in line and '#' not in line: 827 | try: 828 | contents = re.findall(r'\s*\'new\': (.*)', line) 829 | if contents: 830 | if len(contents) == 1: 831 | contents = contents[0] 832 | else: 833 | quit() 834 | subitem = contents.split('"') 835 | subitem = [i for i in subitem if i] 836 | # self.current_key = subitem[0] 837 | self.current_key = subitem[0] 838 | except Exception as exception_message: 839 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 840 | 841 | try: 842 | self.calculate_hash() 843 | self.status_string.set('Keys loaded successfully.') 844 | self.keys_loaded_string.set('Keys copied to memory.') 845 | # self.hash_button_state 846 | # self.change_state_btn.configure(state="normal") 847 | except Exception as exception_message: 848 | self.logger.error(exception_message) 849 | self.flush_keys() 850 | # self.change_state_btn.configure(state="disabled") 851 | 852 | def local_fetch(self): 853 | """ 854 | Popup simple local navigation dialog. 855 | """ 856 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 857 | 858 | self.key_item = tkFileDialog.askopenfilename(title="Local object", message="Select local object:", parent=self.root) 859 | 860 | if self.key_item: 861 | self.status_string.set('Object found.') 862 | self.handle_key_item() 863 | else: 864 | self.status_string.set('No object selected.') 865 | 866 | def read_remote(self): 867 | """ 868 | Handle server connection, keyfile selection and remote volume dismount. 869 | """ 870 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 871 | 872 | tmp_directory = "/tmp/sk/mount" 873 | if not os.path.exists(tmp_directory): 874 | os.makedirs(tmp_directory) 875 | 876 | try: 877 | self.logger.info("%s: %s" % (inspect.stack()[0][3], "Mounting")) 878 | msb.mount_share_at_path_with_credentials(self.remote_hostname.get(), tmp_directory, self.remote_username.get(), self.remote_password.get()) 879 | 880 | self.key_item = tkFileDialog.askopenfilename(initialdir=tmp_directory, title="Remote object", message="Select remote object:", parent=self.root) 881 | 882 | self.logger.info("%s: %s" % (inspect.stack()[0][3], self.key_item)) 883 | 884 | if self.key_item: 885 | self.status_string.set('Object found.') 886 | self.handle_key_item() 887 | else: 888 | self.status_string.set('No object selected.') 889 | 890 | self.logger.info("%s: %s" % (inspect.stack()[0][3], "Dismounting")) 891 | umount_results = subprocess.check_output(["/usr/sbin/diskutil", "unmount", tmp_directory]) 892 | self.logger.info(umount_results) 893 | 894 | except Exception as exception_message: 895 | self.logger.error(exception_message) 896 | 897 | def calculate_hash(self): 898 | """ 899 | Builds hash identical to FWPM binary. 900 | """ 901 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 902 | try: 903 | hashed_key = hashlib.new('sha256') 904 | hashed_key.update(self.current_key) 905 | 906 | for entry in sorted(self.previous_keys): 907 | hashed_key.update(entry) 908 | 909 | fwpw_managed_string = hashed_key.hexdigest() 910 | self.hashed_results.set(fwpw_managed_string) 911 | 912 | self.keys_label['image'] = self.key_icon_photoimage 913 | 914 | self.hash_btn.configure(state='normal') 915 | self.change_state_btn.configure(state='normal') 916 | 917 | self.state_button_state = 'normal' 918 | self.hash_button_state = 'normal' 919 | 920 | except Exception as exception_message: 921 | self.logger.error(exception_message) 922 | 923 | self.flush_keys() 924 | 925 | def copy_hash(self): 926 | """ 927 | Provides single button to copy hash to clipboard. 928 | """ 929 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 930 | 931 | os.system("echo '%s' | /usr/bin/pbcopy" % self.hashed_results.get()) 932 | 933 | def handle_key_item(self): 934 | """ 935 | attempts to open and parse selected keyfile 936 | 937 | plain text 938 | obfuscated 939 | 940 | inside dmg --someday 941 | inside encrypted dmg --someday 942 | """ 943 | 944 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 945 | 946 | self.flush_keys() 947 | 948 | if os.path.exists(self.key_item): 949 | item_filename = self.key_item.split('/')[-1] 950 | item_extension = item_filename.split('.')[-1] 951 | 952 | if item_extension == 'plist': 953 | passwords = [] 954 | try: 955 | keyfile_plist = plistlib.readPlist(self.key_item) 956 | 957 | content_raw = keyfile_plist["data"] 958 | content_raw = base64.b64decode(content_raw) 959 | content_raw = content_raw.split(",") 960 | content_raw = [x for x in content_raw if x] 961 | 962 | for item in content_raw: 963 | label, pword = item.split(':') 964 | pword = base64.b64decode(pword) 965 | 966 | if label == 'new': 967 | self.current_key = pword 968 | else: 969 | self.previous_keys.append(pword) 970 | 971 | except Exception as exception_message: 972 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 973 | return 974 | 975 | elif item_extension == 'txt': 976 | try: 977 | with open(self.key_item, "r") as keyfile: 978 | passwords = keyfile.read().splitlines() 979 | 980 | for item in passwords: 981 | label, pword = item.split(':') 982 | 983 | if label == 'new': 984 | self.current_key = pword 985 | else: 986 | self.previous_keys.append(pword) 987 | 988 | except Exception as exception_message: 989 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 990 | return 991 | 992 | else: 993 | self.logger.error("%s: Error parsing keyfile." % (inspect.stack()[0][3])) 994 | 995 | self.calculate_hash() 996 | 997 | else: 998 | # print('no key item') 999 | # print(self.key_item) 1000 | pass 1001 | 1002 | def injest_config(self): 1003 | """ 1004 | attempts to consume and format configuration file 1005 | """ 1006 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 1007 | 1008 | running_pathname = os.path.abspath(os.path.dirname(sys.argv[0])) 1009 | self.logger.info("%s: Application pathname: %s" % (inspect.stack()[0][3], running_pathname)) 1010 | 1011 | config_name = '/fwpm_config.ini' 1012 | config_path = '' 1013 | 1014 | if os.path.exists(pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name): 1015 | config_path = pwd.getpwuid(os.getuid())[5] + '/Library/Preferences' + config_name 1016 | else: 1017 | if ".app/Contents" in running_pathname: 1018 | running_root = '/'.join(running_pathname.split('/')[0:-3]) 1019 | self.logger.info("%s: Application root folder: %s" % (inspect.stack()[0][3], running_root)) 1020 | 1021 | if os.path.exists(running_root + config_name): 1022 | config_path = running_root + config_name 1023 | else: 1024 | if os.path.exists(running_pathname + config_name): 1025 | config_path = running_pathname + config_name 1026 | 1027 | if not config_path: 1028 | return 1029 | 1030 | self.logger.info("Configuration file: %s" % config_path) 1031 | if not os.access(config_path, os.R_OK): 1032 | self.logger.critical("Unable to access config file, check privileges.") 1033 | return 1034 | 1035 | config = ConfigParser.SafeConfigParser(allow_no_value=True) 1036 | config.read(config_path) 1037 | 1038 | self.config_options["flags"] = {} 1039 | self.config_options["keyfile"] = {} 1040 | self.config_options["logging"] = {} 1041 | self.config_options["slack"] = {} 1042 | 1043 | for section in ["flags", "keyfile", "logging", "slack"]: 1044 | for item in config.options(section): 1045 | if item.startswith("use_"): 1046 | # if "use_" in item: 1047 | try: 1048 | self.config_options[section][item] = config.getboolean(section, item) 1049 | except Exception as exception_message: 1050 | self.config_options[section][item] = False 1051 | self.logger.error("%s: Invalid/Blank value: %s:%s. [%s]" % (inspect.stack()[0][3], section, item, exception_message)) 1052 | elif "path" in item: 1053 | self.config_options[section][item] = config.get(section, item) 1054 | else: 1055 | self.config_options[section][item] = config.get(section, item) 1056 | 1057 | self.logger.info("Configuration file variables:") 1058 | for key, value in self.config_options.items(): 1059 | self.logger.info(key) 1060 | for sub_key, sub_value in value.items(): 1061 | self.logger.info("\t%s %r" % (sub_key, sub_value)) 1062 | 1063 | def change_state(self): 1064 | """ 1065 | Handles toggling of FWPW 1066 | """ 1067 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 1068 | 1069 | current_password = '' 1070 | known_current_password = False 1071 | 1072 | new_fw_tool_path = '/usr/sbin/firmwarepasswd' 1073 | new_fw_tool_exists = os.path.exists(new_fw_tool_path) 1074 | 1075 | if not new_fw_tool_exists: 1076 | self.logger.critical("firmwarepasswd tool not found.") 1077 | 1078 | full_keylist = self.previous_keys 1079 | full_keylist.append(self.current_key) 1080 | 1081 | if self.fwpw_status == 'On': 1082 | self.status_string.set('Attempting to find current password...') 1083 | new_fw_tool_cmd = [new_fw_tool_path, '-verify'] 1084 | self.logger.info(' '.join(new_fw_tool_cmd)) 1085 | 1086 | for index in reversed(xrange(len(full_keylist))): 1087 | 1088 | try: 1089 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -verify']) 1090 | 1091 | exit_condition = False 1092 | while not exit_condition: 1093 | result = child.expect(['Password:', 'password:', 'Correct', 'Incorrect', pexpect.EOF, pexpect.TIMEOUT]) 1094 | 1095 | if result == 0: 1096 | child.sendline(self.admin_password) 1097 | elif result == 1: 1098 | child.sendline(full_keylist[index]) 1099 | elif result == 2: 1100 | current_password = full_keylist[index] 1101 | known_current_password = True 1102 | self.status_string.set('local password found.') 1103 | self.logger.info('local password found.') 1104 | break 1105 | elif result == 3: 1106 | # self.logger.info('#3.') 1107 | break 1108 | elif result == 4: 1109 | # self.logger.info('#4.') 1110 | break 1111 | elif result == 5: 1112 | # self.logger.info('#5.') 1113 | break 1114 | else: 1115 | self.logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) 1116 | return 1117 | 1118 | if known_current_password: 1119 | 1120 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -delete']) 1121 | result = child.expect('Password:') 1122 | 1123 | if result == 0: 1124 | child.sendline(self.admin_password) 1125 | 1126 | result = child.expect('password:') 1127 | if result == 0: 1128 | child.sendline(current_password) 1129 | 1130 | result = child.expect(['NOTE', 'ERROR']) 1131 | if result == 0: 1132 | self.logger.info('Off.') 1133 | elif result == 1: 1134 | self.logger.info('PW incorrect.') 1135 | else: 1136 | self.logger.info('Error turning off.') 1137 | 1138 | try: 1139 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram -d fwpw-hash']) 1140 | result = child.expect('Password:') 1141 | 1142 | if result == 0: 1143 | child.sendline(self.admin_password) 1144 | 1145 | result = child.expect('') 1146 | if result == 0: 1147 | self.logger.info('removed nvram.') 1148 | elif result == 1: 1149 | self.logger.info('nvrmam oops 1') 1150 | else: 1151 | self.logger.info('nvram oops 2') 1152 | 1153 | else: 1154 | pass 1155 | 1156 | except Exception as exception_message: 1157 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 1158 | 1159 | self.lock_label['image'] = self.double_exclamation_icon_photoimage 1160 | self.state_string.set('FW password removed, reboot!') 1161 | self.slack_message("_*" + self.local_identifier + "*_ :unlock:\n" + "FWPW and nvram entry removed.", '', 'info') 1162 | 1163 | break 1164 | 1165 | except Exception as exception_message: 1166 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 1167 | 1168 | else: # self.fwpw_status == 'Off' 1169 | 1170 | # ~/Box Sync/working stuff @ box/FWPM/skeleton key 4:48pm root@t-mcdaniel-mac-laptop #170 ]firmwarepasswd -setpasswd 1171 | # Setting Firmware Password 1172 | # Enter password: 1173 | # Enter new password: 1174 | # Re-enter new password: 1175 | # ERROR | setPasswdFromCommandLine | Unable to verify password 1176 | # ERROR | main | Exiting with error: 4 1177 | 1178 | self.logger.info("Setting FW password") 1179 | 1180 | self.logger.info("Using %s" % self.current_key) 1181 | if not self.current_key: 1182 | self.logger.error('Blank key.') 1183 | return 1184 | 1185 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -setpasswd']) 1186 | result = child.expect('Password:') 1187 | 1188 | if result == 0: 1189 | child.sendline(self.admin_password) 1190 | 1191 | result = child.expect('password:') 1192 | if result == 0: 1193 | child.sendline(self.current_key) 1194 | else: 1195 | pass 1196 | 1197 | result = child.expect('new password:') 1198 | if result == 0: 1199 | child.sendline(self.current_key) 1200 | 1201 | result = child.expect(['NOTE', 'ERROR']) 1202 | if result == 0: 1203 | self.logger.info('On.') 1204 | elif result == 1: 1205 | self.logger.info('PW incorrect.') 1206 | else: 1207 | self.logger.info('Error turning off.') 1208 | 1209 | try: 1210 | 1211 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/nvram fwpw-hash=2:' + self.hashed_results.get()]) 1212 | result = child.expect('Password:') 1213 | 1214 | if result == 0: 1215 | child.sendline(self.admin_password) 1216 | 1217 | result = child.expect('') 1218 | if result == 0: 1219 | self.logger.info('added nvram.') 1220 | elif result == 1: 1221 | self.logger.info('nvrmam oops 3') 1222 | else: 1223 | self.logger.info('nvram oops 4') 1224 | 1225 | else: 1226 | pass 1227 | 1228 | except Exception as exception_message: 1229 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 1230 | 1231 | self.status_string.set('Password activated. Reboot!') 1232 | self.state_string.set('FW password activated, reboot!') 1233 | self.lock_label['image'] = self.shield_icon_photoimage 1234 | self.slack_message("_*" + self.local_identifier + "*_ :closed_lock_with_key:\n" + "FWPW and hash updated.", '', 'info') 1235 | 1236 | def flush_keys(self): 1237 | """ 1238 | Erase loaded keys, reset UI. 1239 | """ 1240 | # "secure" erase keys 1241 | # update label 1242 | # deactivate button(s) 1243 | # change icon 1244 | 1245 | self.previous_keys = [] 1246 | self.current_key = '' 1247 | 1248 | self.keys_label['image'] = self.balloon_icon_photoimage 1249 | self.keys_loaded_string.set('No keys in memory') 1250 | self.hashed_results.set('') 1251 | 1252 | self.state_button_state = 'disabled' 1253 | self.hash_button_state = 'disabled' 1254 | 1255 | self.hash_btn.configure(state='disabled') 1256 | self.change_state_btn.configure(state='disabled') 1257 | 1258 | def slack_message(self, message, icon, msg_type): 1259 | """ 1260 | Sends slack messages. 1261 | """ 1262 | if self.logger: 1263 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 1264 | 1265 | slack_info_channel = False 1266 | slack_error_channel = False 1267 | 1268 | if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_info_url"]: 1269 | slack_info_channel = True 1270 | 1271 | if self.config_options["slack"]["use_slack"] and self.config_options["slack"]["slack_error_url"]: 1272 | slack_error_channel = True 1273 | 1274 | if slack_error_channel and msg_type == 'error': 1275 | slack_url = self.config_options["slack"]["slack_error_url"] 1276 | elif slack_info_channel: 1277 | slack_url = self.config_options["slack"]["slack_info_url"] 1278 | else: 1279 | return 1280 | 1281 | payload = {'text': message, 'username': 'Skeleton Key ' + self.master_version, 'icon_emoji': ':old_key:'} 1282 | 1283 | response = requests.post(slack_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) 1284 | 1285 | self.logger.info('Response: ' + str(response.text)) 1286 | self.logger.info('Response code: ' + str(response.status_code)) 1287 | 1288 | def slack_optionator(self): 1289 | """ 1290 | Builds the local identifier string per configuration file. 1291 | 1292 | 1293 | ip, mac, hostname 1294 | computername 1295 | serial 1296 | 1297 | """ 1298 | if self.logger: 1299 | self.logger.info("%s: activated" % inspect.stack()[0][3]) 1300 | 1301 | if self.verify_network(): 1302 | try: 1303 | full_ioreg = subprocess.check_output(['ioreg', '-l']).decode('utf-8') 1304 | serial_number_raw = re.findall('\"IOPlatformSerialNumber\" = \"(.*)\"', full_ioreg) 1305 | serial_number = serial_number_raw[0] 1306 | 1307 | 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': 1308 | processed_device_list = [] 1309 | 1310 | # Get ordered list of network devices 1311 | base_network_list = subprocess.check_output(["/usr/sbin/networksetup", "-listnetworkserviceorder"]).decode('utf-8') 1312 | network_device_list = re.findall(r'\) (.*)\n\(.*Device: (.*)\)', base_network_list) 1313 | ether_up_list = subprocess.check_output(["/sbin/ifconfig", "-au", "ether"]).decode('utf-8') 1314 | for device in network_device_list: 1315 | device_name = device[0] 1316 | port_name = device[1] 1317 | try: 1318 | if port_name in ether_up_list: 1319 | device_info_raw = subprocess.check_output(["/sbin/ifconfig", port_name]).decode('utf-8') 1320 | mac_address = re.findall('ether (.*) \n', device_info_raw) 1321 | ether_address = re.findall('inet (.*) netmask', device_info_raw) 1322 | 1323 | # if len(ether_address) and len(mac_address): 1324 | if ether_address and mac_address: 1325 | processed_device_list.append([device_name, port_name, ether_address[0], mac_address[0]]) 1326 | except Exception as this_exception: 1327 | self.logger.error("error discovering device info. [%s]" % this_exception) 1328 | 1329 | 1330 | if processed_device_list: 1331 | if self.logger: 1332 | self.logger.info("1 or more active IP addresses. Choosing primary.") 1333 | 1334 | if self.config_options["slack"]["slack_identifier"].lower() == 'ip': 1335 | self.local_identifier = processed_device_list[0][2] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" 1336 | elif self.config_options["slack"]["slack_identifier"].lower() == 'mac': 1337 | self.local_identifier = processed_device_list[0][3] + " (" + processed_device_list[0][0] + ":" + processed_device_list[0][1] + ")" 1338 | elif self.config_options["slack"]["slack_identifier"].lower() == 'hostname': 1339 | try: 1340 | self.local_identifier = socket.getfqdn() 1341 | except Exception as exception_message: 1342 | if self.logger: 1343 | self.logger.error("error discovering hostname. [%s]" % exception_message) 1344 | self.local_identifier = serial_number 1345 | 1346 | else: 1347 | if self.logger: 1348 | self.logger.error("error discovering IP info.") 1349 | self.local_identifier = serial_number 1350 | 1351 | elif self.config_options["slack"]["slack_identifier"].lower() == 'computername': 1352 | try: 1353 | cname_identifier_raw = subprocess.check_output(['/usr/sbin/scutil', '--get', 'ComputerName']) 1354 | self.local_identifier = cname_identifier_raw.split('\n')[0] 1355 | if self.logger: 1356 | self.logger.info("Computername: %r" % self.local_identifier) 1357 | except Exception as exception_message: 1358 | if self.logger: 1359 | self.logger.info("error discovering computername. [%s]" % exception_message) 1360 | self.local_identifier = serial_number 1361 | elif self.config_options["slack"]["slack_identifier"].lower() == 'serial': 1362 | self.local_identifier = serial_number 1363 | if self.logger: 1364 | self.logger.info("Serial number: %r" % self.local_identifier) 1365 | else: 1366 | if self.logger: 1367 | self.logger.info("bad or no identifier flag, defaulting to serial number.") 1368 | self.local_identifier = serial_number 1369 | 1370 | except Exception as this_exception: 1371 | self.logger.error("error verifying network. [%s]" % exception_message) 1372 | self.config_options["slack"]["use_slack"] = False 1373 | else: 1374 | self.config_options["slack"]["use_slack"] = False 1375 | if self.logger: 1376 | self.logger.info("No network detected.") 1377 | 1378 | def verify_network(self): 1379 | """ 1380 | Verifies network availability. 1381 | 1382 | Host: 8.8.8.8 (google-public-dns-a.google.com) 1383 | OpenPort: 53/tcp 1384 | Service: domain (DNS/TCP) 1385 | """ 1386 | 1387 | try: 1388 | _ = requests.get("https://dns.google.com", timeout=3) 1389 | # _ = requests.get("https://8.8.8.8", timeout=3) 1390 | return True 1391 | except requests.ConnectionError as exception_message: 1392 | self.logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 1393 | return False 1394 | 1395 | 1396 | def login(root, logger): 1397 | """ 1398 | aquire admin password 1399 | """ 1400 | logger.info("%s: activated" % inspect.stack()[0][3]) 1401 | 1402 | try: 1403 | root.withdraw() 1404 | 1405 | if platform.system() == 'Darwin': 1406 | tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' 1407 | script = tmpl.format(os.getpid()) 1408 | _ = subprocess.check_call(['/usr/bin/osascript', '-e', script]) 1409 | 1410 | password = tkSimpleDialog.askstring("Password", "Enter admin password:", show='*', parent=root) 1411 | 1412 | if not password: 1413 | logger.error("%s: Canceled login." % (inspect.stack()[0][3])) 1414 | return 1415 | 1416 | cmd_output = [] 1417 | try: 1418 | child = pexpect.spawn('bash', ['-c', '/usr/bin/sudo -k /usr/sbin/firmwarepasswd -check']) 1419 | 1420 | exit_condition = False 1421 | while not exit_condition: 1422 | result = child.expect(['WARNING:', '\n\nPass', 'Password:', 'attempts', pexpect.EOF, pexpect.TIMEOUT]) 1423 | 1424 | cmd_output.append(child.before) 1425 | cmd_output.append(child.after) 1426 | if result == 0: 1427 | continue 1428 | elif result == 1: 1429 | child.sendline(password) 1430 | elif result == 2: 1431 | child.sendline(password) 1432 | elif result == 3: 1433 | logger.error("%s: Incorrect admin password." % (inspect.stack()[0][3])) 1434 | sys.exit() 1435 | elif result == 4: 1436 | exit_condition = True 1437 | elif result == 5: 1438 | exit_condition = True 1439 | else: 1440 | logger.error("%s: Unknown error. Exiting." % (inspect.stack()[0][3])) 1441 | return 1442 | except Exception as exception_message: 1443 | logger.error("%s: Unknown error. [%s]" % (inspect.stack()[0][3], exception_message)) 1444 | 1445 | # 1446 | # begin parsing out useful content 1447 | checked_output = [] 1448 | for value in cmd_output: 1449 | if isinstance(value, basestring): 1450 | if "System\r\nAdministrator." in value: 1451 | pass 1452 | elif "WARNING" in value: 1453 | pass 1454 | elif "Improper" in value: 1455 | pass 1456 | elif value == '\r\n': 1457 | pass 1458 | elif not value: 1459 | pass 1460 | elif value == "Password:": 1461 | pass 1462 | else: 1463 | checked_output.append(value) 1464 | 1465 | for item in checked_output: 1466 | if 'Enabled' in item: 1467 | if 'Yes' in item: 1468 | logger.info("Yes. %r" % item) 1469 | return password, 'On' 1470 | else: 1471 | logger.info("No. %r" % item) 1472 | return password, 'Off' 1473 | else: 1474 | sys.exit() 1475 | 1476 | except ValueError: 1477 | logger.error("%s: Error here." % (inspect.stack()[0][3])) 1478 | return 1479 | 1480 | sys.exit() 1481 | 1482 | 1483 | def main(): 1484 | """ 1485 | Entry into script. 1486 | """ 1487 | master_version = "1.0" 1488 | 1489 | logging.basicConfig(filename='/tmp/skeleton_key_v' + master_version + '.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 1490 | logger = logging.getLogger(__name__) 1491 | logger.info("Running Skeleton Key " + master_version) 1492 | 1493 | root = Tk() 1494 | 1495 | try: 1496 | admin_password, fwpw_status = login(root, logger) 1497 | except Exception as exception_message: 1498 | logger.error("%s: Error logging in. [%s]" % (inspect.stack()[0][3], exception_message)) 1499 | sys.exit(0) 1500 | 1501 | root.deiconify() 1502 | SinglePane(root, logger, admin_password, fwpw_status, master_version) 1503 | 1504 | root.mainloop() 1505 | 1506 | 1507 | if __name__ == '__main__': 1508 | main() 1509 | -------------------------------------------------------------------------------- /skeleton key/sk_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/skeleton key/sk_icon.icns -------------------------------------------------------------------------------- /skeleton key/skeleton key prebuilt binary/Skeleton Key.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/univ-of-utah-marriott-library-apple/firmware_password_manager/198694a650acd3fa61b85d25dd61e83782bb25d5/skeleton key/skeleton key prebuilt binary/Skeleton Key.zip --------------------------------------------------------------------------------