Skip to content

Commit 5584b5c

Browse files
committed
Add ipcrypt-pfx to the playground
1 parent 9d1c0ef commit 5584b5c

File tree

2 files changed

+265
-4
lines changed

2 files changed

+265
-4
lines changed

www/assets/js/lib/ipcrypt.js

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// Global namespace
1010
window.ipcrypt = {
1111
deterministic: {},
12+
pfx: {},
1213
nd: {},
1314
ndx: {}
1415
};
@@ -666,6 +667,254 @@
666667
return bytesToIp(state);
667668
};
668669

670+
// ============================
671+
// IPCrypt PFX Mode (Prefix-preserving)
672+
// ============================
673+
674+
/**
675+
* Check if IP address is IPv4 based on 16-byte representation.
676+
*/
677+
function isIPv4(bytes16) {
678+
return bytes16.slice(0, 10).every(b => b === 0) &&
679+
bytes16[10] === 0xff && bytes16[11] === 0xff;
680+
}
681+
682+
/**
683+
* Extract bit at position from 16-byte array.
684+
* Position: 0 = LSB of byte 15, 127 = MSB of byte 0
685+
*/
686+
function getBit(data, position) {
687+
const byteIndex = 15 - Math.floor(position / 8);
688+
const bitIndex = position % 8;
689+
return (data[byteIndex] >> bitIndex) & 1;
690+
}
691+
692+
/**
693+
* Set bit at position in 16-byte array.
694+
* Position: 0 = LSB of byte 15, 127 = MSB of byte 0
695+
*/
696+
function setBit(data, position, value) {
697+
const byteIndex = 15 - Math.floor(position / 8);
698+
const bitIndex = position % 8;
699+
700+
if (value) {
701+
data[byteIndex] |= (1 << bitIndex);
702+
} else {
703+
data[byteIndex] &= ~(1 << bitIndex);
704+
}
705+
}
706+
707+
/**
708+
* Shift a 16-byte array one bit to the left.
709+
* The most significant bit is lost, and a zero bit is shifted in from the right.
710+
*/
711+
function shiftLeftOneBit(data) {
712+
const result = new Uint8Array(16);
713+
let carry = 0;
714+
715+
// Process from least significant byte (byte 15) to most significant (byte 0)
716+
for (let i = 15; i >= 0; i--) {
717+
// Current byte shifted left by 1, with carry from previous byte
718+
result[i] = ((data[i] << 1) | carry) & 0xFF;
719+
// Extract the bit that will be carried to the next byte
720+
carry = (data[i] >> 7) & 1;
721+
}
722+
723+
return result;
724+
}
725+
726+
/**
727+
* Pad prefix for prefix_len_bits=0 (IPv6).
728+
* Sets separator bit at position 0 (LSB of byte 15).
729+
*/
730+
function padPrefix0() {
731+
const padded = new Uint8Array(16);
732+
padded[15] = 0x01; // Set bit at position 0 (LSB of byte 15)
733+
return padded;
734+
}
735+
736+
/**
737+
* Pad prefix for prefix_len_bits=96 (IPv4).
738+
* Result: 00000001 00...00 0000ffff (separator at pos 96, then 96 bits)
739+
*/
740+
function padPrefix96() {
741+
const padded = new Uint8Array(16);
742+
padded[3] = 0x01; // Set bit at position 96 (bit 0 of byte 3)
743+
padded[14] = 0xFF;
744+
padded[15] = 0xFF;
745+
return padded;
746+
}
747+
748+
/**
749+
* Perform AES-128 encryption on a 16-byte block (for pfx mode)
750+
*/
751+
function aesEncryptPfx(key, input) {
752+
const state = new Uint8Array(input);
753+
const expandedKey = expandKey(key);
754+
755+
// Initial round
756+
for (let i = 0; i < 16; i++) {
757+
state[i] ^= expandedKey[i];
758+
}
759+
760+
// Main rounds
761+
for (let round = 1; round < 10; round++) {
762+
subBytes(state);
763+
shiftRows(state);
764+
mixColumns(state);
765+
for (let i = 0; i < 16; i++) {
766+
state[i] ^= expandedKey[round * 16 + i];
767+
}
768+
}
769+
770+
// Final round
771+
subBytes(state);
772+
shiftRows(state);
773+
for (let i = 0; i < 16; i++) {
774+
state[i] ^= expandedKey[160 + i];
775+
}
776+
777+
return state;
778+
}
779+
780+
ipcrypt.pfx.encrypt = function(ip, key) {
781+
if (!(key instanceof Uint8Array) || key.length !== 32) {
782+
throw new Error('Key must be 32 bytes for pfx mode');
783+
}
784+
785+
// Split the key into two AES-128 keys
786+
const K1 = key.slice(0, 16);
787+
const K2 = key.slice(16, 32);
788+
789+
// Check that K1 and K2 are different
790+
if (K1.every((byte, i) => byte === K2[i])) {
791+
throw new Error('The two halves of the key must be different');
792+
}
793+
794+
// Convert IP to 16-byte representation
795+
const bytes16 = ipToBytes(ip);
796+
797+
// Initialize encrypted result with zeros
798+
const encrypted = new Uint8Array(16);
799+
800+
// Determine starting point
801+
const ipv4 = isIPv4(bytes16);
802+
const prefixStart = ipv4 ? 96 : 0;
803+
804+
// If IPv4, copy the IPv4-mapped prefix
805+
if (ipv4) {
806+
encrypted.set(bytes16.slice(0, 12), 0);
807+
}
808+
809+
// Initialize padded_prefix for the starting prefix length
810+
let paddedPrefix;
811+
if (ipv4) {
812+
paddedPrefix = padPrefix96();
813+
} else {
814+
paddedPrefix = padPrefix0();
815+
}
816+
817+
// Process each bit position
818+
for (let prefixLenBits = prefixStart; prefixLenBits < 128; prefixLenBits++) {
819+
// Compute pseudorandom function with dual AES encryption
820+
const e1 = aesEncryptPfx(K1, paddedPrefix);
821+
const e2 = aesEncryptPfx(K2, paddedPrefix);
822+
823+
// XOR the two encryptions
824+
const e = new Uint8Array(16);
825+
for (let i = 0; i < 16; i++) {
826+
e[i] = e1[i] ^ e2[i];
827+
}
828+
829+
// We only need the least significant bit of byte 15
830+
const cipherBit = e[15] & 1;
831+
832+
// Extract the current bit from the original IP
833+
const currentBitPos = 127 - prefixLenBits;
834+
835+
// Set the bit in the encrypted result
836+
const originalBit = getBit(bytes16, currentBitPos);
837+
setBit(encrypted, currentBitPos, cipherBit ^ originalBit);
838+
839+
// Prepare padded_prefix for next iteration
840+
// Shift left by 1 bit and insert the next bit from bytes16
841+
paddedPrefix = shiftLeftOneBit(paddedPrefix);
842+
const bitToInsert = getBit(bytes16, 127 - prefixLenBits);
843+
setBit(paddedPrefix, 0, bitToInsert);
844+
}
845+
846+
return bytesToIp(encrypted);
847+
};
848+
849+
ipcrypt.pfx.decrypt = function(encryptedIp, key) {
850+
if (!(key instanceof Uint8Array) || key.length !== 32) {
851+
throw new Error('Key must be 32 bytes for pfx mode');
852+
}
853+
854+
// Split the key into two AES-128 keys
855+
const K1 = key.slice(0, 16);
856+
const K2 = key.slice(16, 32);
857+
858+
// Check that K1 and K2 are different
859+
if (K1.every((byte, i) => byte === K2[i])) {
860+
throw new Error('The two halves of the key must be different');
861+
}
862+
863+
// Convert encrypted IP to 16-byte representation
864+
const encryptedBytes = ipToBytes(encryptedIp);
865+
866+
// Initialize decrypted result with zeros
867+
const decrypted = new Uint8Array(16);
868+
869+
// Determine starting point
870+
const ipv4 = isIPv4(encryptedBytes);
871+
const prefixStart = ipv4 ? 96 : 0;
872+
873+
// If IPv4, copy the IPv4-mapped prefix
874+
if (ipv4) {
875+
decrypted.set(encryptedBytes.slice(0, 12), 0);
876+
}
877+
878+
// Initialize padded_prefix for the starting prefix length
879+
let paddedPrefix;
880+
if (prefixStart === 0) {
881+
paddedPrefix = padPrefix0();
882+
} else {
883+
paddedPrefix = padPrefix96();
884+
}
885+
886+
// Process each bit position
887+
for (let prefixLenBits = prefixStart; prefixLenBits < 128; prefixLenBits++) {
888+
// Compute pseudorandom function with dual AES encryption
889+
const e1 = aesEncryptPfx(K1, paddedPrefix);
890+
const e2 = aesEncryptPfx(K2, paddedPrefix);
891+
892+
// XOR the two encryptions
893+
const e = new Uint8Array(16);
894+
for (let i = 0; i < 16; i++) {
895+
e[i] = e1[i] ^ e2[i];
896+
}
897+
898+
// We only need the least significant bit of byte 15
899+
const cipherBit = e[15] & 1;
900+
901+
// Extract the current bit from the encrypted IP
902+
const currentBitPos = 127 - prefixLenBits;
903+
904+
// Set the bit in the decrypted result
905+
const encryptedBit = getBit(encryptedBytes, currentBitPos);
906+
setBit(decrypted, currentBitPos, cipherBit ^ encryptedBit);
907+
908+
// Prepare padded_prefix for next iteration
909+
// Shift left by 1 bit and insert the next bit from decrypted
910+
paddedPrefix = shiftLeftOneBit(paddedPrefix);
911+
const bitToInsert = getBit(decrypted, 127 - prefixLenBits);
912+
setBit(paddedPrefix, 0, bitToInsert);
913+
}
914+
915+
return bytesToIp(decrypted);
916+
};
917+
669918
// ============================
670919
// IPCrypt ND Mode (KIASU-BC)
671920
// ============================

