Skip to content

Commit 1156581

Browse files
committed
Ruby: add CryptoAlgorithms library
1 parent 27f786b commit 1156581

File tree

3 files changed

+588
-0
lines changed

3 files changed

+588
-0
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/**
2+
* Provides classes modeling cryptographic algorithms, separated into strong and weak variants.
3+
*
4+
* The classification into strong and weak are based on Wikipedia, OWASP and google (2017).
5+
*/
6+
7+
/**
8+
* Names of cryptographic algorithms, separated into strong and weak variants.
9+
*
10+
* The names are normalized: upper-case, no spaces, dashes or underscores.
11+
*
12+
* The names are inspired by the names used in real world crypto libraries.
13+
*
14+
* The classification into strong and weak are based on Wikipedia, OWASP and google (2017).
15+
*/
16+
private module AlgorithmNames {
17+
predicate isStrongBlockMode(string name) { name = ["CCM", "GCM"] }
18+
19+
predicate isWeakBlockMode(string name) { name = "ECB" }
20+
21+
predicate isStrongHashingAlgorithm(string name) {
22+
name =
23+
[
24+
"DSA", "ED25519", "ES256", "ECDSA256", "ES384", "ECDSA384", "ES512", "ECDSA512", "SHA2",
25+
"SHA224", "SHA256", "SHA384", "SHA512", "SHA3", "SHA3224", "SHA3256", "SHA3384", "SHA3512"
26+
]
27+
}
28+
29+
predicate isWeakHashingAlgorithm(string name) {
30+
name =
31+
[
32+
"HAVEL128", "MD2", "MD4", "MD5", "PANAMA", "RIPEMD", "RIPEMD128", "RIPEMD256", "RIPEMD160",
33+
"RIPEMD320", "SHA0", "SHA1"
34+
]
35+
}
36+
37+
predicate isStrongEncryptionAlgorithm(string name) {
38+
name =
39+
[
40+
"AES", "AES128", "AES192", "AES256", "AES512", "AES-128", "AES-192", "AES-256", "AES-512",
41+
"RSA", "RABBIT", "BLOWFISH", "BF", "ECIES", "CAST", "CAST5", "CAMELLIA", "CAMELLIA128",
42+
"CAMELLIA192", "CAMELLIA256", "CAMELLIA-128", "CAMELLIA-192", "CAMELLIA-256", "CHACHA",
43+
"GOST", "GOST89"
44+
]
45+
}
46+
47+
predicate isWeakEncryptionAlgorithm(string name) {
48+
name =
49+
[
50+
"DES", "3DES", "DES3", "TRIPLEDES", "DESX", "TDEA", "TRIPLEDEA", "ARC2", "RC2", "ARC4",
51+
"RC4", "ARCFOUR", "ARC5", "RC5"
52+
]
53+
}
54+
55+
predicate isStrongPasswordHashingAlgorithm(string name) {
56+
name = ["ARGON2", "PBKDF2", "BCRYPT", "SCRYPT"]
57+
}
58+
59+
predicate isWeakPasswordHashingAlgorithm(string name) { name = "EVPKDF" }
60+
}
61+
62+
bindingset[algorithmString]
63+
private string algorithmRegex(string algorithmString) {
64+
// Algorithms usually appear in names surrounded by characters that are not
65+
// alphabetical characters in the same case. This handles the upper and lower
66+
// case cases.
67+
result =
68+
"((^|.*[^A-Z])(" + algorithmString + ")([^A-Z].*|$))" +
69+
// or...
70+
"|" +
71+
// For lowercase, we want to be careful to avoid being confused by camelCase
72+
// hence we require two preceding uppercase letters to be sure of a case switch,
73+
// or a preceding non-alphabetic character
74+
"((^|.*[A-Z]{2}|.*[^a-zA-Z])(" + algorithmString.toLowerCase() + ")([^a-z].*|$))"
75+
}
76+
77+
private module OpenSSL {
78+
/**
79+
* A known `OpenSSL::Cipher`. Supported ciphers depend on the version of
80+
* `OpenSSL` installed on a system. In the general case, a name will include
81+
* the cipher name, the key length, and the block encryption mode.
82+
*
83+
* Note that since the cipher name itself always comes first in these names
84+
* and always uses a "-" to demark to block mode, we can safely uppercase
85+
* these names when checking against an `algorithmRegex`.
86+
*
87+
* See https://ruby-doc.org/stdlib-3.0.1/libdoc/openssl/rdoc/OpenSSL/Cipher.html
88+
*/
89+
predicate isOpenSSLCipher(string name) {
90+
name =
91+
[
92+
"AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CFB", "AES-128-CFB1", "AES-128-CFB8",
93+
"AES-128-CTR", "AES-128-ECB", "AES-128-OFB", "AES-128-XTS", "AES-192-CBC", "AES-192-CFB",
94+
"AES-192-CFB1", "AES-192-CFB8", "AES-192-CTR", "AES-192-ECB", "AES-192-OFB", "AES-256-CBC",
95+
"AES-256-CBC-HMAC-SHA1", "AES-256-CFB", "AES-256-CFB1", "AES-256-CFB8", "AES-256-CTR",
96+
"AES-256-ECB", "AES-256-OFB", "AES-256-XTS", "AES128", "AES192", "AES256", "BF", "BF-CBC",
97+
"BF-CFB", "BF-ECB", "BF-OFB", "CAMELLIA-128-CBC", "CAMELLIA-128-CFB", "CAMELLIA-128-CFB1",
98+
"CAMELLIA-128-CFB8", "CAMELLIA-128-ECB", "CAMELLIA-128-OFB", "CAMELLIA-192-CBC",
99+
"CAMELLIA-192-CFB", "CAMELLIA-192-CFB1", "CAMELLIA-192-CFB8", "CAMELLIA-192-ECB",
100+
"CAMELLIA-192-OFB", "CAMELLIA-256-CBC", "CAMELLIA-256-CFB", "CAMELLIA-256-CFB1",
101+
"CAMELLIA-256-CFB8", "CAMELLIA-256-ECB", "CAMELLIA-256-OFB", "CAMELLIA128", "CAMELLIA192",
102+
"CAMELLIA256", "CAST", "CAST-cbc", "CAST5-CBC", "CAST5-CFB", "CAST5-ECB", "CAST5-OFB",
103+
"ChaCha", "DES", "DES-CBC", "DES-CFB", "DES-CFB1", "DES-CFB8", "DES-ECB", "DES-EDE",
104+
"DES-EDE-CBC", "DES-EDE-CFB", "DES-EDE-OFB", "DES-EDE3", "DES-EDE3-CBC", "DES-EDE3-CFB",
105+
"DES-EDE3-CFB1", "DES-EDE3-CFB8", "DES-EDE3-OFB", "DES-OFB", "DES3", "DESX", "DESX-CBC",
106+
"GOST 28147-89", "RC2", "RC2-40-CBC", "RC2-64-CBC", "RC2-CBC", "RC2-CFB", "RC2-ECB",
107+
"RC2-OFB", "RC4", "RC4-40", "RC4-HMAC-MD5", "aes-128-cbc", "aes-128-cbc-hmac-sha1",
108+
"aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", "aes-128-ctr", "aes-128-ecb", "aes-128-gcm",
109+
"aes-128-ofb", "aes-128-xts", "aes-192-cbc", "aes-192-cfb", "aes-192-cfb1", "aes-192-cfb8",
110+
"aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-ofb", "aes-256-cbc",
111+
"aes-256-cbc-hmac-sha1", "aes-256-cfb", "aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr",
112+
"aes-256-ecb", "aes-256-gcm", "aes-256-ofb", "aes-256-xts", "aes128", "aes192", "aes256",
113+
"bf", "bf-cbc", "bf-cfb", "bf-ecb", "bf-ofb", "blowfish", "camellia-128-cbc",
114+
"camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ecb",
115+
"camellia-128-ofb", "camellia-192-cbc", "camellia-192-cfb", "camellia-192-cfb1",
116+
"camellia-192-cfb8", "camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc",
117+
"camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ecb",
118+
"camellia-256-ofb", "camellia128", "camellia192", "camellia256", "cast", "cast-cbc",
119+
"cast5-cbc", "cast5-cfb", "cast5-ecb", "cast5-ofb", "chacha", "des", "des-cbc", "des-cfb",
120+
"des-cfb1", "des-cfb8", "des-ecb", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ofb",
121+
"des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8",
122+
"des-ede3-ofb", "des-ofb", "des3", "desx", "desx-cbc", "gost89", "gost89-cnt", "gost89-ecb",
123+
"id-aes128-GCM", "id-aes192-GCM", "id-aes256-GCM", "rc2", "rc2-40-cbc", "rc2-64-cbc",
124+
"rc2-cbc", "rc2-cfb", "rc2-ecb", "rc2-ofb", "rc4", "rc4-40", "rc4-hmac-md5"
125+
]
126+
}
127+
128+
predicate isWeakOpenSSLCipher(string name) {
129+
isOpenSSLCipher(name) and
130+
name.toUpperCase().regexpMatch(getInsecureAlgorithmRegex())
131+
}
132+
133+
predicate isStrongOpenSSLCipher(string name) {
134+
isOpenSSLCipher(name) and
135+
name.toUpperCase().regexpMatch(getSecureAlgorithmRegex()) and
136+
// exclude algorithms that include a weak component
137+
not name.toUpperCase().regexpMatch(getInsecureAlgorithmRegex())
138+
}
139+
}
140+
141+
private import AlgorithmNames
142+
private import OpenSSL
143+
144+
private string rankedInsecureAlgorithm(int i) {
145+
// In this case we know these are being used for encryption, so we want to match
146+
// weak hash algorithms and block modes as well.
147+
result =
148+
rank[i](string s |
149+
isWeakEncryptionAlgorithm(s) or isWeakHashingAlgorithm(s) or isWeakBlockMode(s)
150+
)
151+
}
152+
153+
private string insecureAlgorithmString(int i) {
154+
i = 1 and result = rankedInsecureAlgorithm(i)
155+
or
156+
result = rankedInsecureAlgorithm(i) + "|" + insecureAlgorithmString(i - 1)
157+
}
158+
159+
/**
160+
* Gets the regular expression used for matching strings that look like they
161+
* contain an algorithm that is known to be insecure.
162+
*/
163+
private string getInsecureAlgorithmRegex() {
164+
result = algorithmRegex(insecureAlgorithmString(max(int i | exists(rankedInsecureAlgorithm(i)))))
165+
}
166+
167+
private string rankedSecureAlgorithm(int i) {
168+
result = rank[i](string s | isStrongEncryptionAlgorithm(s))
169+
}
170+
171+
private string secureAlgorithmString(int i) {
172+
i = 1 and result = rankedSecureAlgorithm(i)
173+
or
174+
result = rankedSecureAlgorithm(i) + "|" + secureAlgorithmString(i - 1)
175+
}
176+
177+
/**
178+
* Gets a regular expression for matching strings that look like they
179+
* contain an algorithm that is known to be secure.
180+
*/
181+
string getSecureAlgorithmRegex() {
182+
result = algorithmRegex(secureAlgorithmString(max(int i | exists(rankedSecureAlgorithm(i)))))
183+
}
184+
185+
/**
186+
* A cryptographic algorithm.
187+
*/
188+
private newtype TCryptographicAlgorithm =
189+
MkHashingAlgorithm(string name, boolean isWeak) {
190+
isStrongHashingAlgorithm(name) and isWeak = false
191+
or
192+
isWeakHashingAlgorithm(name) and isWeak = true
193+
} or
194+
MkEncryptionAlgorithm(string name, boolean isWeak) {
195+
isStrongEncryptionAlgorithm(name) and isWeak = false
196+
or
197+
isWeakEncryptionAlgorithm(name) and isWeak = true
198+
} or
199+
MkPasswordHashingAlgorithm(string name, boolean isWeak) {
200+
isStrongPasswordHashingAlgorithm(name) and isWeak = false
201+
or
202+
isWeakPasswordHashingAlgorithm(name) and isWeak = true
203+
} or
204+
MkOpenSSLCipher(string name, boolean isWeak) {
205+
isStrongOpenSSLCipher(name) and isWeak = false
206+
or
207+
isWeakOpenSSLCipher(name) and isWeak = true
208+
}
209+
210+
/**
211+
* A cryptographic algorithm.
212+
*/
213+
abstract class CryptographicAlgorithm extends TCryptographicAlgorithm {
214+
/** Gets a textual representation of this element. */
215+
string toString() { result = getName() }
216+
217+
/**
218+
* Gets the normalized name of this algorithm (upper-case, no spaces, dashes or underscores).
219+
*/
220+
abstract string getName();
221+
222+
/**
223+
* Holds if the name of this algorithm matches `name` modulo case,
224+
* white space, dashes, underscores, and anything after a dash in the name
225+
* (to ignore modes of operation, such as CBC or ECB).
226+
*/
227+
bindingset[name]
228+
predicate matchesName(string name) {
229+
[name.toUpperCase(), name.toUpperCase().regexpCapture("^(\\w+)(?:-.*)?$", 1)]
230+
.regexpReplaceAll("[-_ ]", "") = getName()
231+
}
232+
233+
/**
234+
* Holds if this algorithm is weak.
235+
*/
236+
abstract predicate isWeak();
237+
}
238+
239+
/**
240+
* A hashing algorithm such as `MD5` or `SHA512`.
241+
*/
242+
class HashingAlgorithm extends MkHashingAlgorithm, CryptographicAlgorithm {
243+
string name;
244+
boolean isWeak;
245+
246+
HashingAlgorithm() { this = MkHashingAlgorithm(name, isWeak) }
247+
248+
override string getName() { result = name }
249+
250+
override predicate isWeak() { isWeak = true }
251+
}
252+
253+
/**
254+
* An encryption algorithm such as `DES` or `AES512`.
255+
*/
256+
class EncryptionAlgorithm extends MkEncryptionAlgorithm, CryptographicAlgorithm {
257+
string name;
258+
boolean isWeak;
259+
260+
EncryptionAlgorithm() { this = MkEncryptionAlgorithm(name, isWeak) }
261+
262+
override string getName() { result = name }
263+
264+
override predicate isWeak() { isWeak = true }
265+
}
266+
267+
/**
268+
* A password hashing algorithm such as `PBKDF2` or `SCRYPT`.
269+
*/
270+
class PasswordHashingAlgorithm extends MkPasswordHashingAlgorithm, CryptographicAlgorithm {
271+
string name;
272+
boolean isWeak;
273+
274+
PasswordHashingAlgorithm() { this = MkPasswordHashingAlgorithm(name, isWeak) }
275+
276+
override string getName() { result = name }
277+
278+
override predicate isWeak() { isWeak = true }
279+
}
280+
281+
/**
282+
* A known OpenSSL cipher. This may include information about the block
283+
* encryption mode, which can affect if the cipher is marked as being weak.
284+
*/
285+
class OpenSSLCipher extends MkOpenSSLCipher, CryptographicAlgorithm {
286+
string name;
287+
boolean isWeak;
288+
289+
OpenSSLCipher() { this = MkOpenSSLCipher(name, isWeak) }
290+
291+
override string getName() { result = name }
292+
293+
override predicate isWeak() { isWeak = true }
294+
}

0 commit comments

Comments
 (0)