1 /// Module handling multiple hash types dynamically.
2 ///
3 /// Authors: dd86k <dd@dax.moe>
4 /// Copyright: No rights reserved
5 /// License: CC0
6 module ddh;
7 
8 import std.digest;
9 import std.digest.sha, std.digest.md, std.digest.ripemd, std.digest.crc, std.digest.murmurhash;
10 import sha3d, blake2d;
11 import std.base64;
12 import std.format : formattedRead;
13 
14 // Adds dynamic seeding to supported hashes
15 private class HashSeeded(T) if (isDigest!T && hasBlockSize!T) : WrapperDigest!T
16 {
17     @trusted nothrow void seed(uint input)
18     {
19         _digest = T(input);
20     }
21 }
22 
23 private alias MurmurHash3_32_SeededDigest = HashSeeded!(MurmurHash3!32);
24 private alias MurmurHash3_128_32_SeededDigest = HashSeeded!(MurmurHash3!(128, 32));
25 private alias MurmurHash3_128_64_SeededDigest = HashSeeded!(MurmurHash3!(128, 64));
26 
27 enum HashType
28 {
29     CRC32,
30     CRC64ISO,
31     CRC64ECMA,
32     MurmurHash3_32,
33     MurmurHash3_128_32,
34     MurmurHash3_128_64,
35     MD5,
36     RIPEMD160,
37     SHA1,
38     SHA224,
39     SHA256,
40     SHA384,
41     SHA512,
42     SHA3_224,
43     SHA3_256,
44     SHA3_384,
45     SHA3_512,
46     SHAKE128,
47     SHAKE256,
48     BLAKE2b512,
49     BLAKE2s256,
50 }
51 
52 enum HashCount = HashType.max + 1;
53 enum InvalidHash = cast(HashType)-1;
54 
55 struct HashInfo
56 {
57     HashType type;
58     string fullName, alias_, tag, tag2;
59 }
60 
61 immutable 
62 {
63    string crc32 = "crc32";
64    string crc64iso = "crc64iso";
65    string crc64ecma = "crc64ecma";
66    string murmur3a = "murmur3a";
67    string murmur3c = "murmur3c";
68    string murmur3f = "murmur3f";
69    string md5 = "md5";
70    string ripemd160 = "ripemd160";
71    string sha1 = "sha1";
72    string sha224 = "sha224";
73    string sha256 = "sha256";
74    string sha384 = "sha384";
75    string sha512 = "sha512";
76    string sha3_224 = "sha3-224";
77    string sha3_256 = "sha3-256";
78    string sha3_384 = "sha3-384";
79    string sha3_512 = "sha3-512";
80    string shake128 = "shake128";
81    string shake256 = "shake256";
82    string blake2b512 = "blake2b512";
83    string blake2s256 = "blake2s256";
84 }
85 
86 // Full name: Should be based on their full specification name
87 // Alias: Should be based on a simple lowercase name. See `openssl dgst -list` for examples.
88 // Tag name: Should be based on an full uppercase name. See openssl dgst output for examples.
89 //TODO: Alternative Alias name
90 //      Some aliases, like sha3-256 and ripemd160, are a little long to type
91 //      "sha3" and "rmd160" fit better.
92 //TODO: Alternative Tag name
93 //      For some reason, NetBSD seems to be using other names such as RMD160,
94 //      SHA512, etc. under OpenSSL. Is this a GNU/BSD thing?
95 immutable HashInfo[HashCount] hashInfo = [
96     // HashType, Full, Alias, Tag (openssl), Tag2 (gnu)
97     { HashType.CRC32,
98         "CRC-32", crc32, "CRC32", },
99     { HashType.CRC64ISO,
100         "CRC-64-ISO",   crc64iso, "CRC64ISO", },
101     { HashType.CRC64ECMA,
102         "CRC-64-ECMA",  crc64ecma, "CRC64ECMA", },
103     { HashType.MurmurHash3_32,
104         "MurmurHash3-32",     murmur3a, "MURMURHASH3-32", },
105     { HashType.MurmurHash3_128_32,
106         "MurmurHash3-128/32", murmur3c, "MURMURHASH3-128-32", },
107     { HashType.MurmurHash3_128_64,
108         "MurmurHash3-128/64", murmur3f, "MURMURHASH3-128-64", },
109     { HashType.MD5,
110         "MD5-128",      md5, "MD5", },
111     { HashType.RIPEMD160,
112         "RIPEMD-160",   ripemd160, "RIPEMD160", "RMD160" },
113     { HashType.SHA1,
114         "SHA-1-160",    sha1, "SHA1", },
115     { HashType.SHA224,
116         "SHA-2-224",    sha224, "SHA2-224", "SHA224" },
117     { HashType.SHA256,
118         "SHA-2-256",    sha256, "SHA2-256", "SHA256" },
119     { HashType.SHA384,
120         "SHA-2-384",    sha384, "SHA2-384", "SHA384" },
121     { HashType.SHA512,
122         "SHA-2-512",    sha512, "SHA2-512", "SHA512" },
123     { HashType.SHA3_224,
124         "SHA-3-224",    sha3_224, "SHA3-224", },
125     { HashType.SHA3_256,
126         "SHA-3-256",    sha3_256, "SHA3-256", },
127     { HashType.SHA3_384,
128         "SHA-3-384",    sha3_384, "SHA3-384", },
129     { HashType.SHA3_512,
130         "SHA-3-512",    sha3_512, "SHA3-512", },
131     { HashType.SHAKE128,
132         "SHAKE-128",    shake128, "SHAKE-128", },
133     { HashType.SHAKE256,
134         "SHAKE-256",    shake256, "SHAKE-256", },
135     { HashType.BLAKE2b512,
136         "BLAKE2b-512",  blake2b512, "BLAKE2B-512", },
137     { HashType.BLAKE2s256,
138         "BLAKE2s-256",  blake2s256, "BLAKE2S-256", },
139 ];
140 
141 struct Ddh
142 {
143     Digest digest;
144     HashType type;
145     ubyte[] result;
146     immutable(HashInfo)* info;
147     bool checksum;
148 
149     int initiate(HashType t)
150     {
151         final switch (t) with (HashType)
152         {
153         case CRC32:     digest = new CRC32Digest(); break;
154         case CRC64ISO:  digest = new CRC64ISODigest(); break;
155         case CRC64ECMA: digest = new CRC64ECMADigest(); break;
156         case MD5:       digest = new MD5Digest(); break;
157         case RIPEMD160: digest = new RIPEMD160Digest(); break;
158         case SHA1:      digest = new SHA1Digest(); break;
159         case SHA224:    digest = new SHA224Digest(); break;
160         case SHA256:    digest = new SHA256Digest(); break;
161         case SHA384:    digest = new SHA384Digest(); break;
162         case SHA512:    digest = new SHA512Digest(); break;
163         case SHA3_224:  digest = new SHA3_224Digest(); break;
164         case SHA3_256:  digest = new SHA3_256Digest(); break;
165         case SHA3_384:  digest = new SHA3_384Digest(); break;
166         case SHA3_512:  digest = new SHA3_512Digest(); break;
167         case SHAKE128:  digest = new SHAKE128Digest(); break;
168         case SHAKE256:  digest = new SHAKE256Digest(); break;
169         case BLAKE2b512:    digest = new BLAKE2b512Digest(); break;
170         case BLAKE2s256:    digest = new BLAKE2s256Digest(); break;
171         case MurmurHash3_32:    digest = new MurmurHash3_32_SeededDigest(); break;
172         case MurmurHash3_128_32:    digest = new MurmurHash3_128_32_SeededDigest(); break;
173         case MurmurHash3_128_64:    digest = new MurmurHash3_128_64_SeededDigest(); break;
174         }
175 
176         type = t;
177         info = &hashInfo[t];
178         checksum = t <= HashType.CRC64ECMA;
179 
180         return 0;
181     }
182 
183     void key(const(ubyte)[] input...)
184     {
185         switch (type) with (HashType)
186         {
187         case BLAKE2b512: (cast(BLAKE2b512Digest)digest).key(input); break;
188         case BLAKE2s256: (cast(BLAKE2s256Digest)digest).key(input); break;
189         default: throw new Exception("Digest does not support keying.");
190         }
191     }
192 
193     void seed(uint input)
194     {
195         switch (type) with (HashType)
196         {
197         case MurmurHash3_32:     (cast(MurmurHash3_32_SeededDigest)digest).seed(input); break;
198         case MurmurHash3_128_32: (cast(MurmurHash3_128_32_SeededDigest)digest).seed(input); break;
199         case MurmurHash3_128_64: (cast(MurmurHash3_128_64_SeededDigest)digest).seed(input); break;
200         default: throw new Exception("Digest does not support seeding.");
201         }
202     }
203 
204     void reset()
205     {
206         digest.reset();
207     }
208 
209     size_t length()
210     {
211         return digest.length();
212     }
213 
214     void put(scope const(ubyte)[] input...)
215     {
216         digest.put(input);
217     }
218 
219     ubyte[] finish()
220     {
221         return (result = digest.finish());
222     }
223 
224     const(char)[] toHex()
225     {
226         //TODO: Test if endianness messes results with checksums
227         return checksum ?
228             result.toHexString!(LetterCase.lower, Order.decreasing) :
229             result.toHexString!(LetterCase.lower);
230     }
231 
232     const(char)[] toBase64()
233     {
234         return Base64.encode(result);
235     }
236 
237     string fullName()
238     {
239         return info.fullName;
240     }
241 
242     string aliasName()
243     {
244         return info.alias_;
245     }
246 
247     string tagName()
248     {
249         return info.tag;
250     }
251 }
252 
253 /// 
254 @system unittest
255 {
256     import std.conv : hexString;
257 
258     Ddh ddh = void;
259     ddh.initiate(HashType.CRC32);
260     ddh.put(cast(ubyte[]) "abc");
261     assert(ddh.finish() == cast(ubyte[]) hexString!"c2412435");
262     assert(ddh.toHex() == "352441c2");
263 
264     ddh.initiate(HashType.SHA3_256);
265     ddh.put(cast(ubyte[]) "abc");
266     assert(ddh.finish() == cast(ubyte[]) hexString!(
267             "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"));
268     assert(ddh.toHex() ==
269             "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532");
270 }
271 
272 /// Read a formatted GNU tag line.
273 /// Params:
274 ///     line = Full GNU formatted tag string
275 ///     hash = Hexadecimal hash string (e.g., "aabbccdd")
276 ///     file = File string (e.g., "nightly.iso")
277 /// Returns: True on error.
278 bool readGNULine(string line, ref const(char)[] hash, ref const(char)[] file)
279 {
280     // Tested to work with one or many spaces
281     return formattedRead(line, "%s %s", hash, file) != 2;
282 }
283 
284 unittest
285 {
286     string line = "f6067df486cbdbb0aac026b799b26261c92734a3  LICENSE";
287     const(char)[] hash, file;
288     assert(readGNULine(line, hash, file) == false);
289     assert(hash == "f6067df486cbdbb0aac026b799b26261c92734a3");
290     assert(file == "LICENSE");
291 }
292 
293 /// Read a formatted BSD tag line.
294 /// Params:
295 ///     line = Full BSD formatted tag string
296 ///     type = Hash type string (e.g., "SHA256")
297 ///     file = File string (e.g., "nightly.iso")
298 ///     hash = Hexadecimal hash string (e.g., "8080aabb")
299 /// Returns: True on error.
300 bool readBSDLine(string line,
301     ref const(char)[] type, ref const(char)[] file, ref const(char)[] hash)
302 {
303     // Tested to work with and without spaces
304     return formattedRead(line, "%s (%s) = %s", type, file, hash) != 3;
305 }
306 
307 unittest
308 {
309     string line =
310         "SHA256 (Fedora-Workstation-Live-x86_64-36-1.5.iso) = " ~
311         "80169891cb10c679cdc31dc035dab9aae3e874395adc5229f0fe5cfcc111cc8c";
312     const(char)[] type, file, hash;
313     assert(readBSDLine(line, type, file, hash) == false);
314     assert(type == "SHA256");
315     assert(file == "Fedora-Workstation-Live-x86_64-36-1.5.iso");
316     assert(hash == "80169891cb10c679cdc31dc035dab9aae3e874395adc5229f0fe5cfcc111cc8c");
317 }
318 
319 /// Read a formatted SRI tag line.
320 /// Params:
321 ///     line = Full RSI formatted tag string
322 ///     type = Hash type string (e.g., "md5")
323 ///     hash = Base64 hash string (e.g., "OFPip4okcUW0qhZmdzb23g==")
324 bool readSRILine(string line, ref const(char)[] type, ref const(char)[] hash)
325 {
326     return formattedRead(line, "%s-%s", type, hash) != 2;
327 }
328 
329 unittest
330 {
331     string line = "sha1-9gZ99IbL27CqwCa3mbJiYcknNKM=";
332     const(char)[] type, hash;
333     assert(readSRILine(line, type, hash) == false);
334     assert(type == "sha1");
335     assert(hash == "9gZ99IbL27CqwCa3mbJiYcknNKM=");
336 }
337 
338 /+bool readPGPMessage(string line)
339 {
340         /* PGP Message example:
341         -----BEGIN PGP SIGNED MESSAGE-----
342         Hash: SHA256
343 
344         # Fedora-Workstation-Live-x86_64-36-1.5.iso: 2018148352 bytes
345         SHA256 (Fedora-Workstation-Live-x86_64-36-1.5.iso) = 80169891cb10c679cdc31dc035dab9aae3e874395adc5229f0fe5cfcc111cc8c
346         -----BEGIN PGP SIGNATURE-----
347         ...*/
348 }+/
349 
350 /// Guess hash type by extension name.
351 /// Params: path = Path, filename will be extract from this.
352 /// Returns: Hash type.
353 HashType guessHash(const(char)[] path) @safe
354 {
355     import std.string : toLower, indexOf;
356     import std.path : extension, baseName, globMatch, CaseSensitive;
357     import std.algorithm.searching : canFind, startsWith;
358 
359     const(char)[] name = baseName(path).toLower;
360 
361     foreach (info; hashInfo)
362     {
363         if (indexOf(name, info.alias_) >= 0)
364             return info.type;
365     }
366 
367     // aliases
368     struct Alias
369     {
370         string name;
371         HashType type;
372     }
373     static immutable Alias[] aliases = [
374         { "sha3", HashType.SHA3_256 }
375     ];
376     foreach (alias_; aliases)
377     {
378         if (indexOf(name, alias_.name) >= 0)
379             return alias_.type;
380     }
381 
382     return InvalidHash;
383 }
384 
385 @safe unittest
386 {
387     assert(guessHash("sha1sum") == HashType.SHA1);
388     assert(guessHash(".SHA256SUM") == HashType.SHA256);
389     assert(guessHash("GE-Proton7-38.sha512sum") == HashType.SHA512);
390     assert(guessHash("CHECKSUM.SHA512-FreeBSD-13.1-RELEASE-amd64") == HashType.SHA512);
391     assert(guessHash("test.crc32") == HashType.CRC32);
392     assert(guessHash("test.sha256") == HashType.SHA256);
393     assert(guessHash("test.md5sum") == HashType.MD5);
394     assert(guessHash("test.sha3sums") == HashType.SHA3_256);
395 }
396 
397 // Check by context
398 // This can be a GNU list, BSD list, or PGP signed message with tag
399 /*HashType guessHashFile(string content) @safe
400 {
401         
402         
403 }
404 
405 @safe unittest
406 {
407 }*/