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 }