Guarantee only a macOS device can decrypt
You'll mint a key once, keep its public half on your server, and from then on encrypt to it whatever only that device may read. The installation needs to be enrolled first.
A Mac's key proves possession of an enclave key, and certifyKey returns it with hardwareBound: false.
Ask for a key challenge
On your server, use issueKeyChallenge with purpose: "decrypt" and the device ID you expect, and send the keyChallenge string to the client with its nonce.
app.post("/key/challenge", async (req, res) => {
const deviceId = await deviceOf(accountOf(req));
const { nonce, keyChallenge } = await rh.issueKeyChallenge({
purpose: "decrypt",
expectedDevices: [deviceId],
});
res.json({ nonce, keyChallenge });
});Mint the key
On the client, answer the key challenge with RootHeraldMintKey. The SDK keeps a second enclave key for decryption. It and the enrolled enclave key both sign the nonce, and the certification lands in one buffer and a selector for the key in the other. Post the certification and its nonce to your server, and once it has kept the key, save the blob wherever your app keeps its files.
size_t cert_len, blob_len;
RH_STATUS st = RootHeraldMintKey(rh, key_challenge, NULL, 0,
cert, sizeof cert, &cert_len,
blob, sizeof blob, &blob_len);
if (st != RH_OK) {
return 0;
}
post_certification("/key/certify", nonce, cert);
save_key_blob(blob, blob_len);Keep the key
On your server, pass the certification and nonce to certifyKey. It returns the key: keep the whole result with the account that deviceId belongs to, since encryptToDevice reads its alg and format as well as the jwk.
app.post("/key/certify", async (req, res) => {
const { nonce, certification } = req.body;
const key = await rh.certifyKey(nonce, certification);
await saveDecryptKey(accountOf(req), key.deviceId, key);
res.sendStatus(204);
});Encrypt to the key
A Mac's key is format: "apple-ecies", and encryptToDevice refuses it with AppleEciesKeyError. Produce the envelope on an Apple-side encryptor with SecKeyCreateEncryptedData and kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM under the key's public point, and send it to the client as one base64url string.
Open it on the device
On the client, load the blob once with RootHeraldLoadKey, then use RootHeraldDecrypt on each envelope. Decrypting only touches the chip: no network, no challenge, no prompt. The plaintext is never longer than the envelope, so a buffer of the envelope's size always fits. Close the key with RootHeraldCloseKey when you're done with it.
RH_KEY_HANDLE key;
st = RootHeraldLoadKey(rh, blob, blob_len, &key);
if (st != RH_OK) {
return 0;
}
size_t jwe_len = strlen(jwe);
uint8_t* token = allocate(jwe_len);
size_t token_len;
st = RootHeraldDecrypt(key, jwe, jwe_len,
token, jwe_len, &token_len);RH_ERR_DECRYPT_FAILED means the envelope did not open under this key: ask your server for a fresh one. Retrying the same bytes cannot succeed.