#pragma once #include "acceptance-state.h" #include #pragma comment(lib, "crypt32.lib") namespace credential_contract { using release_contract::Json; static constexpr DWORD STORE_LIMIT = 1048576; struct Wipe { std::vector bytes; ~Wipe() { if (!bytes.empty()) SecureZeroMemory(bytes.data(), bytes.size()); } }; struct WipeText { std::string &text; ~WipeText() { if (!text.empty()) SecureZeroMemory(text.data(), text.size()); } }; static Json parse(const std::string &text) { release_contract::Parser parser{text}; auto result = parser.parse(); if (parser.offset != text.size()) throw 1; return result; } static bool tokenValid(const std::string &token) { if (token.size() < 16 || token.size() > 4096) return false; for (char c : token) if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '~' || c == '-')) return false; return true; } static Json binding(const Json &request, const release_contract::Assignment &assignment, const Json &state) { if (assignment.managementOrigin.empty() || release_contract::str(request.object.at("deviceId")) != assignment.deviceId || release_contract::str(request.object.at("origin")) != assignment.managementOrigin) throw 1; auto der = release_contract::pemDer(assignment.policyPem); const std::string keyBytes(der.begin(), der.end()); if (release_contract::str(request.object.at("policyKeySha256")) != release_contract::digest(keyBytes)) throw 1; const auto version = release_contract::number(request.object.at("policyVersion")); const auto digest = release_contract::str(request.object.at("policyDigest")); if (!acceptance_contract::sha(digest) || version < assignment.minimumVersion) throw 1; bool matched = false; for (const auto &record : state.object.at("policies").array) { if (release_contract::str(record.object.at("sid")) == assignment.sid && release_contract::str(record.object.at("deviceId")) == assignment.deviceId && release_contract::str(record.object.at("policyKeySha256")) == release_contract::str(request.object.at("policyKeySha256"))) { matched = release_contract::number(record.object.at("minimumVersion")) == version && release_contract::str(record.object.at("acceptedDigest")) == digest; } } if (!matched) throw 1; Json result = request; result.object.erase("token"); result.object.erase("expectedPreviousSHA"); Json sid; sid.kind = Json::String; sid.string = assignment.sid; result.object["sid"] = sid; return result; } static Json validateStore(const std::string &text) { if (text.empty() || text.size() > STORE_LIMIT) throw 1; auto value = parse(text); if (release_contract::canonical(value) != text || !release_contract::exact(value, {"format", "records"}) || release_contract::str(value.object.at("format")) != "safetynet-management-credentials-v1" || value.object.at("records").kind != Json::Array || value.object.at("records").array.size() > 256) throw 1; std::string previous; for (const auto &record : value.object.at("records").array) { if (!release_contract::exact(record, {"sid", "deviceId", "origin", "policyVersion", "policyKeySha256", "policyDigest", "protectedToken"}) || !acceptance_contract::userSid( release_contract::str(record.object.at("sid"))) || !release_contract::managementOrigin( release_contract::str(record.object.at("origin"))) || !acceptance_contract::sha( release_contract::str(record.object.at("policyKeySha256"))) || !acceptance_contract::sha( release_contract::str(record.object.at("policyDigest")))) throw 1; release_contract::number(record.object.at("policyVersion")); const auto device = release_contract::str(record.object.at("deviceId")); if (device.empty() || device.size() > 80 || !isalnum(static_cast(device[0]))) throw 1; for (char c : device) if (!isalnum(static_cast(c)) && c != '.' && c != '_' && c != '-') throw 1; const auto cipher = release_contract::str(record.object.at("protectedToken")); if (cipher.empty() || cipher.size() > 16384) throw 1; release_contract::decode(cipher); Json identity = record; identity.object.erase("protectedToken"); const auto key = release_contract::canonical(identity); if (!previous.empty() && key <= previous) throw 1; previous = key; } return value; } static std::string stableIdentity(Json metadata) { metadata.object.erase("policyVersion"); metadata.object.erase("policyDigest"); metadata.object.erase("protectedToken"); return release_contract::canonical(metadata); } static const Json *selectRecord(const Json &metadata, const std::vector &records) { const auto identity = stableIdentity(metadata); const auto current = release_contract::number(metadata.object.at("policyVersion")); const Json *selected = nullptr; ULONGLONG highest = 0; std::set versions; for (const auto &record : records) { if (stableIdentity(record) != identity) continue; const auto version = release_contract::number(record.object.at("policyVersion")); if (!versions.insert(version).second) throw 1; if (version > current) throw 1; if (version == current && release_contract::str(record.object.at("policyDigest")) != release_contract::str(metadata.object.at("policyDigest"))) throw 1; if (version > highest) { highest = version; selected = &record; } } return selected; } static void replaceRecord(std::vector &records, const Json &record) { const auto identity = stableIdentity(record); records.erase(std::remove_if(records.begin(), records.end(), [&](const Json &old) { return stableIdentity(old) == identity; }), records.end()); records.push_back(record); std::sort(records.begin(), records.end(), [](Json a, Json b) { a.object.erase("protectedToken"); b.object.erase("protectedToken"); return release_contract::canonical(a) < release_contract::canonical(b); }); } #ifdef SAFETYNET_NATIVE_SELF_TEST static bool selectionSelfTest() { auto current = parse("{\"deviceId\":\"synthetic\",\"origin\":\"https://" "management.example\",\"policyDigest\":\"" + std::string(64, 'b') + "\",\"policyKeySha256\":\"" + std::string(64, 'a') + "\",\"policyVersion\":8,\"sid\":\"S-1-5-21-1-2-3-1001\"}"); auto old = current; old.object.at("policyVersion").integer = 7; old.object.at("policyDigest").string = std::string(64, 'c'); auto oldest = old; oldest.object.at("policyVersion").integer = 6; std::vector records{oldest, old}; if (selectRecord(current, records) != &records[1]) return false; auto other = old; other.object.at("origin").string = "https://other.example"; records.push_back(other); replaceRecord(records, current); if (records.size() != 2 || !selectRecord(current, records)) return false; auto fails = [&](std::vector rows) { try { selectRecord(current, rows); return false; } catch (...) { return true; } }; auto conflicting = current; conflicting.object.at("policyDigest").string = std::string(64, 'd'); auto future = current; future.object.at("policyVersion").integer = 9; if (!fails({old, old}) || !fails({conflicting}) || !fails({future})) return false; auto wrongKey = old; wrongKey.object.at("policyKeySha256").string = std::string(64, 'e'); return !selectRecord(current, {other, wrongKey}); } #endif static std::string load(HKEY key) { DWORD type = 0, size = 0; if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type, nullptr, &size) || type != REG_BINARY || !size || size > STORE_LIMIT) throw 1; std::string value(size, '\0'); if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type, reinterpret_cast(value.data()), &size) || type != REG_BINARY || size != value.size()) throw 1; validateStore(value); return value; } static std::vector entropy(const Json &metadata) { const auto value = std::string("SafetyNet management credential v1\0", sizeof("SafetyNet management credential v1\0") - 1) + release_contract::canonical(metadata); auto digest = release_contract::unhex(release_contract::digest(value)); return digest; } static std::string protect(const std::string &token, const Json &metadata) { if (!tokenValid(token)) throw 1; auto bindingBytes = entropy(metadata); Wipe plain; plain.bytes.assign(token.begin(), token.end()); DATA_BLOB input{static_cast(plain.bytes.size()), plain.bytes.data()}, additional{static_cast(bindingBytes.size()), bindingBytes.data()}, output{}; if (!CryptProtectData( &input, L"SafetyNet credential", &additional, nullptr, nullptr, CRYPTPROTECT_LOCAL_MACHINE | CRYPTPROTECT_UI_FORBIDDEN, &output)) throw 1; std::vector cipher(output.pbData, output.pbData + output.cbData); LocalFree(output.pbData); return release_contract::base64url(cipher); } static std::string unprotect(const std::string &cipher, const Json &metadata) { auto encrypted = release_contract::decode(cipher), bindingBytes = entropy(metadata); DATA_BLOB input{static_cast(encrypted.size()), encrypted.data()}, additional{static_cast(bindingBytes.size()), bindingBytes.data()}, output{}; if (!CryptUnprotectData(&input, nullptr, &additional, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)) throw 1; std::string token(reinterpret_cast(output.pbData), output.cbData); SecureZeroMemory(output.pbData, output.cbData); LocalFree(output.pbData); if (!tokenValid(token)) { SecureZeroMemory(token.data(), token.size()); throw 1; } return token; } } // namespace credential_contract