hashgraph / hedera-sdk-js

Hedera™ Hashgraph SDK for JavaScript/TypeScript
https://docs.hedera.com/guides/docs/sdks
Apache License 2.0
255 stars 132 forks source link

Expo: Generating a private key from a mnemonic enters an infinite loop that freezes the application #2458

Closed ivaylonikolov7 closed 1 day ago

ivaylonikolov7 commented 4 weeks ago

Description

This issue happens in Expo environment. When you try to get a private key(it doesn't matter if ECDSA or ED25519) from a mnemonic it enters an infinite loop that causes the application to freeze(both the browser or the simulator freeze/crash). Tested this both on Android and the web version of the expo project. Same outcome. So far I have noticed that the seed phrase doesn't matter and happens on every seed.

This issue isn't reproducible in node.js environment. Only in Expo environment!

Steps to reproduce

Run this code snippet in an Expo project:

 const mnemonic = [
      "outer",
      "drastic",
      "print",
      "brave",
      "edge",
      "student",
      "enemy",
      "matrix",
      "chest",
      "just",
      "away",
      "robust",
      "inspire",
      "indicate",
      "trend",
      "beyond",
      "pony",
      "wood",
      "truly",
      "sword",
      "more",
      "major",
      "regular",
      "vicious",
    ];

    const generatedMnemonic = await Mnemonic.fromWords(mnemonic);

    console.log("Attempting to generate rootPrivateKeyNew3");
    let rootPrivateKeyNew3 =
      await generatedMnemonic.toStandardECDSAsecp256k1PrivateKey();

Start the application via an Android or iOS simulator. Web freezes the browser tab too.

Additional context

No response

Hedera network

mainnet, testnet, previewnet, other

Version

2.48.1 but happens in others too

Operating system

macOS

ivaylonikolov7 commented 4 weeks ago

After thorough investigation this issue happens due to incompatibility of Buffers in environment different than node.js.This is the code that causes this issue:

export async function derive(parentKey, chainCode, index) {
    const isHardened = isHardenedIndex(index);
    const data = new Uint8Array(37);

    const publicKey = hex.decode(
        secp256k1.keyFromPrivate(parentKey).getPublic(true, "hex"),
    );

    // Hardened child
    if (isHardened) {
        // data = 0x00 || ser256(kpar) || ser32(index)
        data[0] = 0x00;
        data.set(parentKey, 1);

        // Normal child
    } else {
        // data = serP(point(kpar)) || ser32(index)
        //      = serP(Kpar) || ser32(index)
        data.set(publicKey, 0);
    }

    new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(
        33,
        index,
        false,
    );

    const I = await hmac.hash(hmac.HashAlgorithm.Sha512, chainCode, data);
    const IL = I.subarray(0, 32);
    const IR = I.subarray(32);

    // if parse256(IL) >= n, proceed with the next value for i
    try {
        // ki = parse256(IL) + kpar (mod n)
        const ki = secp256k1
            .keyFromPrivate(parentKey)
            .getPrivate()
            .add(secp256k1.keyFromPrivate(IL).getPrivate())
            .mod(N);
        const hexZeroPadded = hex.hexZeroPadded(ki.toBuffer(), 32);
        // const ki = Buffer.from(ecc.privateAdd(this.privateKey!, IL)!);

        // In case ki == 0, proceed with the next value for i
        if (ki.eqn(0)) {
            return derive(parentKey, chainCode, index + 1);
        }

        return {
            keyData: hex.decode(hexZeroPadded),
            chainCode: IR,
        };
    } catch {
        return derive(parentKey, chainCode, index + 1);
    }
}

This code is found in cryptography/src/primitive/bip32.native.js. Expo's environment not supporting Buffers causes const hexZeroPadded = hex.hexZeroPadded(ki.toBuffer(), 32); to throw an error which invokes the catch block in the try-catch. The catch block invokes the same derive function, therefore generating a recursive function that never gets resolved. Therefore causing the app to eat all the memory it's given. At some point we run out of memory which freezes the application. Which also makes my laptop turns into a huge heat emitting machine ergo makes me a huge contributor to the climate change.

Anyway... The .toBuffer function code documentation recommends a.toArrayLike(Buffer, endian, length) but this didn't solve the issue. I checked older versions and this happens in older versions too. In the Github history of this file I can't seem to find a version where this behaviour could be different. So I think this issue has been there for a long time. Currently trying to circumvent it using different conversions toString, toArrayLike,toBytes etc but so far with no result.

ivaylonikolov7 commented 2 days ago

Resolved in v.2.51.0