Closed ethanfrey closed 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.
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?
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.
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?
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
👆 is good. I think it makes sense to leave storage up to the application and only do encrypted serialization.
Some additions:
encryptionKey = kdf(password)
in https://github.com/iov-one/iov-core/blob/v2.5.0/packages/iov-keycontrol/src/keyringencryptor.ts#L17-L68. This allows an application to send the password to a webworker, compute the KDF there (while having fancy loading animation on the UI thread) and getting the encryptionKey
response.// not great but can be used on the main thread
const passwordHashingOptionsVersion2: Argon2idOptions = {
outputLength: 32,
opsLimit: 11,
memLimitKib: 8 * 1024,
};
We could store them in a string of an algorithm-specific format.
EncryptOptions
meta information as part of the serialization? Otherwise the data cannot be upgraded easily.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)
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?
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.
Libsodium does this intenally for us.
This statement of mine is incorrect. We need to prepend it ourselves as described in this example.
Nice one :tada:
Depends on #264
Inspiration: