Bridge Champ Integration - part 2 - private keys

For a practical play-to-earn platform where most of the users are not crypto enthusiasts, there is no choice but to manage the users' private keys by one of the platform servers.

To reduce our hack exposure to minimum, Bridge champ relies on the HD wallet functionality provided by the Ardor APIs, it derives all user private/public keys and blockchain addresses from a single seed, so first and foremost, how do we keep this seed secure?

An Ardor seed can be generated by combining two different pieces of secret information the "mnemonic" and the "passphrase". Both of these secrets are typically composed of 12 to 24 random words from a known dictionary. We use this method to provide 2FA setup so that we never need to store the full seed in plain text, the seed will only ever exists in the memory of our super secure server that is never exposed to the web.

We initialize the secure seed using a simple process

  1. "Mnemonic" is loaded from a server configuration file.
  2. "Passphrase" is provided using an API call.
  3. The "mnemonic" and "passphrase" together are used with a bip32 path to compose the cryptographic "root node" from which all account private keys are derived.
    You can learn more about the process here HD Wallet in Ardor

Technically, there are several ways to derive the seed

  1. Using the DeriveAccountFromSeed API which accepts "mnemonic", "passphrase", "bip32Path" parameters and returns the required secret information - while this API provides the necessary functionality there is no point to use it here since we cannot invoke it on a remote node, and thus risk both the Mnemonic and Passphrase to MTM attack, and if we decide invoke it on a local node, we have a better alternative.

  2. Derive the seed directly by adding ardor.jar to our project, since bridge champ already integrates the ardor.jar in its classpath we can use the following code to initialize the root node. This code is triggered after the server is started and loads the "mnemonic", while the boot api invoked manually by operator provides the "passphrase".

    public void boot(String passphrase) {
        seed = KeyDerivation.mnemonicToSeed(mnemonic, passphrase); // mnemonic was loaded from the server config
        var rootPath = isTestnet ? ARDOR_TESTNET_BIP32_ROOT_PATH : ARDOR_MAINNET_BIP32_ROOT_PATH;
        rootNode = KeyDerivation.deriveSeed(rootPath.toString(), seed);
        masterPublicKey = new SerializedMasterPublicKey(rootNode.getSerializedMasterPublicKey());

As you can see, this simple method initializes the root node from which all private keys can be derived without exposing the seed to more than a few microseconds so unless the server itself is completely compromised no access to the seed is available.

Access to the root node is also restricted to the internal server memory, we never store the root node private information in the database, in fact as long as we don't need to sign transactions, it is sufficient to store only the master public key in memory and derive from it the account information for all user accounts.

More on account derivation in the next part of our blog.