blob: 85ac1976cf43fbed337b567dfa574104c27e117a [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.resources.ResourceRepository;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceUrl;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Lint;
import com.android.tools.lint.detector.api.LintFix;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.XmlUtils;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/** Check which makes sure that a network-security-config descriptor file is valid and logical. */
public class NetworkSecurityConfigDetector extends ResourceXmlDetector {
public static final Implementation IMPLEMENTATION =
new Implementation(NetworkSecurityConfigDetector.class, Scope.RESOURCE_FILE_SCOPE);
/** Validate the entire network-security-config descriptor. */
public static final Issue ISSUE =
Issue.create(
"NetworkSecurityConfig",
"Valid Network Security Config File",
"Ensures that a `<network-security-config>` file, which is pointed to by an "
+ "`android:networkSecurityConfig` attribute in the manifest file, is valid",
Category.CORRECTNESS,
5,
Severity.FATAL,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/preview/features/security-config.html");
/** Validate the pin-set expiration attribute and warn if the expiry is in the near future. */
public static final Issue PIN_SET_EXPIRY =
Issue.create(
"PinSetExpiry",
"Validate `<pin-set>` expiration attribute",
"Ensures that the `expiration` attribute of the `<pin-set>` element is valid and has "
+ "not already expired or is expiring soon",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/preview/features/security-config.html");
/** No backup pin specified. */
public static final Issue MISSING_BACKUP_PIN =
Issue.create(
"MissingBackupPin",
"Missing Backup Pin",
"It is highly recommended to declare a backup `<pin>` element. "
+ "Not having a second pin defined can cause connection failures when the "
+ "particular site certificate is rotated and the app has not yet been updated.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/preview/features/security-config.html");
/** Base configuration allows cleartext by default. */
public static final Issue INSECURE_CONFIGURATION =
Issue.create(
"InsecureBaseConfiguration",
"Insecure Base Configuration",
"Permitting cleartext traffic could allow eavesdroppers to intercept data sent "
+ "by your app, which impacts the privacy of your users. Consider only allowing "
+ "encrypted traffic by setting the `cleartextTrafficPermitted` tag to `\"false\"`.",
Category.SECURITY,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/preview/features/security-config.html");
/** Allows user-provided certificates to validate of secure connections. */
public static final Issue ACCEPTS_USER_CERTIFICATES =
Issue.create(
"AcceptsUserCertificates",
"Allowing User Certificates",
"Allowing user certificates could allow eavesdroppers to intercept data sent by your app, '"
+ "which could impact the privacy of your users. Consider nesting your app's "
+ "`trust-anchors` inside a `<debug-overrides>` element to make sure they are only "
+ "available when `android:debuggable` is set to `\"true\"`.",
Category.SECURITY,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/training/articles/security-config#TrustingDebugCa");
public static final String ATTR_DIGEST = "digest";
private static final String TAG_NETWORK_SECURITY_CONFIG = "network-security-config";
private static final String TAG_BASE_CONFIG = "base-config";
private static final String TAG_DOMAIN_CONFIG = "domain-config";
private static final String TAG_DEBUG_OVERRIDES = "debug-overrides";
private static final String TAG_DOMAIN = "domain";
private static final String TAG_PIN_SET = "pin-set";
private static final String TAG_TRUST_ANCHORS = "trust-anchors";
private static final String TAG_CERTIFICATES = "certificates";
private static final String TAG_PIN = "pin";
private static final String ATTR_SRC = "src";
private static final String ATTR_INCLUDE_SUBDOMAINS = "includeSubdomains";
private static final String ATTR_EXPIRATION = "expiration";
private static final String ATTR_CLEARTEXT_TRAFFIC_PERMITTED = "cleartextTrafficPermitted";
private static final String PIN_DIGEST_ALGORITHM = "SHA-256";
// SHA 256 bit = 32 bytes
private static final int PIN_DECODED_DIGEST_LEN_SHA_256 = 32;
private static final Set<String> VALID_CONFIG_TAGS =
ImmutableSet.of(TAG_DOMAIN, TAG_TRUST_ANCHORS, TAG_PIN_SET, TAG_DOMAIN_CONFIG);
public static final Set<String> VALID_BASE_TAGS =
ImmutableSet.of(TAG_DOMAIN_CONFIG, TAG_BASE_CONFIG, TAG_DEBUG_OVERRIDES);
private static final String UNEXPECTED_ELEMENT_MESSAGE = "Unexpected element `<%1$s>`";
private static final String ALREADY_DECLARED_MESSAGE = "Already declared here";
/** Constructs a new {@link NetworkSecurityConfigDetector} */
public NetworkSecurityConfigDetector() {}
/**
* Keep track of whether the debug-overrides element was seen in one of the
* network-security-config files.
*
* <p>Context: When an app is debuggable, a file named $config_resource$_debug.xml is also
* looked up by framework to check for debug overrides.
*/
private Location.Handle mDebugOverridesHandle;
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.XML;
}
@Override
public void beforeCheckRootProject(@NonNull Context context) {
mDebugOverridesHandle = null;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root == null) {
return;
}
if (!TAG_NETWORK_SECURITY_CONFIG.equals(root.getTagName())) {
return;
}
Location.Handle baseConfigHandle = null;
Map<String, Node> seenDomains2Nodes = Maps.newHashMap();
// 0 or 1 of <base-config>
// Any number of <domain-config>
// 0 or 1 of <debug-overrides>
for (Element child : XmlUtils.getSubTags(root)) {
String tagName = child.getTagName();
if (TAG_BASE_CONFIG.equals(tagName)) {
if (baseConfigHandle != null) {
reportExceeded(context, TAG_BASE_CONFIG, child, baseConfigHandle);
} else {
baseConfigHandle = context.createLocationHandle(child);
handleConfigElement(context, child, seenDomains2Nodes);
handleBaseConfigElement(context, child);
}
} else if (TAG_DEBUG_OVERRIDES.equals(tagName)) {
if (mDebugOverridesHandle != null) {
reportExceeded(context, TAG_DEBUG_OVERRIDES, child, mDebugOverridesHandle);
} else {
mDebugOverridesHandle = context.createLocationHandle(child);
handleConfigElement(context, child, seenDomains2Nodes);
}
} else if (TAG_DOMAIN_CONFIG.equals(tagName)) {
handleConfigElement(context, child, seenDomains2Nodes);
} else {
// It's possible to check for only the tags that can appear
// by looking at `seenBaseConfig` and `seenDebugOverrides` but that may
// be unnecessary. We can let the developer first fix the spelling
// and then revalidate the values to check for duplicates (according to rules).
if (!checkForTyposInTags(context, child, VALID_BASE_TAGS)) {
context.report(
ISSUE,
child,
context.getNameLocation(child),
String.format(UNEXPECTED_ELEMENT_MESSAGE, tagName));
}
}
}
}
private void handleConfigElement(
XmlContext context, Element config, @NonNull Map<String, Node> seenDomainsToLocations) {
String configName = config.getTagName();
boolean isDomainConfig = TAG_DOMAIN_CONFIG.equals(configName);
String message = "`%1$s` element not allowed in `%2$s`";
// Assumption: Multiple trust-anchors and pinSetNode elements are not allowed within
// a single domain-config. Nested domain-config elements can still have them.
Node trustAnchorsNode = null;
Node pinSetNode = null;
checkForTyposInAttributes(context, config, ATTR_CLEARTEXT_TRAFFIC_PERMITTED, false);
for (Element node : XmlUtils.getSubTags(config)) {
String tagName = node.getTagName();
if (TAG_DOMAIN.equals(tagName)) {
if (!isDomainConfig) {
context.report(
ISSUE,
node,
context.getNameLocation(node),
String.format(message, TAG_DOMAIN, configName));
} else {
checkForTyposInAttributes(context, node, ATTR_INCLUDE_SUBDOMAINS, true);
String domainName = node.getTextContent().trim().toLowerCase(Locale.US);
if (seenDomainsToLocations.containsKey(domainName)) {
String duplicateMessage = "Duplicate domain names are not allowed";
Node previousNode = seenDomainsToLocations.get(domainName);
context.report(
ISSUE,
node.getFirstChild(),
context.getLocation(node.getFirstChild())
.withSecondary(
context.getLocation(previousNode),
ALREADY_DECLARED_MESSAGE),
duplicateMessage);
} else {
seenDomainsToLocations.put(domainName, node.getFirstChild());
}
}
} else if (TAG_TRUST_ANCHORS.equals(tagName)) {
if (trustAnchorsNode != null) {
String anchorMessage = "Multiple `<trust-anchors>` elements are not allowed";
context.report(
ISSUE,
node,
context.getNameLocation(node)
.withSecondary(
context.getNameLocation(trustAnchorsNode),
ALREADY_DECLARED_MESSAGE),
anchorMessage);
} else {
trustAnchorsNode = node;
handleTrustAnchors(context, node);
}
} else if (TAG_DOMAIN_CONFIG.equals(tagName)) {
if (!isDomainConfig) {
// If the parent is any config other than a domain-config report an error
context.report(
ISSUE,
node,
context.getNameLocation(node),
String.format(
"Nested `<domain-config>` elements are not allowed in `%1$s`",
configName));
} else {
handleConfigElement(context, node, seenDomainsToLocations);
}
} else if (TAG_PIN_SET.equals(tagName)) {
if (!isDomainConfig) {
context.report(
ISSUE,
node,
context.getNameLocation(node),
String.format(message, TAG_PIN_SET, configName));
}
if (pinSetNode != null) {
String pinSetMessage = "Multiple `<pin-set>` elements are not allowed";
context.report(
ISSUE,
node,
context.getNameLocation(node)
.withSecondary(
context.getNameLocation(pinSetNode),
ALREADY_DECLARED_MESSAGE),
pinSetMessage);
} else {
pinSetNode = node;
handlePinSet(context, node);
}
} else {
// Note: Only typos are marked as errors here to be forward compatible
// where new elements are added here.
checkForTyposInTags(context, node, VALID_CONFIG_TAGS);
}
}
if (isDomainConfig && seenDomainsToLocations.isEmpty()) {
context.report(
ISSUE,
config,
context.getNameLocation(config),
"No `<domain>` elements in `<domain-config>`");
}
}
/**
* The following checks happen in addition to the {@link #handleConfigElement(XmlContext,
* Element, Map)} checks, but are specific to the {@code <base-config>} element.
*/
private void handleBaseConfigElement(XmlContext context, Element node) {
if (node.hasAttribute(ATTR_CLEARTEXT_TRAFFIC_PERMITTED)) {
Attr cleartextTrafficAttribute =
node.getAttributeNode(ATTR_CLEARTEXT_TRAFFIC_PERMITTED);
if (cleartextTrafficAttribute.getValue().equals("true")) {
Location attributeLocation = context.getValueLocation(cleartextTrafficAttribute);
LintFix fix = fix().replace().range(attributeLocation).with("false").build();
context.report(
INSECURE_CONFIGURATION,
node,
attributeLocation,
"Insecure Base Configuration",
fix);
}
}
for (Element child : XmlUtils.getSubTags(node)) {
if (TAG_TRUST_ANCHORS.equals(child.getTagName())) {
for (Element grandchild : XmlUtils.getSubTags(child)) {
if (TAG_CERTIFICATES.equals(grandchild.getTagName())) {
Attr sourceIdAttr = grandchild.getAttributeNode(ATTR_SRC);
if (sourceIdAttr != null && "user".equals(sourceIdAttr.getValue())) {
context.report(
ACCEPTS_USER_CERTIFICATES,
grandchild,
context.getLocation(grandchild),
"The Network Security Configuration allows the use of user certificates in the release version of your app");
}
}
}
}
}
}
private static void handlePinSet(XmlContext context, Element node) {
if (node.hasAttribute(ATTR_EXPIRATION)) {
Attr expirationAttr = node.getAttributeNode(ATTR_EXPIRATION);
String message = null;
try {
LocalDate date =
LocalDate.parse(
expirationAttr.getValue(), DateTimeFormatter.ISO_LOCAL_DATE);
// If the pin-set has already expired report a warning.
LocalDate now = LocalDate.now();
if (date.isBefore(now)) {
message = "`pin-set` has already expired";
} else if (date.isBefore(now.plusDays(10))) {
// OR if the pin-set will expire within 10 days from now
message = "`pin-set` is expiring soon";
}
} catch (DateTimeParseException e) {
context.report(
ISSUE,
expirationAttr,
context.getValueLocation(expirationAttr),
"Invalid expiration in `pin-set`");
}
if (message != null) {
context.report(
PIN_SET_EXPIRY,
expirationAttr,
context.getValueLocation(expirationAttr),
message);
}
} else {
checkForTyposInAttributes(context, node, ATTR_EXPIRATION, false);
}
int pinElementCount = 0;
boolean foundTyposInPin = false;
for (Element child : XmlUtils.getSubTags(node)) {
String tagName = child.getTagName();
if (TAG_PIN.equals(tagName)) {
pinElementCount += 1;
if (child.hasAttribute(ATTR_DIGEST)) {
Attr digestAttr = child.getAttributeNode(ATTR_DIGEST);
if (!PIN_DIGEST_ALGORITHM.equalsIgnoreCase(digestAttr.getValue())) {
String values = Lint.formatList(getSupportedPinDigestAlgorithms(), 2);
LintFix.GroupBuilder fixBuilder = LintFix.create().group();
for (String algorithm : getSupportedPinDigestAlgorithms()) {
fixBuilder.add(
LintFix.create()
.name(
String.format(
"Set digest to \"%1$s\"", algorithm))
.replace()
.all()
.with(algorithm)
.build());
}
LintFix fix = fixBuilder.build();
context.report(
ISSUE,
digestAttr,
context.getValueLocation(digestAttr),
String.format(
"Invalid digest algorithm. Supported digests: `%1$s`",
values),
fix);
}
} else {
checkForTyposInAttributes(context, child, ATTR_DIGEST, true);
}
Node digestNode;
if (!child.hasChildNodes()
|| (digestNode = child.getFirstChild()) == null
|| digestNode.getNodeType() != Node.TEXT_NODE) {
// missing text node
context.report(ISSUE, child, context.getLocation(child), "Missing pin digest");
} else {
try {
// Validate the actual data
byte[] decodedDigest =
Base64.getDecoder().decode(digestNode.getNodeValue());
if (decodedDigest.length != PIN_DECODED_DIGEST_LEN_SHA_256) {
// incorrect digest length
String message =
String.format(
"Decoded digest length `%1$d` does not match expected "
+ "length for `%2$s` of `%3$d`",
decodedDigest.length,
PIN_DIGEST_ALGORITHM,
PIN_DECODED_DIGEST_LEN_SHA_256);
context.report(
ISSUE, digestNode, context.getLocation(digestNode), message);
}
} catch (Exception ex) {
context.report(
ISSUE,
digestNode,
context.getLocation(digestNode),
"Invalid pin digest");
}
}
} else {
foundTyposInPin |=
checkForTyposInTags(context, child, Collections.singleton(TAG_PIN));
}
}
// Let the developer fix the typos before we can ascertain that the pin is missing
if (!foundTyposInPin) {
if (pinElementCount == 0) {
context.report(
ISSUE, node, context.getNameLocation(node), "Missing `<pin>` element(s)");
} else if (pinElementCount == 1) {
// We should probably check to see if both hashes are the same here.
context.report(
MISSING_BACKUP_PIN,
node,
context.getNameLocation(node),
"A backup `<pin>` declaration is highly recommended");
}
}
}
private static void handleTrustAnchors(XmlContext context, Element node) {
for (Element child : XmlUtils.getSubTags(node)) {
if (TAG_CERTIFICATES.equals(child.getTagName())) {
if (!child.hasAttribute(ATTR_SRC)) {
checkForTyposInAttributes(context, child, ATTR_SRC, true);
} else {
Attr sourceIdAttr = child.getAttributeNode(ATTR_SRC);
String sourceId = sourceIdAttr.getValue();
ResourceUrl resourceUrl = ResourceUrl.parse(sourceId);
if (context.getClient().supportsProjectResources()
&& resourceUrl != null
&& !resourceUrl.isFramework()) {
// ensure that this is a valid resource
ResourceRepository resources =
context.getClient()
.getResourceRepository(context.getProject(), true, false);
if (resources != null
&& !resources.hasResources(
ResourceNamespace.TODO(),
resourceUrl.type,
resourceUrl.name)) {
context.report(
ISSUE,
sourceIdAttr,
context.getValueLocation(sourceIdAttr),
"Missing `src` resource");
}
}
// The value should be either "system", "user" or a resource Id
if (resourceUrl == null
&& !"user".equals(sourceId)
&& !"system".equals(sourceId)) {
context.report(
ISSUE,
sourceIdAttr,
context.getValueLocation(sourceIdAttr),
"Unknown certificates `src` attribute. "
+ "Expecting `system`, `user` or an @resource value");
}
}
} else {
checkForTyposInTags(context, child, Collections.singleton(TAG_CERTIFICATES));
}
}
}
private static boolean checkForTyposInTags(
XmlContext context, Element node, Collection<String> validPossibleTags) {
String tagName = node.getTagName();
List<String> suggestions = generateTypoSuggestions(tagName, validPossibleTags);
if (suggestions != null) {
assert !suggestions.isEmpty();
String suggestionString;
if (suggestions.size() == 1) {
suggestionString = suggestions.get(0);
} else if (suggestions.size() == 2) {
suggestionString =
String.format("%1$s or %2$s", suggestions.get(0), suggestions.get(1));
} else {
suggestionString = Lint.formatList(suggestions, -1);
}
String message =
String.format(
"Misspelled tag `<%1$s>`: Did you mean `%2$s` ?",
tagName, suggestionString);
context.report(ISSUE, node, context.getNameLocation(node), message);
return true;
}
return false;
}
private static void checkForTyposInAttributes(
XmlContext context, Element node, String attrName, boolean requiredAttribute) {
if (node.hasAttribute(attrName)) {
return;
}
List<String> suggestions = null;
NamedNodeMap attributes = node.getAttributes();
boolean foundSpellingError = false;
Set<String> validAttributeNames = Collections.singleton(attrName);
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
String nodeName = attr.getNodeName();
if (nodeName != null) {
suggestions = generateTypoSuggestions(nodeName, validAttributeNames);
}
if (suggestions != null && suggestions.size() == 1) {
context.report(
ISSUE,
attr,
context.getNameLocation(attr),
String.format(
"Misspelled attribute `%1$s`: Did you mean `%2$s` ?",
nodeName, attrName));
foundSpellingError |= true;
}
}
if (!foundSpellingError && requiredAttribute) {
context.report(
ISSUE,
node,
context.getNameLocation(node),
String.format("Missing `%1$s` attribute", attrName));
}
}
private static List<String> generateTypoSuggestions(
@NonNull String name, @NonNull Collection<String> validAttributeNames) {
List<String> suggestions = null;
for (String suggestion : validAttributeNames) {
if (Lint.isEditableTo(suggestion, name, 3)) {
if (suggestions == null) {
suggestions = new ArrayList<>(validAttributeNames.size());
}
suggestions.add(suggestion);
}
}
return suggestions;
}
private static void reportExceeded(
XmlContext context,
String elementName,
Element element,
@NonNull Location.Handle handle) {
context.report(
ISSUE,
element,
context.getNameLocation(element)
.withSecondary(handle.resolve(), ALREADY_DECLARED_MESSAGE),
String.format("Expecting at most 1 `<%1$s>`", elementName));
}
/**
* For a given error message created by this lint detector, returns whether the error was due to
* a typo in an attribute name. This is primarily for use by IDE quick fixes.
*
* @param errorMessage The error message associated with this detector.
* @return true if this is a spelling error in an attribute.
*/
@SuppressWarnings("unused")
public static boolean isAttributeSpellingError(@NonNull String errorMessage) {
return errorMessage.startsWith("Misspelled attribute");
}
/**
* For a given misspelled attribute, return the allowed suggestions/corrections.
*
* @param errorAttribute the misspelled attribute
* @param parentTag the parent tag used for determining the allowed attributes
* @return list of strings containing the suggestions or null if no suggestions
*/
@SuppressWarnings("unused")
@NonNull
public static List<String> getAttributeSpellingSuggestions(
@NonNull String errorAttribute, @NonNull String parentTag) {
Collection<String> validAttributes;
switch (parentTag) {
case TAG_BASE_CONFIG: // fallthrough
case TAG_DOMAIN_CONFIG: // fallthrough
case TAG_DEBUG_OVERRIDES:
validAttributes = Collections.singleton(ATTR_CLEARTEXT_TRAFFIC_PERMITTED);
break;
case TAG_CERTIFICATES:
validAttributes = Collections.singleton(ATTR_SRC);
break;
case TAG_DOMAIN:
validAttributes = Collections.singleton(ATTR_INCLUDE_SUBDOMAINS);
break;
case TAG_PIN_SET:
validAttributes = Collections.singleton(ATTR_EXPIRATION);
break;
case TAG_PIN:
validAttributes = Collections.singleton(ATTR_DIGEST);
break;
default:
return Collections.emptyList();
}
List<String> result = generateTypoSuggestions(errorAttribute, validAttributes);
return result == null ? Collections.emptyList() : result;
}
/**
* @param errorMessage The error message associated with this detector.
* @return true if this is a spelling error in the element name.
*/
@SuppressWarnings("unused")
public static boolean isTagSpellingError(@NonNull String errorMessage) {
return errorMessage.startsWith("Misspelled tag");
}
/**
* For a given misspelled attribute, return the allowed suggestions/corrections.
*
* @param errorTag the misspelled attribute
* @param parentTag the parent tag used for determining the allowed attributes
* @return list of strings containing the suggestions or null if no suggestions
*/
@SuppressWarnings("unused")
@NonNull
public static List<String> getTagSpellingSuggestions(
@NonNull String errorTag, @NonNull String parentTag) {
Collection<String> validTags;
switch (parentTag) {
case TAG_NETWORK_SECURITY_CONFIG:
validTags = VALID_BASE_TAGS;
break;
case TAG_BASE_CONFIG: // fallthrough
case TAG_DOMAIN_CONFIG: // fallthrough
case TAG_DEBUG_OVERRIDES:
validTags = VALID_CONFIG_TAGS;
break;
case TAG_TRUST_ANCHORS:
validTags = Collections.singleton(TAG_CERTIFICATES);
break;
case TAG_PIN_SET:
validTags = Collections.singleton(TAG_PIN);
break;
default:
return Collections.emptyList();
}
List<String> result = generateTypoSuggestions(errorTag, validTags);
return result == null ? Collections.emptyList() : result;
}
/**
* Used by the IDE for quick fixes.
*
* @return supported pin digest algorithms
*/
public static List<String> getSupportedPinDigestAlgorithms() {
return Collections.singletonList(PIN_DIGEST_ALGORITHM);
}
}