Introduce a generic keystore

Hello everyone,

I would like to propose a new LIP for the roadmap objective “Improve wallet user experience". This LIP
proposes a standard to encrypt and decrypt information (like users private keys) in the Lisk ecosystem.

I’m looking forward to your feedback.

Here is the complete LIP draft:

LIP: <LIP number>
Title: Introduce a generic keystore
Author: Maxime Gagnebin <maxime.gagnebin@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-a-generic-keystore/360
Type: Informational
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>

Abstract

We describe a format for encrypted information to be used in the Lisk ecosystem. This could be used in the wallet to encrypt a user’s private keys or by the block generator module to store the generator keys.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

A common encryption standard allows different wallets and third party tools to be compatible with each other.

Rationale

The Lisk protocol uses different types of signature schemes for different use cases. For example, transactions must be signed by the account sending it using an Ed25519 signature and commits must be signed using a BLS signature. The proposed keystore is agnostic to the private key type and could allow user facing products to abstract away the signature type from the user. The way private keys are generated from secret recovery phrases is specified in LIP “Introduce tree based key derivation and account recovery”.

Encrypting Secret Recovery Phrases

The secret recovery phrase is a sequence of 12 (or 24) words that follow the BIP 39 standards and that is used to derive all private keys a user might need. Naturally, it is the first thing to be generated and shared with the user to be stored safely. However, it is possible that users lose their phrase and need to back it up once more. For this reason, the keystore proposed below can easily be used to encrypt secret recovery phrases.

In the same encrypted file, we can store metadata indicating how the secret recovery phrase was used and which private keys were already generated with it. This allows users to not only recover all their accounts when importing the encrypted file in a new device, but also to make sure that newly generated private keys are using a new derivation path.

Encrypting Private Keys

The keystore presented below is also designed to encrypt private keys. The reason to encrypt and store private keys directly is two fold. First it improves the efficiency of the signing process. Indeed, if we decrypt the private key directly, there is no need to derive the key again from the secret recovery phrase. Secondly, in the case the device of the user was corrupted, decrypting just one private key would compromise the account linked to this private key, but not the others generated with the same secret recovery phrase.

Specification

The specifications below are inspired from Web3 Secret Storage with the addition of a metadata property which allows to store all needed information regarding the encrypted material.

Encryption File Format

We store secret recovery phrases and private keys in a JSON file following the format below.

keystoreSchema = {
  "type": "object",
  "required": ["encryptedPassphrase", "metadata", "id"],
  "properties": {
    "encryptedPassphrase": {
      "type": "object",
      "properties": {
        "version": {"type": "string"},
        "ciphertext": {"type": "string"},
        "mac": {"type": "string"},
        "kdf": {"type": "string"},
        "kdfparams": {"type": "object"},
        "cipher": {"type": "string"},
        "cipherparams": {"type": "object"}
      }
    },
    "metadata": {
        "name": {"type": "string"},
        "description": {"type": "string"},
        "pubkey": {"type": "string"},
        "address": {"type": "string"},
        "path": {"type": "string"},
        "derivedFromID": {"type": "string"},
        "creationTime": {"type": "string"},
        "pathsUsed": {"type": "array"},
        "tags": {"type": "array"}
    },
    "id": {
        "type": "string",
        "format": "uuid"
    }
  }
}

In the following sections, we describe the uses of the properties of keystoreSchema.

encryptedPassphrase

version

The version is set to "1".

ciphertext

The encrypted message in hexadecimal format.

mac

Computed as SHA256(last 16 bytes of derivation key || ciphertext). It can be used to check the verification key before starting the decryption process.

kdf and kdfparams

The encryption/decryption key is an intermediate key derived from the user password. It is used to generate the secret key for decryption, and verify if the given password is correct. The function, and the params used to derive this key from the password are specified in kdf. The following values of kdf and kdfparams are allowed, depending on the key derivation function:

kdf function kdfparams Definition
“PBKDF2-SHA-256” pbkdf2 {iterations: uint32, salt: string} RFC 2898
“argon2id” argon2id {parallelism: uint32, iterations: uint32, memory: uint32, salt: string} RFC 9106
cipher and cipherparams

The specified function encrypts the secret using the decryption key; to decrypt it, the decryption key along with cipher and cipherparams must be used. If the decryption key is longer than the key size required by the encoding function, it is truncated to the correct number of bits. The following option is supported:

cipher function cipherparams Definition
“AES-256-GCM” aes-256-gcm {iv: string, tag: string} RFC 5116

Note that when using AES-256-GCM, the tag is an output of the encryption and is needed for decryption, this is why it is stored in the cipherparams property.

metadata

All information that is useful when using the file. None of the properties are required and they can be left empty depending on the usage of the encrypted file. Other properties could also be included in the metadata property, but they might not be supported by other implementations of this proposal.

name

A name given by the user to allow easier identification of the file.

description

The description field indicates the nature of the encrypted material. We specify the following description for commonly encrypted messages in Lisk:

Description value Uses
“Secret recovery phrase” The description for secret recovery phrases.
“Ed25519 private key” The description for derived ed25519 private key, encoded as a hex string for encryption.
“BLS private key” The description for derived BLS private key, encoded as a hex string for encryption.

