Skip to content

Commit

Permalink
chore: Several share improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
icidasset committed Apr 4, 2024
1 parent 0f322fe commit e05dd8e
Show file tree
Hide file tree
Showing 7 changed files with 572 additions and 53 deletions.
39 changes: 26 additions & 13 deletions packages/nest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ A layer around the `wnfs` package that provides a `FileSystem` class, a root tre
- A root tree, holding references to all the needed individual parts (public fs, private forest, exchange, etc)
- A mounting system for private nodes, mount specific paths.
- A unix-fs compatibility layer for the public file system (allows for public files to be viewed through, for example, IPFS gateways)
- Private data sharing helpers
- Private data sharing + helpers
- Provides a transaction system, rewinding the state if an error occurs.
- Creates a private forest automatically with a RSA modules using the Web Crypto API (supported on multiple platforms)
- Creates a private forest automatically with a RSA modulus using the Web Crypto API (supported on multiple platforms)
- In addition to the default symmetric key, use an RSA-OAEP asymmetric key to mount a private node (essentially sharing to self). Can be used to load a private directory, or file, using a passkey + the PRF extension.
- Ability to verify commits to the file system. If a commit, aka. modification, is not verified, it will result in a no-op.
- And more: typed paths, events, path helpers, data casting, …

Expand Down Expand Up @@ -121,11 +122,11 @@ fs.rename
fs.write
```

## Identity
## Identifier

```ts
fs.identity()
fs.assignIdentity('did')
fs.identifier()
fs.assignIdentifier('did')
```

## Private Data Sharing
Expand All @@ -134,32 +135,44 @@ Flow:

1. The receiver of a share register their exchange key. An app could do this automatically when the app starts, or at some other time.
2. The data root of the receiver is passed to the sharer. Ideally this is done through some naming system. For example, you use DNS to map a username to the data root (eg. `TXT file-system.tokono.ma` could resolve to the data root, a CID). That said, this could also be done without a naming system, maybe by presenting a QR code.
3. The sharer creates the share.
4. This step is the reverse of step 2, where we pass the sharer's data root to the receiver.
5. Use the shared item.
3. Make sure the sharer's file system has an identity assigned.
4. The sharer creates the share.
5. This step is the reverse of step 2, where we pass the sharer's data root to the receiver.
6. Use the shared item.

```ts
// Step 1 & 2
// Step 1 & 2 (Receiver)
const { dataRoot } = await fs.registerExchangeKey('key-id', publicKey)
const receiverDataRoot = dataRoot
// Step 3 & 4

// Step 3, 4 & 5 (Sharer)
await fs.assignIdentifier('did')

const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot)
const sharerDataRoot = dataRoot
// Step 5
const { share } = await fs.receive(sharerDataRoot, { publicKey, privateKey })

// Step 6 (Receiver)
const share = await fs.receive(sharerDataRoot, { publicKey, privateKey })

await share.read('utf8')
```

## Manage private node using exchange key pair

Instead of keeping the symmetric capsule key around we can use an exchange key pair to mount a private node. This basically creates a share for ourselves.
Instead of keeping the (symmetric) capsule key around we can use an (asymmetric) exchange key pair to mount a private node. This basically creates a share for ourselves.

```ts
// 🚀 Create & mount
await fs.createPrivateNode({
path: Path.root(),
exchangeKeyPair: { publicKey, privateKey }, // 🔑 Pass in key pair here
})

// 🧳 Load
await fs.mountPrivateNode({
path: Path.root(),
exchangeKeyPair: { publicKey, privateKey },
})
```

## Transactions
Expand Down
76 changes: 59 additions & 17 deletions packages/nest/src/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,29 @@ export class FileSystem {
* @param mutationOptions
* @group Mounting
*/
async createPrivateNode(
node: {
path: Path.Distinctive<Path.Segments>
exchangeKeyPair: {
publicKey: CryptoKey | Uint8Array
privateKey: CryptoKey
}
},
mutationOptions?: MutationOptions
): Promise<{
path: Path.Distinctive<Path.Partitioned<Private>>
capsuleKey: Uint8Array
shareId: string
}>
async createPrivateNode(
node: {
path: Path.Distinctive<Path.Segments>
},
mutationOptions?: MutationOptions
): Promise<{
path: Path.Distinctive<Path.Partitioned<Private>>
capsuleKey: Uint8Array
}>
async createPrivateNode(
node: {
path: Path.Distinctive<Path.Segments>
Expand All @@ -274,6 +297,7 @@ export class FileSystem {
): Promise<{
path: Path.Distinctive<Path.Partitioned<Private>>
capsuleKey: Uint8Array
shareId?: string
}> {
const { path } = node
const absolutePosixPath = Path.toPosix(path, { absolute: true })
Expand Down Expand Up @@ -354,7 +378,15 @@ export class FileSystem {

// Share to self, pt. 2
if (node.exchangeKeyPair !== undefined) {
await this.share(pathWithPartition, dataRoot, { mutationOptions })
const { shareId } = await this.share(pathWithPartition, dataRoot, {
mutationOptions,
})

return {
path: pathWithPartition,
capsuleKey: accessKey.toBytes(),
shareId,
}
}

// Fin
Expand Down Expand Up @@ -382,6 +414,7 @@ export class FileSystem {
publicKey: CryptoKey | Uint8Array
privateKey: CryptoKey
}
shareId?: string
}
): Promise<void> {
await this.mountPrivateNodes([node])
Expand Down Expand Up @@ -409,6 +442,7 @@ export class FileSystem {
publicKey: CryptoKey | Uint8Array
privateKey: CryptoKey
}
shareId?: string
}
>
): Promise<void> {
Expand All @@ -432,6 +466,7 @@ export class FileSystem {
await this.calculateDataRoot(),
args.exchangeKeyPair,
{
shareId: args.shareId,
sharerBlockstore: this.#blockstore,
}
).then((a) => a.sharedNode)
Expand Down Expand Up @@ -780,8 +815,8 @@ export class FileSystem {
)
}

// IDENTITY
// --------
// IDENTIFIER
// ----------

async assignIdentifier(
did: string,
Expand Down Expand Up @@ -828,14 +863,14 @@ export class FileSystem {
* NOTE: A share can only be received if the exchange key was registered
* and the receiver is in possession of the associated private key.
*
* @param itemName
* @param sharerDataRoot The data root CID from the sharer
* @param exchangeKeyPair A RSA-OAEP-256 key pair
* @param exchangeKeyPair.publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes
* @param exchangeKeyPair.privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey`
* @param opts Optional overrides
* @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system
* @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system
* @param opts.shareId Specify what shareId to use, otherwise this'll load the last share that was made to the given exchange key.
* @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system.
* @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system.
*
* @group Sharing
*/
Expand All @@ -846,21 +881,16 @@ export class FileSystem {
privateKey: CryptoKey
},
opts: {
shareId?: string
sharerBlockstore?: Blockstore
sharerRootTreeClass?: typeof RootTree
} = {}
): Promise<{
share: Share
}> {
const share = await this.#transactionContext().receive(
): Promise<Share> {
return await this.#transactionContext().receive(
sharerDataRoot,
exchangeKeyPair,
opts
)

