Skip to content

Commit eefda61

Browse files
add a query that checks for the use of static IVs
1 parent 37869e8 commit eefda61

File tree

6 files changed

+291
-0
lines changed

6 files changed

+291
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>When a cipher is used in certain modes (such as CBC or GCM), it requires an initialization vector (IV). Under the same secret key, IVs should be unique and ideally unpredictable. If the same IV is used with the same secret key, then the same plaintext results in the same ciphertext. This behavior may enable an attacker to learn if the same data pieces are transferred or stored, or help the attacker run a dictionary attack.</p>
6+
</overview>
7+
8+
<recommendation>
9+
<p>Use a randomly generated IV.</p>
10+
</recommendation>
11+
12+
<example>
13+
<p>The following example shows a few cases of instantiating a cipher with various encryption keys. In the 'BAD' cases, the IV is hardcoded or constant, making the encrypted data vulnerable to recovery. In the 'GOOD' cases, the IV is randomly generated and not hardcoded, which protects the encrypted data against recovery.</p>
14+
<sample src="StaticInitializationVector.swift" />
15+
</example>
16+
17+
<references>
18+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Initialization_vector">Initialization vector</a>.</li>
19+
<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>
20+
<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>
21+
</references>
22+
</qhelp>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @name Static initialization vector for encryption
3+
* @description Using a static initialization vector (IV) for encryption is not secure. To maximize encryption and prevent dictionary attacks, IVs should rather be unique and unpredictable.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.5
7+
* @precision high
8+
* @id swift/static-initialization-vector
9+
* @tags security
10+
* external/cwe/cwe-329
11+
* external/cwe/cwe-1204
12+
*/
13+
14+
import swift
15+
import codeql.swift.dataflow.DataFlow
16+
import codeql.swift.dataflow.TaintTracking
17+
import DataFlow::PathGraph
18+
19+
// from DataFlow::PathNode source, DataFlow::PathNode sink, StaticInitializationVectorConfig conf
20+
// where conf.hasFlowPath(source, sink)
21+
// select sink.getNode(), source, sink, "A $@ should not be used for encryption.", source.getNode(),
22+
// "static initialization vector"
23+
/**
24+
* A static IV is created through either a byte array or string literals.
25+
*/
26+
class StaticInitializationVectorSource extends Expr {
27+
StaticInitializationVectorSource() {
28+
this = any(ArrayExpr arr | arr.getType().getName() = "Array<UInt8>") or
29+
this instanceof StringLiteralExpr
30+
}
31+
}
32+
33+
/**
34+
* A class for all ways to set an IV.
35+
*/
36+
class EncryptionInitializationSink extends Expr {
37+
EncryptionInitializationSink() {
38+
// `iv` arg in `init` is a sink
39+
exists(CallExpr call, string fName, int arg |
40+
call.getStaticTarget()
41+
.(MethodDecl)
42+
.hasQualifiedName([
43+
"AES", "ChaCha20", "Blowfish", "Rabbit", "CBC", "CFB", "GCM", "OCB", "OFB", "PCBC",
44+
"CCM", "CTR"
45+
], fName) and
46+
fName.matches("%init(%iv:%") and
47+
arg = [0, 1] and
48+
call.getArgument(arg).getExpr() = this
49+
)
50+
}
51+
}
52+
53+
/**
54+
* A dataflow configuration from the source of a static IV to expressions that use
55+
* it to initialize a cipher.
56+
*/
57+
class StaticInitializationVectorConfig extends TaintTracking::Configuration {
58+
StaticInitializationVectorConfig() { this = "StaticInitializationVectorConfig" }
59+
60+
override predicate isSource(DataFlow::Node node) {
61+
node.asExpr() instanceof StaticInitializationVectorSource
62+
}
63+
64+
override predicate isSink(DataFlow::Node node) {
65+
node.asExpr() instanceof EncryptionInitializationSink
66+
}
67+
}
68+
69+
// The query itself
70+
from
71+
StaticInitializationVectorConfig config, DataFlow::PathNode sourceNode,
72+
DataFlow::PathNode sinkNode
73+
where config.hasFlowPath(sourceNode, sinkNode)
74+
select sinkNode.getNode(), sourceNode, sinkNode,
75+
"The static value '" + sourceNode.getNode().toString() +
76+
"' is used as an initialization vector for encryption."
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
func encrypt(padding : Padding) {
3+
// ...
4+
5+
// BAD: Using static IVs for encryption
6+
let iv: Array<UInt8> = [0x2a, 0x3a, 0x80, 0x05]
7+
let ivString = "this is a constant string"
8+
let key = getRandomKey()
9+
_ = try AES(key: key, iv: ivString)
10+
_ = try CBC(iv: iv)
11+
12+
// GOOD: Using randomly generated IVs for encryption
13+
let iv = (0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
14+
let ivString = String(cString: iv)
15+
let key = getRandomKey())
16+
_ = try AES(key: key, iv: ivString)
17+
_ = try CBC(iv: iv)
18+
19+
// ...
20+
}

