1 module dcrypt.nacl.secretbox; 2 3 /// High level API for symmetric authenticated encryption. 4 /// Compatible to http://nacl.cr.yp.to/secretbox.html. 5 6 import dcrypt.macs.poly1305; 7 import dcrypt.streamcipher.salsa; 8 import dcrypt.util; 9 import dcrypt.exceptions; 10 11 private { 12 enum tag_bytes = 16; 13 enum key_bytes = 32; 14 enum nonce_bytes = 24; 15 16 alias XSalsa20 StreamCipher; 17 alias Poly1305Raw Auth; 18 } 19 20 /// High-level symmetric authenticated encryption. 21 /// 22 /// Params: 23 /// msg = Plaintext message. 24 /// nonce = 24 bytes used once per key. 25 /// key = Secret shared key. 32 bytes. 26 /// 27 /// Returns: 28 /// Authentication tag and encrypted message. The output is 16 bytes longer than the input. 29 public ubyte[] secretbox(in ubyte[] msg, in ubyte[] nonce, in ubyte[] key) @safe nothrow 30 in { 31 assert(key.length == key_bytes, "Invalid key length."); 32 assert(nonce.length == nonce_bytes, "Invalid nonce length."); 33 } body { 34 35 StreamCipher streamcipher; 36 streamcipher.start(true, key, nonce); 37 38 ubyte[32] auth_key = 0; 39 scope(exit) { 40 wipe(auth_key); 41 } 42 43 // Derive authentication key by encrypting 32 zeros. 44 streamcipher.processBytes(auth_key, auth_key); 45 46 Poly1305Raw auth; 47 auth.start(auth_key); 48 49 ubyte[] output = new ubyte[tag_bytes + msg.length]; 50 51 ubyte[] ciphertext = streamcipher.processBytes(msg, output[tag_bytes .. $]); 52 auth.put(ciphertext); 53 output[0..tag_bytes] = auth.finish(); 54 55 return output; 56 } 57 58 /// High-level symmetric authenticated decryption. 59 /// 60 /// Params: 61 /// boxed = Ciphertext and authentication tag as created by `secretbox()`. 62 /// nonce = 24 bytes used once per key. 63 /// key = Secret shared key. 32 bytes. 64 /// 65 /// Returns: Returns the plaintext if the authentication tag is correct. 66 /// 67 /// Throws: Throws an exception if the authentication tag is invalid. 68 public ubyte[] secretbox_open(in ubyte[] boxed, in ubyte[] nonce, in ubyte[] key) @safe 69 in { 70 assert(key.length == key_bytes, "Invalid key length."); 71 assert(nonce.length == nonce_bytes, "Invalid nonce length."); 72 assert(boxed.length >= tag_bytes, "Message too short. Can't even contain a 16 byte tag."); 73 } body { 74 75 StreamCipher streamcipher; 76 streamcipher.start(false, key, nonce); 77 78 ubyte[32] auth_key = 0; 79 80 // Derive authentication key by encrypting 32 zeros. 81 streamcipher.processBytes(auth_key, auth_key); 82 83 Poly1305Raw auth; 84 auth.start(auth_key); 85 86 const ubyte[] ciphertext = boxed[tag_bytes..$]; 87 88 auth.put(ciphertext); 89 ubyte[tag_bytes] recv_tag = auth.finish(); 90 const ubyte[] expected_tag = boxed[0..tag_bytes]; 91 92 if(crypto_equals(recv_tag, expected_tag)) { 93 // Tag is correct. 94 95 ubyte[] plaintext = new ubyte[ciphertext.length]; 96 97 streamcipher.processBytes(ciphertext, plaintext); 98 99 return plaintext; 100 } else { 101 throw new InvalidCipherTextException("Invalid tag!"); 102 } 103 } 104 105 106 unittest { 107 alias immutable ubyte[] octets; 108 109 octets key = cast(octets) x" 110 1b27556473e985d462cd51197a9a46c7 111 6009549eac6474f206c4ee0844f68389"; 112 113 octets nonce = cast(octets) x" 114 69696ee955b62b73cd62bda875fc73d6 115 8219e0036b7a0b37"; 116 117 octets msg = cast(octets) x" 118 be075fc53c81f2d5cf141316ebeb0c7b 119 5228c52a4c62cbd44b66849b64244ffc 120 e5ecbaaf33bd751a1ac728d45e6c6129 121 6cdc3c01233561f41db66cce314adb31 122 0e3be8250c46f06dceea3a7fa1348057 123 e2f6556ad6b1318a024a838f21af1fde 124 048977eb48f59ffd4924ca1c60902e52 125 f0a089bc76897040e082f93776384864 126 5e0705"; 127 128 octets boxed_ref = cast(octets) x" 129 f3ffc7703f9400e52a7dfb4b3d3305d9 130 8e993b9f48681273c29650ba32fc76ce 131 48332ea7164d96a4476fb8c531a1186a 132 c0dfc17c98dce87b4da7f011ec48c972 133 71d2c20f9b928fe2270d6fb863d51738 134 b48eeee314a7cc8ab932164548e526ae 135 90224368517acfeabd6bb3732bc0e9da 136 99832b61ca01b6de56244a9e88d5f9b3 137 7973f622a43d14a6599b1f654cb45a74 138 e355a5"; 139 140 141 test_secret_box(msg, boxed_ref, key, nonce); 142 } 143 144 // Test with pseudo random input. 145 unittest { 146 import dcrypt.random.drng; 147 HashDRNG_SHA256 drng; 148 drng.setSeed(0); 149 150 ubyte[32] key; 151 ubyte[24] nonce; 152 153 drng.nextBytes(key); 154 drng.nextBytes(nonce); 155 156 ubyte[1001] message; 157 drng.nextBytes(message); 158 159 test_secret_box(message, null, key, nonce); 160 } 161 162 version(unittest) { 163 /// Helper function for testing. 164 /// Params: 165 /// msg = Plaintext. 166 /// boxed_ref = Expected ciphertext with authentication tag. 167 /// key = Symmetric encryption key. 168 void test_secret_box(in ubyte[] msg, in ubyte[] boxed_ref, in ubyte[] key, in ubyte[] nonce) { 169 // test encryption 170 ubyte[] boxed = secretbox(msg, nonce, key); 171 if(boxed_ref !is null) { 172 assert(boxed == boxed_ref, "secretbox failed"); 173 } 174 175 // test decryption 176 if(boxed_ref !is null) { 177 ubyte[] unboxed = secretbox_open(boxed_ref, nonce, key); 178 assert(unboxed == msg, "secretbox_open failed"); 179 } else { 180 ubyte[] unboxed = secretbox_open(boxed, nonce, key); 181 assert(unboxed == msg, "secretbox_open failed"); 182 } 183 184 // test invalid authentication 185 ubyte[] tampered_box = boxed.dup; 186 tampered_box[$-1] ^= 1; 187 188 bool exception = false; 189 try { 190 ubyte[] unboxed = secretbox_open(tampered_box, nonce, key); 191 assert(false, "Invalid ciphertext passed as valid."); 192 } catch(InvalidCipherTextException e) { 193 exception = true; 194 } finally { 195 assert(exception, "Expected exception has not been thrown."); 196 } 197 } 198 }