|
9 | 9 | // Global namespace |
10 | 10 | window.ipcrypt = { |
11 | 11 | deterministic: {}, |
| 12 | + pfx: {}, |
12 | 13 | nd: {}, |
13 | 14 | ndx: {} |
14 | 15 | }; |
|
666 | 667 | return bytesToIp(state); |
667 | 668 | }; |
668 | 669 |
|
| 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 | + |
669 | 918 | // ============================ |
670 | 919 | // IPCrypt ND Mode (KIASU-BC) |
671 | 920 | // ============================ |
|
0 commit comments