www/pages/playground.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ permalink: /playground/
3030
<label for="encryption-mode" class="block mb-2 font-medium">Encryption Mode:</label>
3131
<select id="encryption-mode" class="w-full p-2 border rounded">
3232
<option value="deterministic">ipcrypt-deterministic</option>
33+
<option value="pfx">ipcrypt-pfx</option>
3334
<option value="nd">ipcrypt-nd</option>
3435
<option value="ndx">ipcrypt-ndx</option>
3536
</select>
@@ -65,6 +66,7 @@ permalink: /playground/
6566
6667
<ul class="list-disc pl-6 mb-4">
6768
<li><strong>ipcrypt-deterministic</strong>: Format-preserving encryption that always produces the same output for the same input and key. Requires a 16-byte (32 hex chars) key.</li>
69+
<li><strong>ipcrypt-pfx</strong>: Prefix-preserving encryption that maintains network structure. Addresses from the same subnet share encrypted prefixes. Requires a 32-byte (64 hex chars) key with different halves.</li>
6870
<li><strong>ipcrypt-nd</strong>: Non-deterministic encryption using KIASU-BC with an 8-byte tweak. Requires a 16-byte (32 hex chars) key and produces a 24-byte output.</li>
6971
<li><strong>ipcrypt-ndx</strong>: Non-deterministic encryption using AES-XTS with a 16-byte tweak. Requires a 32-byte (64 hex chars) key and produces a 32-byte output.</li>
7072
</ul>
@@ -99,6 +101,11 @@ document.addEventListener('DOMContentLoaded', function() {
99101
generateTweakBtn.style.display = 'none';
100102
encryptionKeyInput.placeholder = 'Enter a 16-byte hex key (32 characters)';
101103
keyHelp.textContent = 'For deterministic mode: 16 bytes (32 hex chars). Default key provided for demonstration.';
104+
} else if (this.value === 'pfx') {
105+
tweakContainer.style.display = 'none';
106+
generateTweakBtn.style.display = 'none';
107+
encryptionKeyInput.placeholder = 'Enter a 32-byte hex key (64 characters)';
108+
keyHelp.textContent = 'For pfx mode: 32 bytes (64 hex chars) with different halves. Generate a new key for best results.';
102109
} else {
103110
tweakContainer.style.display = 'block';
104111
generateTweakBtn.style.display = 'inline-flex';
@@ -121,8 +128,8 @@ document.addEventListener('DOMContentLoaded', function() {
121128
const mode = encryptionModeSelect.value;
122129
let keyLength = 32; // 16 bytes = 32 hex chars for deterministic and nd modes
123130

124-
if (mode === 'ndx') {
125-
keyLength = 64; // 32 bytes = 64 hex chars for ndx mode
131+
if (mode === 'ndx' || mode === 'pfx') {
132+
keyLength = 64; // 32 bytes = 64 hex chars for ndx and pfx modes
126133
}
127134

128135
const key = generateRandomHex(keyLength);
@@ -157,7 +164,7 @@ document.addEventListener('DOMContentLoaded', function() {
157164
throw new Error('Please enter an IP address');
158165
}
159166

160-
const expectedKeyLength = mode === 'ndx' ? 32 : 16;
167+
const expectedKeyLength = (mode === 'ndx' || mode === 'pfx') ? 32 : 16;
161168
if (key.length !== expectedKeyLength) {
162169
throw new Error(`Key must be ${expectedKeyLength} bytes (${expectedKeyLength * 2} hex characters) for ${mode} mode`);
163170
}
@@ -168,6 +175,9 @@ document.addEventListener('DOMContentLoaded', function() {
168175
if (mode === 'deterministic') {
169176
console.log('Using deterministic mode');
170177
result = ipcrypt.deterministic.encrypt(ip, key);
178+
} else if (mode === 'pfx') {
179+
console.log('Using pfx mode');
180+
result = ipcrypt.pfx.encrypt(ip, key);
171181
} else {
172182
const tweak = hexToBytes(tweakInput.value.trim());
173183

@@ -205,7 +215,7 @@ document.addEventListener('DOMContentLoaded', function() {
205215
throw new Error('Please enter an encrypted value');
206216
}
207217

208-
const expectedKeyLength = mode === 'ndx' ? 32 : 16;
218+
const expectedKeyLength = (mode === 'ndx' || mode === 'pfx') ? 32 : 16;
209219
if (key.length !== expectedKeyLength) {
210220
throw new Error(`Key must be ${expectedKeyLength} bytes (${expectedKeyLength * 2} hex characters) for ${mode} mode`);
211221
}
@@ -214,6 +224,8 @@ document.addEventListener('DOMContentLoaded', function() {
214224

215225
if (mode === 'deterministic') {
216226
result = ipcrypt.deterministic.decrypt(encryptedValue, key);
227+
} else if (mode === 'pfx') {
228+
result = ipcrypt.pfx.decrypt(encryptedValue, key);
217229
} else {
218230
const tweak = hexToBytes(tweakInput.value.trim());
219231

0 commit comments

Comments
 (0)