Merge pull request #6357 from artem-smotrakov/static-iv

Java: Static initialization vector
This commit is contained in:
Chris Smowton
2021-08-26 13:45:43 +01:00
committed by GitHub
8 changed files with 434 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
byte[] iv = new byte[16]; // all zeroes
GCMParameterSpec params = new GCMParameterSpec(128, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, key, params);

View File

@@ -0,0 +1,6 @@
byte[] iv = new byte[16];
SecureRandom random = SecureRandom.getInstanceStrong();
random.nextBytes(iv);
GCMParameterSpec params = new GCMParameterSpec(128, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, key, params);

View File

@@ -0,0 +1,46 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
A cipher needs an initialization vector (IV) when it is used in certain modes
such as CBC or GCM. Under the same secret key, IVs should be unique and ideally unpredictable.
Given a secret key, if the same IV is used for encryption, the same plaintexts result in the same ciphertexts.
This lets an attacker learn if the same data pieces are transferred or stored,
or this can help the attacker run a dictionary attack.
</p>
</overview>
<recommendation>
<p>
Use a random IV generated by <code>SecureRandom</code>.
</p>
</recommendation>
<example>
<p>
The following example initializes a cipher with a static IV which is unsafe:
</p>
<sample src="BadStaticInitializationVector.java" />
<p>
The next example initializes a cipher with a random IV:
</p>
<sample src="GoodRandomInitializationVector.java" />
</example>
<references>
<li>
Wikipedia:
<a href="https://en.wikipedia.org/wiki/Initialization_vector">Initialization vector</a>.
</li>
<li>
National Institute of Standards and Technology:
<a href="https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf">Recommendation for Block Cipher Modes of Operation</a>.
</li>
<li>
National Institute of Standards and Technology:
<a href="https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf">FIPS 140-2: Security Requirements for Cryptographic Modules</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,26 @@
/**
* @name Using a static initialization vector for encryption
* @description A cipher needs an initialization vector (IV) in some cases,
* for example, when CBC or GCM modes are used. IVs are used to randomize the encryption,
* therefore they should be unique and ideally unpredictable.
* Otherwise, the same plaintexts result in same ciphertexts under a given secret key.
* If a static IV is used for encryption, this lets an attacker learn
* if the same data pieces are transferred or stored,
* or this can help the attacker run a dictionary attack.
* @kind path-problem
* @problem.severity warning
* @precision high
* @id java/static-initialization-vector
* @tags security
* external/cwe/cwe-329
* external/cwe/cwe-1204
*/
import java
import experimental.semmle.code.java.security.StaticInitializationVectorQuery
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink, StaticInitializationVectorConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "A $@ should not be used for encryption.", source.getNode(),
"static initialization vector"

View File

