package duo.labs.webauthn.models; import android.util.Base64; import android.util.Pair; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import duo.labs.webauthn.util.Base64ByteArrayAdapter; import rocks.xmpp.precis.PrecisProfile; import rocks.xmpp.precis.PrecisProfiles; public class AuthenticatorMakeCredentialOptions { @SerializedName("clientDataHash") public byte[] clientDataHash; @SerializedName("rp") public RpEntity rpEntity; @SerializedName("user") public UserEntity userEntity; @SerializedName("requireResidentKey") public boolean requireResidentKey; @SerializedName("requireUserPresence") public boolean requireUserPresence; @SerializedName("requireUserVerification") public boolean requireUserVerification; @SerializedName("credTypesAndPubKeyAlgs") public List<Pair<String, Long>> credTypesAndPubKeyAlgs; @SerializedName("excludeCredentials") public List<PublicKeyCredentialDescriptor> excludeCredentialDescriptorList; // TODO: possibly support extensions in the future // @SerializedName("authenticatorExtensions") public byte[] extensions; public boolean areWellFormed() { PrecisProfile profile = PrecisProfiles.USERNAME_CASE_PRESERVED; if (clientDataHash.length != 32) { return false; } if (rpEntity.id.isEmpty()) { return false; } try { profile.enforce(rpEntity.name); profile.enforce(userEntity.name); } catch (Exception e) { return false; } if (userEntity.id.length <= 0 || userEntity.id.length > 64) { return false; } if (!(requireUserPresence ^ requireUserVerification)) { // only one may be set return false; } if (credTypesAndPubKeyAlgs.isEmpty()) { return false; } return true; } public static AuthenticatorMakeCredentialOptions fromJSON(String json) { TypeToken<List<Pair<String, Long>>> credTypesType = new TypeToken<List<Pair<String, Long>>>() { }; TypeToken<List<PublicKeyCredentialDescriptor>> excludeListType = new TypeToken<List<PublicKeyCredentialDescriptor>>() { }; Gson gson = new GsonBuilder() .registerTypeAdapter(byte[].class, new Base64ByteArrayAdapter()) .registerTypeAdapter(credTypesType.getType(), new CredTypesDeserializer()) .registerTypeAdapter(excludeListType.getType(), new ExcludeCredentialListDeserializer()) .create(); return gson.fromJson(json, AuthenticatorMakeCredentialOptions.class); } private static class CredTypesDeserializer implements JsonDeserializer<List<Pair<String, Long>>> { @Override public List<Pair<String, Long>> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { List<Pair<String, Long>> credTypes = new ArrayList<>(); for (JsonElement element : json.getAsJsonArray()) { // all elements are arrays like ["public-key", "-7"] JsonArray pair = element.getAsJsonArray(); String type = pair.get(0).getAsString(); try { long alg = Long.parseLong(pair.get(1).getAsString()); credTypes.add(new Pair<>(type, alg)); } catch (NumberFormatException e) { continue; } } return credTypes; } } private static class ExcludeCredentialListDeserializer implements JsonDeserializer<List<PublicKeyCredentialDescriptor>> { @Override public List<PublicKeyCredentialDescriptor> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { List<PublicKeyCredentialDescriptor> excludeList = new ArrayList<>(); if (json.isJsonArray()) { for (JsonElement element : json.getAsJsonArray()) { // elements are JSON objects that take the form: // {"type": "public-key", "id": "<base64-bytes>", "transports": ["usb", "nfc", "ble", "internal"] } if (element.isJsonObject()) { JsonObject entryObject = element.getAsJsonObject(); String type = entryObject.get("type").getAsString(); String idString = entryObject.get("id").getAsString(); byte[] id = Base64.decode(idString, Base64.NO_WRAP); List<String> transports = new ArrayList<>(); // "transports" is an optional member if (entryObject.has("transports")) { for (JsonElement transport : entryObject.getAsJsonArray("transports")) { transports.add(transport.getAsString()); } } excludeList.add(new PublicKeyCredentialDescriptor(type, id, transports)); } } } return excludeList; } } }