Skip to content

Instantly share code, notes, and snippets.

@geekquixotic
Last active November 29, 2022 18:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geekquixotic/ab62f340f22f8c85809affe2b0fbc279 to your computer and use it in GitHub Desktop.
Save geekquixotic/ab62f340f22f8c85809affe2b0fbc279 to your computer and use it in GitHub Desktop.
Meetup Notes - Byte-Sized Fun Edition

Meetup Notes - Byte-Sized Fun Edition

In this month's meetup, I demonstrated a few "tricks and treats" that I've been tinkering with for use within the Jamf Toolkit in the future.

Including:

  1. Detecting TCC Settings
  2. Configuring Scripts with Profiles
  3. Encrypting Text and Files
  4. Leveraging the Keychain

Detecting TCC Settings

If you've ever had to give an app permission to use the camera, microphone, screen recording, and more; you've encountered TCC ("Transparency, Consent and Control") in MacOS. Created back in 2012 with 10.8 (Mountain Lion), TCC makes sure that only the registered apps you permit have access to the resources they need and nothing more.

Traditionally, settings like this have been stored in "Property List" (plist) files and can be accessed with command line tools like "defaults read", PlistBuddy, or plutil. But plist files get read into when the app is loaded and saved out when the app closes. So for large amounts of data or things that need to change frequently, plist files are insufficient.

That's where sqlite3 comes in! SQLite databases are small, self-contained, and highly efficient. They can be read from and written to very quickly and can contain associations and data types that plists can't.

So it's not surprising that Apple uses sqlite3 for things like TCC access.

But there isn't just one database for TCC permissions? Like many macOS resources, there are multiple levels of overlapping settings, including, but not limited to:

  • /Library/Application Support/com.apple.TCC/TCC.db
  • /Users/[USERNAME]/Library/Application Support/com.apple.TCC/TCC.db

The /Library level includes things like:

  • kTCCServiceAccessibility
  • kTCCServiceDeveloperTool
  • kTCCServiceListenEvent
  • kTCCServicePostEvent
  • kTCCServiceScreenCapture
  • kTCCServiceSystemPolicyAllFiles

And the /Users level includes things like:

  • kTCCServiceAddressBook
  • kTCCServiceAppleEvents
  • kTCCServiceBluetoothAlways
  • kTCCServiceCalendar
  • kTCCServiceCamera
  • kTCCServiceFileProviderDomain
  • kTCCServiceFocusStatus
  • kTCCServiceLiverpool
  • kTCCServiceMicrophone
  • kTCCServicePhotos
  • kTCCServiceReminders
  • kTCCServiceSystemPolicyDesktopFolder
  • kTCCServiceSystemPolicyDocumentsFolder
  • kTCCServiceSystemPolicyDownloadsFolder
  • kTCCServiceSystemPolicyNetworkVolumes
  • kTCCServiceSystemPolicyRemovableVolumes

So if we want to see if our people have enabled given Zoom access to their microphone, we can use:

sqlite3 -separator ',' "/Users/[USERNAME]/Library/Application Support/com.apple.TCC/TCC.db" "select service from access where service like '%microphone%' and client like '%zoom%'"; 

Or to check if Teams has access to screen sharing:

sqlite3 -separator ',' "/Library/Application Support/com.apple.TCC/TCC.db" "select service from access where service like '%screen%' and client like '%teams%'"; 

And if you needed to do a lot of this in a script, you could write some code like this:

USERNAME=$([[ $3 ]] && echo "$3" || defaults read /Library/Preferences/com.apple.loginwindow.plist lastUserName)

###
### Functions
###
function checkAccess() {
	service=$1
	app=$2

	## Determine which database we need
	case ${service} in
		## Screen recording is handled at the /Library layer
		'screen')
			db="/Library/Application Support/com.apple.TCC/TCC.db"
		;;
			
		## Microphone and camera are handled at the /User/Library layer
		'microphone' | 'camera')
			db="/Users/${USERNAME}/Library/Application Support/com.apple.TCC/TCC.db"
		;;
	esac
	
	## Read the sqlite database
	access=$(sqlite3 \
		-separator ',' \
		"${db}" \
		"select client,service,auth_value from access where service like '%${service}%' and client like '%${app}%'"; \
	)

	## Send back "On" or "Off"
	[[ ${access: -1}  -gt 0 ]] && echo "On" || echo "Off"
}

if [[ $(checkAccess "camera" "zoom") == "Off" ]]; then
  echo "The camera is not enabled for Zoom"
fi

Configuring Scripts with Profiles

Running Scripts in a Jamf Policy allows one to configure up to eight different aspects of your script with Parameters. Parameters one through three are reserved for computer name, mount point, and the username of the current user respectively. But parameters four through eleven are all yours.