@@ -0,0 +1,165 @@
import java
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.dataflow.TaintTracking2
/**
* Holds if `array` is initialized only with constants.
*/
private predicate initializedWithConstants(ArrayCreationExpr array) {
// creating an array without an initializer, for example `new byte[8]`
not exists(array.getInit())
or
// creating a multidimensional array with an initializer like `{ new byte[8], new byte[16] }`
// This works around https://github.com/github/codeql/issues/6552 -- change me once there is
// a better way to distinguish nested initializers that create zero-filled arrays
// (e.g. `new byte[1]`) from those with an initializer list (`new byte[] { 1 }` or just `{ 1 }`)
array.getInit().getAnInit().getAChildExpr() instanceof IntegerLiteral
or
// creating an array wit an initializer like `new byte[] { 1, 2 }`
forex(Expr element | element = array.getInit().getAnInit() |
element instanceof CompileTimeConstantExpr
)
}
/**
* An expression that creates a byte array that is initialized with constants.
*/
private class StaticByteArrayCreation extends ArrayCreationExpr {
StaticByteArrayCreation() {
this.getType().(Array).getElementType().(PrimitiveType).getName() = "byte" and
initializedWithConstants(this)
}
}
/** An expression that updates `array`. */
private class ArrayUpdate extends Expr {
Expr array;
ArrayUpdate() {
exists(Assignment assign |
assign = this and
assign.getDest().(ArrayAccess).getArray() = array and
not assign.getSource() instanceof CompileTimeConstantExpr
)
or
exists(StaticMethodAccess ma |
ma.getMethod().hasQualifiedName("java.lang", "System", "arraycopy") and
ma = this and
ma.getArgument(2) = array
)
or
exists(MethodAccess ma, Method m |
m = ma.getMethod() and
ma = this and
ma.getArgument(0) = array
|
m.hasQualifiedName("java.io", "InputStream", "read") or
m.hasQualifiedName("java.nio", "ByteBuffer", "get") or
m.hasQualifiedName("java.security", "SecureRandom", "nextBytes") or
m.hasQualifiedName("java.util", "Random", "nextBytes")
)
}
/** Returns the updated array. */
Expr getArray() { result = array }
}
/**
* A config that tracks dataflow from creating an array to an operation that updates it.
*/
private class ArrayUpdateConfig extends TaintTracking2::Configuration {
ArrayUpdateConfig() { this = "ArrayUpdateConfig" }
override predicate isSource(DataFlow::Node source) {
source.asExpr() instanceof StaticByteArrayCreation
}
override predicate isSink(DataFlow::Node sink) {
exists(ArrayUpdate update | update.getArray() = sink.asExpr())
}
}
/**
* A source that defines an array that doesn't get updated.
*/
private class StaticInitializationVectorSource extends DataFlow::Node {
StaticInitializationVectorSource() {
exists(StaticByteArrayCreation array | array = this.asExpr() |
not exists(ArrayUpdateConfig config | config.hasFlow(DataFlow2::exprNode(array), _))
)
}
}
/**
* A config that tracks initialization of a cipher for encryption.
*/
private class EncryptionModeConfig extends TaintTracking2::Configuration {
EncryptionModeConfig() { this = "EncryptionModeConfig" }
override predicate isSource(DataFlow::Node source) {
source
.asExpr()
.(FieldRead)
.getField()
.hasQualifiedName("javax.crypto", "Cipher", "ENCRYPT_MODE")
}
override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
m.hasQualifiedName("javax.crypto", "Cipher", "init") and
ma.getArgument(0) = sink.asExpr()
)
}
}
/**
* A sink that initializes a cipher for encryption with unsafe parameters.
*/
private class EncryptionInitializationSink extends DataFlow::Node {
EncryptionInitializationSink() {
exists(MethodAccess ma, Method m, EncryptionModeConfig config | m = ma.getMethod() |
m.hasQualifiedName("javax.crypto", "Cipher", "init") and
m.getParameterType(2)
.(RefType)
.hasQualifiedName("java.security.spec", "AlgorithmParameterSpec") and
ma.getArgument(2) = this.asExpr() and
config.hasFlowToExpr(ma.getArgument(0))
)
}
}
/**
* Holds if `fromNode` to `toNode` is a dataflow step
* that creates cipher's parameters with initialization vector.
*/
private predicate createInitializationVectorSpecStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
exists(ConstructorCall cc, RefType type |
cc = toNode.asExpr() and type = cc.getConstructedType()
|
type.hasQualifiedName("javax.crypto.spec", "IvParameterSpec") and
cc.getArgument(0) = fromNode.asExpr()
or
type.hasQualifiedName("javax.crypto.spec", ["GCMParameterSpec", "RC2ParameterSpec"]) and
cc.getArgument(1) = fromNode.asExpr()
or
type.hasQualifiedName("javax.crypto.spec", "RC5ParameterSpec") and
cc.getArgument(3) = fromNode.asExpr()
)
}
/**
* A config that tracks dataflow to initializing a cipher with a static initialization vector.
*/
class StaticInitializationVectorConfig extends TaintTracking::Configuration {
StaticInitializationVectorConfig() { this = "StaticInitializationVectorConfig" }
override predicate isSource(DataFlow::Node source) {
source instanceof StaticInitializationVectorSource
}
override predicate isSink(DataFlow::Node sink) { sink instanceof EncryptionInitializationSink }
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
createInitializationVectorSpecStep(fromNode, toNode)
}
}

View File

