blob: ab1a0c148331279b1fd6b9a26d586c028779d009 [file] [log] [blame]
/*
* Copyright (C) 2013 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 static com.android.SdkConstants.CONSTRUCTOR_NAME;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.ClassScanner;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
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.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
/** Checks for pseudo random number generator initialization issues */
public class SecureRandomGeneratorDetector extends Detector implements ClassScanner {
private static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom";
@SuppressWarnings("SpellCheckingInspection")
private static final String BLOG_URL =
"https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html";
/** Whether the random number generator is initialized correctly */
public static final Issue ISSUE =
Issue.create(
"TrulyRandom",
"Weak RNG",
"Key generation, signing, encryption, and random number generation may not "
+ "receive cryptographically strong values due to improper initialization of "
+ "the underlying PRNG on Android 4.3 and below.\n"
+ "\n"
+ "If your application relies on cryptographically secure random number generation "
+ "you should apply the workaround described in "
+ BLOG_URL
+ " .\n"
+ "\n"
+ "This lint rule is mostly informational; it does not accurately detect whether "
+ "cryptographically secure RNG is required, or whether the workaround has already "
+ "been applied. After reading the blog entry and updating your code if necessary, "
+ "you can disable this lint issue.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
SecureRandomGeneratorDetector.class, Scope.CLASS_FILE_SCOPE))
.addMoreInfo(BLOG_URL);
private static final String WRAP = "wrap";
private static final String UNWRAP = "unwrap";
private static final String INIT = "init";
private static final String INIT_SIGN = "initSign";
private static final String GET_INSTANCE = "getInstance";
private static final String FOR_NAME = "forName";
private static final String JAVA_LANG_CLASS = "java/lang/Class";
private static final String JAVAX_CRYPTO_KEY_GENERATOR = "javax/crypto/KeyGenerator";
private static final String JAVAX_CRYPTO_KEY_AGREEMENT = "javax/crypto/KeyAgreement";
private static final String JAVA_SECURITY_KEY_PAIR_GENERATOR = "java/security/KeyPairGenerator";
private static final String JAVAX_CRYPTO_SIGNATURE = "javax/crypto/Signature";
private static final String JAVAX_CRYPTO_CIPHER = "javax/crypto/Cipher";
private static final String JAVAX_NET_SSL_SSLENGINE = "javax/net/ssl/SSLEngine";
/** Constructs a new {@link SecureRandomGeneratorDetector} */
public SecureRandomGeneratorDetector() {}
// ---- Implements ClassScanner ----
@Nullable
@Override
public List<String> getApplicableCallOwners() {
return Arrays.asList(
JAVAX_CRYPTO_KEY_GENERATOR,
JAVA_SECURITY_KEY_PAIR_GENERATOR,
JAVAX_CRYPTO_KEY_AGREEMENT,
OWNER_SECURE_RANDOM,
JAVAX_NET_SSL_SSLENGINE,
JAVAX_CRYPTO_SIGNATURE,
JAVAX_CRYPTO_CIPHER);
}
@Nullable
@Override
public List<String> getApplicableCallNames() {
return Collections.singletonList(FOR_NAME);
}
/** Location of first call to key generator (etc), if any */
private Location mLocation;
/**
* Whether the issue should be ignored (because we have a workaround, or because we're only
* targeting correct implementations, etc
*/
private boolean mIgnore;
@Override
public void checkCall(
@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode call) {
if (mIgnore) {
return;
}
String owner = call.owner;
String name = call.name;
// Look for the workaround code: if we see a Class.forName on the harmony NativeCrypto,
// we'll consider that a sign.
if (name.equals(FOR_NAME)) {
if (call.getOpcode() != Opcodes.INVOKESTATIC || !owner.equals(JAVA_LANG_CLASS)) {
return;
}
AbstractInsnNode prev = Lint.getPrevInstruction(call);
if (prev instanceof LdcInsnNode) {
Object cst = ((LdcInsnNode) prev).cst;
//noinspection SpellCheckingInspection
if (cst instanceof String
&& "org.apache.harmony.xnet.provider.jsse.NativeCrypto".equals(cst)) {
mIgnore = true;
}
}
return;
}
// Look for calls that probably require a properly initialized random number generator.
assert owner.equals(JAVAX_CRYPTO_KEY_GENERATOR)
|| owner.equals(JAVA_SECURITY_KEY_PAIR_GENERATOR)
|| owner.equals(JAVAX_CRYPTO_KEY_AGREEMENT)
|| owner.equals(OWNER_SECURE_RANDOM)
|| owner.equals(JAVAX_CRYPTO_CIPHER)
|| owner.equals(JAVAX_CRYPTO_SIGNATURE)
|| owner.equals(JAVAX_NET_SSL_SSLENGINE)
: owner;
boolean warn = false;
if (owner.equals(JAVAX_CRYPTO_SIGNATURE)) {
warn = name.equals(INIT_SIGN);
} else if (owner.equals(JAVAX_CRYPTO_CIPHER)) {
if (name.equals(INIT)) {
int arity = getDescArity(call.desc);
AbstractInsnNode node = call;
for (int i = 0; i < arity; i++) {
node = Lint.getPrevInstruction(node);
if (node == null) {
break;
}
}
if (node != null) {
int opcode = node.getOpcode();
if (opcode == Opcodes.ICONST_3
|| // Cipher.WRAP_MODE
opcode == Opcodes.ICONST_1) { // Cipher.ENCRYPT_MODE
warn = true;
}
}
}
} else if (name.equals(GET_INSTANCE)
|| name.equals(CONSTRUCTOR_NAME)
|| name.equals(WRAP)
|| name.equals(UNWRAP)) { // For SSLEngine
warn = true;
}
if (warn) {
if (mLocation != null) {
return;
}
if (context.getMainProject().getMinSdk() > 18) {
// Fix no longer needed
mIgnore = true;
return;
}
if (context.getDriver().isSuppressed(ISSUE, classNode, method, call)) {
mIgnore = true;
} else {
mLocation = context.getLocation(call);
}
}
}
@VisibleForTesting
static int getDescArity(String desc) {
int arity = 0;
// For example, (ILjava/security/Key;)V ⇒ 2
for (int i = 1, max = desc.length(); i < max; i++) {
char c = desc.charAt(i);
if (c == ')') {
break;
} else if (c == 'L') {
arity++;
i = desc.indexOf(';', i);
assert i != -1 : desc;
} else {
arity++;
}
}
return arity;
}
@Override
public void afterCheckRootProject(@NonNull Context context) {
if (mLocation != null && !mIgnore) {
String message =
"Potentially insecure random numbers on Android 4.3 and older. "
+ "Read "
+ BLOG_URL
+ " for more info.";
context.report(ISSUE, mLocation, message);
}
}
}