diff options
author | Tor Norbye <tnorbye@google.com> | 2012-07-19 17:57:30 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-07-23 15:06:58 -0700 |
commit | 2c611aed27d75bd816b7887b64f98ff1ed87b097 (patch) | |
tree | f73f0d4c49d4eda4c48b8266bf0bffab2a81b06a | |
parent | 4ca1c4d2c780b345854b9824963cd6cf930a7276 (diff) | |
download | sdk-2c611aed27d75bd816b7887b64f98ff1ed87b097.zip sdk-2c611aed27d75bd816b7887b64f98ff1ed87b097.tar.gz sdk-2c611aed27d75bd816b7887b64f98ff1ed87b097.tar.bz2 |
Add lint checks for wakelock and secure random problems
This changeset adds new two lint bytecode-based detectors.
The WakelockDetector looks for problems with wakelocks:
- Calling release() in onDestroy() rather than in onPause()
- Calling acquire() but never calling release() anywhere in the app.
The SecureRandomDetector looks for problems with the SecureRandom
random number generator:
- Calling setSeed() with a fixed number, such as a string literal,
or something equivalent (such as a static field reference of
this or some other class)
- Calling setSeed() and passing in System.nanoTime or
currentTimeMillis since these are considered predictable seeds.
These are both using the new fast-dispatch mechanism for bytecode
detectors in lint. In both cases, there are more complex control flow
we should be checking using flow analysis; that's for an upcoming CL
where we add ASM's analysis library.
Change-Id: Iec2a95b042b8a3e4e976923cc62e9dccf2dfeca5
12 files changed, 445 insertions, 1 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index 1985473..1cc5443 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -331,6 +331,8 @@ varargs verbosity viewport vs +wakelock +wakelocks wallpaper webtools whilst diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java index 2ad7f12..1dec5f6 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -54,7 +54,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { private static final List<Issue> sIssues; static { - final int initialCapacity = 98; + final int initialCapacity = 100; List<Issue> issues = new ArrayList<Issue>(initialCapacity); issues.add(AccessibilityDetector.ISSUE); @@ -116,6 +116,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(SecurityDetector.OPEN_PROVIDER); issues.add(SecurityDetector.WORLD_READABLE); issues.add(SecurityDetector.WORLD_WRITEABLE); + issues.add(SecureRandomDetector.ISSUE); issues.add(IconDetector.GIF_USAGE); issues.add(IconDetector.ICON_DENSITIES); issues.add(IconDetector.ICON_MISSING_FOLDER); @@ -150,6 +151,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(JavaPerformanceDetector.PAINT_ALLOC); issues.add(JavaPerformanceDetector.USE_VALUEOF); issues.add(JavaPerformanceDetector.USE_SPARSEARRAY); + issues.add(WakelockDetector.ISSUE); issues.add(SetJavaScriptEnabledDetector.ISSUE); issues.add(ToastDetector.ISSUE); issues.add(SharedPrefsDetector.ISSUE); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/SecureRandomDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/SecureRandomDetector.java new file mode 100644 index 0000000..79f5d47 --- /dev/null +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/SecureRandomDetector.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2012 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.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.Detector; +import com.android.tools.lint.detector.api.Detector.ClassScanner; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.tools.lint.detector.api.Scope; +import com.android.tools.lint.detector.api.Severity; +import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Arrays; +import java.util.List; + +/** + * Checks for hardcoded seeds with random numbers. + */ +public class SecureRandomDetector extends Detector implements ClassScanner { + /** Unregistered activities and services */ + public static final Issue ISSUE = Issue.create( + "SecureRandom", //$NON-NLS-1$ + "Looks for suspicious usage of the SecureRandom class", + + "Specifying a fixed seed will cause the instance to return a predictable sequence " + + "of numbers. This may be useful for testing but it is not appropriate for secure use.", + + Category.PERFORMANCE, + 9, + Severity.WARNING, + SecureRandomDetector.class, + Scope.CLASS_FILE_SCOPE). + setMoreInfo("http://developer.android.com/reference/java/security/SecureRandom.html"); + + private static final String SET_SEED = "setSeed"; //$NON-NLS-1$ + private static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom"; //$NON-NLS-1$ + private static final String OWNER_RANDOM = "java/util/Random"; //$NON-NLS-1$ + /** Method description for a method that takes a long argument (no return type specified */ + private static final String LONG_ARG = "(J)"; //$NON-NLS-1$ + + /** Constructs a new {@link SecureRandomDetector} */ + public SecureRandomDetector() { + } + + // ---- Implements ClassScanner ---- + + @Override + @Nullable + public List<String> getApplicableCallNames() { + return Arrays.asList(SET_SEED); + } + + @Override + public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull MethodInsnNode call) { + String owner = call.owner; + String desc = call.desc; + if (owner.equals(OWNER_SECURE_RANDOM)) { + if (desc.startsWith(LONG_ARG)) { + checkValidSetSeed(context, call); + } else if (desc.startsWith("([B)")) { //$NON-NLS-1$ + // setSeed(byte[]) ... + // We could do some flow analysis here to see whether the byte array getting + // passed in appears to be fixed. + // However, people calling this constructor rather than the simpler one + // with a fixed integer are probably less likely to make that mistake... right? + } + } else if (owner.equals(OWNER_RANDOM) && desc.startsWith(LONG_ARG)) { + // Called setSeed(long) on an instanceof a Random object. Flag this if the instance + // is likely a SecureRandom. + // TODO + } + } + + private void checkValidSetSeed(ClassContext context, MethodInsnNode call) { + assert call.name.equals(SET_SEED); + + // Make sure the argument passed is not a literal + AbstractInsnNode prev = LintUtils.getPrevInstruction(call); + if (prev == null) { + return; + } + int opcode = prev.getOpcode(); + if (opcode == Opcodes.LCONST_0 || opcode == Opcodes.LCONST_1 || opcode == Opcodes.LDC) { + context.report(ISSUE, context.getLocation(call), + "Do not call setSeed() on a SecureRandom with a fixed seed: " + + "it is not secure. Use getSeed().", + null); + } else if (opcode == Opcodes.INVOKESTATIC) { + String methodName = ((MethodInsnNode) prev).name; + if (methodName.equals("currentTimeMillis") || methodName.equals("nanoTime")) { + context.report(ISSUE, context.getLocation(call), + "It is dangerous to seed SecureRandom with the current time because " + + "that value is more predictable to an attacker than the default seed.", + null); + } + } + } +} diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/WakelockDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/WakelockDetector.java new file mode 100644 index 0000000..7ed0edf --- /dev/null +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/WakelockDetector.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2012 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.tools.lint.detector.api.LintConstants.ANDROID_APP_ACTIVITY; + +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.Context; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Detector.ClassScanner; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; +import com.android.tools.lint.detector.api.Severity; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Arrays; +import java.util.List; + +/** + * Checks for problems with wakelocks (such as failing to release them) + * which can lead to unnecessary battery usage. + */ +public class WakelockDetector extends Detector implements ClassScanner { + + /** Problems using wakelocks */ + public static final Issue ISSUE = Issue.create( + "Wakelock", //$NON-NLS-1$ + "Looks for problems with wakelock usage", + + "Failing to release a wakelock properly can keep the Android device in " + + "a high power mode, which reduces battery life. There are several causes " + + "of this, such as releasing the wake lock in onDestroy() instead of in " + + "onPause(), failing to call release() in all possible code paths after " + + "an acquire(), and so on.\n" + + "\n" + + "NOTE: If you are using the lock just to keep the screen on, you should " + + "strongly consider using FLAG_KEEP_SCREEN_ON instead. This window flag " + + "will be correctly managed by the platform as the user moves between " + + "applications and doesn't require a special permission. See " + + "http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_KEEP_SCREEN_ON.", + + Category.PERFORMANCE, + 9, + Severity.WARNING, + WakelockDetector.class, + Scope.CLASS_FILE_SCOPE); + + private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock"; //$NON-NLS-1$ + private static final String RELEASE_METHOD = "release"; //$NON-NLS-1$ + private static final String ACQUIRE_METHOD = "acquire"; //$NON-NLS-1$ + + /** Constructs a new {@link WakelockDetector} */ + public WakelockDetector() { + } + + @Override + public void afterCheckProject(@NonNull Context context) { + if (mHasAcquire && !mHasRelease && context.getDriver().getPhase() == 1) { + // Gather positions of the acquire calls + context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE); + } + } + + // ---- Implements ClassScanner ---- + + /** Whether any {@code acquire()} calls have been encountered */ + private boolean mHasAcquire; + + /** Whether any {@code release()} calls have been encountered */ + private boolean mHasRelease; + + @Override + @Nullable + public List<String> getApplicableCallNames() { + return Arrays.asList(ACQUIRE_METHOD, RELEASE_METHOD); + } + + @Override + public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull MethodInsnNode call) { + if (call.owner.equals(WAKELOCK_OWNER)) { + String name = call.name; + if (name.equals(ACQUIRE_METHOD)) { + mHasAcquire = true; + + if (context.getDriver().getPhase() == 2) { + assert !mHasRelease; + context.report(ISSUE, method, context.getLocation(call), + "Found a wakelock acquire() but no release() calls anywhere", + null); + } else { + assert context.getDriver().getPhase() == 1; + // Perform flow analysis in this method to see if we're + // performing an acquire/release block, where there are code paths + // between the acquire and release which can result in the + // release call not getting reached. + // TODO: Implement this. + } + } else if (name.equals(RELEASE_METHOD)) { + mHasRelease = true; + + // See if the release is happening in an onDestroy method, in an + // activity. + if ("onDestroy".equals(method.name) //$NON-NLS-1$ + && context.getDriver().isSubclassOf( + classNode, ANDROID_APP_ACTIVITY)) { + context.report(ISSUE, method, context.getLocation(call), + "Wakelocks should be released in onPause, not onDestroy", + null); + } + } + } + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/SecureRandomDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/SecureRandomDetectorTest.java new file mode 100644 index 0000000..2fbdaad --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/SecureRandomDetectorTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 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.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class SecureRandomDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new SecureRandomDetector(); + } + + public void test1() throws Exception { + assertEquals( + "SecureRandomTest.java:12: Warning: It is dangerous to seed SecureRandom with the current time because that value is more predictable to an attacker than the default seed.\n" + + "SecureRandomTest.java:14: Warning: Do not call setSeed() on a SecureRandom with a fixed seed: it is not secure. Use getSeed().\n" + + "SecureRandomTest.java:15: Warning: Do not call setSeed() on a SecureRandom with a fixed seed: it is not secure. Use getSeed().\n" + + "SecureRandomTest.java:16: Warning: Do not call setSeed() on a SecureRandom with a fixed seed: it is not secure. Use getSeed().\n" + + "SecureRandomTest.java:17: Warning: Do not call setSeed() on a SecureRandom with a fixed seed: it is not secure. Use getSeed().\n" + + "SecureRandomTest.java:18: Warning: Do not call setSeed() on a SecureRandom with a fixed seed: it is not secure. Use getSeed().", + // Missing errors on lines 28 and 40, using flow analysis to determine that + // setSeed on a random instance is really a call on a SecureRandom instance, + // and using flow analysis to determine that the seed byte array passed into + // the SecureRandom constructor is static. + + lintProject( + "bytecode/.classpath=>.classpath", + "bytecode/AndroidManifest.xml=>AndroidManifest.xml", + "res/layout/onclick.xml=>res/layout/onclick.xml", + "bytecode/SecureRandomTest.java.txt=>src/test/pkg/SecureRandomTest.java", + "bytecode/SecureRandomTest.class.data=>bin/classes/test/pkg/SecureRandomTest.class" + )); + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WakelockDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WakelockDetectorTest.java new file mode 100644 index 0000000..d0662cb --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WakelockDetectorTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 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.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class WakelockDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new WakelockDetector(); + } + + public void test1() throws Exception { + assertEquals( + "WakelockActivity1.java:15: Warning: Found a wakelock acquire() but no release() calls anywhere", + + lintProject( + "bytecode/.classpath=>.classpath", + "bytecode/AndroidManifest.xml=>AndroidManifest.xml", + "res/layout/onclick.xml=>res/layout/onclick.xml", + "bytecode/WakelockActivity1.java.txt=>src/test/pkg/WakelockActivity1.java", + "bytecode/WakelockActivity1.class.data=>bin/classes/test/pkg/WakelockActivity1.class" + )); + } + + public void test2() throws Exception { + assertEquals( + "WakelockActivity2.java:13: Warning: Wakelocks should be released in onPause, not onDestroy", + + lintProject( + "bytecode/.classpath=>.classpath", + "bytecode/AndroidManifest.xml=>AndroidManifest.xml", + "res/layout/onclick.xml=>res/layout/onclick.xml", + "bytecode/WakelockActivity2.java.txt=>src/test/pkg/WakelockActivity2.java", + "bytecode/WakelockActivity2.class.data=>bin/classes/test/pkg/WakelockActivity2.class" + )); + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.class.data b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.class.data Binary files differnew file mode 100644 index 0000000..3236187 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.class.data diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.java.txt new file mode 100644 index 0000000..c05fbd9 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/SecureRandomTest.java.txt @@ -0,0 +1,42 @@ +package test.pkg; + +import java.security.SecureRandom; +import java.util.Random; + +public class SecureRandomTest { + private static final long FIXED_SEED = 1000L; + protected int getDynamicSeed() { return 1; } + + public void testLiterals() { + SecureRandom random1 = new SecureRandom(); + random1.setSeed(System.currentTimeMillis()); // OK + random1.setSeed(getDynamicSeed()); // OK + random1.setSeed(0); // Wrong + random1.setSeed(1); // Wrong + random1.setSeed((int)1023); // Wrong + random1.setSeed(1023L); // Wrong + random1.setSeed(FIXED_SEED); // Wrong + } + + public void testRandomTypeOk() { + Random random2 = new Random(); + random2.setSeed(0); // OK + } + + public void testRandomTypeWrong() { + Random random3 = new SecureRandom(); + random3.setSeed(0); // Wrong: owner is java/util/Random, but applied to SecureRandom object + } + + public void testBytesOk() { + SecureRandom random1 = new SecureRandom(); + byte[] seed = random1.generateSeed(4); + random1.setSeed(seed); // OK + } + + public void testBytesWrong() { + SecureRandom random2 = new SecureRandom(); + byte[] seed = new byte[3]; + random2.setSeed(seed); // Wrong + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.class.data b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.class.data Binary files differnew file mode 100644 index 0000000..d4733e6 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.class.data diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.java.txt new file mode 100644 index 0000000..0b14691 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity1.java.txt @@ -0,0 +1,17 @@ +package test.pkg; + +import android.app.Activity; +import android.os.Bundle; +import android.os.PowerManager; + +public class WakelockActivity1 extends Activity { + private PowerManager.WakeLock mWakeLock; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + PowerManager manager = (PowerManager) getSystemService(POWER_SERVICE); + mWakeLock = manager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Test"); + mWakeLock.acquire(); // Never released + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.class.data b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.class.data Binary files differnew file mode 100644 index 0000000..89e35c1 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.class.data diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.java.txt new file mode 100644 index 0000000..fa91a43 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/bytecode/WakelockActivity2.java.txt @@ -0,0 +1,24 @@ +package test.pkg; + +import android.app.Activity; +import android.os.PowerManager; + +public class WakelockActivity2 extends Activity { + private PowerManager.WakeLock mWakeLock; + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.release(); // Should be done in onPause instead + } + } + + @Override + protected void onPause() { + super.onDestroy(); + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.release(); // OK + } + } +} |