Closed coyotespike closed 5 years ago
Copying the gist below for posterity.
The dApp-Orbit integration work involves data modeling. The implementation is similar, at least in principle, to the PoC we've already completed. This is known territory.
The pinningService is mostly complete. It is ready to be improved through usage.
The solution to keys is WIP. We have ideas but they are not yet proven.
Dynamic database permissions has several suggested implementations, but is not yet implemented, and there's a fair amount of work to do. This is a known unknown.
State: accepts generic storage, intended to work with LocalStorage. Gen, sign, verify
Problem statement: This library generates a keypair for the local orbit node, and stores it locally. To continue to sign entries (update boards with same identity), a user must transfer the key to the new device.
Problem statement: MetaMask does not give access to keypair.
Problem statement: if we use Ethereum wallet keypair, user must manually sign entries.
Unique username / subdomain is tied to an ethereum address. Can we tie the address to a database, or set of databases?
From Thiago's Notes:
Wallets we need to cater for specifically
Capability-based or role-based model
Whitelist model and signing payloads
This architecture, once fully understood, largely explains the proposed changes :-)
OrbitDB.js uses ipfs-access-controller (which inherits from access-controller) to create and save into IPFS an access-controller.
ipfs-log/log.js, log.append, checks for write permissions. It doesn't verify, but decorates the entry.
ipfs-log/log.js, log.join, checks for write permissions, and also verifies each entry.
append
for our own entries, where we check we're in the write permissions,
and then decorate the entry with signature and public key.join
, where we check write permissions and verify the entries.src/OrbitDB.js
to create the database using a passed-in access controller, or a default.
orbit-db-store
in the same wayipfs-log
so that it does not sign or verify entries
access-controller
so that it is a base class for any access controlleripfs-access-controller
so that it handles signing and verifying, not just storageorbit-access-controller
, contract-access-controller
AccessControllerWithChildKeys
(probably its own epic)Laurent's Gist on OrbitDB Permissions for posterity.
The idea of dogfooding for permissions would be perfect to deal with external systems.
One limitation I see is that it limits us to using orbitdb-backed approach, which might not be as fine grained as we'd like to.
For example, we would like to:
A simple approach would be to let app developpers customize the create entry
and the validate entry
operations:
Could orbitdb implement an inversion of control / middleware approach to let us define different permission protocols?
the IPFS manifest and the (wip) store approaches would be provided as default middlewares. We would be able to define access control specific to our needs & combine them.
Auth-related code would be refactored out of orbitdb ipfs-logs to separate concerns more clearly:
- IPFS: store & Exchange Data
- Orbit Log: update & merge operations
- Access Middleware: add auth info to updates & validate their permissions.
The access middleware would look something like:
interface AccessMiddleware:
isWriteAllowed: (entry) => bool
- Accept or reject entries
decorateEntry: (entry) => entry
- Update an entry payload with permission informations
A few practical examples:
Scenario: Allow everything.
class AccessControllerAllowAll {
this.isWriteAllowed = async (entry) => true
this.decorateEntry = async (entry) => entry
}
Scenario: Allow based on ipfs manifest:
class AccessControllerIPFSManifest {
constructor(key, ipfs, manifestAddress) {
this.keyStore = keyStore;
this.manifest = await loadManifest(ipfs, manifestAddress)
}
async isWriteAllowed(entry) {
if (!this.keyStore.verify(entry, entry.signature) === entry.key) {
return false; // wrong signature
}
// manifest allows this key
return (this.manifest.writes.include('*') || this.manifest.writes.include(entry.key));
}
async decorateEntry(entry) {
entry.key = this.keyStore.publicKey;
entry.signature = this.keyStore.sign(entry);
}
}
Scenario: Allow writes depending on roles defined on chain.
class AccessControllerAllowFromBlockchain {
async isWriteAllowed(entry) {
if (!(await myBlockchainAPI.isUserAdmin(entry.key))) {
return false;
}
return this.keyStore.verify(entry, entry.signature) === entry.key;
}
decorateEntry: (entry) => {
entry.key = this.keyStore.publicKey;
entry.signature = this.keyStore.sign(entry);
return entry;
}
Scenario: We want roles defined on-chain: "I'm the owner of a Colony", "I'm the owner of a user ID". AND we want users to be able to change devices. We don't want to have them sign every orbitdb update with their hardware / metamask wallet.
For this we'd need to verify that a payload was signed by a key (localKey
)
and that this localKey
is owned by the owner of an ethereum address (globalKey
).
const AccessControllerWithChildKeys {
constructor(rootKeyStore) {
this.localKeyStore = generateKeyPairs();
this.localKeySignature = rootKeyStore.sign(this.childKeyStore.publicKey); // happens once
}
async isWriteAllowed(entry) {
// Check the payload was signed by a given local key
const verified = this.localKeyStore.verify(entry, entry.signature) === entry.key;
if (!verified) {
return false;
}
// Check that the local key was signed by the owner of a ressource on chain
const globalKey = this.keyStore.verify(entry.key, entry.rootSignature);
return (await myBlockchainAPI.isUserAdmin(globalKey))
}
async decorateEntry(entry) {
entry.key = this.keyStore.publicKey;
entry.signature = this.localKeyStore.sign(entry);
entry.rootSignature = this.localKeySignature;
return entry;
}
}
We'd pass a controller, either as an instance, or as a factory (that takes this.ipfs
and other parameters).
function myStore() {
const accessController = new AccessControllerWithChildKeys(rootKeyStore);
const store = orbitdb.kvstore({accessController});
}
This would let us:
key, value
, if and only if, they own the rights to the key
.Limitations:
revokeAt(key, time)
to the Access Controller. On change the user can call this and trigger an orbitdb trimming process.Code change (WIP):
Thiago notes.md
NOTE: We can’t plug in a wallet into orbit. Taking actions to sign every entry, it’d be a terrible experience for users and we need to be able to revoke keys, doing that per ethereum account would get pretty annoying too.
NOTE: In the case that ethereum node isn’t up-to-date, it might be necessary to put new entries in some kind of queue to be retried later otherwise they’d get invalidated
NOTE2: Is it possible for us to have a contract that just generates a unique id per entry and sign it if the wallet plugged to the contract is allowed to do the change?
NOTE3: orbit-db-keystore, ipfs-log and access controllers should be changed. ipfs-log shouldn’t know about keystore implementation details (use sign and verify functions passed via constructor to the log instead), the keystore has a few unused methods, the access controller should get the keystore and interface with the ipfs-log instead of passing the keystore to the log
NOTE: Currently, orbit stores users keys on the localStorage. That needs to change!
In the ideal scenario, we generate a master-key from an ethereum account that will be used to manage permissions on the blockchain and that master-key generates child keys that are used to sign entries on orbit (per db instance or per dapp)
This is the minimum set of issues we gotta close to use orbit the way we intend to
Please see this gist for a more complete breakdown. In particular, section 3 explains the architecture