1 /*
2 MIT License
3 
4 Copyright (c) 2025 Matheus C. França
5 
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12 
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15 
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24 
25 /++
26 + D bindings for IPCrypt2, a simple and secure IP address obfuscation scheme.
27 +
28 + IPCrypt2 is a format-preserving encryption scheme for IPv4 and IPv6 addresses.
29 + It allows IP addresses to be encrypted while maintaining their format, making it
30 + suitable for logging and data retention purposes where IP addresses need to be
31 + pseudonymized.
32 +
33 + Features:
34 + - Format-preserving encryption for both IPv4 and IPv6 addresses
35 + - Cryptographically secure using AES-128 as the underlying cipher
36 + - Preserves subnets: addresses sharing a prefix are encrypted to addresses sharing the same prefix
37 + - Deterministic: same input and key always produces the same output
38 + - Fast and constant-time operation
39  +/
40 
41 module ipcrypt2;
42 
43 /// IPCrypt2 C bindings
44 public import c.ipcrypt2c; // @system
45 
46 import std.exception : enforce, assertThrown;
47 import std.string : toStringz, fromStringz;
48 
49 /**
50  * IPCrypt context, providing encryption/decryption of IP addresses.
51  * Ensures proper initialization and cleanup of the underlying IPCrypt context.
52  */
53 struct IPCrypt2
54 {
55     private IPCrypt context; // Opaque IPCrypt context
56 
57     /**
58      * Constructs an IPCrypt2 with the given 16-byte key.
59      * Throws: Exception if the key length is not 16 bytes.
60      */
61     this(scope const(ubyte)* key) nothrow @nogc @trusted
62     {
63         ipcrypt_init(&context, &key[0]);
64     }
65 
66     /// Ditto, but constructs from a hexadecimal key string.
67     this(ref string hexKey) nothrow @nogc @trusted
68     {
69         ubyte[IPCRYPT_KEYBYTES] key;
70         ipcrypt_init(&context, &key[0]);
71     }
72 
73     /// Destructor ensures the IPCrypt context is cleaned up.
74     ~this() nothrow @nogc @trusted
75     {
76         ipcrypt_deinit(&context);
77     }
78 
79     // Disable copying to prevent double-free
80     @disable this(this);
81 
82     /**
83      * Encrypts a 16-byte IP address (IPv4 or IPv6).
84      * Params:
85      *   ip16 = The 16-byte IP address to encrypt.
86      * Returns: The encrypted 16-byte IP address.
87      */
88     ubyte[IPCRYPT_KEYBYTES] encryptIP16(scope const(ubyte)* ip16) nothrow @trusted
89     {
90         ubyte[IPCRYPT_KEYBYTES] result = (
91             cast(const(ubyte)[IPCRYPT_KEYBYTES]) ip16[0 .. IPCRYPT_KEYBYTES]).dup;
92         ipcrypt_encrypt_ip16(&context, &result[0]);
93         return result;
94     }
95 
96     /**
97      * Decrypts a 16-byte IP address (IPv4 or IPv6).
98      * Params:
99      *   ip16 = The 16-byte encrypted IP address.
100      * Returns: The decrypted 16-byte IP address.
101      */
102     ubyte[IPCRYPT_KEYBYTES] decryptIP16(scope const(ubyte)* ip16) nothrow @trusted
103     {
104         ubyte[IPCRYPT_KEYBYTES] result = (
105             cast(const(ubyte)[IPCRYPT_KEYBYTES]) ip16[0 .. IPCRYPT_KEYBYTES]).dup;
106         ipcrypt_decrypt_ip16(&context, &result[0]);
107         return result;
108     }
109 
110     /**
111      * Encrypts an IP address string (IPv4 or IPv6).
112      * Params:
113      *   ipStr = The IP address string to encrypt.
114      * Returns: The encrypted IP address as a string.
115      */
116     string encryptIPStr(ref string ipStr) nothrow @trusted
117     {
118         char[IPCRYPT_MAX_IP_STR_BYTES] result;
119         size_t len = ipcrypt_encrypt_ip_str(&context, &result[0], &ipStr[0]);
120         return result[0 .. len].idup;
121     }
122 
123     /**
124      * Decrypts an encrypted IP address string.
125      * Params:
126      *   encryptedIPStr = The encrypted IP address string.
127      * Returns: The decrypted IP address as a string.
128      */
129     string decryptIPStr(ref string encryptedIPStr) nothrow @trusted
130     {
131         char[IPCRYPT_MAX_IP_STR_BYTES] result;
132         size_t len = ipcrypt_decrypt_ip_str(&context, &result[0], &encryptedIPStr[0]);
133         return result[0 .. len].idup;
134     }
135 
136     /**
137      * Non-deterministic encryption of a 16-byte IP address.
138      * Params:
139      *   ip16 = The 16-byte IP address to encrypt.
140      *   random = 8-byte random data for non-determinism.
141      * Returns: The 24-byte encrypted IP address.
142      */
143     ubyte[IPCRYPT_NDIP_BYTES] ndEncryptIP16(scope const(ubyte)* ip16, scope const(ubyte)* random) nothrow @nogc @trusted
144     {
145         ubyte[IPCRYPT_NDIP_BYTES] result;
146         ipcrypt_nd_encrypt_ip16(&context, &result[0], &ip16[0], &random[0]);
147         return result;
148     }
149 
150     /**
151      * Non-deterministic decryption of a 24-byte encrypted IP address.
152      * Params:
153      *   ndip = The 24-byte encrypted IP address.
154      * Returns: The 16-byte decrypted IP address.
155      */
156     ubyte[IPCRYPT_KEYBYTES] ndDecryptIP16(scope const(ubyte)* ndip) nothrow @nogc @trusted
157     {
158         ubyte[IPCRYPT_KEYBYTES] result;
159         ipcrypt_nd_decrypt_ip16(&context, &result[0], &ndip[0]);
160         return result;
161     }
162 
163     /**
164      * Non-deterministic encryption of an IP address string.
165      * Params:
166      *   ipStr = The IP address string to encrypt.
167      *   random = 8-byte random data for non-determinism.
168      * Returns: The encrypted IP address as a string.
169      */
170     string ndEncryptIPStr(scope const(char)* ipStr, scope const(ubyte)* random) nothrow @trusted
171     {
172         char[IPCRYPT_NDIP_STR_BYTES] result;
173         size_t len = ipcrypt_nd_encrypt_ip_str(&context, &result[0], &ipStr[0], &random[0]);
174         return result[0 .. len].idup;
175     }
176 
177     /**
178      * Non-deterministic decryption of an encrypted IP address string.
179      * Params:
180      *   encryptedIPStr = The encrypted IP address string.
181      * Returns: The decrypted IP address as a string.
182      */
183     string ndDecryptIPStr(scope const(char)* encryptedIPStr) nothrow @trusted
184     {
185         char[IPCRYPT_MAX_IP_STR_BYTES] result;
186         size_t len = ipcrypt_nd_decrypt_ip_str(&context, &result[0], &encryptedIPStr[0]);
187         return result[0 .. len].idup;
188     }
189 }
190 
191 /**
192  * IPCryptNDX context, providing extended encryption/decryption.
193  * Ensures proper initialization and cleanup of the underlying IPCryptNDX context.
194  */
195 struct IPCryptNDXCtx
196 {
197     private IPCryptNDX context; // Opaque IPCryptNDX context
198 
199     /**
200      * Constructs an IPCryptNDXCtx with the given 32-byte key.
201      * Throws: Exception if the key length is not 32 bytes.
202      */
203     this(scope const(ubyte)* key) nothrow @nogc @trusted
204     {
205         ipcrypt_ndx_init(&context, &key[0]);
206     }
207 
208     /// Ditto, but constructs from a hexadecimal key string.
209     this(ref string hexKey) nothrow @nogc @trusted
210     {
211         ubyte[IPCRYPT_KEYBYTES] key;
212         ipcrypt_ndx_init(&context, &key[0]);
213     }
214 
215     /// Destructor ensures the IPCryptNDX context is cleaned up.
216     ~this() nothrow @nogc @trusted
217     {
218         ipcrypt_ndx_deinit(&context);
219     }
220 
221     // Disable copying to prevent double-free
222     @disable this(this);
223 
224     /**
225      * Encrypts a 16-byte IP address (IPv4 or IPv6) with extended non-determinism.
226      * Params:
227      *   ip16 = The 16-byte IP address to encrypt.
228      *   random = 16-byte random data for non-determinism.
229      * Returns: The 16-byte encrypted IP address.
230      */
231     ubyte[IPCRYPT_NDX_KEYBYTES] encryptIP16(scope const(ubyte)* ip16, scope const(ubyte)* random) nothrow @nogc @trusted
232     {
233         ubyte[IPCRYPT_NDX_KEYBYTES] result;
234         ipcrypt_ndx_encrypt_ip16(&context, &result[0], &ip16[0], &random[0]);
235         return result;
236     }
237 
238     /**
239     * Decrypt a non-deterministically encrypted 16-byte IP address, previously encrypted with
240     * `ipcrypt_ndx_encrypt_ip16`.333333
241     *
242     * Input is ndip, and output is written to ip16.
243     */
244     ubyte[IPCRYPT_KEYBYTES] decryptIP16(scope const(ubyte)* ndip) nothrow @nogc @trusted
245     {
246         ubyte[IPCRYPT_KEYBYTES] result;
247         ipcrypt_ndx_decrypt_ip16(&context, &result[0], &ndip[0]);
248         return result;
249     }
250 
251     /**
252      * Encrypts an IP address string with extended non-determinism.
253      * Params:
254      *   ipStr = The IP address string to encrypt.
255      *   random = 16-byte random data for non-determinism.
256      * Returns: The encrypted IP address as a string.
257      */
258     string encryptIPStr(ref string ipStr, scope const(ubyte)* random) nothrow @trusted
259     {
260         char[IPCRYPT_NDX_NDIP_STR_BYTES] result;
261         size_t len = ipcrypt_ndx_encrypt_ip_str(&context, &result[0], &ipStr[0], &random[0]);
262         return result[0 .. len].idup;
263     }
264 
265     /**
266      * Decrypts an encrypted IP address string.
267      * Params:
268      *   encryptedIPStr = The encrypted IP address string.
269      * Returns: The decrypted IP address as a string.
270      */
271     string decryptIPStr(ref string encryptedIPStr) nothrow @trusted
272     {
273         char[IPCRYPT_MAX_IP_STR_BYTES] result;
274         size_t len = ipcrypt_ndx_decrypt_ip_str(&context, &result[0], &encryptedIPStr[0]);
275         return result[0 .. len].idup;
276     }
277 }
278 
279 /**
280  * Converts an IP address string to a 16-byte representation.
281  * Params:
282  *   ipStr = The IP address string (IPv4 or IPv6).
283  * Returns: The 16-byte IP address.
284  * Throws: Exception if the conversion fails.
285  */
286 ubyte[IPCRYPT_KEYBYTES] ipStrToIP16(ref string ipStr) @trusted
287 {
288     ubyte[IPCRYPT_KEYBYTES] result;
289     enforce(ipcrypt_str_to_ip16(&result[0], &ipStr[0]) == 0, "Invalid IP address string");
290     return result;
291 }
292 
293 /**
294  * Converts a 16-byte IP address to a string.
295  * Params:
296  *   ip16 = The 16-byte IP address.
297  * Returns: The IP address as a string.
298  */
299 string ip16ToStr(scope const(ubyte)* ip16) nothrow @trusted
300 {
301     char[IPCRYPT_MAX_IP_STR_BYTES] result;
302     size_t len = ipcrypt_ip16_to_str(&result[0], &ip16[0]);
303     return result[0 .. len].idup;
304 }
305 
306 /**
307  * Converts a sockaddr to a 16-byte IP address.
308  * Params:
309  *   sa = The sockaddr structure.
310  * Returns: The 16-byte IP address.
311  * Throws: Exception if the conversion fails.
312  */
313 ubyte[IPCRYPT_KEYBYTES] sockaddrToIP16(scope sockaddr* sa) @trusted
314 {
315     ubyte[IPCRYPT_KEYBYTES] result;
316     enforce(ipcrypt_sockaddr_to_ip16(&result[0], sa) == 0, "Invalid sockaddr");
317     return result;
318 }
319 
320 /**
321  * Converts a 16-byte IP address to a sockaddr_storage.
322  * Params:
323  *   ip16 = The 16-byte IP address.
324  * Returns: The sockaddr_storage structure.
325  */
326 sockaddr_storage ip16ToSockaddr(scope const(ubyte)* ip16) nothrow @nogc @trusted
327 {
328     sockaddr_storage result;
329     ipcrypt_ip16_to_sockaddr(&result, &ip16[0]);
330     return result;
331 }
332 
333 @("Format-Preserving") unittest
334 {
335     import core.stdc.stdio;
336     import core.stdc.string : strcmp;
337 
338     // Test key
339     ubyte[IPCRYPT_KEYBYTES] key = [
340         0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
341         0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51
342     ];
343 
344     // Test IPv4 address
345     string original_ip = "192.168.0.100";
346 
347     // Use wrapper class
348     auto crypt = IPCrypt2(&key[0]);
349 
350     // Perform encryption and decryption
351     auto encrypted = crypt.encryptIPStr(original_ip);
352     auto decrypted = crypt.decryptIPStr(encrypted);
353 
354     // Verify results
355     assert(original_ip == decrypted, "Decryption failed to match original IP");
356     assert(strcmp(&original_ip[0], &encrypted[0]) != 0, "Encryption produced identical output");
357 
358     // Print results
359     printf("Original IP: %s\n", original_ip.toStringz);
360     printf("Encrypted IP: %s\n", encrypted.toStringz);
361     printf("Decrypted IP: %s\n", decrypted.toStringz);
362 }
363 
364 @("Related functions")
365 @safe unittest
366 {
367     import std.random;
368 
369     // Test key for IPCrypt2 (16 bytes)
370     ubyte[IPCRYPT_KEYBYTES] key = [
371         0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
372         0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
373     ];
374 
375     // Test IP address string (IPv4)
376     string ipStr = "192.168.1.1";
377     ubyte[IPCRYPT_KEYBYTES] ip16 = ipStrToIP16(ipStr);
378 
379     // Test 1: RAII lifecycle (init/deinit)
380     {
381         auto crypt = IPCrypt2(&key[0]);
382         // Context is initialized; destructor will call ipcrypt_deinit automatically
383     }
384 
385     // Test 2: Encrypt and decrypt IP16
386     {
387         auto crypt = IPCrypt2(&key[0]);
388         auto encrypted = crypt.encryptIP16(&ip16[0]);
389         auto decrypted = crypt.decryptIP16(&encrypted[0]);
390         assert(decrypted == ip16, "IP16 encryption/decryption failed");
391     }
392 
393     // Test 3: Encrypt and decrypt IP string
394     {
395         auto crypt = IPCrypt2(&key[0]);
396         auto encryptedStr = crypt.encryptIPStr(ipStr);
397         auto decryptedStr = crypt.decryptIPStr(encryptedStr);
398         assert(decryptedStr == ipStr, "IP string encryption/decryption failed");
399     }
400 
401     // Test 4: Non-deterministic encryption/decryption (IP16)
402     {
403         auto crypt = IPCrypt2(&key[0]);
404         ubyte[IPCRYPT_TWEAKBYTES] random;
405         foreach (ref b; random)
406         {
407             b = cast(ubyte) uniform(0, 256);
408         }
409         auto ndEncrypted = crypt.ndEncryptIP16(&ip16[0], &random[0]);
410         auto ndDecrypted = crypt.ndDecryptIP16(&ndEncrypted[0]);
411         assert(ndDecrypted == ip16, "ND IP16 encryption/decryption failed");
412     }
413 
414     // Test 5: Non-deterministic encryption/decryption (IP string)
415     {
416         auto crypt = IPCrypt2(&key[0]);
417         ubyte[IPCRYPT_TWEAKBYTES] random;
418         foreach (ref b; random)
419         {
420             b = cast(ubyte) uniform(0, 256);
421         }
422         auto ndEncryptedStr = crypt.ndEncryptIPStr(&ipStr[0], &random[0]);
423         auto ndDecryptedStr = crypt.ndDecryptIPStr(&ndEncryptedStr[0]);
424         assert(ndDecryptedStr == ipStr, "ND IP string encryption/decryption failed");
425     }
426 
427     // Test 6: Hexadecimal key initialization
428     {
429         string hexKey = "0102030405060708090A0B0C0D0E0F10"; // Matches `key`
430         auto crypt = IPCrypt2(hexKey);
431         auto encrypted = crypt.encryptIP16(&ip16[0]);
432         auto decrypted = crypt.decryptIP16(&encrypted[0]);
433         assert(decrypted == ip16, "Hex key initialization failed");
434     }
435 
436     // Test 7: Invalid hexadecimal key
437     // {
438     //     string invalidHexKey = "invalid_hex_key";
439     //     assertThrown!Exception(IPCrypt2(invalidHexKey), "Expected exception for invalid hex key");
440     // }
441 
442     // Test 8: IP string to IP16 and back
443     {
444         ubyte[IPCRYPT_KEYBYTES] ip16Converted = ipStrToIP16(ipStr);
445         string ipStrConverted = ip16ToStr(&ip16Converted[0]);
446         assert(ipStrConverted == ipStr, "IP string to IP16 conversion failed");
447     }
448 
449     // Test 9: Invalid IP string
450     {
451         string invalidIP = "invalid_ip_address";
452         assertThrown!Exception(ipStrToIP16(invalidIP), "Expected exception for invalid IP string");
453     }
454 }
455 
456 @("IPCryptNDXCtx")
457 @safe unittest
458 {
459     import std.random;
460 
461     // Test key for IPCryptNDXCtx (16 bytes)
462     ubyte[IPCRYPT_KEYBYTES] key = [
463         0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
464         0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
465     ];
466 
467     // Test IP address string (IPv6)
468     string ipStr = "2001:db8::1";
469     ubyte[IPCRYPT_KEYBYTES] ip16 = ipStrToIP16(ipStr);
470 
471     // Test 1: RAII lifecycle (init/deinit)
472     {
473         auto crypt = IPCryptNDXCtx(&key[0]);
474         // Context is initialized; destructor will call ipcrypt_ndx_deinit automatically
475     }
476 
477     // Test 2: Encrypt and decrypt IP16
478     {
479         auto crypt = IPCryptNDXCtx(&key[0]);
480         ubyte[IPCRYPT_KEYBYTES] random;
481         foreach (ref b; random)
482         {
483             b = cast(ubyte) uniform(0, 256);
484         }
485         auto encrypted = crypt.encryptIP16(&ip16[0], &random[0]);
486         auto decrypted = crypt.decryptIP16(&encrypted[0]);
487         assert(decrypted == ip16, "NDX IP16 encryption/decryption failed");
488     }
489 
490     // Test 3: Encrypt and decrypt IP string
491     {
492         auto crypt = IPCryptNDXCtx(&key[0]);
493         ubyte[IPCRYPT_KEYBYTES] random;
494         foreach (ref b; random)
495         {
496             b = cast(ubyte) uniform(0, 256);
497         }
498         auto encryptedStr = crypt.encryptIPStr(ipStr, &random[0]);
499         auto decryptedStr = crypt.decryptIPStr(encryptedStr);
500         assert(decryptedStr == ipStr, "NDX IP string encryption/decryption failed");
501     }
502 
503     // Test 4: Hexadecimal key initialization
504     {
505         string hexKey = "0102030405060708090A0B0C0D0E0F10" ~
506             "1112131415161718191A1B1C1D1E1F20"; // Matches `key`
507         auto crypt = IPCryptNDXCtx(hexKey);
508         ubyte[IPCRYPT_KEYBYTES] random;
509         foreach (ref b; random)
510         {
511             b = cast(ubyte) uniform(0, 256);
512         }
513         auto encrypted = crypt.encryptIP16(&ip16[0], &random[0]);
514         auto decrypted = crypt.decryptIP16(&encrypted[0]);
515         assert(decrypted == ip16, "NDX hex key initialization failed");
516     }
517 
518     // Test 5: Invalid hexadecimal key
519     // {
520     //     string invalidHexKey = "invalid_hex_key";
521     //     assertThrown!Exception(IPCryptNDXCtx(invalidHexKey), "Expected exception for invalid NDX hex key");
522     // }
523 }
524 
525 @("sockaddr conversions")
526 @safe unittest
527 {
528     // Note: Testing sockaddr conversions requires platform-specific setup.
529     // This is a placeholder test; actual sockaddr testing depends on the environment.
530     string ipStr = "127.0.0.1";
531     ubyte[IPCRYPT_KEYBYTES] ip16 = ipStrToIP16(ipStr);
532 
533     // Test ip16ToSockaddr and sockaddrToIP16
534     auto sa = ip16ToSockaddr(&ip16[0]);
535     ubyte[IPCRYPT_KEYBYTES] ip16Converted = sockaddrToIP16(cast(sockaddr*)&sa);
536     assert(ip16Converted == ip16, "sockaddr conversion failed");
537 }