But what if you want more than that? Or what if you want to configure a block of text that uses multiple lines. While there are ways to do it with parameters, there's a better way altogether: Configuration Profiles.

By creating a configuration profile with the "Application & Custom Settings" payload and selecting "Upload" you can create a plist file that, once deployed, can be read by a policy script.

Custom Configuration Profile

Consider the following plist file:

<plist version="1.0">
<dict>
	<key>LaunchCode</key>
	<string>CPE-1704-TKS</string>
	<key>myID</key>
	<string>$JSSID</string>
	<key>UDID</key>
	<string>$UDID</string>
	<key>computerID</key>
	<string>$JSSID</string>
	<key>Jeffsum Text</key>
	<string>
My dad once told me, laugh and the world laughs with you, Cry, and I'll give you something to cry about you little bastard! Do you have any idea how long it takes those cups to decompose. Yeah, but John, if The Pirates of the Caribbean breaks down, the pirates don’t eat the tourists.

Jaguar shark! So tell me - does it really exist? Just my luck, no ice. God creates dinosaurs. God destroys dinosaurs. God creates Man. Man destroys God. Man creates Dinosaurs. Eventually, you do plan to have dinosaurs on your dinosaur tour, right? You really think you can fly that thing? This thing comes fully loaded. AM/FM radio, reclining bucket seats, and... power windows.

Yeah, but your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should. Remind me to thank John for a lovely weekend. What do they got in there? King Kong? Just my luck, no ice. What do they got in there? King Kong?
</string>
</dict>
</plist>

Now inside a script, I can get that information like this:

#!/bin/bash

PLIST="/Library/Managed Preferences/tech.rocketman.prefs.plist"
if [[ -f "${PLIST}" ]]; then
	## File exists. Let's load in some variables
	JeffsumText=$(defaults read "${PLIST}" "Jeffsum Text")
	computerID=$(defaults read "${PLIST}" computerID)
fi

And like in the example above with computerID, this can include any of the variables that jamf provides. This includes getting things like the computer's ID in Jamf without having to make an API call first.

Encrypting Text and Files

I've seen plenty of people temporarily store sensitive information inside plain-text and/or plist files. I'm guilty of it myself. When I've done it, I've made sure to put the file someplace that only root/admins can get to and restricted the access to the file, but it's still not a great long-term solution.

So what is?

Since the beginning of macOS, there has always been a really good command line tool for encrypting/decrypting information: openssl. So I'm surprised I didn't think to use it earlier?

Oh yeah! That's because even with a decent understand of cryptography, it's still far from intuitive. In short, if I want to encrypt an entire file using the command:

#!/bin/bash

## Files and Key
PLAIN="/tmp/plaintext.txt"
CIPHER="/tmp/ciphertext.txt"
KEY="SuperSecretEncryptionKey"

## Encrypt the file
openssl enc -aes256 -e -base64 -k "${KEY}" -in "${PLAIN}" -out "${CIPHER}"
cat ${CIPHER}

## Decrypt the file
openssl enc -aes256 -d -base64 -k "${KEY}" -in "${CIPHER}" -out "${PLAIN}"
cat ${PLAIN}

Where "-aes256" is the cipher being used and "-base64" keeps the encrypted file in a human-readable format that can be sent safely through email or other channels.

If you don't want to encrypt an entire file, you can just protect individual pieces like inside a plist file. For example:

#!/bin/bash

PLIST="/Library/Preferences/com.pretendco.localprefs.plist"
KEYSTORE="/Library/Managed Preferences/com.pretendco.managed.plist"
KEYNAME="SuperSecretKey"

## Make sure the key exists
if [[ -f "${KEYSTORE}" ]]; then
	secretKey=$(defaults read "${KEYSTORE} ${KEYNAME}")
else
	echo "Decryption key is missing!"
	exit 1
fi

## Get the old secret
ciphertext=$(defaults read "${PLIST}" SecureData)

## Decrypt the string so we can use it
plaintext=$(echo ${ciphertext} | openssl enc -aes256 -d -base64 -k "${secretKey}")

In this example, a configuration profile (KEYSTORE) contains the a key/value pair of "SuperSecretKey" which points to the key used to encrypt and decrypt the data securely stored in the plist file (PLIST) but only that one field in the plist file is encrypted. The rest of the plist can be plaintext.

These examples all use "secret key cryptography," meaning that the same key is used for both encryption and decryption. Openssl also support "public key cryptography" where there are two keys: 1. the public key which can be freely distributed and is used to encrypt information to be sent and 2. the private key which only the receiver has and can be used to decrypt the data.