@@ -0,0 +1,167 @@
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
public class StaticInitializationVector {
// BAD: AES-GCM with static IV from a byte array
public byte[] encryptWithStaticIvByteArrayWithInitializer(byte[] key, byte[] plaintext) throws Exception {
byte[] iv = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5 };
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// BAD: AES-GCM with static IV from zero-initialized byte array
public byte[] encryptWithZeroStaticIvByteArray(byte[] key, byte[] plaintext) throws Exception {
byte[] iv = new byte[16];
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// BAD: AES-CBC with static IV from zero-initialized byte array
public byte[] encryptWithStaticIvByteArray(byte[] key, byte[] plaintext) throws Exception {
byte[] iv = new byte[16];
for (byte i = 0; i < iv.length; i++) {
iv[i] = 1;
}
IvParameterSpec ivSpec = new IvParameterSpec(iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// BAD: AES-GCM with static IV from a multidimensional byte array
public byte[] encryptWithOneOfStaticIvs01(byte[] key, byte[] plaintext) throws Exception {
byte[][] staticIvs = new byte[][] {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5 },
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 42 }
};
GCMParameterSpec ivSpec = new GCMParameterSpec(128, staticIvs[1]);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// BAD: AES-GCM with static IV from a multidimensional byte array
public byte[] encryptWithOneOfStaticIvs02(byte[] key, byte[] plaintext) throws Exception {
byte[][] staticIvs = new byte[][] {
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5 },
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 42 }
};
GCMParameterSpec ivSpec = new GCMParameterSpec(128, staticIvs[1]);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// BAD: AES-GCM with static IV from a multidimensional byte array
public byte[] encryptWithOneOfStaticZeroIvs(byte[] key, byte[] plaintext) throws Exception {
byte[][] ivs = new byte[][] {
new byte[8],
new byte[16]
};
GCMParameterSpec ivSpec = new GCMParameterSpec(128, ivs[1]);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // $staticInitializationVector
cipher.update(plaintext);
return cipher.doFinal();
}
// GOOD: AES-GCM with a random IV
public byte[] encryptWithRandomIv(byte[] key, byte[] plaintext) throws Exception {
byte[] iv = new byte[16];
SecureRandom random = SecureRandom.getInstanceStrong();
random.nextBytes(iv);
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
cipher.update(plaintext);
return cipher.doFinal();
}
// GOOD: AES-GCM with a random IV
public byte[] encryptWithRandomIvByteByByte(byte[] key, byte[] plaintext) throws Exception {
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] iv = new byte[16];
for (int i = 0; i < iv.length; i++) {
iv[i] = (byte) random.nextInt();
}
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
cipher.update(plaintext);
return cipher.doFinal();
}
// GOOD: AES-GCM with a random IV
public byte[] encryptWithRandomIvWithSystemArrayCopy(byte[] key, byte[] plaintext) throws Exception {
byte[] randomBytes = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(randomBytes);
byte[] iv = new byte[16];
System.arraycopy(randomBytes, 0, iv, 0, 16);
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
cipher.update(plaintext);
return cipher.doFinal();
}
// GOOD: AES-GCM with a random IV
public byte[] encryptWithRandomIvWithArraysCopy(byte[] key, byte[] plaintext) throws Exception {
byte[] randomBytes = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(randomBytes);
byte[] iv = new byte[16];
iv = Arrays.copyOf(randomBytes, 16);
GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
cipher.update(plaintext);
return cipher.doFinal();
}
}

View File

@@ -0,0 +1,20 @@
import java
import experimental.semmle.code.java.security.StaticInitializationVectorQuery
import TestUtilities.InlineExpectationsTest
class StaticInitializationVectorTest extends InlineExpectationsTest {
StaticInitializationVectorTest() { this = "StaticInitializationVectorTest" }
override string getARelevantTag() { result = "staticInitializationVector" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "staticInitializationVector" and
exists(DataFlow::Node src, DataFlow::Node sink, StaticInitializationVectorConfig conf |
conf.hasFlow(src, sink)
|
sink.getLocation() = location and
element = sink.toString() and
value = ""
)
}
}