├── LICENSE ├── README.md ├── mkuser.sh └── utilities ├── create-mkuser-installation-package.sh ├── download-and-install-mkuser.sh └── download-and-run-mkuser.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Free Geek 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 | # `mkuser` for macOS 2 | 3 | `mkuser` **m**a**k**es **user** accounts for macOS with more options, more validation of inputs, and more verification of the created user account than any other user creation tool, including `sysadminctl -addUser` and System Preferences/Settings! 4 | 5 | `mkuser` supports and has been thoroughly tested with macOS 10.13 High Sierra and newer (it likely works on older versions of macOS as well, but that hasn't been tested). The newest version of macOS that `mkuser` has been tested with as of writing this is macOS 13 Ventura. Because of how `mkuser` is written and the built-in tools it uses to create user accounts, it should support future versions of macOS without any major issues as the fundamentals of user creation have been consistent for years across many versions of macOS. If somehow an issue does occur with a future version of macOS, `mkuser`'s excessive verifications should detect the issue and output a detailed warning or error message. 6 | 7 | Along with abundant options and excessive verifications, `mkuser` has detailed help info which explains each available option and what affect it will have on the created user account. This info may be informative beyond just using `mkuser` since it's really all about all the different kinds of advanced customizations user accounts can have on macOS. 8 | 9 | `mkuser` is also focused on precision and accuracy. The user accounts created by `mkuser` should be indistinguishable from a user account created by `sysadminctl -addUser` or System Preferences/Settings. This may not sound like much, but if you read through the source you'll see that there are a variety of subtleties and nuances that took quite a bit of research and effort to match exactly what `sysadminctl -addUser` and System Preferences/Settings does. Also, `mkuser` actually does better in some situations to avoid possible errors or macOS bugs! If you're interested in this, there are many detailed technical notes throughout the source that basically serve as a study in user account creation. No other scripted user account creation that I'm aware of creates user accounts as accurately as `mkuser`. 10 | 11 | `mkuser` is a single function within a single script written in `bash` with no 3rd-party dependencies (every command that `mkuser` calls is included in macOS). If you want to incorporate `mkuser`'s functionality into your own `bash` (not `zsh`) script without requiring another file, you can simply copy-and-paste the whole function into your code. Or, of course, you can call the separate `mkuser` script file from any code written in any language or directly on the command line. 12 | 13 | Some of the features of `mkuser` that are not available in other user creation tools are: create a user immediately or save a user creation package, setup automatic login, skip Setup Assistant on first boot and/or first login, prohibit standard users from changing their own password or picture, and prevent the first user from getting a Secure Token, along with all other normal user creation options you would expect and more. 14 | 15 |
16 | 17 | ## ⬇️ INSTALLATION 18 | 19 | Other than simply copy-and-pasting the entire `mkuser` function into your own `bash` scripts, you can also install the signed `mkuser` script into the `/usr/local/bin` folder which is included in the default `PATH` so that you can easily run `mkuser` in Terminal. 20 | 21 | ### Local Installation 22 | 23 | To install the signed `mkuser` script into `/usr/local/bin` for convenient usage in Terminal, you can manually download and install latest the *notarized installation package* from the *Assets* of the [latest release](https://github.com/freegeek-pdx/mkuser/releases/latest) in this GitHub repository. 24 | 25 | A *zip archive of the signed script* is also available in the *Assets* of the [latest release](https://github.com/freegeek-pdx/mkuser/releases/latest) in this GitHub repository if you just want the signed script for usage in any scenario other than installing into the `/usr/local/bin` folder. In the *Assets*, there is also a file containing SHA512 checksums of the installation package, zip archive, and script that can be used to verify downloads and installations (as well as verifying the notarization of the package and signature of the script). 26 | 27 | Also, if you want to install (or update) `mkuser` directly from your Terminal (or script), you can run **`curl mkuser.sh | sh`** which will download the latest notarized installation package from this GitHub repository and fully verify that the correct notarized package was downloaded before installation and the installed script will also be verified to be properly signed after installation. 28 | 29 | While it is wise to be wary of `curl ... | sh` type commands that directly download and execute arbitrary code, `curl`ing the [mkuser.sh](https://mkuser.sh) URL is a convenient way to download the latest [`download-and-install-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-install-mkuser.sh) script from this GitHub repository. If you prefer, you can also run `curl https://raw.githubusercontent.com/freegeek-pdx/mkuser/main/utilities/download-and-install-mkuser.sh | sh` to access that same exact installation script directly instead of through the shorter [mkuser.sh](https://mkuser.sh) convenience URL. The installation script can be examined to be safe by directly accessing any of the links or URLs above, or by running `curl mkuser.sh` without piping the script to `sh` to just display its contents in your Terminal ([mkuser.sh](https://mkuser.sh) will redirect to when accessed through a browser, but when accessed via `curl` it will load ). 30 | 31 | ### Running Without Installation 32 | 33 | It is also possible to just *run* `mkuser` without fully installing it by using the [`download-and-run-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-run-mkuser.sh) script which can be accessed by `curl`ing another convenience URL: `curl run.mkuser.sh` (which is equivalent to `curl https://raw.githubusercontent.com/freegeek-pdx/mkuser/main/utilities/download-and-run-mkuser.sh`). 34 | 35 | This [`download-and-run-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-run-mkuser.sh) script downloads the latest zip of the signed script into a temporary `/private/tmp/mkuser-run` folder and unarchives the signed script into that same folder to run it from there with your specified options and parameters. Then the entire `/private/tmp/mkuser-run` folder is deleted after `mkuser` has been run to leave nothing behind (other than the new user or user creation package that you've made with `mkuser`). Before the zip is unarchived its SHA512 checksum is verified and then before `mkuser` is run its checksum and code signature is also verified. 36 | 37 | To conveniently pass your desired options and parameters to this temporary `mkuser` script, a different technique than piping to `sh` (which is used above for installation) can be used, and that technique uses process substitution: **`sh <(curl run.mkuser.sh) [MKUSER OPTIONS AND PARAMETERS]`**. This technique is more convient because it passes all the specified options and parameter to the temporary `mkuser` script instead of to `sh` without any extra complexity and also allows passing "stdin" to the temporary `mkuser` script such as if you're using the `--stdin-password` option, for example: **`echo [PASSWORD] | sh <(curl run.mkuser.sh) --stdin-password [OTHER MKUSER OPTIONS AND PARAMETERS]`** (which would not be doable with the `curl run.mkuser.sh | sh` technique). 38 | 39 | Using this technique to run `mkuser` without fully installing it, all of the `mkuser` options and parameters *except for one* can be used normally. The *one exception* is that the `--fd-secure-token-admin-password` option cannot be used when run this way since the process substitution file descriptor would get consumed prematurely by the parent [`download-and-run-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-run-mkuser.sh) script and cannot get passed through to the temporary `mkuser` script that is run as a child process. If using the other `--secure-token-admin-password` or `--secure-token-admin-password-prompt` options cannot work for your needs and you must use the `--fd-secure-token-admin-password` option, you should do a regular installation or manually download and run the `mkuser` script to a temporary location so that your command is running the actual `mkuser` script directly. 40 | 41 | One other catch (that is similar to why `--fd-secure-token-admin-password` cannot be used) is that you *must not* manually run this command using `sudo` (like `sudo sh <(curl run.mkuser.sh)`) even though `mkuser` itself *does* need to be run as root. This is because the process substitution file descriptor containing the contents of the [`download-and-run-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-run-mkuser.sh) script would get consumed by the parent `sudo` process instead of by the `sh` child process as is needed to run properly. Instead, the [`download-and-run-mkuser.sh`](https://github.com/freegeek-pdx/mkuser/blob/main/utilities/download-and-run-mkuser.sh) script itself will handle elevating to root using `sudo` for you and prompt for an administrator password if/when that is needed. 42 | 43 | ### Installation Summary 44 | 45 | To install `mkuser` into `/usr/local/bin`: 46 | 47 | - Manually download and install the latest notarized installation package from the *Assets* of the [latest release](https://github.com/freegeek-pdx/mkuser/releases/latest) in this GitHub repository. 48 | - Or, run **`curl mkuser.sh | sh`** in your Terminal (or script) to download, verify, and install the latest notarized installation package for you. 49 | 50 | To download and run `mkuser` from a temporary location without fully installing it: 51 | 52 | - Run **`sh <(curl run.mkuser.sh) [MKUSER OPTIONS AND PARAMETERS]`** in your Terminal (or script). 53 | 54 | And, of course, you can copy-and-paste the `mkuser` function directly into your `bash` scripts, or download the zip archive of the signed script from the *Assets* of the [latest release](https://github.com/freegeek-pdx/mkuser/releases/latest) in this GitHub repository to integrate into your scripts or processes in any other way you need. 55 | 56 |
57 | 58 | ## ℹ️ USAGE NOTES 59 | 60 | For long form options (multicharacter options starting with two hyphens), case doesn't matter.
61 | For example, `--help`, `--HELP`, and `--Help` are all equal. 62 | 63 | For short form options (single character options starting with one hyphen), case DOES matter.
64 | For example, `-h` and `-H` are NOT equal. 65 | 66 | Short form options can be grouped together or passed individually.
67 | But, only a single option within a group can take a parameter and it must be the last option specified within the group.
68 | For example, `-qaAn [ACCOUNT NAME]` is valid but `-qanA [ACCOUNT NAME]` is not.
69 | Also, `-qan [ACCOUNT NAME] -Af [FULL NAME]` is valid but `-qaAnf [ACCOUNT NAME] [FULL NAME]` is not.
70 | An error will be displayed if options with parameters are grouped incorrectly. 71 | 72 | Long form options can have their word separating hyphens omitted.
73 | For example, `--user-id`, `--userid`, and `--userID` are all equal (since the case also doesn't matter).
74 | This does NOT mean word separating hyphen placement doesn't matter, all of the word separating hyphens must be correct, or all omitted. 75 | 76 | Options and their parameters can be separated by whitespace, equals (=), and can also be combined without using whitespace or equals (=).
77 | For example, `--uid [UID]`, `--uid=[UID]`, `--uid[UID]`, `-u [UID]`, `-u=[UID]`, and `-u[UID]` are all valid. 78 | 79 | If ANY options or parameters are invalid, the user or package WILL NOT be created.
80 | Instead, the invalid option errors and errors from other checks will be shown. 81 | 82 | When creating a user in an interactive Terminal on the current system (not using the `--package` option), you will be prompted for confirmation before the user is created.
83 | To NOT be prompted for confirmation in the Terminal, you must specify `--do-not-confirm` (`-F`), `--suppress-status-messages` (`-q`), or `--stdin-password`.
84 | When NOT running in an interactive Terminal, such as within an automated script, confirmation will NOT be prompted. 85 | 86 |
87 | 88 | ## 👤 PRIMARY OPTIONS 89 | 90 | #### `--account-name, --record-name, --short-name, --username, --user, --name, -n` < *string* > 91 | 92 | > Must only contain lowercase letters, numbers, hyphen/minus (-), underscore (_), or period (.) characters.
93 | > The account name cannot start with a period (.) or hyphen/minus (-).
94 | > Must be 244 characters/bytes or less and must contain at least one letter.
95 | > The account name must not already be assigned to another user.
96 | > If omitted, the full name will be converted into a valid account name by converting it to meet the requirements stated above. 97 | > 98 | > #### 244 CHARACTER/BYTE ACCOUNT NAME LENGTH LIMIT NOTES: 99 | > The account name is used as the OpenDirectory RecordName, which has a hard 244 byte length limit (and the allowed characters are always 1 byte each).
100 | > Attempting to create a user with an account name over 244 characters will fail regardless of if you try to use `sysadminctl`, `dscl`, or `dsimport`. 101 | > 102 | > #### ACCOUNT NAMES STARTING WITH PERIOD (.) NOTES: 103 | > System Preferences/Settings actually allows account names to start with a period (.), but that causes the account name to not show up in `dscacheutil -q user` or `dscl . -list /Users` even though the user does actually exist.
104 | > Also, since users with account names starting with a period (.) are NOT properly detected by macOS, their existence can break next available UID assignment by `sysadminctl -addUser` and System Preferences/Settings and both could keep incorrectly assigning the UID of the account name starting with a period (.) which fails and results in users created with no UID.
105 | > Since allowing account names starting with a period (.) would cause those issues and `mkuser` would not be able to verify that the user was properly created, starting with a period (.) is not allowed by `mkuser`. 106 | 107 |
108 | 109 | #### `--full-name, --real-name, -f` < *string* > 110 | 111 | > The only limitations on the characters allowed in the full name are that it cannot be only whitespace and cannot contain control characters other than tabs (such as line breaks).
112 | > See notes below about the non-specific length limit of the full name.
113 | > The full name must not already be assigned to another user.
114 | > If omitted, the account name will be used as the full name. 115 | > 116 | > #### FULL NAME LENGTH LIMIT NOTES: 117 | > While there is no explicit length limit, there is a combined byte length limit of the account name, full name, login shell, and home folder path.
118 | > If the combined byte length of these 4 attributes is over *1010 bytes*, the full name will not load in the "Log Out" menu item of the "Apple" menu.
119 | > While this is not a serious issue, it does indicate a bug or limitation within some part of macOS that we do not want to trigger.
120 | > `mkuser` will do this math for you and show an error with all of the byte lengths as well as how many bytes need to be removed for these 4 attributes to fit within the combined 1010 byte length limitation.
121 | > This 1010 byte length limit should not be hit under normal circumstances, so you will generally not need to worry about hitting this limit.
122 | > For a bit more technical information about this issue from my testing, search for *1010 bytes* within the source of this script. 123 | > 124 | > Even though `mkuser` will not allow it, if the byte length of these 4 combined attributes was over 1010 bytes, the account still logs in and seems to work properly other than not loading the full name in the "Log Out" menu item of the "Apple" menu.
125 | > But, if this combined byte length is over 2034 bytes, the account cannot login via login window as well as when using the `login` or `su` commands.
126 | > For a bit more technical information about this issue from my testing, search for *2034 bytes* within the source of this script. 127 | 128 |
129 | 130 | #### `--unique-id, --user-id, --uid, -u` < *integer* > 131 | 132 | > Must be an integer between -2147483648 and 2147483647 (signed 32-bit range).
133 | > The User ID (UniqueID) must not already be assigned to another user.
134 | > If omitted, the next User ID available from *501* will be used, unless creating a `--role-account` or `--service-account`, then starting from *200*.
135 | > If you're the kind of person that has noticed that UIDs may be represented outside of this range, you may be interested in reading the *UIDs CAN BE REPRESENTED IN DIFFERENT FORMS* comments in this script. 136 | > 137 | > #### NEGATIVE USER ID NOTES: 138 | > Negative User IDs should not be created under normal circumstances.
139 | > Negative User IDs are normally reserved for special system users and users with negative User IDs may not behave properly or as expected. 140 | 141 |
142 | 143 | #### `--generated-uid, --guid, --uuid, -G` < *string* > 144 | 145 | > Must be 36 characters of only capital letters, numbers, and hyphens/minuses (-) in the following format: *EIGHT888-4444-FOUR-4444-TWELVE121212*
146 | > The Generated UID (GUID) must not already be assigned to another user.
147 | > If omitted, a random Generated UID will be assigned by macOS.
148 | > You should not normally need to manually specify a Generated UID. 149 | 150 |
151 | 152 | #### `--primary-group-id, --group-id, --group, --gid, -g` < *integer* > 153 | 154 | > Must be an integer between -2147483648 and 2147483647 (signed 32-bit range).
155 | > The Group ID must already exist, non-existent Group IDs will not be created.
156 | > If omitted, the default Primary Group ID of *20* (staff) will be used, unless creating a `--service-account`, then *-2* (nobody) will be used.
157 | > If you're the kind of person that has noticed that GIDs may be represented outside of this range, you may be interested in reading the *UIDs CAN BE REPRESENTED IN DIFFERENT FORMS* comments in this script. 158 | 159 |
160 | 161 | #### `--login-shell, --user-shell, --shell, -s` < *existing path* || *command name* > 162 | 163 | > The login shell must be the path to an existing executable file, or a valid command name whose file exists within "/usr/bin", "/bin", "/usr/sbin", or "/sbin".
164 | > You must specify the path if the desired login shell is in another location.
165 | > If omitted, "/bin/zsh" will be used on macOS 10.15 Catalina and newer and "/bin/bash" will be used on macOS 10.14 Mojave and older. 166 | 167 |
168 | 169 | ## 🔐 PASSWORD OPTIONS 170 | 171 | #### `--password, --pass, -p` < *string* > 172 | 173 | > The password must meet the systems password content policy requirements.
174 | > The default password content requirements are that it must be at least 4 characters, or a blank/empty password when FileVault IS NOT enabled. 175 | > 176 | > If no password content policy is set (such as by default on macOS 10.13 High Sierra), the default requirements *will still be enforced* by `mkuser`.
177 | > Also, only the default password requirements will be enforced when outputting a user creation package, see notes below for more information. 178 | > 179 | > *Regardless of the password content policy*, `mkuser` enforces a maximum password length of 511 bytes, or 251 bytes when enabling auto-login.
180 | > See notes below for more details about these maximum length limitations. 181 | > 182 | > The only limitation on the characters allowed in the password that `mkuser` enforces is that it cannot contain any control characters such as line breaks or tabs (but a custom password content policy may enforce other limitations).
183 | > If omitted, a blank/empty password will be specified. 184 | > 185 | > #### BLANK/EMPTY PASSWORD NOTES: 186 | > Blank/empty passwords are not allowed by default when FileVault is enabled.
187 | > When FileVault is not enabled, a user with a blank/empty password WILL be able to log in and authenticate GUI prompts, but WILL NOT be able to authenticate "Terminal" commands like `sudo`, `su`, or `login`, for example. 188 | > 189 | > #### AUTO-LOGIN 251 BYTE PASSWORD LENGTH LIMIT NOTES: 190 | > Auto-login simply does not work with passwords longer than 251 bytes.
191 | > I am not sure if this is a bug or an intentional limitation, but if you set a password of 252 bytes or more and enable auto-login, macOS will boot to the login window instead of automatically logging in the user.
192 | > I am not sure what exactly is failing internally, but the behavior is as if the encoded auto-login password is incorrect.
193 | > I have confirmed this IS NOT an issue with the auto-login password encoding within `mkuser` since the same thing happens when enabling auto-login in the "Users & Groups" section of System Preferences/Settings. 194 | > 195 | > #### 511 BYTE PASSWORD LENGTH LIMIT NOTES: 196 | > Most of macOS can technically support passwords longer than 511 bytes, but both the `login` and `su` commands fail with passwords over 511 bytes.
197 | > Since 512 byte or longer passwords cannot work in all possible situations, they are not allowed since `mkuser` exists to make fully functional users.
198 | > If not being able to use the `login` and `su` commands is not an issue, and you want to use a longer password, you can just set a temporary password when creating a user with `mkuser` and then change the password to something 512 bytes or longer manually using `dscl . -passwd`.
199 | > If you manually set a password 512 bytes or longer, you will be able to login via login window as well as authenticate graphical prompts, such as unlocking System Preferences/Settings sections if the user in an admin.
200 | > For fun, I tested logging in via login window with passwords up to 10,000 bytes (typed via an Arduino) and unlocking System Preferences/Settings sections with passwords up to 150,000 bytes (copy-and-pasted).
201 | > Longer passwords took overly long for the Arduino to type or macOS to paste.
202 | > But, that longer password testing was done with non-Secure Token accounts.
203 | > When an account has a Secure Token, there are other limitations described in the *SECURE TOKEN ADMIN 1022 BYTE PASSWORD LENGTH LIMIT NOTES* in the help information for the `--secure-token-admin-password` option below. 204 | > 205 | > #### PASSWORDS IN PACKAGE NOTES: 206 | > When outputting a user creation package (with the `--package` option), only the default password content requirements are checked since the password content policy may be different on the target system.
207 | > The target systems password content policy will be checked when the package is installed and the user will not be created if the password does not meet the target systems password content policy requirements. 208 | > 209 | > The specified password (along with the existing Secure Token admin password, if specified) will be securely obfuscated within the package in such a way that the passwords can only be deobfuscated by the specific and unique script generated during package creation and only when run during the package installation process.
210 | > For more information about how passwords are securely obfuscated within the package, read the comments within the code of this script starting at: *OBFUSCATE PASSWORDS INTO RUN-ONLY APPLESCRIPT*
211 | > Also, when the passwords are deobfuscated during the package installation, they will NOT be visible in the process list or written to the filesystem since they will only exist as variables within the script and be passed to an internal `mkuser` function. 212 | 213 |
214 | 215 | #### `--stdin-password, --stdin-pass, --sp` < *no parameter* (stdin) > 216 | 217 | > Include this option with no parameter to pass the password via "stdin" using a pipe (`|`) or here-string (`<<<`), etc.
218 | > **Although, it is recommended to use a pipe instead of a here-string** because a pipe is more secure since a here-string creates a temporary file which contains the specified password while a pipe does not.
219 | > If you haven't used an `echo` and pipe (`|`) before, it looks like this: `echo [PASSWORD] | mkuser [OPTIONS] --stdin-password [OPTIONS]`
220 | > Passing the password via "stdin" instead of directly with the `--password` option hides the password from the process list.
221 | > Since `echo` is a builtin in `bash` and `zsh` and not an external binary command, the `echo` command containing the password as an argument is also never visible in the process list.
222 | > The help information for the `--password` option above also applies to passwords passed via "stdin".
223 | > **NOTICE:** Specifying `--stdin-password` also ENABLES `--do-not-confirm` since accepting "stdin" disrupts the ability to use other command line inputs. 224 | 225 |
226 | 227 | #### `--password-prompt, --pass-prompt, --pp` < *GUI* || *CLI* (or *no parameter*) > 228 | 229 | > Include this option with no parameter or specify "*CLI*" to be prompted for the new user password on the command line before creating the user or package. 230 | > 231 | > Or, specify "*GUI*" to instead be prompted graphically via AppleScript dialog.
232 | > When "*GUI*" is specified, any password errors will also be presented graphically via AppleScript dialog. 233 | > 234 | > This option allows you to specify a password without it being saved in your command line history as well as hides the password from the process list.
235 | > The help information for the `--password` option above also applies to passwords entered via command line prompt. 236 | 237 |
238 | 239 | #### `--no-password, --no-pass, --np` < *no parameter* > 240 | 241 | > Include this option with no parameter to set no password at all instead of a blank/empty password (like when the `--password` option is omitted).
242 | > This option is equivalent to setting the password to "\*" with `--password '*'` and is here as a separate option for convenience and information.
243 | > Setting the password to "\*" is a special character that indicates to macOS that this user does not have any meaningful password set.
244 | > When a user has the "\*" password set, it cannot login by any means and it will also not get any AuthenticationAuthority set in the user record.
245 | > When the "\*" password is set AND no AuthenticationAuthority exists, the user will not show in the users list in "Users & Groups" section of System Preferences/Settings and will also not show up in the login window.
246 | > If you choose to start a user out with no password for some reason, you can always set their password later with `dscl . -passwd`. 247 | > 248 | > If you include the `--prevent-secure-token-on-big-sur-and-newer` option with this option, that would create an AuthenticationAuthority attribute with the special tag to prevent a Secure Token from being granted.
249 | > Since that user would no longer have BOTH no AuthenticationAuthority AND the "\*" password, they would show in the users list in "Users & Groups" section of System Preferences/Settings as well as the login window list of users, but could not log in since no meaningful password is set. 250 | 251 |
252 | 253 | #### `--password-hint, --hint, --ph` < *string* > 254 | 255 | > Must be 280 characters or less and the only limitations on the characters allowed in the password hint are that it cannot be only whitespace and can't contain control characters other than line breaks (\n) or tabs (\t).
256 | > If omitted, no password hint will be set. 257 | > 258 | > #### 280 CHARACTER PASSWORD HINT LENGTH LIMIT NOTES: 259 | > The password hint popover in the non-FileVault login window will only display up to 7 lines at about 40 characters per line.
260 | > This results in 280 characters being a reasonable maximum length.
261 | > Since each character is a different width, 40 characters per line is just an estimation and less or more may fit depending on the characters, for example, only 14 smiley face emoji fit on a single line.
262 | > If line breaks are included, they are rendered in the password hint popover and that can make less characters show since only up to 7 lines will show.
263 | > If for some reason you need or want a longer password hint, you can just set a temporary password hint when creating a user with `mkuser` and then change the password hint to something longer manually with: `dscl . -create /Users/[ACCOUNT NAME] AuthenticationHint [PASSWORD HINT]` 264 | 265 |
266 | 267 | #### `--prohibit-user-password-changes` < *no parameter* > 268 | 269 | > Include this option with no parameter to prohibit the user from being able to change their own password without administrator authentication.
270 | > The password can still be changed in the "Users & Groups" section of System Preferences/Settings when unlocked and authenticated by an administrator.
271 | > **NOTICE:** If the password is changed with administrator authentication, the user will no longer be prohibited from changing their own password. 272 | 273 |
274 | 275 | ## 📁 HOME FOLDER OPTIONS 276 | 277 | #### `--home-folder, --home-path, --home, -H` < *non-existing path* > 278 | 279 | > The home folder path must not currently exist and must be directly within "/Users/" or "/private/var/" (or "/var/"), or on an external drive (but that is not recommended).
280 | > The special "/var/empty" and "/dev/null" paths are also allowed.
281 | > The total length of the home folder path must be 511 bytes or less, or home folder creation will fail during login or `createhomedir`.
282 | > Each folder within the home folder path must be 255 bytes or less each, as that is the max folder/file name length set by macOS.
283 | > If the home folder is not within the "/Users/" folder, the users Public folder will not be shared.
284 | > If omitted, the home folder will be set to "/Users/[ACCOUNT NAME]". 285 | 286 |
287 | 288 | #### `--do-not-share-public-folder, --dont-share-public` < *no parameter* > 289 | 290 | > Include this option with no parameter to NOT share the users Public folder.
291 | > The users Public folder will be shared by default unless the users home folder is hidden or is not within the "/Users/" folder.
292 | > The users Public folder can still be shared manually in the "File Sharing" section of the "Sharing" section of System Preferences/Settings. 293 | 294 |
295 | 296 | #### `--do-not-create-home-folder, --dont-create-home` < *no parameter* > 297 | 298 | > Include this option with no parameter to NOT create the users home folder.
299 | > The users home folder will be created by macOS when the user is logged in graphically via login window, but will not be created when logging in via "Terminal" using the `login` or `su` commands, for example.
300 | > To create the home folder at anytime via "Terminal" or script, you can use the `createhomedir -cu [ACCOUNT NAME]` command.
301 | > When using this option, you CANNOT also specify `--hide homeOnly` or `--skip-setup-assistant firstLoginOnly` since they require the home folder. 302 | 303 |
304 | 305 | ## 🖼 PICTURE OPTIONS 306 | 307 | #### `--picture, --photo, --pic, -P` < *existing path* || *default picture filename* > 308 | 309 | > Must be a path to an existing image file that is 1 MB or under, or be the filename of one of the default user pictures located within the "/Library/User Pictures/" folder (with or without the file extension, such as "Earth" or "Penguin.tif").
310 | > When outputting a user creation package (with the `--package` option), the specified picture file will be included in the user creation package.
311 | > If omitted, a random default user picture will be assigned. 312 | 313 |
314 | 315 | #### `--no-picture, --no-photo, --no-pic` < *no parameter* > 316 | 317 | > Include this option with no parameter to not set any picture instead of a random default user picture (like when the `--picture` option is omitted).
318 | > When no picture is set, a grey head and shoulders silhouette icon is used. 319 | 320 |
321 | 322 | #### `--prohibit-user-picture-changes` < *no parameter* > 323 | 324 | > Include this option with no parameter to prohibit the user from being able to change their own picture without administrator authentication.
325 | > **NOTICE:** On macOS 12 Monterey and older, the picture can still be changed in the "Users & Groups" pane of System Preferences when unlocked by an administrator, but on macOS 13 Ventura the picture can NOT be changed in the "Users & Groups" section of System Settings even when authenticated by an an administrator (unclear if this is a bug or intentional change). 326 | 327 |
328 | 329 | ## 🎛 ACCOUNT TYPE OPTIONS 330 | 331 | #### `--administrator, --admin, -a` < *no parameter* > 332 | 333 | > Include this option with no parameter to make the user an administrator.
334 | > Administrators can manage other users, install apps, and change settings. 335 | > 336 | > If omitted, a standard user will be created.
337 | > Standard users can install apps and change their own settings, but can't add other users or change other users' settings. 338 | > 339 | > For more information about administrator and standard account types, visit: 340 | 341 |
342 | 343 | #### `--hidden, --hide` < *userOnly* || *homeOnly* || *both* (or *no parameter*) > 344 | 345 | > Include this option with either no parameter or specify "*both*" to hide both the user and their home folder. 346 | > 347 | > Specify "*userOnly*" to hide only the user and keep the home folder visible.
348 | > Hidden users will not show in the users list in "Users & Groups" section of System Preferences/Settings unless they are currently logged in, and will also not show up in the login window list of users (unless they have a Secure Token and FileVault is enabled).
349 | > A hidden user can still be logged into by using text input fields in the non-FileVault login window. 350 | > 351 | > Specify "*homeOnly*" to hide only the home folder and keep the user visible.
352 | > If the home folder is hidden, the users Public folder will not be shared. 353 | > 354 | > Any other parameters are invalid and will cause the user to not be created. 355 | 356 |
357 | 358 | #### `--sharing-only-account, --sharing-account, --sharing-only, --sharing, --soa` < *no parameter* > 359 | 360 | > Include this option with no parameter to create a "Sharing Only" account. 361 | > 362 | > This is identical to a "Sharing Only" account that can be created in the "Users & Groups" section of System Preferences/Settings when adding a new user and changing the "New Account" pop-up menu to "Sharing Only".
363 | > A "Sharing Only" account can access shared files remotely, but can't log in or change settings on the computer. 364 | > 365 | > A "Sharing Only" account is equivalent to creating a user with the login shell set to "/usr/bin/false" and home set to "/dev/null" .
366 | > This can also be done manually with `--shell /usr/bin/false --home /dev/null`, or `--no-login --home /dev/null` (see `--no-login` help for more information).
367 | > Make sure to specify a password when creating a "Sharing Only" account, or it will have *a blank/empty password*. 368 | > 369 | > Also, when running on macOS 11 Big Sur and newer, "Sharing Only" accounts get a special tag added to the AuthenticationAuthority attribute of the user record to let macOS know not to grant a Secure Token.
370 | > See `--prevent-secure-token-on-big-sur-and-newer` help for more information about preventing macOS from granting an account the first Secure Token. 371 | > 372 | > This is here as a separate option for convenience and information.
373 | > When using this option, you CANNOT also specify `--administrator`, since "Sharing Only" accounts should not be administrators.
374 | > Also, you cannot specify `--role-account` or `--service-account` with this option since they are mutually exclusive account types.
375 | > For more information about "Sharing Only" accounts, visit: 376 | 377 |
378 | 379 | #### `--role-account, --role, -r` < *no parameter* > 380 | 381 | > Include this option with no parameter to create a "Role Account". 382 | > 383 | > A `-roleAccount` option was added to `sysadminctl -addUser` in macOS 11 Big Sur, but sadly there is not really any documentation from Apple about what exactly a "Role Account" is or when and why you would want to use one.
384 | > I believe you would want to use a "Role Account" when you want a user exclusively to be the owner of files and/or processes and ***have a password***.
385 | > All `sysadminctl` states about them is the following: **Role accounts require name starting with _ and UID in 200-400 range.**
386 | > And `mkuser` has these same requirements to create a "Role Account".
387 | > Even though the `-roleAccount` option was only added to `sysadminctl -addUser` in macOS 11 Big Sur, `mkuser` can make "Role Accounts" with the same attributes on older versions of macOS as well. 388 | > 389 | > Using this option is the same as creating a "Role Account" using `sysadminctl -addUser` with a command like: `sysadminctl -addUser _role -UID 201 -roleAccount`
390 | > This example `sysadminctl -addUser` command would create a "Role Account" with the account name and full name of "_role" and the User ID "201".
391 | > **IMPORTANT:** The example account would be created with *a blank/empty password*. 392 | > 393 | > If you want to make an account exclusively to be the owner of files and/or processes that *has NO password*, you probably want to use the `--service-account` option instead of this `--role-account` option. 394 | > 395 | > Through investigation of a "Role Account" created by `sysadminctl -addUser`, a "Role Account" is equivalent to creating a hidden user with account name starting with "_" and login shell "/usr/bin/false" and home "/var/empty".
396 | > The previous example account could be created manually with `mkuser` using: `-n _role -u 201 -s /usr/bin/false -H /var/empty --hide userOnly` or `--name _role --uid 201 --no-login --home /var/empty --hide userOnly`.
397 | > See `--no-login` help for more information about login shell "/usr/bin/false".
398 | > See `--hidden` help for more information about hiding users (`--hide userOnly`). 399 | > 400 | > This is here as a separate option for convenience and information.
401 | > So, this same example account could be created with `mkuser` using: `--account-name _role --uid 201 --role-account` 402 | > 403 | > Unlike `sysadminctl -addUser` which requires the User ID to be specified manually, `mkuser` can assign the next available User ID starting from *200*.
404 | > So if the User ID is not important, you can just use `--name _role --role` to make this same example account with the next User ID in the 200-400 range. 405 | > 406 | > `sysadminctl -addUser` does not allow creating an admin "Role Account".
407 | > If you run `sysadminctl -addUser _role -UID 201 -roleAccount -admin`, the `-admin` option is silently ignored by `sysadminctl -addUser`.
408 | > `mkuser` also does not allow a "Role Account" to be an admin, but errors when using the `--admin` option with `--role-account` instead of ignoring it.
409 | > Also, you cannot specify `--sharing-only` or `--service-account` with this option since they are mutually exclusive account types. 410 | 411 |
412 | 413 | #### `--service-account, --service, --sa` < *no parameter* > 414 | 415 | > Include this option with no parameter to create a "Service Account". 416 | > 417 | > A "Service Account" is similar to a "Role Account" in that it exists exclusively to be the owner of files and/or processes but ***has NO password***.
418 | > This is like macOS built-in accounts, such as the "FTP Daemon" (_ftp) user. 419 | > 420 | > Through investigation of the built-in macOS "Service Accounts", a "Service Account" is roughly equivalent to creating a standard user with name starting with "_", login shell "/usr/bin/false", home "/var/empty", and *NO password* (see `--no-password` for more information about that).
421 | > See `--no-login` help for more information about login shell "/usr/bin/false".
422 | > But, this is just a basic template of a "Service Accounts". 423 | > 424 | > These are not all hard requirements for a "Service Account".
425 | > The hard requirements are that the account name must start with "_", must have NO password, must have no picture, CANNOT be an admin, and the home folder cannot be within the "/Users/" folder.
426 | > But, you can specify any User ID, Primary Group ID, or login shell.
427 | > If `--user-id` is omitted, the next available User ID starting from *200* will be assigned by default (the same as a "Role Account").
428 | > If `--group-id` is omitted, the *-2* (nobody) group will be used.
429 | > If `--login-shell` is omitted, "/usr/bin/false" will be used.
430 | > If `--home-folder` is omitted, "/var/empty" will be used. 431 | > 432 | > Also, you cannot specify `--sharing-only` or `--role-account` with this option since they are mutually exclusive account types. 433 | > 434 | > While you can pretty much make a "Service Account" manually using the other `mkuser` options, there is a difference when you specify `--service-account`.
435 | > All other account types get a variety of attributes added to the user record that allow the user to manage some aspects of their own account, but none of these attributes are included for built-in macOS "Service Accounts".
436 | > To match the built-in macOS "Service Accounts", these management attributes will not be included in the user record when specifying `--service-account`.
437 | > Excluding some (not all) of these specific management attributes is how the `--prohibit-user-password-changes` and `--prohibit-user-picture-changes` options work. 438 | > 439 | > #### GROUPS SPECIFICALLY FOR SERVICE ACCOUNTS NOTES: 440 | > Many built-in macOS "Service Accounts" have a group specifically for them, and often that Group ID is the same as the "Service Accounts" User ID and the Group ID is set to the Primary Group ID of the "Service Account". 441 | > 442 | > If you specify a Primary Group ID (`--group-id`), it must already exist.
443 | > If you want to create a group just to be used with a "Service Account", you can do that easily before making the "Service Account" with: `dseditgroup -o create -i [GROUP ID] -r [GROUP FULL NAME] [GROUP NAME]`
444 | > When you do this before creating a "Service Account" with `mkuser`, you can set the "Service Account" Primary Group ID to this Group ID with `--gid`.
445 | > After creating the "Service Account", you can also add it to the group with: `dseditgroup -o edit -a [SERVICE ACCOUNT NAME] -t user [GROUP NAME]`
446 | > But, that is not really necessary if the "Service Account" already has its Primary Group ID set to the Group ID. 447 | 448 |
449 | 450 | #### `--prevent-secure-token-on-big-sur-and-newer, --prevent-secure-token, --no-st` < *no parameter* > 451 | 452 | > Include this option with no parameter to prevent the user from being automatically granted the first Secure Token on macOS 11 Big Sur and newer when and if they are being created when the first Secure Token has not yet been automatically granted by macOS.
453 | > This option is helpful when creating scripted users before going through Setup Assistant that you do not want to be granted the first Secure Token, which would prevent the Setup Assistant user from getting a Secure Token.
454 | > This option will add a special tag to the AuthenticationAuthority attribute of the user record to let macOS know not to grant a Secure Token.
455 | > For more information about this Secure Token prevention tag, visit:
456 | > A Secure Token could still be manually granted to this user after specifying this option on macOS 11 Big Sur and newer with `sysadminctl -secureTokenOn`, or by an MDM Bootstrap Token when logging in graphically via login window.
457 | > This option has no effect on macOS 10.15 Catalina and older, but there is useful information below about first Secure Token behavior all the way back to macOS 10.13 High Sierra when Secure Tokens were first introduced. 458 | > 459 | > #### VOLUME OWNER ON APPLE SILICON NOTES: 460 | > On Apple Silicon Macs, users that do not have a Secure Token cannot be Volume Owners, which means they will not be able to approve system updates (among other things).
461 | > For more information about Volume Ownership on Apple Silicon, visit the Apple Platform Deployment link above. 462 | > 463 | > #### macOS 11 Big Sur AND NEWER FIRST SECURE TOKEN NOTES: 464 | > On macOS 11 Big Sur and newer, the first Secure Token is granted to the first administrator or standard user when their password is set, regardless of their UID.
465 | > This essentially means the first Secure Token is granted right when the first user is created.
466 | > This is different from previous versions of macOS which would grant the first Secure Token upon first login or authentication.
467 | > Since this behavior is more aggressive than previous first Secure Token behavior, a new way has been added to selectively prevent a user from being granted the first Secure Token.
468 | > This is done by adding a special tag to the AuthenticationAuthority attribute in the user record before the users password has been set.
469 | > While `mkuser` includes this option and takes care of the necessary timing, it's worth noting that when creating users with `sysadminctl -addUser` it's actually impossible to prevent a Secure Token in this way since the password is always set during that user creation process, even if it's just a blank/empty password.
470 | > When users are created with this tag in their AuthenticationAuthority, the first user that does not have this special tag will get the first Secure Token when their password is set (basically, upon creation).
471 | > An exception to this behavior is when utilizing MDM along with the MDM-created Managed Administrator, which will not be granted the first Secure Token unless it is the first to login or authenticate (similar to the macOS 10.15 Catalina behavior described below) because this user is created with their password pre-hashed and placed directly into their user record rather than the password being set by "normal" methods (if you're familiar with `pycreateuserpkg`, it also pre-hashes the passwords resulting in the users it creates also not being granted the first Secure Token unless they are the first to login or authenticate).
472 | > In general, you will want to make sure the the first user being granted a Secure Token is also an administrator so that they are allowed to do all possible operations on macOS (especially on T2 and Apple Silicon Macs). 473 | > 474 | > #### macOS 10.15 Catalina FIRST SECURE TOKEN NOTES: 475 | > On macOS 10.15 Catalina, the first Secure Token is granted to the first administrator (not standard user) to login or authenticate, regardless of their UID.
476 | > Even though `mkuser` will always verify the password (using native `OpenDirectory` methods) during the user creation process (which is an authentication that could trigger granting the first Secure Token), this authentication happens before the user is added to the "admin" group (if they are configured to be an administrator).
477 | > This means that users will never be an administrator during this authentication within the `mkuser` process and therefore will not be granted the first Secure Token at that moment.
478 | > The first Secure Token will then be granted by macOS to the first administrator to login or authenticate after `mkuser` has finished.
479 | > This is the same first Secure Token behavior that can be expected from any other user creation method that I'm aware of.
480 | > If for some reason you want to immediately grant an administrator created by `mkuser` the first Secure Token, you can manually run `dscl . -authonly` after `mkuser` has finished. 481 | > 482 | > #### macOS 10.14 Mojave AND macOS 10.13 High Sierra FIRST SECURE TOKEN NOTES: 483 | > The following information only applies to macOS on an APFS volume (and not HFS+) as Secure Tokens are exclusively an APFS feature.
484 | > The Secure Token behavior is slightly different on macOS 10.14 Mojave and macOS 10.13 High Sierra than it is on new versions of macOS.
485 | > Also, `mkuser`'s process has an effect on the default macOS behavior of granting the first Secure Token.
486 | > Basically, the first Secure Token is granted to the first administrator or standard user to login or authenticate which has a UID of 500 or greater if and only if they are the only user with a UID of 500 or greater.
487 | > This means that if multiple users with UIDs of 500 or greater were to be created before any of them logged in or authenticated, no first Secure Token would be granted automatically by macOS (which is not a great situation to get into by accident).
488 | > But, `mkuser` simplifies this complexity since the password will always be verified during the user creation process (using native `OpenDirectory` methods), which means the users first authentication actually happens during the `mkuser` user creation process.
489 | > Therefore, when using `mkuser`, the first Secure Token will always be granted to the first user created with a UID of 500 or greater when their password is verified during the `mkuser` process.
490 | > If you do not want the first user you are creating with `mkuser` to be granted the first Secure Token, such as for a management account, simply set their UID below 500 and macOS will not grant them the first Secure Token when their password is verified by `mkuser`.
491 | > Then, the first user created by `mkuser` with a UID of 500 or greater or the first user created by going through first boot Setup Assistant will get the first Secure Token as intended.
492 | > You can also simply adjust the order of users created to be sure the user with a UID of 500 or greater that you want to be granted the first Secure Token is created first.
493 | > In general, you will want to make sure the first user being granted a Secure Token is also an administrator so that they are allowed to do all possible operations on macOS, such as grant other users a Secure Token. 494 | > 495 | > #### ALL VERSIONS OF macOS SECURE TOKEN NOTES: 496 | > Once the first Secure Token has been granted, any subsequent users created by `mkuser` or by going through first boot Setup Assistant will not automatically be granted a Secure Token by macOS since the first Secure Token has already been granted.
497 | > If you're using `mkuser` to create users before going through Setup Assistant, and you want the user created by first boot Setup Assistant to be granted the first Secure Token, be sure to take the necessary steps for each version of macOS (as outline above) to ensure any users created by `mkuser` are not granted the first Secure Token.
498 | > Once the first Secure Token has been granted by macOS, you must use `sysadminctl -secureTokenOn` to grant other users a Secure Token and authenticate the command with an existing Secure Token administrator either interactively or by passing their credentials with the `-adminUser` and `-adminPassword` options.
499 | > Or, `mkuser` can securely take care of this for you when creating new users if you pass an existing Secure Token admins credentials using the `--secure-token-admin-account-name` option along with one of the three different Secure Token admin password options below.
500 | > See the *SECURE TOKEN ADMIN 1022 BYTE PASSWORD LENGTH LIMIT NOTES* in the help information for the `--secure-token-admin-password` option below and the *PASSWORDS IN PACKAGE NOTES* in help information for the `--password` option above for more information about how passwords are handled securely by `mkuser`, all of which also apply to Secure Token admin passwords.
501 | > Users created in the "Users & Groups" section of System Preferences/Settings will only get a Secure Token when the section has been unlocked by an existing Secure Token administrator.
502 | > Similarly, users created using `sysadminctl -addUser` will only get a Secure Token when the command is authenticated with an existing Secure Token administrator (the same way as when using the `sysadminctl -secureTokenOn` option).
503 | > The only exception to this subsequent Secure Token behavior is when utilizing MDM with a Bootstrap Token. 504 | > 505 | > #### BOOTSTRAP TOKEN NOTES (MDM-ENROLLED macOS 10.15 Catalina AND NEWER ONLY): 506 | > The Apple Platform Deployment link above also explains the Bootstrap Token.
507 | > But, some useful details are included below as well as information about how `mkuser` can simplify the creation of the Bootstrap Token on macOS 11 Big Sur and newer when the system is enrolled in a supported MDM. 508 | > 509 | > For a Bootstrap Token to be able to be created, the MDM must support it.
510 | > The Bootstrap Token was first introduced in macOS 10.15 Catalina, but required Automated Device Enrollment (ADE/DEP) and was limited to granting Secure Tokens to mobile accounts logging in graphically via login window (but not when using the `login` or `su` commands) as well as the optional MDM-created Managed Administrator.
511 | > Starting in macOS 11 Big Sur, the Bootstrap Token functionality was expanded to support all User Approved MDM Enrollment (UAMDM) methods and also to grant Secure Tokens to local users logging in graphically.
512 | > Also, more functionality was added for Apple Silicon in macOS 11 Big Sur.
513 | > On Apple Silicon, the Bootstrap Token can be used to authorize installation of both kernel extensions and software updates when managed using MDM.
514 | > Starting in macOS 12 Monterey, the Bootstrap Token can also be used to silently authorize an Erase All Content and Settings command for Apple Silicon Macs (not required for T2 Macs) when triggered through MDM.
515 | > One way to think of the Bootstrap Token is that it is like an invisible Secure Token/Volume Owner administrator account that can be used to automate actions via MDM that normally require authentication by a regular Secure Token/Volume Owner administrator account. 516 | > 517 | > Under normal circumstances, the first user would be created manually during Setup Assistant and then be granted the first Secure Token.
518 | > The Bootstrap Token would also be created during that process as that user is automatically logged in graphically. 519 | > 520 | > While it is generally recommended that the first administrator be created manually by the end user during Setup Assistant (since macOS will grant them the first Secure Token and then create the Bootstrap Token), if you choose to have `mkuser` create the first Secure Token user before that point, or choose to skip manual user creation during Setup Assistant, then a Secure Token user would need to manually log in graphically for the Bootstrap Token to be created.
521 | > On macOS 11 Big Sur and newer, `mkuser` simplifies this when `mkuser` is used to create the first Secure Token administrator by running the `profiles install -type bootstraptoken` command and securely authorizing it with the credentials of the newly created user during the `mkuser` process.
522 | > `mkuser` will only do this on macOS 11 Big Sur and newer because the first Secure Token will be granted by macOS when the password is set during the `mkuser` process (see *macOS 11 Big Sur AND NEWER FIRST SECURE TOKEN NOTES* above for more information).
523 | > On macOS 10.15 Catalina, the first Secure Token will NOT be granted by macOS during the `mkuser` process (see *macOS 10.15 Catalina FIRST SECURE TOKEN NOTES* above for more information) and therefore `mkuser` will not be able to create and escrow the Bootstrap Token. 524 | > 525 | > On macOS 10.15.4 Catalina and newer, when a Secure Token enabled user logs in graphically for the first time, the Bootstrap Token is created and escrowed to the supported MDM when internet is available (on older versions of macOS 10.15 Catalina, the Bootstrap Token was only created and escrowed automatically during the Setup Assistant user creation process).
526 | > This would normally be when the first administrator logs in graphically and is granted the first Secure Token by macOS which will also create and escrow the Bootstrap Token during that same graphical login process.
527 | > If internet is not available during any Bootstrap Token creation event, the Bootstrap Token will be created but will NOT be escrowed to MDM and will therefore not be able to grant other users a Secure Token until it has been escrowed to MDM.
528 | > If this happens, the Bootstrap Token will be escrowed to MDM the next time that user logs in graphically when internet is available.
529 | > Also, the Bootstrap Token can be manually created and/or escrowed to the supported MDM using the `profiles install -type bootstraptoken` command. 530 | > 531 | > For `mkuser` to create and escrow the Bootstrap Token on macOS 11 Big Sur and newer, the account name and password must be passed to the `profiles install -type bootstraptoken` command.
532 | > To do this in the most secure way possible (so that the password is never visible in the process list or written to the filesystem), the password is NOT passed directly as an argument but is instead passed using the interactive command line prompt (via `expect` automation).
533 | > But, the `profiles install -type bootstraptoken` command line password prompt fails to accept passwords over 128 bytes even if the password is correct.
534 | > Using `expect` to pass the password securely has one other limitation, which is that it does not support emoji characters.
535 | > If the password is over 128 bytes or contains emoji (even though both are quite rare), then the Bootstrap Token creation will fail with a warning.
536 | > Longer passwords (up to 512 bytes) as well as passwords containing emoji can be passed to `profiles install -type bootstraptoken` directly using the `-user` and `-password` arguments, but that would make the password visible in the process list.
537 | > Since `mkuser` strives to handle passwords in the most secure ways possible, only the secure command line prompt method using `expect` will be attempted, and if it fails then the user will need to be logged in graphically to create and escrow the Bootstrap Token, or the insecure `profiles install -type bootstraptoken -user [USER] -password [PASSWORD]` command will need to be run manually after the `mkuser` process is done.
538 | > Also, if the first Secure Token user is created with a blank/empty password, they cannot authenticate the `profiles install -type bootstraptoken` command and a Bootstrap Token will also NOT be created when logged in graphically.
539 | > The Secure Token user having some password set is simply a requirement to be able to create the Bootstrap Token. 540 | > 541 | > Once the Bootstrap Token has been created and escrowed, it will only grant Secure Tokens to users logging in graphically via login window (but not when using the `login` or `su` commands) and internet must be available during the macOS login process to communicate with the MDM.
542 | > Except if a user has a blank/empty password, then the Bootstrap Token will not grant that user a Secure Token.
543 | > Otherwise, there is *NO WAY* to prevent the Bootstrap Token from granting an account a Secure Token when logging in graphically, not even when this `--prevent-secure-token-on-big-sur-and-newer` option is specified as that only applies to macOS granting the *first* Secure Token, not to subsequent Secure Tokens granted by the Bootstrap Token. 544 | 545 |
546 | 547 | #### `--secure-token-admin-account-name, --st-admin-name, --st-admin-user, --st-name` < *string* > 548 | 549 | > Specify an existing Secure Token administrator account name (not full name) along with their password (using one of the three different options below) to be used to grant the new user a Secure Token.
550 | > This option is ignored on HFS+ volumes since Secure Tokens are APFS-only. 551 | 552 |
553 | 554 | #### `--secure-token-admin-password, --st-admin-pass, --st-pass` < *string* > 555 | 556 | > The password will be validated to be correct for the specified `--secure-token-admin-account-name`.
557 | > The password must be 1022 bytes or less (see notes below for more info).
558 | > If omitted, blank/empty password will be specified.
559 | > This option is ignored on HFS+ volumes since Secure Tokens are APFS-only. 560 | > 561 | > See *PASSWORDS IN PACKAGE NOTES* in help information for the `--password` option above for more information about how the Secure Token admin password is securely obfuscated within a package. 562 | > 563 | > #### SECURE TOKEN ADMIN 1022 BYTE PASSWORD LENGTH LIMIT NOTES: 564 | > To grant the new user a Secure Token, the user and existing Secure Token admin passwords must be passed to `sysadminctl -secureTokenOn`.
565 | > To do this in the most secure way possible (so that they are never visible in the process list or written to the filesystem), the passwords are NOT passed directly as arguments but are instead passed via "stdin" using the command line prompt options.
566 | > But, this technique fails with Secure Token admin passwords over 1022 bytes.
567 | > For a bit more technical information about this limitation from my testing, search for *1022 bytes* within the source of this script.
568 | > The length of the new user password is not an issue for this command since it is limited to a maximum of 511 bytes as described in the *511 BYTE PASSWORD LENGTH LIMIT NOTES* in help information for the `--password` option above.
569 | > Since `mkuser` strives to handle passwords in the most secure ways possible, the password length of Secure Token admin is limited to 1022 bytes so that the password can be passed to `sysadminctl -secureTokenOn` in a secure way that never makes it visible in the process list or writes it to the filesystem.
570 | > If your existing Secure Token admin has a longer password for any reason, you can use it to manually grant a Secure Token after creating a non-Secure Token account with `mkuser` by insecurely passing the password directly to `sysadminctl -secureTokenOn` as an argument since longer passwords are properly accepted when passed that way. 571 | 572 |
573 | 574 | #### `--fd-secure-token-admin-password, --fd-st-admin-pass, --fd-st-pass` < *file descriptor path* (via process substitution) > 575 | 576 | > The file descriptor path must be specified via process substitution.
577 | > The process substitution command must `echo` the Secure Token admin password.
578 | > If you haven't used process substitution before, it looks like this: `mkuser [OPTIONS] --fd-secure-token-admin-password <(echo [PASSWORD]) [OPTIONS]`
579 | > Passing the password via process substitution instead of directly with the `--secure-token-admin-password` option hides the password from the process list and does not create any temporary file containing the password.
580 | > Since `echo` is a builtin in `bash` and `zsh` and not an external binary command, the `echo` command containing the password as an argument is also never visible in the process list.
581 | > The help information for the `--secure-token-admin-password` option above also applies to Secure Token admin passwords passed via process substitution.
582 | > This option is ignored on HFS+ volumes since Secure Tokens are APFS-only. 583 | 584 |
585 | 586 | #### `--secure-token-admin-password-prompt, --st-admin-pass-prompt, --st-pass-prompt` < *GUI* || *CLI* (or *no parameter*) > 587 | 588 | > Include this option with no parameter or specify "*CLI*" to be prompted for the Secure Token admin password on the command line before creating the user or package. 589 | > 590 | > Or, specify "*GUI*" to instead be prompted graphically via AppleScript dialog.
591 | > When "*GUI*" is specified, any password errors will also be presented graphically via AppleScript dialog. 592 | > 593 | > This option allows you to specify a Secure Token admin password without it being saved in your command line history as well as hides the password from the process list.
594 | > The help information for the `--secure-token-admin-password` option above also applies to Secure Token admin passwords entered via command line prompt.
595 | > This option is ignored on HFS+ volumes since Secure Tokens are APFS-only.
596 | > **NOTICE:** This option with the "*CLI*" parameter cannot be used when `--stdin-password` is specified since accepting "stdin" disrupts the ability to use other command line inputs. 597 | 598 |
599 | 600 | ## 🚪 LOGIN OPTIONS 601 | 602 | #### `--automatic-login, --auto-login, -A` < *no parameter* > 603 | 604 | > Include this option with no parameter to set automatic login for the user.
605 | > Enabling automatic login stores the users password in the filesystem in an obfuscated but insecure way.
606 | > If automatic login is already setup for another user, it'll be overwritten.
607 | > If FileVault is enabled, automatic login is not possible or allowed and this option will be ignored (and a warning will be displayed). 608 | 609 |
610 | 611 | #### `--prevent-login, --no-login, --nl` < *no parameter* > 612 | 613 | > Include this option with no parameter to prevent this user from logging in.
614 | > This option is equivalent to setting the login shell to "/usr/bin/false" which can also be done directly with `--login-shell /usr/bin/false`.
615 | > This is here as a separate option for convenience and information.
616 | > When the login shell is set to "/usr/bin/false", the user is will not show in the "Users & Groups" section of System Preferences/Settings and will also not show up in the non-FileVault login window list of users. 617 | > 618 | > If FileVault is enabled and one of these users has a password and is granted a Secure Token, they WILL show in the FileVault login window and can decrypt the volume, but then the non-FileVault login will be hit to fully login to macOS with another user account.
619 | > Unlike hidden users, these user CANNOT be logged into using text input fields in the non-FileVault login window. 620 | > 621 | > Even if one of these users has a password set, they CANNOT authenticate "Terminal" commands like `su`, or `login` as well as NOT being able to log in remotely via `ssh`.
622 | > They also CANNOT authenticate graphical prompts, such as unlocking System Preferences/Settings sections if they are an administrator.
623 | > But, if these users are an admin, they CAN run AppleScript `do shell script` commands `with administrator privileges`. 624 | 625 |
626 | 627 | #### `--skip-setup-assistant, --skip-setup, -S` < *firstBootOnly* || *firstLoginOnly* || *both* (or *no parameter*) > 628 | 629 | > Include this option with either no parameter or specify "*both*" to skip both the first boot and first login Setup Assistant screens. 630 | > 631 | > Specify "*firstBootOnly*" to skip only the first boot Setup Assistant screens.
632 | > This affects all users and has no effect if first boot Setup Assistant has already been completed.
633 | > If Setup Assistant is already running when the user is being created, `mkuser` will exit Setup Assistant after the user creation process is done. 634 | > 635 | > Specify "*firstLoginOnly*" to skip only the users first login Setup Assistant screens.
636 | > This affects only this user and will also skip any and all future user Setup Assistant screens that may appear when and if macOS is updated. 637 | > 638 | > Any other parameters are invalid and will cause the user to not be created. 639 | 640 |
641 | 642 | ## 📦 PACKAGING OPTIONS 643 | 644 | #### `--package-path, --pkg-path, --package, --pkg` < *folder path* || *pkg file path* || *no parameter* (working directory) > 645 | 646 | > Save distribution package to create a user with the other specified options.
647 | > This will not create a user immediately on the current system, but will save a distribution package file that can be used on another system.
648 | > The distribution package (product archive) created will be suitable for use with `startosinstall --installpackage` or `installer -pkg` or "Installer" app, and is also "no payload" which only runs scripts and leaves no receipt.
649 | > If no path is specified, the current working directory will be used along with the default filename: *[PKG ID]-[PKG VERSION].pkg*
650 | > If a folder path is specified, the default filename will be used within the specified folder.
651 | > If a full file path ending in ".pkg" is specified, that whole path and filename will be used.
652 | > For any of these path options, if the exact filename already exists in the specified folder, it will be OVERWRITTEN by a newly created package. 653 | 654 |
655 | 656 | #### `--package-identifier, --pkg-identifier, --package-id, --pkg-id` < *string* > 657 | 658 | > Specify the bundle identifier string to use for the package (only valid when using the `--package` option).
659 | > Must be 248 characters/bytes or less and start with a letter or number and can only contain alphanumeric, hyphen/minus (-), underscore (_), or dot (.) characters.
660 | > If the package identifier is over 248 characters, the installation would fail to extract the package scripts since they are extracted into a folder named with the package identifier and appended with a period plus 6 random characters which would make that folder name over the macOS 255 byte max.
661 | > If omitted, the default identifier will be used: *mkuser.pkg.[ACCOUNT NAME]* 662 | 663 |
664 | 665 | #### `--package-version, --pkg-version, --pkg-v` < *version string* > 666 | 667 | > Specify the version string to use for the package (only valid when using the `--package` option).
668 | > Must start with a number or letter and can only contain alphanumeric, hyphen/minus (-), or dot (.) characters.
669 | > If omitted, the current date will be used in the format: *YYYY.M.D* 670 | 671 |
672 | 673 | #### `--package-signing-identity, --package-sign, --pkg-sign` < *string* > 674 | 675 | > Specify the installer package signing identity string to use for the package (only valid when using the `--package` option).
676 | > The string must be for an existing installer package signing identity in the Keychain, and in the proper format: *Developer ID Installer: Name (Team ID)*
677 | > If omitted, the package will not be signed. 678 | 679 |
680 | 681 | ## ⚙️ MKUSER OPTIONS 682 | 683 | #### `--do-not-confirm, --no-confirm, --force, -F` < *no parameter* > 684 | 685 | > By default when run in Terminal, `mkuser` prompts for confirmation on the command line before creating a user on the current system.
686 | > Include this option with no parameter to NOT prompt for confirmation when run in an interactive Terminal.
687 | > But, when `mkuser` is NOT run in a Terminal where an interactive command line is available for user input (such as an automated script), confirmation will NOT be prompted and it is NOT necessary to specify this option. 688 | > 689 | > This option is ignored when outputting a user creation package (with the `--package` option) since no user will be created on the current system.
690 | > **NOTICE:** Specifying `--suppress-status-messages` OR `--stdin-password` also ENABLES `--do-not-confirm`. 691 | 692 |
693 | 694 | #### `--suppress-status-messages, --quiet, -q` < *no parameter* > 695 | 696 | > Include this option with no parameter to not output any status messages that would be sent to "stdout".
697 | > Any errors and warning that are sent to "stderr" will still be outputted.
698 | > **NOTICE:** Specifying `--suppress-status-messages` also ENABLES `--do-not-confirm`. 699 | 700 |
701 | 702 | #### `--check-only, --dry-run, --check, -c` < *no parameter* > 703 | 704 | > Include this option with no parameter to check if the other specified options are valid and output the settings a user would be created with.
705 | > This option is ignored when outputting a user creation package (with the `--package` option) since checking against the current system isn't useful when installing packages on other systems. 706 | 707 |
708 | 709 | #### `--version, -v` < *online* (or *o*) || *no parameter* > 710 | 711 | > Include this option with no parameter to display the `mkuser` version, and also check for updates when connected to the internet and display the newest version if an update is available. 712 | > 713 | > Specify "*online*" (or "*o*") to also open the `mkuser` [Releases](https://github.com/freegeek-pdx/mkuser/releases) page on GitHub in the default web browser to be able to quickly and easily view the latest release notes as well as download the latest version. 714 | > 715 | > This option overrides all other options (including `--help`). 716 | 717 |
718 | 719 | #### `--help, -h` < *brief* (or *b*) || *online* (or *o*) || *no parameter* > 720 | 721 | > Include this option with no parameter to display this help information in Terminal. 722 | > 723 | > Specify "*brief*" (or "*b*") to only show options without their descriptions. 724 | > This can be helpful for quick reference to check option or parameter names. 725 | > 726 | > Specify "*online*" (or "*o*") to instead open this README section of the `mkuser` GitHub page in the default web browser to be able quickly and easily view this help information from here. 727 | > 728 | > This option overrides all other options (except `--version`). 729 | -------------------------------------------------------------------------------- /utilities/create-mkuser-installation-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck enable=add-default-case,avoid-nullary-conditions,check-unassigned-uppercase,deprecate-which,quote-safe-variables,require-double-brackets 3 | 4 | # 5 | # Created by Pico Mitchell (of Free Geek) on 1/4/22 6 | # 7 | # https://mkuser.sh 8 | # 9 | # MIT License 10 | # 11 | # Copyright (c) 2022 Free Geek 12 | # 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | # to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | # 23 | 24 | # NOTICE: This script IS NOT for user creation packages. The mkuser script itself is used for that. 25 | # This script IS AN INTERNAL DEVELOPMENT TOOL which is used when new mkuser versions are released 26 | # to create the package to install the mkuser script into "/usr/local/bin". 27 | 28 | PATH='/usr/bin:/bin:/usr/sbin:/sbin' 29 | 30 | SCRIPT_DIR="$(cd "${BASH_SOURCE[0]%/*}/.." &> /dev/null && pwd -P)" 31 | readonly SCRIPT_DIR 32 | 33 | TMPDIR="$([[ -d "${TMPDIR}" && -w "${TMPDIR}" ]] && echo "${TMPDIR%/}/" || echo '/private/tmp/')" # Make sure "TMPDIR" is always set and that it always has a trailing slash for consistency regardless of the current environment. 34 | 35 | id_prefix='org.freegeek.' 36 | script_name='mkuser' 37 | 38 | script_path="${SCRIPT_DIR}/${script_name}.sh" 39 | 40 | if [[ ! -f "${script_path}" || ! -x "${script_path}" ]]; then 41 | >&2 echo -e "\nSOURCE SCRIPT NOT FOUND AT PATH: ${script_path}" 42 | exit 1 43 | fi 44 | 45 | package_id="${id_prefix}pkg.${script_name}" 46 | 47 | payload_tmp_dir="${TMPDIR}${script_name}_installation_payload" 48 | 49 | rm -rf "${payload_tmp_dir}" 50 | mkdir -p "${payload_tmp_dir}" 51 | 52 | cat "${script_path}" > "${payload_tmp_dir}/${script_name}" # Instead of copying the file, write the *contents* to a new file to be sure that no xattrs are ever included in the distributed script (such as "com.apple.macl" which is TCC protected). 53 | 54 | if [[ ! -f "${payload_tmp_dir}/${script_name}" ]] || (( $(stat -f '%z' "${payload_tmp_dir}/${script_name}") == 0 )); then 55 | rm -rf "${payload_tmp_dir}" 56 | >&2 echo -e "\nFAILED TO WRITE SCRIPT SOURCE FROM \"${script_path}\" TO \"${payload_tmp_dir}/${script_name}\"" 57 | exit 2 58 | fi 59 | 60 | chmod +x "${payload_tmp_dir}/${script_name}" 61 | 62 | script_version="$(awk -F "'" '/VERSION=/ { print $(NF-1); exit }' "${payload_tmp_dir}/${script_name}")" 63 | if [[ -z "${script_version}" ]]; then script_version="$(date '+%Y.%-m.%-d')"; fi # https://strftime.org 64 | 65 | echo -e "\nCode Signing ${script_name} Version ${script_version} Script for Package..." 66 | codesign -s 'Developer ID Application' --prefix "${id_prefix}" --strict "${payload_tmp_dir}/${script_name}" # Set a proper identifier prefix since just the filename would be used if none is specified. 67 | 68 | codesign_exit_code="$?" 69 | 70 | spctl_assess_last_line="$(spctl -avvt open --context context:primary-signature "${payload_tmp_dir}/${script_name}" 2>&1 | tail -1)" # Only capture the last line to output and check (which will be the "origin" line when successful or an error if the siganture was invalid) since that's all thats relevant 71 | # because "spctl -avvt open ..." will "fail" with "rejected" since it rejects any flat files that are not notarized, but scripts cannot be notarized so signing is the most that can be done (packages, disk images, and Mach-O binaries are the only flat files that can be notarized). 72 | echo "${spctl_assess_last_line}" 73 | 74 | readonly INTENDED_CODE_SIGNATURE_TEAM_ID='YRW6NUGA63' 75 | 76 | if ! codesign -vv --strict -R "=identifier \"${id_prefix}${script_name}\" and certificate leaf[subject.OU] = \"${INTENDED_CODE_SIGNATURE_TEAM_ID}\"" "${payload_tmp_dir}/${script_name}" || (( codesign_exit_code != 0 )) || [[ "${spctl_assess_last_line}" != *"(${INTENDED_CODE_SIGNATURE_TEAM_ID})" ]]; then 77 | rm -rf "${payload_tmp_dir}" 78 | >&2 echo -e "\nCODESIGN ERROR OCCURRED: EXIT CODE ${codesign_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 79 | exit 3 80 | fi 81 | 82 | script_checksum="$(openssl dgst -sha512 "${payload_tmp_dir}/${script_name}" | awk '{ print $NF; exit }')" 83 | 84 | zip_output_filename="${script_name}-${script_version}.zip" 85 | # Assets on a GitHub Release cannot contain spaces. If spaces exist, they will be replaced with periods. 86 | # Instead of separating the name and version with a period, use a hyphen which matches the filename style of the source code downloads on GitHub Releases. 87 | 88 | ditto -ck --sequesterRsrc --zlibCompressionLevel 9 "${payload_tmp_dir}/${script_name}" "${payload_tmp_dir}/${zip_output_filename}" 89 | # IMPORTANT: On macOS 10.15 Catalina and older, it appears that extended attributes (xattr) ARE NOT preserved and are removed during the package installation process. 90 | # This means that the script's code signature (store in the extended attributes) ARE LOST if the script is installed directly by a package on macOS 10.15 Catalina and older. 91 | # To workaround this issue, ZIPPING the script first preserves the code signature extended attributes since the package installation process would only be removing 92 | # any (non-existant) extended attributes from the zip file itself and not the script within the zip, and zipping/unzipping using "ditto" properly preserves the extended attributes. 93 | # So, the zipped script will be installed into a temporary location "/private/tmp/[SCRIPT NAME]-[SCRIPT VERSION].zip" and then unzipped and 94 | # installed into "/usr/local/bin" final destination by the "postinstall" script which also verifies the scripts code signature and checksum. 95 | # NOTE: Zipping and unzipping MUST be done with "ditto" since it properly preserves and restores extended attributes, unlike "zip" and "unzip". 96 | 97 | zip_checksum="$(openssl dgst -sha512 "${payload_tmp_dir}/${zip_output_filename}" | awk '{ print $NF; exit }')" 98 | 99 | release_dir="${SCRIPT_DIR}/Release ${script_version}" 100 | rm -rf "${release_dir}" 101 | 102 | # Save a copy of the signed "zip" to include in the GitHub release. 103 | ditto "${payload_tmp_dir}/${zip_output_filename}" "${release_dir}/${zip_output_filename}" # "ditto" will create missing parent folders. 104 | 105 | rm -f "${payload_tmp_dir}/.DS_Store" 106 | 107 | echo -e "\nCreating \"postinstall\" Script for ${script_name} Version ${script_version} Package..." 108 | 109 | scripts_tmp_dir="${TMPDIR}${script_name}_installation_scripts" 110 | 111 | rm -rf "${scripts_tmp_dir}" 112 | mkdir -p "${scripts_tmp_dir}" 113 | 114 | cat << POSTINSTALL_EOF > "${scripts_tmp_dir}/postinstall" 115 | #!/bin/bash 116 | 117 | PATH='/usr/bin:/bin:/usr/sbin:/sbin' 118 | 119 | TMPDIR="\$([[ -d "\${TMPDIR}" && -w "\${TMPDIR}" ]] && echo "\${TMPDIR%/}/" || echo '/private/tmp/')" # Make sure "TMPDIR" is always set and that it always has a trailing slash for consistency regardless of the current environment. 120 | 121 | echo '${script_name} INSTALL: Verifying ${script_name} Version ${script_version} Code Signature and Checksum in Temporary Location...' 122 | 123 | tmp_script_path="\${TMPDIR}${script_name}" 124 | rm -rf "\${tmp_script_path}" 125 | 126 | intended_zip_checksum='${zip_checksum}' 127 | echo "${script_name} INSTALL: Intended Archive Checksum = \${intended_zip_checksum}" 128 | 129 | tmp_zip_path='/private/tmp/${zip_output_filename}' 130 | 131 | actual_zip_checksum="\$(openssl dgst -sha512 "\${tmp_zip_path}" | awk '{ print \$NF; exit }')" 132 | echo "${script_name} INSTALL: Actual Archive Checksum = \${actual_zip_checksum}" 133 | 134 | if [[ "\${actual_zip_checksum}" != "\${intended_zip_checksum}" ]]; then 135 | rm -f "\${tmp_zip_path}" 136 | >&2 echo '${script_name} INSTALL ERROR: INVALID ARCHIVE CHECKSUM (SEE OUTPUT ABOVE FOR MORE INFO)' 137 | exit 1 138 | fi 139 | 140 | # NOTE: The script is within a zip (created using with "ditto") to preserve the code signature extended attributes which would be 141 | # removed by the package installation process on macOS 10.15 Catalina and older if the script was installed directly by the package. 142 | ditto -xkvV "\${tmp_zip_path}" "\${TMPDIR}" # Also, unzipping MUST be done with "ditto" since it properly restores extended attributes, unlike "unzip". 143 | ditto_exit_code="\$?" 144 | 145 | rm -f "\${tmp_zip_path}" 146 | 147 | if (( ditto_exit_code != 0 )) || [[ ! -f "\${tmp_script_path}" || ! -x "\${tmp_script_path}" ]]; then 148 | rm -f "\${tmp_script_path}" 149 | >&2 echo '${script_name} INSTALL ERROR: FAILED TO UNZIP SCRIPT TO TEMPORARY LOCATION FOR VERIFICATION' 150 | exit 2 151 | fi 152 | 153 | verify_code_signature_and_checksum_at_path() { 154 | codesign -vv --strict -R '=$(codesign -dr - "${payload_tmp_dir}/${script_name}" 2> /dev/null | awk -F ' => ' '($1 == "designated") { print $2; exit }')' "\$1" 155 | local codesign_verify_exit_code="\$?" 156 | 157 | local spctl_assess_last_line 158 | spctl_assess_last_line="\$(spctl -avvt open --context context:primary-signature "\$1" 2>&1 | tail -1)" # Only capture the last line to output and check (which will be the "origin" line when successful or an error if the siganture was invalid) since that's all thats relevant 159 | # because "spctl -avvt open ..." will "fail" with "rejected" since it rejects any flat files that are not notarized, but scripts cannot be notarized so signing is the most that can be done (packages, disk images, and Mach-O binaries are the only flat files that can be notarized). 160 | echo "\${spctl_assess_last_line}" 161 | 162 | local intended_script_checksum='${script_checksum}' 163 | echo "${script_name} INSTALL: Intended Script Checksum = \${intended_script_checksum}" 164 | 165 | local actual_script_checksum 166 | actual_script_checksum="\$(openssl dgst -sha512 "\$1" | awk '{ print \$NF; exit }')" 167 | echo "${script_name} INSTALL: Actual Script Checksum = \${actual_script_checksum}" 168 | 169 | if (( codesign_verify_exit_code != 0 )) || [[ "\${spctl_assess_last_line}" != *'(${INTENDED_CODE_SIGNATURE_TEAM_ID})' || "\${actual_script_checksum}" != "\${intended_script_checksum}" ]]; then # Checksum verification is part of "codesign" verification and "spctl" assessment, but manually verify it anyways. 170 | return 1 171 | fi 172 | 173 | return 0 174 | } 175 | 176 | if ! verify_code_signature_and_checksum_at_path "\${tmp_script_path}"; then 177 | rm -f "\${tmp_script_path}" 178 | >&2 echo '${script_name} INSTALL ERROR: INVALID CODE SIGNATURE OR CHECKSUM AT TEMPORARY LOCATION (SEE OUTPUT ABOVE FOR MORE INFO)' 179 | exit 3 180 | fi 181 | 182 | install_folder='/usr/local/bin' 183 | install_script_path="\${install_folder}/${script_name}" 184 | 185 | echo "${script_name} INSTALL: Moving Verified ${script_name} Version ${script_version} to \"\${install_script_path}\" and Re-Verifying Code Signature and Checksum..." 186 | 187 | mkdir -p "\${install_folder}" 188 | rm -rf "\${install_script_path}" 189 | mv -f "\${tmp_script_path}" "\${install_script_path}" 190 | 191 | if ! verify_code_signature_and_checksum_at_path "\${install_script_path}"; then 192 | rm -f "\${install_script_path}" 193 | >&2 echo "${script_name} INSTALL ERROR: INVALID CODE SIGNATURE OR CHECKSUM AT \"\${install_script_path}\" (SEE OUTPUT ABOVE FOR MORE INFO)" 194 | exit 4 195 | fi 196 | 197 | echo "${script_name} INSTALL: Successfully installed and verified ${script_name} version ${script_version} to \"\${install_script_path}\"!" 198 | POSTINSTALL_EOF 199 | 200 | chmod +x "${scripts_tmp_dir}/postinstall" 201 | rm -f "${scripts_tmp_dir}/.DS_Store" 202 | 203 | rm -f "${payload_tmp_dir}/${script_name}" 204 | 205 | package_tmp_dir="${TMPDIR}${script_name}_installation_package" 206 | 207 | rm -rf "${package_tmp_dir}" 208 | mkdir -p "${package_tmp_dir}" 209 | 210 | package_tmp_output_path="${package_tmp_dir}/${script_name}.pkg" 211 | 212 | echo -e "\nCreating ${script_name} Version ${script_version} Installation Package..." 213 | pkgbuild \ 214 | --install-location '/private/tmp' \ 215 | --root "${payload_tmp_dir}" \ 216 | --scripts "${scripts_tmp_dir}" \ 217 | --identifier "${package_id}" \ 218 | --version "${script_version}" \ 219 | "${package_tmp_output_path}" 220 | 221 | pkgbuild_exit_code="$?" 222 | 223 | rm -rf "${payload_tmp_dir}" 224 | rm -rf "${scripts_tmp_dir}" 225 | 226 | if (( pkgbuild_exit_code != 0 )) || [[ ! -f "${package_tmp_output_path}" ]]; then 227 | rm -rf "${package_tmp_dir}" 228 | >&2 echo -e "\nPKGBUILD ERROR OCCURRED CREATING INITIAL PACKAGE: EXIT CODE ${pkgbuild_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 229 | exit 4 230 | fi 231 | 232 | package_distribution_xml_output_path="${package_tmp_dir}/distribution.xml" 233 | 234 | productbuild \ 235 | --synthesize \ 236 | --package "${package_tmp_output_path}" \ 237 | "${package_distribution_xml_output_path}" 238 | 239 | productbuild_synthesize_exit_code="$?" 240 | 241 | if (( productbuild_synthesize_exit_code != 0 )) || [[ ! -f "${package_distribution_xml_output_path}" ]]; then 242 | rm -rf "${package_tmp_dir}" 243 | >&2 echo -e "\nPRODUCTBUILD SYNTHESIZE ERROR OCCURRED CREATING DISTRIBUTION XML: EXIT CODE ${productbuild_synthesize_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 244 | exit 5 245 | fi 246 | 247 | package_distribution_xml_header="$(head -2 "${package_distribution_xml_output_path}")" 248 | package_distribution_xml_footer="$(tail +3 "${package_distribution_xml_output_path}")" 249 | 250 | # Make sure this package is marked as Universal (to run without needing Rosetta on Apple Silicon) no matter what version of macOS it's being created on. 251 | package_distribution_host_architectures_attribute_before="$(xmllint --xpath '//options/@hostArchitectures' "${package_distribution_xml_output_path}" 2> /dev/null)" 252 | if [[ ! "${package_distribution_host_architectures_attribute_before}" =~ arm64[,\"] ]]; then 253 | if [[ "${package_distribution_host_architectures_attribute_before}" == *'hostArchitectures='* ]]; then # I'm not sure that it's actually possible for the "hostArchitectures" attribute to be set by any version of macOS when it wouldn't have already added arm64 to it as an option (it just doesn't exist by default on macOS 10.15 Catalina and older), but check for and add to an existing attribute anyways. 254 | package_distribution_xml_footer="${package_distribution_xml_footer//hostArchitectures=\"/hostArchitectures=\"arm64,}" # There should only be one "hostArchitectures" arribute, but update them all just in case. 255 | else # On macOS 10.15 Catalina and older, the "hostArchitectures" attribute will not be set at all and that will make Apple Silicon Macs think this package needs Rosetta when it really doesn't. 256 | # This is adding "hostArchitectures" as the first specified attribute instead of the last (as newer versions of macOS do), but the order of XML attributes within a tag doesn't matter. 257 | package_distribution_xml_footer="${package_distribution_xml_footer// "${package_distribution_xml_output_path}" 262 | ${package_distribution_xml_header} 263 | ${script_name} ${script_version} 264 | 275 | 285 | 286 | 287 | 288 | 289 | 290 | ${package_distribution_xml_footer} 291 | CUSTOM_DISTRIBUTION_XML_EOF 292 | 293 | if [[ ! "$(xmllint --xpath '//options/@hostArchitectures' "${package_distribution_xml_output_path}" 2> /dev/null)" =~ arm64[,\"] ]]; then # Make sure the updated "distribution.xml" file is marked as Universal (in case the manual edits above failed somehow). 294 | rm -rf "${package_tmp_dir}" 295 | >&2 echo -e "\nDISTRIBUTION.XML ERROR OCCURRED: Failed to mark package as Universal to be able to run on Apple Silicon Macs without requiring Rosetta." 296 | exit 6 297 | fi 298 | 299 | package_output_filename="${script_name}-${script_version}.pkg" 300 | # Assets on a GitHub Release cannot contain spaces. If spaces exist, they will be replaced with periods. 301 | # Instead of separating the name and version with a period, use a hyphen which matches the filename style of the source code downloads on GitHub Releases. 302 | 303 | package_output_path="${release_dir}/${package_output_filename}" 304 | 305 | productbuild \ 306 | --distribution "${package_distribution_xml_output_path}" \ 307 | --package-path "${package_tmp_dir}" \ 308 | --identifier "${package_id}" \ 309 | --version "${script_version}" \ 310 | --sign 'Developer ID Installer' \ 311 | "${package_output_path}" 312 | 313 | productbuild_exit_code="$?" 314 | 315 | rm -rf "${package_tmp_dir}" 316 | 317 | if (( productbuild_exit_code != 0 )) || [[ ! -f "${package_output_path}" ]]; then 318 | >&2 echo -e "\nPRODUCTBUILD ERROR OCCURRED CREATING/SIGNING INSTALLATION PACKAGE: EXIT CODE ${productbuild_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 319 | exit 7 320 | fi 321 | 322 | echo -en "\nEnter \"Y\" to Notarize ${script_name} Version ${script_version} Installation Package: " 323 | read -r confirm_notarization 324 | 325 | pkg_checksum='UNNOTARIZED' 326 | 327 | if [[ "${confirm_notarization}" =~ ^[Yy] ]]; then 328 | # Setting up "notarytool": https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/ & https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow 329 | 330 | notarization_submission_log_path="${TMPDIR}${script_name}_package_notarization_submission.log" 331 | rm -rf "${notarization_submission_log_path}" 332 | 333 | echo -e "\nNotarizing ${script_name} Version ${script_version} Installation Package..." 334 | xcrun notarytool submit "${package_output_path}" --keychain-profile 'notarytool App Specific Password' --wait | tee "${notarization_submission_log_path}" # Show live log since it may take a moment AND save to file to extract submission ID from to be able to load full notarization log. 335 | notarytool_exit_code="$?" 336 | 337 | notarization_submission_id="$(awk '($1 == "id:") { print $NF; exit }' "${notarization_submission_log_path}")" 338 | rm -f "${notarization_submission_log_path}" 339 | 340 | echo 'Notarization Log:' 341 | xcrun notarytool log "${notarization_submission_id}" --keychain-profile 'notarytool App Specific Password' # Always load and show full notarization log regardless of success or failure (since documentation states there could be warnings). 342 | 343 | if (( notarytool_exit_code != 0 )); then 344 | >&2 echo -e "\nNOTARIZATION ERROR OCCURRED: EXIT CODE ${notarytool_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 345 | exit 8 346 | fi 347 | 348 | echo -e "\nStapling Notarization Ticket to ${script_name} Version ${script_version} Installation Package..." 349 | xcrun stapler staple "${package_output_path}" 350 | stapler_exit_code="$?" 351 | 352 | if (( stapler_exit_code != 0 )); then 353 | >&2 echo -e "\nSTAPLING ERROR OCCURRED: EXIT CODE ${stapler_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 354 | exit 9 355 | fi 356 | 357 | echo -e "\nAssessing Notarized ${script_name} Version ${script_version} Installation Package..." 358 | spctl_assess_output="$(spctl -avvt install "${package_output_path}" 2>&1)" 359 | spctl_assess_exit_code="$?" 360 | 361 | echo "${spctl_assess_output}" 362 | 363 | pkgutil_check_signature_output="$(pkgutil --check-signature "${package_output_path}" 2>&1)" 364 | pkgutil_check_signature_exit_code="$?" 365 | 366 | echo "${pkgutil_check_signature_output}" 367 | 368 | if (( spctl_assess_exit_code != 0 || pkgutil_check_signature_exit_code != 0 )) || [[ "${spctl_assess_output}" != *$'\nsource=Notarized Developer ID\n'*"(${INTENDED_CODE_SIGNATURE_TEAM_ID})" || "${pkgutil_check_signature_output}" != *$'\n Notarization: trusted by the Apple notary service\n'* || "${pkgutil_check_signature_output}" != *$'\n 1. Developer ID Installer: '*" (${INTENDED_CODE_SIGNATURE_TEAM_ID})"$'\n'* ]]; then # Double-check that the package got assessed to be signed with "Notarized Developer ID" and the correct Team ID. 369 | # The "spctl -avv" output will only ever include "source=Notarized Developer ID" when running on macOS 10.14 Mojave and newer and the "pkgutil --check-signature" output will only contain the "Notarization" line on macOS 12 Monterey and newer, but we should only be building on the latest version of macOS so don't need to worry about checking the current OS version for these verifications. 370 | >&2 echo -e "\nASSESSMENT ERROR OCCURRED: SPCTL EXIT CODE ${spctl_assess_exit_code} & PKGUTIL EXIT CODE ${pkgutil_check_signature_exit_code} (ALSO SEE ERROR MESSAGES ABOVE)" 371 | exit 10 372 | fi 373 | 374 | echo -e "\nSuccessfully notarized ${script_name} version ${script_version} installation package!" 375 | 376 | pkg_checksum="$(openssl dgst -sha512 "${package_output_path}" | awk '{ print $NF; exit }')" 377 | else 378 | echo -e "\nChose NOT to notarize the ${script_name} version ${script_version} installation package." 379 | mv "${package_output_path}" "${package_output_path/.pkg/-UNNOTARIZED.pkg}" # Rename unnotarized package so that I never accidentally publish it. 380 | fi 381 | 382 | echo -e '\nVerifying Checksums of Package and Archive (and currently installed script which may fail if it is not the latest version)...' 383 | 384 | printf '%s %s\n' \ 385 | "${pkg_checksum}" "${package_output_filename}" \ 386 | "${zip_checksum}" "${zip_output_filename}" \ 387 | "${script_checksum}" "/usr/local/bin/${script_name}" > "${release_dir}/${script_name}-sha512-checksums-${script_version}.txt" # Save all checksums in a format that can be used with "shasum -c". 388 | 389 | cd "${release_dir}" || exit 11 # Must "cd" into "release_dir" to be able to verify the checksums file using "shasum -c". 390 | shasum -c "${script_name}-sha512-checksums-${script_version}.txt" # The "/usr/local/bin/mkuser" verification may fail if I don't currently have the latest version being built installed (since it's not uncommon to keep the previous release version installed until after I run this script to finalize a new release). 391 | 392 | open "${release_dir}" 393 | -------------------------------------------------------------------------------- /utilities/download-and-install-mkuser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck enable=add-default-case,avoid-nullary-conditions,check-unassigned-uppercase,deprecate-which,quote-safe-variables,require-double-brackets 3 | 4 | # 5 | # Created by Pico Mitchell (of Free Geek) on 12/1/22 6 | # 7 | # https://mkuser.sh 8 | # 9 | # MIT License 10 | # 11 | # Copyright (c) 2022 Free Geek 12 | # 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | # to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | # 23 | 24 | # NOTICE: This script IS NOT for user creation packages. The mkuser script itself is used for that. 25 | # FOR INFORMATION ABOUT HOW TO USE THIS SCRIPT, SEE: https://github.com/freegeek-pdx/mkuser#local-installation 26 | 27 | PATH='/usr/bin:/bin:/usr/sbin:/sbin' 28 | 29 | if [ -d '/System/Installation' ] && [ ! -f '/usr/bin/pico' ]; then # The specified folder should exist in recoveryOS and the file should not. 30 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND INSTALL ERROR: This tool cannot be run within recoveryOS.' 31 | exit 255 32 | elif [ "$(uname)" != 'Darwin' ]; then # Check this AFTER checking if running in recoveryOS since "uname" doesn't exist in recoveryOS. 33 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND INSTALL ERROR: This tool can only run on macOS.' 34 | exit 254 35 | elif ! dseditgroup -o checkmember -m "$(id -un)" 'admin' > /dev/null 2>&1; then 36 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND INSTALL ERROR: This tool must be run as root or as an administrator.' 37 | exit 253 38 | fi 39 | 40 | run_as_sudo_if_needed() { 41 | if [ "$(id -u)" -ne 0 ]; then # Only need to run with "sudo" if this script itself IS NOT already running as root. 42 | sudo -vn 2> /dev/null || echo '' # IF SUDO REQUIRES A PASSWORD (which won't be the case if it was already authorized less than 5 mins ago), add a line break before the prompt just for display to separate from likely "curl" output when downloading this script. 43 | sudo -p 'Enter Password for "%p" to DOWNLOAD AND INSTALL mkuser: ' "$@" 44 | else 45 | "$@" 46 | fi 47 | } 48 | 49 | # NOTE: The actual download and install script is a bash script which is run via the "bash" command below which is done like this for a couple of reasons: 50 | # - The parent script can be run as "sh" (or "bash" or "zsh") and the actual install script will always be properly run as "bash" without the user having to worry about that in the invocation. 51 | # - The install script needs to be run as root, and running is as a sub-command like this means that we can launch "bash" with "sudo" as needed without the user having to worry about that in the invocation. 52 | # ALSO NOTE: A here-doc is used with expansion disabled so that normal quoting and variables can be used within the sub-script without any added nested quoting issues. 53 | 54 | run_as_sudo_if_needed bash << 'ACTUAL_INSTALL_SCRIPT_EOF' 55 | PATH='/usr/bin:/bin:/usr/sbin:/sbin' 56 | 57 | echo '' # Add a line break before the following output just for display to separate from likely "sudo" prompt or "curl" output when downloading this script. 58 | 59 | TMPDIR="$([[ -d "${TMPDIR}" && -w "${TMPDIR}" ]] && echo "${TMPDIR%/}/" || echo '/private/tmp/')" # Make sure "TMPDIR" is always set and that it always has a trailing slash for consistency regardless of the current environment. 60 | 61 | script_pid="$$" # This script_pid will be used for both "caffeinate" and "shlock". 62 | caffeinate -dimsuw "${script_pid}" & # Use "caffeinate" to keep computer awake while "mkuser" is being installed (or running) which should always be pretty quick, but this does not hurt. 63 | 64 | # Block simultaneous mkuser installation processes from running simultaneously since only one "installer" process can run at a time anyways. 65 | # Simply not allowing simultaneous runs solves all these possible issues and simplifies the logic in this script. 66 | 67 | # Use "trap" to catch all EXITs to always delete the "/private/var/run/mkuser-install.pid" file upon completion. This appears to always run for any "exit" statement, and also runs after SIGINT in bash, but that may not be true for other shells: https://unix.stackexchange.com/questions/57940/trap-int-term-exit-really-necessary 68 | trap 'rm -rf /private/var/run/mkuser-install.pid' EXIT # Even though this command runs last, it does NOT seem to override the final exit code. 69 | 70 | until shlock -p "${script_pid}" -f '/private/var/run/mkuser-install.pid' &> /dev/null; do # Loop and sleep until no other mkuser install/run processes are running. 71 | echo "mkuser DOWNLOAD AND INSTALL NOTICE: Waiting for another mkuser DOWNLOAD AND INSTALL process (PID $(head -1 '/private/var/run/mkuser-install.pid' 2> /dev/null || echo '?')) to finish before starting this one (PID ${script_pid})." 72 | sleep 3 73 | done 74 | 75 | readonly INTENDED_CODE_SIGNATURE_TEAM_ID='YRW6NUGA63' 76 | 77 | echo 'mkuser DOWNLOAD AND INSTALL: Retrieving Latest mkuser Version and Download URL...' 78 | 79 | if ! latest_version_json="$(curl -m 5 -sfL 'https://update.mkuser.sh' 2> /dev/null)" || [[ "${latest_version_json}" != *'"tag_name"'* || "${latest_version_json}" != *'"browser_download_url"'* ]]; then 80 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO RETRIEVE LATEST VERSION OR DOWNLOAD URL (INTERNET REQUIRED)' 81 | exit 1 82 | fi 83 | 84 | install_location='/usr/local/bin/mkuser' 85 | 86 | installed_version='NOT INSTALLED' 87 | 88 | if [[ -f "${install_location}" ]]; then 89 | installed_version="$(awk -F " |=|'" '($2 == "MKUSER_VERSION") { print $(NF-1); exit }' "${install_location}")" 90 | 91 | if [[ ! "${installed_version}" =~ ^[0123456789][0123456789.-]*$ ]]; then 92 | installed_version='INVALID VERSION (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)' 93 | fi 94 | fi 95 | 96 | echo "Installed mkuser Version: ${installed_version}" 97 | 98 | latest_version="$(osascript -l 'JavaScript' -e 'run = argv => JSON.parse(argv[0]).tag_name' -- "${latest_version_json}" 2> /dev/null)" 99 | # Parsing JSON with JXA: https://paulgalow.com/how-to-work-with-json-api-data-in-macos-shell-scripts & https://twitter.com/n8henrie/status/1529513429203300352 100 | 101 | fallback_version_note='' 102 | if [[ ! "${latest_version}" =~ ^[0123456789][0123456789.-]*$ ]]; then 103 | # Make sure the latest version string is valid. If JSON.parse() failed somehow, just try to get the latest version string using "awk" instead. 104 | latest_version="$(echo "${latest_version_json}" | awk -F '"' '($2 == "tag_name") { print $4; exit }')" 105 | fallback_version_note=' (USED FALLBACK TECHNIQUE TO RETRIEVE VERSION, PLEASE REPORT THIS ISSUE)' 106 | 107 | if [[ ! "${latest_version}" =~ ^[0123456789][0123456789.-]*$ ]]; then 108 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO RETRIEVE LATEST VERSION (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)' 109 | exit 2 110 | fi 111 | fi 112 | 113 | echo "Latest mkuser Version: ${latest_version}${fallback_version_note}" 114 | 115 | latest_checksums_download_url="$(osascript -l 'JavaScript' -e 'run = argv => JSON.parse(argv[0]).assets[2].browser_download_url' -- "${latest_version_json}" 2> /dev/null)" 116 | 117 | fallback_checksums_download_url_note='' 118 | if [[ "${latest_checksums_download_url}" != 'https://'*'.txt' ]]; then 119 | # Make sure the checksums URL is valid. If JSON.parse() failed somehow, just try to get the checksums URL using "awk" instead. 120 | latest_checksums_download_url="$(echo "${latest_version_json}" | awk -F '"' '(($2 == "browser_download_url") && ($4 ~ /\.txt$/)) { print $4; exit }')" 121 | fallback_checksums_download_url_note=' (USED FALLBACK TECHNIQUE TO RETRIEVE CHECKSUMS DOWNLOAD URL, PLEASE REPORT THIS ISSUE)' 122 | 123 | if [[ "${latest_checksums_download_url}" != 'https://'*'.txt' ]]; then 124 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO RETRIEVE RETRIEVE CHECKSUMS DOWNLOAD URL (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)' 125 | exit 3 126 | fi 127 | fi 128 | 129 | echo "Latest mkuser Checksums URL: ${latest_checksums_download_url}${fallback_checksums_download_url_note}" 130 | 131 | if ! latest_checksums="$(curl -m 5 -sfL "${latest_checksums_download_url}" 2> /dev/null)" || [[ "${latest_checksums}" != *'mkuser'* ]]; then 132 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO RETRIEVE CHECKSUMS (INTERNET REQUIRED)' 133 | exit 4 134 | fi 135 | 136 | intended_script_checksum="$(echo "${latest_checksums}" | awk '($NF ~ /\/mkuser$/) { print $1; exit }')" 137 | 138 | verify_code_signature_at_path() { 139 | codesign -vv --strict -R '=identifier "org.freegeek.mkuser" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = '"\"${INTENDED_CODE_SIGNATURE_TEAM_ID}\"" "$1" 140 | local codesign_verify_exit_code="$?" 141 | 142 | local spctl_assess_last_line 143 | spctl_assess_last_line="$(spctl -avvt open --context context:primary-signature "$1" 2>&1 | tail -1)" # Only capture the last line to output and check (which will be the "origin" line when successful or an error if the siganture was invalid) since that's all thats relevant 144 | # because "spctl -avvt open ..." will "fail" with "rejected" since it rejects any flat files that are not notarized, but scripts cannot be notarized so signing is the most that can be done (packages, disk images, and Mach-O binaries are the only flat files that can be notarized). 145 | echo "${spctl_assess_last_line}" 146 | 147 | if (( codesign_verify_exit_code != 0 )) || [[ "${spctl_assess_last_line}" != *"(${INTENDED_CODE_SIGNATURE_TEAM_ID})" ]]; then 148 | return 1 149 | fi 150 | 151 | return 0 152 | } 153 | 154 | if [[ "${installed_version}" == "${latest_version}" ]]; then 155 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Verifying Existing mkuser Version ${latest_version} Code Signature and Checksum at Install Location..." 156 | 157 | verify_code_signature_at_path "${install_location}" 158 | verify_code_signature_at_path_exit_code="$?" 159 | 160 | echo "Intended Script Checksum = ${intended_script_checksum}" 161 | 162 | actual_script_checksum="$(openssl dgst -sha512 "${install_location}" | awk '{ print $NF; exit }')" 163 | echo "Installed Script Checksum = ${actual_script_checksum}" 164 | 165 | if (( verify_code_signature_at_path_exit_code == 0 )) && [[ -x "${install_location}" && "${actual_script_checksum}" == "${intended_script_checksum}" ]]; then # Checksum verification is part of "codesign" verification and "spctl" assessment, but manually verify it anyways. 166 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Verified existing installation of latest mkuser version ${latest_version}!" 167 | exit 0 168 | else 169 | rm -f "${install_location}" 170 | echo -e "Latest mkuser version ${latest_version} already installed BUT RE-INSTALLING BECAUSE FAILED VERIFICATION!" 171 | fi 172 | fi # NOTE: Not checking if NEWER version is installed since this scripts intention is just to always install the latest release version. 173 | 174 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Downloading mkuser Version ${latest_version} Installation Package..." 175 | 176 | latest_pkg_download_url="$(osascript -l 'JavaScript' -e 'run = argv => JSON.parse(argv[0]).assets[0].browser_download_url' -- "${latest_version_json}" 2> /dev/null)" 177 | 178 | fallback_pkg_download_url_note='' 179 | if [[ "${latest_pkg_download_url}" != 'https://'*'.pkg' ]]; then 180 | # Make sure the package URL is valid. If JSON.parse() failed somehow, just try to get the package URL using "awk" instead. 181 | latest_pkg_download_url="$(echo "${latest_version_json}" | awk -F '"' '(($2 == "browser_download_url") && ($4 ~ /\.pkg$/)) { print $4; exit }')" 182 | fallback_pkg_download_url_note=' (USED FALLBACK TECHNIQUE TO RETRIEVE PKG DOWNLOAD URL, PLEASE REPORT THIS ISSUE)' 183 | 184 | if [[ "${latest_pkg_download_url}" != 'https://'*'.pkg' ]]; then 185 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO RETRIEVE RETRIEVE PKG DOWNLOAD URL (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)' 186 | exit 5 187 | fi 188 | fi 189 | 190 | echo "Latest mkuser Package Download URL: ${latest_pkg_download_url}${fallback_pkg_download_url_note}" 191 | 192 | package_download_path="${TMPDIR}mkuser-${latest_version}.pkg" 193 | rm -rf "${package_download_path}" # Only one instance can be running at a time, so it is always safe to delete this package if it happens to exist. 194 | curl --connect-timeout 5 --progress-bar -fL "${latest_pkg_download_url}" -o "${package_download_path}" 195 | curl_exit_code="$?" 196 | 197 | if (( curl_exit_code != 0 )) || [[ ! -f "${package_download_path}" ]]; then 198 | rm -f "${package_download_path}" 199 | >&2 echo "mkuser DOWNLOAD AND INSTALL ERROR: DOWLOAD INSTALLATION PACKAGE FAILED WITH EXIT CODE ${curl_exit_code} (INTERNET REQUIRED, SEE OUTPUT ABOVE FOR MORE INFO)" 200 | exit 6 201 | fi 202 | 203 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Verifying mkuser Version ${latest_version} Installation Package Code Signature and Checksum..." 204 | 205 | spctl_assess_output="$(spctl -avvt install "${package_download_path}" 2>&1)" 206 | spctl_assess_exit_code="$?" 207 | 208 | echo "${spctl_assess_output}" 209 | 210 | pkgutil_check_signature_output="$(pkgutil --check-signature "${package_download_path}" 2>&1)" 211 | pkgutil_check_signature_exit_code="$?" 212 | 213 | echo "${pkgutil_check_signature_output}" 214 | 215 | darwin_major_version="$(uname -r | cut -d '.' -f 1)" # 17 = 10.13, 18 = 10.14, 19 = 10.15, 20 = 11.0, etc. 216 | if (( spctl_assess_exit_code != 0 || pkgutil_check_signature_exit_code != 0 )) || [[ "${spctl_assess_output}" != *$'\nsource='"$( (( darwin_major_version >= 18 )) && echo 'Notarized ' )"$'Developer ID\n'*"(${INTENDED_CODE_SIGNATURE_TEAM_ID})" || "${pkgutil_check_signature_output}" != *$'\n 1. Developer ID Installer: '*" (${INTENDED_CODE_SIGNATURE_TEAM_ID})"$'\n'* || ( darwin_major_version -ge 21 && "${pkgutil_check_signature_output}" != *$'\n Notarization: trusted by the Apple notary service\n'* ) ]]; then 217 | # The "spctl -avv" output on macOS 10.13 High Sierra will only ever include "source=Developer ID" even if it is actually notarized while macOS 10.14 Mojave and newer will include "source=Notarized Developer ID" and the "pkgutil --check-signature" output will only contain the "Notarization" line on macOS 12 Monterey and newer. 218 | rm -f "${package_download_path}" 219 | >&2 echo "mkuser DOWNLOAD AND INSTALL ERROR: INSTALLATION PACKAGE VERIFICATION FAILED WITH SPCTL EXIT CODE ${spctl_assess_exit_code} & PKGUTIL EXIT CODE ${pkgutil_check_signature_exit_code} (SEE OUTPUT ABOVE FOR MORE INFO)" 220 | exit 7 221 | fi 222 | 223 | intended_pkg_checksum="$(echo "${latest_checksums}" | awk '($NF ~ /\.pkg$/) { print $1; exit }')" 224 | echo "Intended Package Checksum = ${intended_pkg_checksum}" 225 | 226 | actual_pkg_checksum="$(openssl dgst -sha512 "${package_download_path}" | awk '{ print $NF; exit }')" 227 | echo "Downloaded Package Checksum = ${actual_pkg_checksum}" 228 | 229 | if [[ "${actual_pkg_checksum}" != "${intended_pkg_checksum}" ]]; then # Checksum verification is part of "spctl" assessment, but manually verify it anyways. 230 | rm -f "${package_download_path}" 231 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: INVALID PACKAGE CHECKSUM (SEE OUTPUT ABOVE FOR MORE INFO)' 232 | exit 8 233 | fi 234 | 235 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Installing mkuser Version ${latest_version} Package..." 236 | 237 | installer -pkg "${package_download_path}" -target '/' 238 | installer_exit_code="$?" 239 | 240 | rm -f "${package_download_path}" 241 | 242 | if (( installer_exit_code != 0 )); then 243 | >&2 echo "mkuser DOWNLOAD AND INSTALL ERROR: FAILED TO INSTALL PACKAGE WITH EXIT CODE ${installer_exit_code} (SEE OUTPUT ABOVE AND \"/var/log/install.log\" FOR MORE INFO)" 244 | exit 9 245 | fi 246 | 247 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Verifying mkuser Version ${latest_version} Code Signature and Checksum at Install Location..." 248 | # NOTE: The package installer "postinstall" verifies the code signature and checksum and will delete everything and error if somehow anything was invalid, but verify it all again anyways. 249 | 250 | if ! verify_code_signature_at_path "${install_location}"; then 251 | rm -f "${install_location}" 252 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: INVALID CODE SIGNATURE AT INSTALL LOCATION (SEE OUTPUT ABOVE FOR MORE INFO)' 253 | exit 10 254 | fi 255 | 256 | echo "Intended Script Checksum = ${intended_script_checksum}" 257 | 258 | actual_script_checksum="$(openssl dgst -sha512 "${install_location}" | awk '{ print $NF; exit }')" 259 | echo "Installed Script Checksum = ${actual_script_checksum}" 260 | 261 | if [[ "${actual_script_checksum}" != "${intended_script_checksum}" ]]; then # Checksum verification is part of "codesign" verification and "spctl" assessment, but manually verify it anyways. 262 | rm -f "${install_location}" 263 | >&2 echo 'mkuser DOWNLOAD AND INSTALL ERROR: INVALID CHECKSUM AT INSTALL LOCATION (SEE OUTPUT ABOVE FOR MORE INFO)' 264 | exit 11 265 | fi 266 | 267 | echo -e "\nmkuser DOWNLOAD AND INSTALL: Successfully installed and verified mkuser version ${latest_version}!" 268 | ACTUAL_INSTALL_SCRIPT_EOF 269 | -------------------------------------------------------------------------------- /utilities/download-and-run-mkuser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck enable=add-default-case,avoid-nullary-conditions,check-unassigned-uppercase,deprecate-which,quote-safe-variables,require-double-brackets 3 | 4 | # 5 | # Created by Pico Mitchell (of Free Geek) on 2/14/23 6 | # 7 | # https://mkuser.sh 8 | # 9 | # MIT License 10 | # 11 | # Copyright (c) 2023 Free Geek 12 | # 13 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | # to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | # 23 | 24 | # NOTICE: This script IS NOT for user creation packages. The mkuser script itself is used for that. 25 | # FOR INFORMATION ABOUT HOW TO USE THIS SCRIPT, SEE: https://github.com/freegeek-pdx/mkuser#running-without-installation 26 | 27 | PATH='/usr/bin:/bin:/usr/sbin:/sbin' 28 | 29 | if [ -d '/System/Installation' ] && [ ! -f '/usr/bin/pico' ]; then # The specified folder should exist in recoveryOS and the file should not. 30 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND RUN ERROR: This tool cannot be run within recoveryOS.' 31 | exit 255 32 | elif [ "$(uname)" != 'Darwin' ]; then # Check this AFTER checking if running in recoveryOS since "uname" doesn't exist in recoveryOS. 33 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND RUN ERROR: This tool can only run on macOS.' 34 | exit 254 35 | elif ! dseditgroup -o checkmember -m "$(id -un)" 'admin' > /dev/null 2>&1; then 36 | >&2 printf '\n%s\n' 'mkuser DOWNLOAD AND RUN ERROR: This tool must be run as root or as an administrator.' 37 | exit 253 38 | fi 39 | 40 | run_as_sudo_if_needed() { 41 | if [ "$(id -u)" -ne 0 ]; then # Only need to run with "sudo" if this script itself IS NOT already running as root. 42 | sudo -vn 2> /dev/null || echo '' # IF SUDO REQUIRES A PASSWORD (which won't be the case if it was already authorized less than 5 mins ago), add a line break before the prompt just for display to separate from likely "curl" output when downloading this script. 43 | sudo -p 'Enter Password for "%p" to DOWNLOAD AND RUN mkuser: ' "$@" 44 | else 45 | "$@" 46 | fi 47 | } 48 | 49 | # NOTE: The actual download and run script is a bash script which is run via the "bash" command below which is done like this for a couple of reasons: 50 | # - The parent script can be run as "sh" (or "bash" or "zsh") and the actual install script will always be properly run as "bash" without the user having to worry about that in the invocation. 51 | # - The install script needs to be run as root, and running is as a sub-command like this means that we can launch "bash" with "sudo" as needed without the user having to worry about that in the invocation since running 52 | # "sudo sh <(curl mkuser.sh)" would fail because the process substitution FD would get consumed by "sudo" instead of "sh". This way just "sh <(curl mkuser.sh)" can be run and "sudo" will be added as needed by this script. 53 | # ALSO NOTE: A here-doc IS NOT used since a here-doc would be passed to the "bash" command using standard input (stdin) and we need any stdin from the parent script to be passed though to the actual "mkuser" script that it run in case 54 | # a password is being passed using "--stdin-password" and a using here-doc would prevent/mask that since only a single stdin can exist. This makes quoting more complicated since the whole script must exist within a single quoted string. 55 | 56 | # Suppress ShellCheck warning about expressions not expanding in single quotes since it is intentional (as described above). 57 | # shellcheck disable=SC2016 58 | run_as_sudo_if_needed bash -c ' 59 | PATH="/usr/bin:/bin:/usr/sbin:/sbin" 60 | 61 | echo "" # Add a line break before the following output just for display to separate from likely "sudo" prompt or "curl" output when downloading this script. 62 | 63 | script_pid="$$" # This script_pid will be used for both "caffeinate" and "shlock". 64 | caffeinate -dimsuw "${script_pid}" & # Use "caffeinate" to keep computer awake while "mkuser" is being downloaded and run which should always be pretty quick, but this does not hurt. 65 | 66 | # Block simultaneous mkuser temporary run processes from running simultaneously since only one "mkuser" process can run at a time anyways, 67 | # and simultaneous temporary runs could conflict if one deletes the temporary script while another process is still running from it. 68 | # Simply not allowing simultaneous runs solves all these possible issues and simplifies the logic in this script. 69 | 70 | # Use "trap" to catch all EXITs to always delete the "/private/var/run/mkuser-run.pid" file upon completion. This appears to always run for any "exit" statement, and also runs after SIGINT in bash, but that may not be true for other shells: https://unix.stackexchange.com/questions/57940/trap-int-term-exit-really-necessary 71 | trap "rm -rf /private/var/run/mkuser-run.pid" EXIT # Even though this command runs last, it does NOT seem to override the final exit code. 72 | 73 | until shlock -p "${script_pid}" -f "/private/var/run/mkuser-run.pid" &> /dev/null; do # Loop and sleep until no other mkuser run processes are running. 74 | echo "mkuser DOWNLOAD AND RUN NOTICE: Waiting for another mkuser DOWNLOAD AND RUN process (PID $(head -1 "/private/var/run/mkuser-run.pid" 2> /dev/null || echo "?")) to finish before starting this one (PID ${script_pid})." 75 | sleep 3 76 | done 77 | 78 | echo "mkuser DOWNLOAD AND RUN: Retrieving Latest mkuser Version and Download URL..." 79 | 80 | if ! latest_version_json="$(curl -m 5 -sfL "https://update.mkuser.sh" 2> /dev/null)" || [[ "${latest_version_json}" != *"\"tag_name\""* || "${latest_version_json}" != *"\"browser_download_url\""* ]]; then 81 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO RETRIEVE LATEST VERSION OR DOWNLOAD URL (INTERNET REQUIRED)" 82 | exit 1 83 | fi 84 | 85 | latest_version="$(osascript -l "JavaScript" -e "run = argv => JSON.parse(argv[0]).tag_name" -- "${latest_version_json}" 2> /dev/null)" 86 | # Parsing JSON with JXA: https://paulgalow.com/how-to-work-with-json-api-data-in-macos-shell-scripts & https://twitter.com/n8henrie/status/1529513429203300352 87 | 88 | fallback_version_note="" 89 | if [[ ! "${latest_version}" =~ ^[0123456789][0123456789.-]*$ ]]; then 90 | # Make sure the latest version string is valid. If JSON.parse() failed somehow, just try to get the latest version string using "awk" instead. 91 | latest_version="$(echo "${latest_version_json}" | awk -F "\"" '\''($2 == "tag_name") { print $4; exit }'\'')" 92 | fallback_version_note=" (USED FALLBACK TECHNIQUE TO RETRIEVE VERSION, PLEASE REPORT THIS ISSUE)" 93 | 94 | if [[ ! "${latest_version}" =~ ^[0123456789][0123456789.-]*$ ]]; then 95 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO RETRIEVE LATEST VERSION (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)" 96 | exit 2 97 | fi 98 | fi 99 | 100 | echo "Latest mkuser Version: ${latest_version}${fallback_version_note}" 101 | 102 | echo -e "\nmkuser DOWNLOAD AND RUN: Downloading mkuser Version ${latest_version} Archive..." 103 | 104 | latest_zip_download_url="$(osascript -l "JavaScript" -e "run = argv => JSON.parse(argv[0]).assets[1].browser_download_url" -- "${latest_version_json}" 2> /dev/null)" 105 | 106 | fallback_zip_download_url_note="" 107 | if [[ "${latest_zip_download_url}" != "https://"*".zip" ]]; then 108 | # Make sure the archive URL is valid. If JSON.parse() failed somehow, just try to get the archive URL using "awk" instead. 109 | latest_zip_download_url="$(echo "${latest_version_json}" | awk -F "\"" '\''(($2 == "browser_download_url") && ($4 ~ /\.zip$/)) { print $4; exit }'\'')" 110 | fallback_zip_download_url_note=" (USED FALLBACK TECHNIQUE TO RETRIEVE ARCHIVE DOWNLOAD URL, PLEASE REPORT THIS ISSUE)" 111 | 112 | if [[ "${latest_zip_download_url}" != "https://"*".zip" ]]; then 113 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO RETRIEVE RETRIEVE ARCHIVE DOWNLOAD URL (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)" 114 | exit 3 115 | fi 116 | fi 117 | 118 | echo "Latest mkuser Archive Download URL: ${latest_zip_download_url}${fallback_zip_download_url_note}" 119 | 120 | install_location_folder="/private/tmp/mkuser-run" 121 | rm -rf "${install_location_folder}" # Only one instance can be running at a time, so it is always safe to delete this folder if it happens to exist. 122 | mkdir -p "${install_location_folder}" 123 | 124 | zip_download_path="${install_location_folder}/mkuser-${latest_version}.zip" 125 | curl --connect-timeout 5 --progress-bar -fL "${latest_zip_download_url}" -o "${zip_download_path}" 126 | curl_exit_code="$?" 127 | 128 | if (( curl_exit_code != 0 )) || [[ ! -f "${zip_download_path}" ]]; then 129 | rm -rf "${install_location_folder}" 130 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: DOWLOAD ARCHIVE FAILED WITH EXIT CODE ${curl_exit_code} (INTERNET REQUIRED, SEE OUTPUT ABOVE FOR MORE INFO)" 131 | exit 4 132 | fi 133 | 134 | echo -e "\nmkuser DOWNLOAD AND RUN: Verifying mkuser Version ${latest_version} Archive Checksum..." 135 | 136 | latest_checksums_download_url="$(osascript -l "JavaScript" -e "run = argv => JSON.parse(argv[0]).assets[2].browser_download_url" -- "${latest_version_json}" 2> /dev/null)" 137 | 138 | fallback_checksums_download_url_note="" 139 | if [[ "${latest_checksums_download_url}" != "https://"*".txt" ]]; then 140 | # Make sure the checksums URL is valid. If JSON.parse() failed somehow, just try to get the checksums URL using "awk" instead. 141 | latest_checksums_download_url="$(echo "${latest_version_json}" | awk -F "\"" '\''(($2 == "browser_download_url") && ($4 ~ /\.txt$/)) { print $4; exit }'\'')" 142 | fallback_checksums_download_url_note=" (USED FALLBACK TECHNIQUE TO RETRIEVE CHECKSUMS DOWNLOAD URL, PLEASE REPORT THIS ISSUE)" 143 | 144 | if [[ "${latest_checksums_download_url}" != "https://"*".txt" ]]; then 145 | rm -rf "${install_location_folder}" 146 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO RETRIEVE RETRIEVE CHECKSUMS DOWNLOAD URL (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)" 147 | exit 5 148 | fi 149 | fi 150 | 151 | echo "Latest mkuser Checksums URL: ${latest_checksums_download_url}${fallback_checksums_download_url_note}" 152 | 153 | if ! latest_checksums="$(curl -m 5 -sfL "${latest_checksums_download_url}" 2> /dev/null)" || [[ "${latest_checksums}" != *"mkuser"* ]]; then 154 | rm -rf "${install_location_folder}" 155 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO RETRIEVE CHECKSUMS (INTERNET REQUIRED)" 156 | exit 6 157 | fi 158 | 159 | intended_zip_checksum="$(echo "${latest_checksums}" | awk '\''($NF ~ /\.zip$/) { print $1; exit }'\'')" 160 | echo "Intended Archive Checksum = ${intended_zip_checksum}" 161 | 162 | actual_zip_checksum="$(openssl dgst -sha512 "${zip_download_path}" | awk '\''{ print $NF; exit }'\'')" 163 | echo "Downloaded Archive Checksum = ${actual_zip_checksum}" 164 | 165 | if [[ "${actual_zip_checksum}" != "${intended_zip_checksum}" ]]; then 166 | rm -rf "${install_location_folder}" 167 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: INVALID ARCHIVE CHECKSUM (SEE OUTPUT ABOVE FOR MORE INFO)" 168 | exit 7 169 | fi 170 | 171 | echo -e "\nmkuser DOWNLOAD AND RUN: Unarchiving mkuser Version ${latest_version}..." 172 | 173 | ditto -xkvV "${zip_download_path}" "${install_location_folder}" # NOTE: Unzipping MUST be done with "ditto" since it properly preserve/restores code signature extended attributes, unlike "unzip". 174 | ditto_exit_code="$?" 175 | 176 | rm -f "${zip_download_path}" 177 | 178 | if (( ditto_exit_code != 0 )) || [[ ! -f "${install_location_folder}/mkuser" || ! -x "${install_location_folder}/mkuser" ]]; then 179 | rm -rf "${install_location_folder}" 180 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: FAILED TO UNARCHIVE WITH EXIT CODE ${ditto_exit_code}" 181 | exit 8 182 | fi 183 | 184 | echo -e "\nmkuser DOWNLOAD AND RUN: Verifying mkuser Version ${latest_version} Code Signature and Checksum at Temporary Location..." 185 | 186 | readonly INTENDED_CODE_SIGNATURE_TEAM_ID="YRW6NUGA63" 187 | 188 | codesign -vv --strict -R "=identifier \"org.freegeek.mkuser\" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"${INTENDED_CODE_SIGNATURE_TEAM_ID}\"" "${install_location_folder}/mkuser" 189 | codesign_verify_exit_code="$?" 190 | 191 | spctl_assess_last_line="$(spctl -avvt open --context context:primary-signature "${install_location_folder}/mkuser" 2>&1 | tail -1)" # Only capture the last line to output and check (which will be the "origin" line when successful or an error if the siganture was invalid) since thats all thats relevant 192 | # because "spctl -avvt open ..." will "fail" with "rejected" since it rejects any flat files that are not notarized, but scripts cannot be notarized so signing is the most that can be done (packages, disk images, and Mach-O binaries are the only flat files that can be notarized). 193 | echo "${spctl_assess_last_line}" 194 | 195 | intended_script_checksum="$(echo "${latest_checksums}" | awk '\''($NF ~ /\/mkuser$/) { print $1; exit }'\'')" 196 | echo "Intended Script Checksum = ${intended_script_checksum}" 197 | 198 | actual_script_checksum="$(openssl dgst -sha512 "${install_location_folder}/mkuser" | awk '\''{ print $NF; exit }'\'')" 199 | echo "Unarchived Script Checksum = ${actual_script_checksum}" 200 | 201 | if (( codesign_verify_exit_code != 0 )) || [[ "${spctl_assess_last_line}" != *"(${INTENDED_CODE_SIGNATURE_TEAM_ID})" || "${actual_script_checksum}" != "${intended_script_checksum}" ]]; then # Checksum verification is part of "codesign" verification and "spctl" assessment, but manually verify it anyways. 202 | rm -rf "${install_location_folder}" 203 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: INVALID CODE SIGNATURE OR CHECKSUM AT TEMPORARY LOCATION (SEE OUTPUT ABOVE FOR MORE INFO)" 204 | exit 9 205 | fi 206 | 207 | echo -e "\nmkuser DOWNLOAD AND RUN: Successfully unarchived and verified mkuser version ${latest_version}!" 208 | 209 | echo -e "\nmkuser DOWNLOAD AND RUN: Running mkuser Version ${latest_version} with Specified Options..." 210 | 211 | "${install_location_folder}/mkuser" "$@" 212 | mkuser_exit_code="$?" 213 | 214 | rm -rf "${install_location_folder}" # Can delete this file without worrying about another temporary instances running simultaniously since this script uses "shlock" to only allow one instance to run at a time. 215 | 216 | if [[ -d "${install_location_folder}" ]]; then 217 | >&2 echo "mkuser DOWNLOAD AND RUN ERROR: MKUSER FINISHED WITH EXIT CODE ${mkuser_exit_code}, BUT FAILED TO DELETE TEMPORARY MKUSER AFTER RUNNING (THIS SHOULD NOT HAVE HAPPENED, PLEASE REPORT THIS ISSUE)" 218 | exit 10 219 | fi 220 | 221 | exit "${mkuser_exit_code}" # Always exit with mkusers exit code instead of always being successful after deleting the temporary installation. 222 | ' -- "$@" 223 | --------------------------------------------------------------------------------