So far, I've only briefly tinkered with having the public key distributed in a configuration profile so that secrets can be stored locally on the computer in a plist (or other storage method) and have an Extension Attribute that has the private key and decrpyts and stores the information in Jamf.

Leveraging the Keychain

And yet there is another option for storing sensative data. One that is built into the operating system and already secures our application, web, and wifi passwords:

The Keychain.

There are command line tools that allow us to create and retrieve data from the keychain.

Here are a few examples of storing, retreiving, updating, and removing information from the keychain:

#!/bin/bash

## Pretend we used AppleScript or some other tool to prompt the user
## for a username/password combo that we need to store for a little while.
SHORTNAME="joshua"
PASSWORD="Double"
KEYCHAIN="/Library/Keychains/System.keychain"
SERVICE="Custom Script"

## Store the password in the keychain
security add-generic-password -a "${SHORTNAME}" -s "${SERVICE}" -w "${PASSWORD}" -A "${KEYCHAIN}" 2>/

## Now let's get it back
foundPassword=$(security find-generic-password -w -a "${SHORTNAME}" -s "${SERVICE}" "${KEYCHAIN}" 2>/dev/null)

## Later we need to change it for some reason
NEWPASSWORD="Pencil"
security delete-generic-password -a "${SHORTNAME}" -s "${SERVICE}" "${KEYCHAIN}" &>/dev/null
security add-generic-password -a "${SHORTNAME}" -s "${SERVICE}" -w "${NEWPASSWORD}" -A "${KEYCHAIN}" 2>/

## Finally, we can delete it
security delete-generic-password -a "${SHORTNAME}" -s "${SERVICE}" "${KEYCHAIN}" &>/dev/null

But wait a minute?! Why are we deleting and re-adding the item instead of just updating it? The add-generic-password command has an update option!

 -U		    Update item if it already exists (if omitted, the item cannot already exist)

In my, albeit brief experimentation, any attempt to update the item prompted the user to authenticate before it would proceed. The only way I found to do it silently was to delete and recreate the item.

Putting it All Together?

Well I don't have any single place to use all these techniques together, but one of my examples above demonstrated how you could combine configuration profiles and cryptography to securely store information. I'm sure there are other ideas.

If you have one, please add a note below. I'd love to hear it.

Bearer Tokens and the Jamf API

As mentioned in the Meetup, Jamf has previously stated that they will stop accepting Basic Authentication (username and password) via the API in the near future.

Starting with 10.42.0, Basic Auth is disabled by default. To turn it back on for now, go to System Settings -> User accounts and groups -> Password Policy and check the box for Allow Basic authentication in addition to Bearer Token authentication.

Getting Ready

Consider the following block of code:

#!/bin/zsh

JAMFURL="https://rocketman.jamfcloud.com"
JAMFUSER="api-user"
JAMFPASS="TopSecretSuperSecurePassword"

## First we get the token using Basic Authorization
basicAuth=$(echo -n "${JAMFUSER}:${JAMFPASS}" | base64)
authResponse=$(curl -s \
	--request POST \
	--url "${JAMFURL}/api/v1/auth/token" \
	--header "Accept: application/json"\
	--header "Authorization: Basic ${basicAuth}"
)

## Courtesy of Der Flounder
## Source: https://derflounder.wordpress.com/2021/12/10/obtaining-checking-and-renewing-bearer-tokens-for-the-jamf-pro-api/
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
	api_token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "${authResponse}" | /usr/bin/xargs)
else
	api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "${authResponse}")
fi

## Now our subsequent API calls use the token and Bearer Authorization
policyList=$(curl -s \
	--header "Authorization: Bearer ${api_token}" \
	--url "${JAMFURL}/JSSResource/policies" \
)

In the first call, we use the token endpoint of the Jamf Pro API with our basic credentials to get our token. This can either be done using the Authorization Basic header and bas64 encoding our credentials or we can just use something like:

curl -s -u "${JAMFUSER}:${JAMFPASS}" ...

While the Classic API can provide results in JSON or XML, the Jamf Pro API will only return results in JSON format.

macOS doesn't provide the same quantity and quality of tools for parsing JSON as they do for XML. Starting in Monterey the plutil command can parse JSON just fine. But for older systems we need to use the old school tools like sed, or awk to get the part(s) we want in a useful format.

The sample code includes code from Rich Trouton (aka Der Flounder) on how to detect the OS and extract the token.

From then on, all our API calls just swap the Authorization: Basic header for Authorization: Bearer.

Caveat: The token is only valid for 15 minutes. This should be long enough for any of your policy scripts, but please keep it in mind. It can be renewed ahead of time by tracking the expiration date but that's additional code that I didn't included here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment