All files / lib/icloud icloud.crypto.ts

99.01% Statements 202/204
92% Branches 23/25
100% Functions 11/11
99.01% Lines 202/204

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 2051x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 151x 151x 151x 151x 151x 1x 1x 1x 1x 1x 114x 114x 114x 114x 1x 1x 1x 1x 1x 86x 86x 86x 86x 1x 1x 1x 1x 1x 94x 94x 94x 1x 1x 1x 1x 82x 82x 82x 82x 133533x 66864x 66864x 133533x 133533x 133533x 133533x 82x 82x 82x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 54x 54x 54x 54x 54x 54x 54x 54x 54x 54x 54x 1x 1x 1x 1x 1x 1x 1x 1x 54x 54x 54x 54x 1x 1x 1x 1x 1x 16x 16x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 2x 2x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 16x 16x 16x 16x 16x 2x 2x 14x 14x 14x 16x     14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 1x 1x 1x 1x 1x 1x 1x 10x 1x 1x 9x 9x 9x 1x  
import crypto from "crypto";
import {Resources} from "../resources/main.js";
import {SRPProtocol} from "../resources/network-types.js";
 
/**
 * The 2048-bit group used by Apple's SRP implementation (RFC 5054, Appendix A)
 */
const SRP_GROUP = {
    N: BigInt(`0x` + [
        `AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 3DB56050`,
        `A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D CD7F48A9 DA04FD50`,
        `E8083969 EDB767B0 CF609517 9A163AB3 661A05FB D5FAAAE8 2918A996 2F0B93B8`,
        `55F97993 EC975EEA A80D740A DBF4FF74 7359D041 D5C33EA7 1D281E44 6B14773B`,
        `CA97B43A 23FB8016 76BD207A 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748`,
        `544523B5 24B0D57D 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6`,
        `AF874E73 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6`,
        `94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F 9E4AFF73`,
    ].join(``).replace(/\s/g, ``)),
    g: 2n,
    /**
     * Byte length of N, used to pad values
     */
    length: 256,
};
 
/**
 * @param parts - The byte arrays to hash
 * @returns The SHA-256 hash of the concatenated parts
 */
function hash(...parts: Uint8Array[]): Buffer {
    const sha = crypto.createHash(`sha256`);
    parts.forEach(part => sha.update(part));
    return sha.digest();
}
 
/**
 * @param value - The value to convert
 * @returns The minimal big-endian byte representation of the value
 */
function toBytes(value: bigint): Buffer {
    const hex = value.toString(16);
    return Buffer.from(hex.length % 2 === 0 ? hex : `0${hex}`, `hex`);
}
 
/**
 * @param value - The value to convert
 * @returns The big-endian byte representation of the value, left-padded with zeros to the byte length of N
 */
function pad(value: bigint): Buffer {
    const bytes = toBytes(value);
    return Buffer.concat([Buffer.alloc(SRP_GROUP.length - bytes.length), bytes]);
}
 
/**
 * @param bytes - The big-endian byte representation
 * @returns The value as bigint
 */
function toBigint(bytes: Uint8Array): bigint {
    return bytes.length === 0 ? 0n : BigInt(`0x${Buffer.from(bytes).toString(`hex`)}`);
}
 
/**
 * @returns base^exponent mod modulus
 */
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
    let result = 1n;
    base %= modulus;
    while (exponent > 0n) {
        if (exponent & 1n) {
            result = result * base % modulus;
        }
 
        exponent >>= 1n;
        base = base * base % modulus;
    }
 
    return result;
}
 
/**
 * This class handles the GSA SRP-6a protocol required for authentication
 * The implementation mirrors Apple's iCloud web client (webSRPClientWorker.js): A, g and S are padded to the length of N, B is used as received and the account name is lowercased when hashed
 */
export class iCloudCrypto {
    /**
     * The account name used within the SRP protocol
     */
    accountName: string;
    /**
     * The client's private ephemeral value (a)
     */
    privateValue: bigint;
    /**
     * The client's public ephemeral value (A = g^a mod N)
     */
    publicValue: bigint;
    /**
     * The shared session key (K) - only available after the proof values have been generated
     */
    sessionKey?: Buffer;
 
    /**
     * Creates a new crypto object and initiates the ephemeral values for this session
     * @param accountName - The account name used within the SRP protocol - defaults to the configured username. The escrow flow uses an empty account name.
     * @param privateValue - The client's private ephemeral value - randomly generated by default, parameterized for testing purposes
     */
    constructor(accountName: string = Resources.manager().username, privateValue: bigint = toBigint(crypto.randomBytes(SRP_GROUP.length))) {
        this.accountName = accountName;
        this.privateValue = privateValue;
        this.publicValue = modPow(SRP_GROUP.g, privateValue, SRP_GROUP.N);
    }
 
    /**
     * @returns A Promise that will resolve to the clients's ephemeral challenge used in the SRP protocol, formatted as a base64 string
     */
    async getClientEphemeral(): Promise<string> {
        return pad(this.publicValue).toString(`base64`);
    }
 
    /**
     * This function will use the PBKDF2 algorithm to derive the password key
     * @param protocol - The protocol to use for hashing the password
     * @param salt - The salt value to use for password hashing as a base64 string
     * @param iterations - Number of iterations to use for key derivation
     * @returns A Promise that will resolve to the derived password key
     */
    async derivePassword(protocol: SRPProtocol, salt: string, iterations: number): Promise<Uint8Array> {
        let passHash: Buffer = hash(Buffer.from(Resources.manager().password, `utf8`));
        if (protocol === `s2k_fo`) {
            passHash = Buffer.from(passHash.toString(`hex`));
        }
 
        const imported = await crypto.subtle.importKey(
            `raw`,
            new Uint8Array(passHash),
            {name: `PBKDF2`},
            false,
            [`deriveBits`],
        );
 
        const derived = await crypto.subtle.deriveBits({
            name: `PBKDF2`,
            hash: `SHA-256`,
            salt: Buffer.from(salt, `base64`),
            iterations,
        }, imported, 256);
 
        return new Uint8Array(derived);
    }
 
    /**
     * Generates the proof values required for authentication
     * @param derivedPassword - The PBKDF2 derived password key
     * @param serverPublicValue - The public value shared by the server - base64 encoded string
     * @param salt - The salt value shared by the server - base64 encoded string
     * @returns A tuple containing the proof values m1 and m2, formatted as base64 strings
     * @throws An error, if the server's public value is invalid
     */
    async getProofValues(derivedPassword: Uint8Array, serverPublicValue: string, salt: string): Promise<[m1: string, m2: string]> {
        const {N, g} = SRP_GROUP;
        const saltBytes = Buffer.from(salt, `base64`);
        const serverPublicBytes = Buffer.from(serverPublicValue, `base64`);
        const B = toBigint(serverPublicBytes);
        if (B % N === 0n) {
            throw new Error(`Invalid server public value`);
        }
 
        const A = pad(this.publicValue);
        const u = toBigint(hash(A, pad(B)));
        if (u === 0n) {
            throw new Error(`Invalid server public value`);
        }
 
        const k = toBigint(hash(toBytes(N), pad(g)));
        // GSA: The account name is not part of the private key
        const x = toBigint(hash(saltBytes, hash(Buffer.from(`:`), derivedPassword)));
        const base = ((B - (k * modPow(g, x, N))) % N + N) % N;
        const S = modPow(base, this.privateValue + (u * x), N);
        this.sessionKey = hash(pad(S));
 
        const hN = hash(toBytes(N));
        const hG = hash(pad(g));
        const hNxorHg = Buffer.from(hN.map((byte, i) => byte ^ hG[i]));
        const hI = hash(Buffer.from(this.accountName.toLowerCase(), `utf8`));
 
        const m1 = hash(hNxorHg, hI, saltBytes, A, serverPublicBytes, this.sessionKey);
        const m2 = hash(A, m1, this.sessionKey);
 
        return [m1.toString(`base64`), m2.toString(`base64`)];
    }
 
    /**
     * The shared session key, which is required by the escrow flow - only available after the proof values have been generated
     * @returns A Promise that will resolve to the session key K, formatted as base64 string
     * @throws An error, if the proof values have not been generated yet
     */
    async getSessionKey(): Promise<string> {
        if (!this.sessionKey) {
            throw new Error(`Session key not available before generating proof values`);
        }
 
        return this.sessionKey.toString(`base64`);
    }
}