Other descriptions could also be possible, but do not need to be supported by products implementing this proposal.

pubkey

The public key of the key pair. This property is only used if the encoded data is an Ed25519 private key or a BLS private key.

address

The address corresponding to the key pair. This property is only used if the encoded data is an Ed25519 private key.

path

The path used to derive the key pair from the secret recovery phrase. This property is only used if the encoded data is an Ed25519 private key or a BLS private key.

derivedFromID

This property contains the UUID of the file encrypting the corresponding secret recovery phrase.This property is only used if the encoded data is an Ed25519 private key or a BLS private key.

creationTime

Time when the file was created.

pathsUsed

List of paths used with the store recovery phrate to derive key pairs. This property should be used only if the encoded data is a secret recovery phrase. This information is useful to recover all accounts that were generated with this recovery phrase. It is also useful when creating a new account and selecting the next unused path.

tags

List of tags associated with the file.

id

The id property stores a provided uuid (version 4 UUID as specified in RFC 4122), this is a randomly generated ID. It is used if the keystore needs to be referred to. Implementation help: the generation of the id is supported by node.js with the uuid package for example.

Recommended Parameters

Argon2id

We recommend using argon2id (instead of PBKDF2) to derive the encryption key, as it is recognised as a more secure key derivation method (see for example OWASP recommendations). We recommend to follow RFC 9106 for basic parameter choices. Their first recommend options are:

  • iterations=1,
  • parallelism=4 lanes,
  • memory=2048 (2 GiB of RAM),
  • 16 bytes salt,
  • 32 bytes output.

Password Strength General Recommendations

The password submitted by the user should be validated to be long enough and use a variety of numbers and lower and upper case letters (see for example https://www.securden.com/blog/top-10-password-policies.html). Further password requirements can be found in EIP 2335.

PBKDF2

Using PBKDF2 as a KDF is currently implemented in Lisk Elements with 10^6 iterations. PBKDF2 is considered secure, but slightly less future proof than argon2id.

Backwards Compatibility

There are no incompatibilities since the protocol is not changed.

Reference Implementation

TBD

Appendix

Help for Implementation

This audit can help to create a better implementation https://github.com/trailofbits/publications/blob/master/reviews/ETH2DepositCLI.pdf

Examples

Secret Recovery Phrase

Password: testpassword.
Secret recovery phrase: target cancel solution recipe vague faint bomb convince pink vendor fresh patrol.

{
  "encryptedPassphrase": {
    "version": "1",
    "ciphertext": "866c6f1cab3ef67514bdc54cf0143b8b824ebe7c045efb97707c158c81d313cd1a6399b7aa3002248984d39ea2604b0263fe7bdbd8cb04286a9cbd2d353fc79908daab9af04b2528bf4f06a82d79483c",
    "mac": "a476979ca68fe90f3c96f8a5f3f0a9fe33aef8b091d1169861e44a11a680aae9",
    "cipher": "aes-256-gcm",
    "cipherparams": {
      "iv": "da7a74acbf34d20ffd3658f9",
      "tag": "f4282899ed6cb0193e2981dca0d2ae8e"
    },
    "kdf": "argon2id",
    "kdfparams": {
      "parallelism": 4,
      "iterations": 1,
      "memory": 2024,
      "salt": "2d4d7f0b7c68ccd977eae30ee10726f3"
    }
  },
  "metadata": {
    "name": "Maxime",
    "description": "secret recovery phrase",
    "pathsUsed": "m/44'/134'/0'",
  },
  "uuid": "fa3e4ceb-10dc-41ad-810e-17bf51ed93aa"
}

Ed25519 Key Pair

Password: testpassword.
Key pair derived from the secret recovery phrase above, and the path m/44'/134'/0'.

{
  "encryptedPassphrase": {
    "version": "1",
    "ciphertext": "086a59889e0e311422eeb15bb6c753aeead210c4494eb37cf7b8f01b0ed372d64e6a08cc77e0bc8170f79f199e2ce7b4c47fe5353e97e67d53c846c029c6cd08",
    "mac": "9497dd4a84f05c941b22df1cce0cb7558fb3bdd66481c462d63e003dab837c7c",
    "cipher": "aes-256-gcm",
    "cipherparams": { 
      "iv": "aa97507e9f8574b2e7c7ba8b",
      "tag": "4b3362626c82b0ba1b2de62fb84a1e73" 
    },
    "kdf": "argon2id",
    "kdfparams": {
      "parallelism": 4,
      "iterations": 1,
      "memory": 2024,
      "salt": "12209d2c085ccbe40c09bb5a3ec7cefb"
    }
  },
  "metadata": {
    "name": "my lisk account",
    "description": "ed25519 key pair",
    "pubkey": "c6bae83af23540096ac58d5121b00f33be6f02f05df785766725acdd5d48be9d",
    "address": "ed629c34f72e276ba38be61b6f289f84627f2b81",
    "path": "m/44'/134'/0'",
    "derivedFromUUID": "fa3e4ceb-10dc-41ad-810e-17bf51ed93aa",
  },
  "uuid": "ef52c117-d7cc-4246-bc9d-4dd506bef82f"
}

I opened a PR on the LIPs repository.

The PR has been merged.