swift/ql/test/query-tests/Security/CWE-1204/StaticInitializationVector.expected

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/Security/CWE-1204/StaticInitializationVector.ql
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
2+
// --- stubs ---
3+
4+
// These stubs roughly follows the same structure as classes from CryptoSwift
5+
class AES
6+
{
7+
init(key: Array<UInt8>, blockMode: BlockMode, padding: Padding) { }
8+
init(key: Array<UInt8>, blockMode: BlockMode) { }
9+
init(key: String, iv: String) { }
10+
init(key: String, iv: String, padding: Padding) { }
11+
}
12+
13+
class Blowfish
14+
{
15+
init(key: Array<UInt8>, blockMode: BlockMode, padding: Padding) { }
16+
init(key: Array<UInt8>, blockMode: BlockMode) { }
17+
init(key: String, iv: String) { }
18+
init(key: String, iv: String, padding: Padding) { }
19+
}
20+
21+
class ChaCha20
22+
{
23+
init(key: Array<UInt8>, iv: Array<UInt8>) { }
24+
init(key: String, iv: String) { }
25+
}
26+
27+
class Rabbit
28+
{
29+
init(key: Array<UInt8>) { }
30+
init(key: String) { }
31+
init(key: Array<UInt8>, iv: Array<UInt8>) { }
32+
init(key: String, iv: String) { }
33+
}
34+
35+
protocol BlockMode { }
36+
37+
struct CBC: BlockMode {
38+
init(iv: Array<UInt8>) { }
39+
}
40+
41+
struct CFB: BlockMode {
42+
enum SegmentSize: Int {
43+
case cfb8 = 1
44+
case cfb128 = 16
45+
}
46+
47+
init(iv: Array<UInt8>, segmentSize: SegmentSize = .cfb128) { }
48+
}
49+
50+
final class GCM: BlockMode {
51+
enum Mode { case combined, detached }
52+
init(iv: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil, tagLength: Int = 16, mode: Mode = .detached) { }
53+
convenience init(iv: Array<UInt8>, authenticationTag: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil, mode: Mode = .detached) {
54+
self.init(iv: iv, additionalAuthenticatedData: additionalAuthenticatedData, tagLength: authenticationTag.count, mode: mode)
55+
}
56+
}
57+
58+
struct OFB: BlockMode {
59+
init(iv: Array<UInt8>) { }
60+
}
61+
62+
struct PCBC: BlockMode {
63+
init(iv: Array<UInt8>) { }
64+
}
65+
66+
typealias StreamMode = BlockMode
67+
68+
struct CCM: StreamMode {
69+
init(iv: Array<UInt8>, tagLength: Int, messageLength: Int, additionalAuthenticatedData: Array<UInt8>? = nil) { }
70+
init(iv: Array<UInt8>, tagLength: Int, messageLength: Int, authenticationTag: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil) { }
71+
}
72+
73+
struct CTR: StreamMode {
74+
init(iv: Array<UInt8>, counter: Int = 0) { }
75+
}
76+
77+
protocol PaddingProtocol { }
78+
79+
enum Padding: PaddingProtocol {
80+
case noPadding, zeroPadding, pkcs7, pkcs5, eme_pkcs1v15, emsa_pkcs1v15, iso78164, iso10126
81+
}
82+
83+
// Helper functions
84+
func getConstantString() -> String {
85+
"this string is constant"
86+
}
87+
88+
func getConstantArray() -> Array<UInt8> {
89+
[UInt8](getConstantString().utf8)
90+
}
91+
92+
func getRandomArray() -> Array<UInt8> {
93+
(0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
94+
}
95+
96+
// --- tests ---
97+
98+
func test() {
99+
let iv: Array<UInt8> = [0x2a, 0x3a, 0x80, 0x05, 0xaf, 0x46, 0x58, 0x2d, 0x66, 0x52, 0x10, 0xae, 0x86, 0xd3, 0x8e, 0x8f]
100+
let iv2 = getConstantArray()
101+
let ivString = getConstantString()
102+
103+
let randomArray = getRandomArray()
104+
let randomIv = getRandomArray()
105+
let randomIvString = String(cString: getRandomArray())
106+
107+
let blockMode = CBC(iv: randomIv)
108+
let padding = Padding.noPadding
109+
let key = getRandomArray()
110+
let keyString = String(cString: key)
111+
112+
// AES test cases
113+
let ab1 = AES(key: keyString, iv: ivString) // BAD
114+
let ab2 = AES(key: keyString, iv: ivString, padding: padding) // BAD
115+
let ag1 = AES(key: keyString, iv: randomIvString) // GOOD
116+
let ag2 = AES(key: keyString, iv: randomIvString, padding: padding) // GOOD
117+
118+
// ChaCha20 test cases
119+
let cb1 = ChaCha20(key: keyString, iv: ivString) // BAD
120+
let cg1 = ChaCha20(key: keyString, iv: randomIvString) // GOOD
121+
122+
// Blowfish test cases
123+
let bb1 = Blowfish(key: keyString, iv: ivString) // BAD
124+
let bb2 = Blowfish(key: keyString, iv: ivString, padding: padding) // BAD
125+
let bg1 = Blowfish(key: keyString, iv: randomIvString) // GOOD
126+
let bg2 = Blowfish(key: keyString, iv: randomIvString, padding: padding) // GOOD
127+
128+
// Rabbit
129+
let rb1 = Rabbit(key: key, iv: iv) // BAD
130+
let rb2 = Rabbit(key: key, iv: iv2) // BAD [NOT DETECTED]
131+
let rb3 = Rabbit(key: keyString, iv: ivString) // BAD
132+
let rg1 = Rabbit(key: key, iv: randomIv) // GOOD
133+
let rg2 = Rabbit(key: keyString, iv: randomIvString) // GOOD
134+
135+
// CBC
136+
let cbcb1 = CBC(iv: iv) // BAD
137+
let cbcg1 = CBC(iv: randomIv) // GOOD
138+
139+
// CFB
140+
let cfbb1 = CFB(iv: iv) // BAD
141+
let cfbb2 = CFB(iv: iv, segmentSize: CFB.SegmentSize.cfb8) // BAD
142+
let cfbg1 = CFB(iv: randomIv) // GOOD
143+
let cfbg2 = CFB(iv: randomIv, segmentSize: CFB.SegmentSize.cfb8) // GOOD
144+
145+
// GCM
146+
let cgmb1 = GCM(iv: iv) // BAD
147+
let cgmb2 = GCM(iv: iv, additionalAuthenticatedData: randomArray, tagLength: 8, mode: GCM.Mode.combined) // BAD
148+
let cgmb3 = GCM(iv: iv, authenticationTag: randomArray, additionalAuthenticatedData: randomArray, mode: GCM.Mode.combined) // BAD
149+
let cgmg1 = GCM(iv: randomIv) // GOOD
150+
let cgmg2 = GCM(iv: randomIv, additionalAuthenticatedData: randomArray, tagLength: 8, mode: GCM.Mode.combined) // GOOD
151+
let cgmg3 = GCM(iv: randomIv, authenticationTag: randomArray, additionalAuthenticatedData: randomArray, mode: GCM.Mode.combined) // GOOD
152+
153+
// OFB
154+
let ofbb1 = OFB(iv: iv) // BAD
155+
let ofbg1 = OFB(iv: randomIv) // GOOD
156+
157+
// PCBC
158+
let pcbcb1 = PCBC(iv: iv) // BAD
159+
let pcbcg1 = PCBC(iv: randomIv) // GOOD
160+
161+
// CCM
162+
let ccmb1 = CCM(iv: iv, tagLength: 0, messageLength: 0, additionalAuthenticatedData: randomArray) // BAD
163+
let ccmb2 = CCM(iv: iv, tagLength: 0, messageLength: 0, authenticationTag: randomArray, additionalAuthenticatedData: randomArray) // BAD
164+
let ccmg1 = CCM(iv: randomIv, tagLength: 0, messageLength: 0, additionalAuthenticatedData: randomArray) // GOOD
165+
let ccmg2 = CCM(iv: randomIv, tagLength: 0, messageLength: 0, authenticationTag: randomArray, additionalAuthenticatedData: randomArray) // GOOD
166+
167+
// CTR
168+
let ctrb1 = CTR(iv: iv) // BAD
169+
let ctrb2 = CTR(iv: iv, counter: 0) // BAD
170+
let ctrg1 = CTR(iv: randomIv) // GOOD
171+
let ctrg2 = CTR(iv: randomIv, counter: 0) // GOOD
172+
}

0 commit comments

Comments
 (0)