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 }