return {
share,
}
}

/**
Expand Down Expand Up @@ -914,14 +944,26 @@ export class FileSystem {
receiverBlockstore?: Blockstore
receiverRootTreeClass?: typeof RootTree
} = {}
): Promise<MutationResult<Private>> {
return await this.#infusedTransaction(
): Promise<{ shareId: string } & MutationResult<Private>> {
let shareId: string | undefined

const result = await this.#infusedTransaction(
async (t) => {
await t.share(path, receiverDataRoot, opts)
const shareResult = await t.share(path, receiverDataRoot, opts)
shareId = shareResult.shareId
},
path,
opts.mutationOptions
)

if (shareId === undefined) {
throw new Error('`shareId` was not set')
}

return {
...result,
shareId,
}
}

// TRANSACTIONS
Expand Down
15 changes: 15 additions & 0 deletions packages/nest/src/share.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Blockstore } from 'interface-blockstore'
import type { PrivateNode } from 'wnfs'

import * as Queries from './queries.js'
import * as Path from './path.js'
Expand All @@ -22,30 +23,44 @@ import { dataFromBytes } from './data.js'
// CLASS

export class Share {
readonly id: string

readonly #blockstore: Blockstore
readonly #privateNodes: MountedPrivateNodes
readonly #rootTree: RootTree
readonly #rng: Rng

/**
* @param id
* @param blockstore
* @param privateNodes
* @param rng
* @param rootTree
* @internal
*/
constructor(
id: string,
blockstore: Blockstore,
privateNodes: MountedPrivateNodes,
rng: Rng,
rootTree: RootTree
) {
this.id = id
this.#blockstore = blockstore
this.#privateNodes = privateNodes
this.#rng = rng
this.#rootTree = rootTree
}

// EXPORT

export(): PrivateNode {
const node = this.#privateNodes['/']
if (node === undefined)
throw new Error('Expected a node to be mounted at root')
return node.node
}

// QUERIES

/**
Expand Down
31 changes: 22 additions & 9 deletions packages/nest/src/sharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ExchangeKey } from './exchange-key.js'
* @param exchangeKeyPair.publicKey
* @param exchangeKeyPair.privateKey
* @param opts
* @param opts.shareId
* @param opts.sharerBlockstore
* @param opts.sharerRootTreeClass
*/
Expand All @@ -34,10 +35,12 @@ export async function loadShare(
privateKey: CryptoKey
},
opts: {
shareId?: string
sharerBlockstore: Blockstore
sharerRootTreeClass?: typeof RootTree
}
): Promise<{
shareId: string
sharedNode: PrivateNode
sharerRootTree: RootTree
}> {
Expand All @@ -60,14 +63,20 @@ export async function loadShare(
throw new Error("The sharer's file system is missing an identifier")

// Find the share number
const shareNumber: bigint = await findLatestShareCounter(
0,
sharerCounter < 1 ? 1 : sharerCounter,
publicKeyResult,
sharerIdentifier,
sharerForest,
Store.wnfs(sharerBlockstore)
)
const shareNumber: undefined | number | bigint =
opts.shareId === undefined
? await findLatestShareCounter(
0,
sharerCounter < 1 ? 1 : sharerCounter,
publicKeyResult,
sharerIdentifier,
sharerForest,
Store.wnfs(sharerBlockstore)
)
: Number.parseInt(opts.shareId)

if (shareNumber === undefined)
throw new Error('Failed to determine share number')

// Determine share name
const shareLabel = createShareName(
Expand Down Expand Up @@ -95,5 +104,9 @@ export async function loadShare(
Store.wnfs(sharerBlockstore)
)

return { sharedNode, sharerRootTree }
return {
shareId: Number(shareNumber).toString(),
sharedNode,
sharerRootTree,
}
}
Loading

0 comments on commit e05dd8e

Please sign in to comment.