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