cosmos / cosmjs

The Swiss Army knife to power JavaScript based client solutions ranging from Web apps/explorers over browser extensions to server-side clients like faucets/scrapers.
https://cosmos.github.io/cosmjs/
Apache License 2.0
645 stars 330 forks source link

Implement secure local storage for software wallet #266

Closed ethanfrey closed 4 years ago

ethanfrey commented 4 years ago

Depends on #264

Inspiration:

webmaster128 commented 4 years ago

I don't think we need all the keyring overhead anytime soon. If we stick with one mnemonic per Wallet, we can still have as many accounts as we want in arbitrary dimensions with HD paths. If an application really needs multiple wallets, it can encrypt them individually.

ethanfrey commented 4 years ago

I agree.

How do we handle multiple chains? I assume each has a different bech32 prefix and possibly a different derivation path.

Shall we add createAccount(bech32prefix, derivationPath) to the object, not the interface, so I can record all available keys?

webmaster128 commented 4 years ago

Plugable storage backend (node and browser implementations provided)

Why would we care about storage at all as long as we're not building a wallet? It results in annoying platform-dependent dependencies. We could allow encrypted serialization and leave storage to the entity that controlls storage.

How do we handle multiple chains? I assume each has a different bech32 prefix and possibly a different derivation path.

Shall we add createAccount(bech32prefix, derivationPath) to the object, not the interface, so I can record all available keys?

Sounds good.

ethanfrey commented 4 years ago

Why would we care about storage at all as long as we're not building a wallet? It results in annoying platform-dependent dependencies. We could allow encrypted serialization and leave storage to the entity that controlls storage.

I think we have two choices:

I assume you prefer the first, which has the bonus of us not trying to define some generic storage interface. Can you please define such interfaces?

ethanfrey commented 4 years ago

I would propose a class something like this:

// note this can contain any number of mnemonics and encrypt them all
export class SecureOfflineWallet {
    // maybe we also include initial addKey here at the same time? Or as two ops?
    public static generate(password: string, options?: EncryptOptions): SecureOfflineWallet {}

    public static load(encryptedData: Uint8Array, password: string, options?: EncryptOptions): SecureOfflineWallet {}

    public addKey(bech32prefix: string, path: HdPath) {}

    // this will encrypt current state with the same pasword/EncryptOptions it was generated/loaded with, unless you provide others
    public save(password?: string, options?: EncryptOptions): Uint8Array {}

    // and of course the normal OfflineSigner interface functions
}

export interface EncryptOptions {
    pbdf : "argon2" | "scrypt",
    difficulty: number, // determines time factor for above algorithm
    encrypt: "libsodium", // what do we use for aead???
}

Note you must load the wallet with the same password and EncryptOptions as the last save

webmaster128 commented 4 years ago

👆 is good. I think it makes sense to leave storage up to the application and only do encrypted serialization.

Some additions:

ethanfrey commented 4 years ago

Open question: Do we want to store the EncryptOptions meta information as part of the serialization? Otherwise the data cannot be upgraded easily.

Sounds good. That this is stored on the first save (maybe require this to be set on generate?). Then load and save will default to the same options. save can optionally override them if it wants to save with something better.

in short generate requires options, save can take options to change, load takes no options (reads the metadata from stored data)

ethanfrey commented 4 years ago

The KDF difficulty is algorithm specific and multi-dimensional. For Argon2 they look like

Sure, this can be a string or just any. And I think it only makes sense to support argon2 (the newest and "best") and scrypt (for compatibility). Maybe something else in the future, but no need to make this two complex. We can just define these two types.

And maybe just hardcode the poly-chacha stuff for now.

export type EncryptOptions = Argon2Options | ScryptOptions;

export interface Argon2Options {
    pbdf : "argon2",
    outputLength: number,  // shall we enforce 32 bytes?
    opsLimit: number,
    memLimitKib: number,
}

export interface ScryptOptions {
    pbdf : "scrypt",
    outputLength: number,  // shall we enforce 32 bytes?
    costFactor: number,
    blockSizeFactor: number,
    parallelizationFactor: number,
}

Also, maybe we need to take a salt as an argument?

webmaster128 commented 4 years ago

Also, maybe we need to take a salt as an argument?

The encryption salt (= "IV" or "nonce") is of fixed length and typically prepended or appended to the ciphertext. Libsodium does this intenally for us.

webmaster128 commented 4 years ago

Libsodium does this intenally for us.

This statement of mine is incorrect. We need to prepend it ourselves as described in this example.

ethanfrey commented 4 years ago

Nice one :tada: