javascriptnode.jscryptojsapple-business-chat

How to Derive the Key and Initial Vector in Node.js


I have a shared key that I need to derive an iv from so I can decipher.

The apple business chat docs state:

Generate the Derived Key and Initial Vector Run the shared key through the X9.63 Key Derivation Function with SHA256 hash function. This results in a 48-byte payload. Your results should be rV3qrszd0PMPgeRhNnlOYA==

Heres what I tried. I used scryptSync and pbkdf2Sync crypto functions with many 'salt' configurations. I'm unsure if these are the correct functions for this job.

const crypto = require('crypto');

const keyLength = 48;
// sharedKey is a base64 string
const sharedKey = "2lvSJsBO2keUHRfvPG6C1RMUmGpuDbdgNrZ9YD7RYnvAcfgq/fjeYr1p0hWABeif";
// publicKey is a base64 string
const publicKey = "BDiRKNnPiPUb5oala31nkmCaXMB0iyWy3Q93p6fN7vPxEQSUlFVsInkJzPBBqmW1FUIY1KBA3BQb3W3Qv4akZ8kblqbmvupE/EJzPKbROZFBNvxpvVOHHgO2qadmHAjHSg=="

const key1 = crypto.scryptSync(sharedKey, 'salt', keyLength);
console.log(key2.toString('base64'));

const key2 = crypto.pbkdf2Sync(sharedKey, 'salt', 10000, keyLength, 'sha256');
console.log(key2.toString('base64'));

// results should be:
// mAzkYatDlz4SzrCyM23NhgL/+mE3eGgfUz9h1CFPhZM=
// iv: rV3qrszd0PMPgeRhNnlOYA==

Below is the Apple sample code for Deriving the Key and Initial Vector with a X9.63 Key Derivation Function.

    def ITOSP(self, longint, length):
        """ITOSP, short for Integer-to-Octet-String Primitive, converts a non-negative integer
        to an octet string of a specified length. This particular function is defined in the
        PKCS #1 v2.1: RSA Cryptography Standard (June 14, 2002)
        https://www.cryptrec.go.jp/cryptrec_03_spec_cypherlist_files/PDF/pkcs-1v2-12.pdf"""

        hex_string = "%X" % longint
        assert len(hex_string) <= 2 * length, "ITOSP function: Insufficient length for encoding"
        return binascii.a2b_hex(hex_string.zfill(2 * length))

    def KDFX963(self, inbyte_x, shared_data, key_length, hashfunct=sha256, hash_len=32):
        """KDFX963 is a key derivation function (KDF) that takes as input byte sequence inbyte_x
        and additional shared data shared_data and outputs a byte sequence key of length
        key_length. This function is defined in ANSI-X9.63-KDF, and this particular flavor of
        KDF is known as X9.63. You can read more about it from:
        http://www.secg.org/sec1-v2.pdf"""

        assert key_length >= 0, "KDFX963 function: key_length should be positive integer"
        k = key_length / float(hash_len)
        k = int(ceil(k))

        acc_str = ""
        for i in range(1, k+1):
            h = hashfunct()
            h.update(inbyte_x)
            h.update(self.ITOSP(i, 4))
            h.update(shared_data)
            acc_str = acc_str + h.hexdigest()

        return acc_str[:key_length * 2]

Solution

  • X9.63 KDF is a key derivation function, described e.g. here and here. scrypt and PBKDF2 are also KDFs, but different ones, so of course the expected result cannot be reproduced with them.

    So you need a NodeJS library that supports X.963 KDF. If you can't find one, you could also implement your own.

    X9.63 KDF expects a shared secret and a shared info and determines a keysize large key as follows:

    More formally, including various checks, the algorithm is described in the links above. The Python code posted later in the question also implements this logic.

    One possible NodeJS implementation (omitting the checks from the specification) is:

    var crypto = require('crypto');
    
    var digest = 'sha256';
    var digestLen = 32;
    
    function X963KDF(sharedSecret, sharedInfo, keySize){
      var maxCount = Math.ceil(keySize/digestLen);
      var result = Buffer.allocUnsafe(0);
      for (var count = 1; count < maxCount + 1; count++){
          var counter = Buffer.allocUnsafe(4);
          counter.writeUInt32BE(count, 0);
          var current = Buffer.concat([sharedSecret, counter, sharedInfo]);
          var hash = crypto.createHash(digest).update(current).digest();
          result = Buffer.concat([result, hash]);
      }
      
      return result.slice(0, keySize);
    }
    

    Test:

    In the question the shared secret is posted, but not the shared info. An internet search reveals that the posted problem is described e.g. here, so that the shared info can also be determined (nevertheless it would be better if you add this information to your question):

    var sharedSecret = Buffer.from('2lvSJsBO2keUHRfvPG6C1RMUmGpuDbdgNrZ9YD7RYnvAcfgq/fjeYr1p0hWABeif', 'base64')
    var sharedInfo = Buffer.from('04389128d9cf88f51be686a56b7d6792609a5cc0748b25b2dd0f77a7a7cdeef3f111049494556c227909ccf041aa65b5154218d4a040dc141bdd6dd0bf86a467c91b96a6e6beea44fc42733ca6d139914136fc69bd53871e03b6a9a7661c08c74a', 'hex');
    
    var keyiv = X963KDF(sharedSecret, sharedInfo, 48);
    var key = keyiv.slice(0,32).toString('base64');
    var iv = keyiv.slice(32, 48).toString('base64');
    console.log("Key: ", key); // Key:  mAzkYatDlz4SzrCyM23NhgL/+mE3eGgfUz9h1CFPhZM=
    console.log("IV: ", iv); // IV:  rV3qrszd0PMPgeRhNnlOYA==
    

    The generated key and IV are equal to the expected values.