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 }