1 module ddata.crypto.encrypt;
2 
3 import std.format: format;
4 import deimos.openssl.evp;
5 import ddata.common;
6 import ddata.crypto.algorithm;
7 
8 version (OSX) {
9     extern(C) @nogc nothrow private @system {
10         void arc4random_buf(scope void* buf, size_t nbytes);
11     }
12 }
13 
14 private void randomFill(ubyte[] buffer) @trusted {
15     // Shamelessly plucked from https://github.com/LightBender/SecureD/
16     version (OSX) {
17         arc4random_buf(buffer.ptr, buffer.length);
18     } else version (Posix) {
19         import std.stdio: File, _IONBF;
20         try {
21             File urandom = File("/dev/urandom", "rb");
22             urandom.setvbuf(null, _IONBF);
23             scope(exit) urandom.close();
24             buffer = urandom.rawRead(buffer);
25         } catch (Exception ex) {
26             throw new Exception("failed to get random bytes - %s".format(ex.msg));
27         }
28     } else {
29         static assert(0, "Unsupported OS for secure random byte generation");
30     }
31 }
32 
33 /**
34     Encrypts a string using a key with the specified algorithm
35 
36     See_Also:
37         `ddata.crypto.decrypt`
38 */
39 public ubyte[] encrypt(const string data, const string key, Algorithm algorithm = Algorithm.aes128) @safe {
40     final switch (algorithm) {
41     case Algorithm.aes128:
42         return encrypt(cast(const ubyte[])data, cast(const ubyte[])key, () @trusted { return EVP_aes_128_cbc(); }() );
43     }
44 }
45 
46 private ubyte[] encrypt(const ubyte[] data, const ubyte[] key, const(EVP_CIPHER)* cipher) @trusted {
47     auto ctx = EVP_CIPHER_CTX_new();
48     if (ctx is null) {
49         throw new Exception("Failed to create EVP cipher context - %s".format(getLastError));
50     }
51     scope(exit) EVP_CIPHER_CTX_free(ctx);
52 
53     static ubyte[16] iv16;
54 
55     ubyte[] iv = void;
56     int cipherBlockSize = EVP_CIPHER_block_size(cipher);
57     switch (cipherBlockSize) {
58     case 16:
59         iv = iv16[];
60         break;
61     default:
62         throw new Exception("Cannot handle cipher block size of %s".format(cipherBlockSize));
63     }
64 
65     randomFill(iv);
66 
67     EVP_EncryptInit(ctx, cipher, key.ptr, cast(const(ubyte)*)iv.ptr);
68 
69     const bufferSize = data.length + cipherBlockSize;
70     ubyte[] buffer = new ubyte[bufferSize];
71 
72     int updateLength = void;
73     EVP_EncryptUpdate(ctx, buffer.ptr, &updateLength, data.ptr, cast(int)data.length);
74 
75     int finalLength = void;
76     EVP_EncryptFinal(ctx, &buffer.ptr[updateLength], &finalLength);
77 
78     auto ret = iv ~ buffer[0 .. updateLength + finalLength];
79     return ret;
80 }
81 
82 @("should encrypt and decrypt to same message")
83 @safe unittest {
84     import ddata.crypto: decrypt;
85     string password = "some random password";
86     string message = "this is a call to all you people how is this even happening";
87     auto encryptedMessage = encrypt(message, password);
88     auto decryptedMessage = decrypt(encryptedMessage, password);
89     assert(message == decryptedMessage);
90 }
91 
92 @("should test all algorithms for multiple text and password sizes")
93 @safe unittest {
94     import std.range: generate, take, array;
95     import std.random: uniform;
96     import ddata.crypto: decrypt;
97 
98     alias genChars = generate!(() => cast(char)uniform(32, 127));
99 
100     int maxMessageLength = 2000;
101     int maxPasswordLength = 200;
102     int numSteps = 10;
103     foreach (m; 0 .. maxMessageLength / numSteps) {
104         foreach (p; 0 .. maxPasswordLength / numSteps) {
105             int msize = (m + 1) * numSteps;
106             int psize = (p + 1) * numSteps;
107 
108             string message = genChars.take(msize).array.idup;
109             string password = genChars.take(psize).array.idup;
110 
111             auto encryptedMessage = encrypt(message, password);
112             auto decryptedMessage = decrypt(encryptedMessage, password);
113             assert(message == decryptedMessage);
114         }
115     }
116 }