From ba35e6a8a75c974b5a1f9e10e8152533d8c13ea3 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Wed, 17 Apr 2024 17:01:24 +0200 Subject: [PATCH] wip --- packages/nest/README.md | 59 +++++++++++++++++++++++++++++++- packages/nest/src/class.ts | 34 ++++++++++-------- packages/nest/src/share.ts | 8 ++--- packages/nest/src/sharing.ts | 38 ++++++++++---------- packages/nest/src/transaction.ts | 52 ++++++++++++++++++++++------ packages/nest/test/class.test.ts | 53 ++++++++++++++++++++++++++-- 6 files changed, 192 insertions(+), 52 deletions(-) diff --git a/packages/nest/README.md b/packages/nest/README.md index 01c57f5..df23797 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -144,7 +144,7 @@ const { dataRoot } = await fs.registerExchangeKey('key-id', publicKey) const receiverDataRoot = dataRoot // Step 3, 4 & 5 (Sharer) -await fs.assignIdentifier('did') +await fs.assignIdentifier('id') const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot) const sharerDataRoot = dataRoot @@ -155,6 +155,63 @@ const share = await fs.receive(sharerDataRoot, { publicKey, privateKey }) await share.read('utf8') ``` +## Receiving older/multiple shares + +By default when using `fs.receive()` you'll receive the latest share made to the given public key and matching the identifier of the sharer. If you want to load older, or multiple, shares, you'll have to provide the share index. + +```ts +// `share` gives you the `shareIndex`, along with the `dataRoot` +const { shareIndex, dataRoot } = await fs.share( + pathToPrivateItem, + receiverDataRoot +) + +// This can then be used by the receiver +const share = await fs.receive( + sharerDataRoot, + { publicKey, privateKey }, + { shareIndex } +) +``` + +If you don't want to pass along the share index, you can see if any shares are available for a given number (starts from zero). + +```ts +// To do this, load the file system of the sharer. +const sharerFs = await FileSystem.fromCID(sharerDataRoot, { blockstore }) + +const bool = await sharerFs.hasShare({ + exchangePublicKey, + shareIndex: 0, +}) + +if (bool === true) + console.log(`Share with index '0' exists for public key: ${publicKey}`) +``` + +## Sharing a private file with a custom identifier + +Instead of using `fs.assignIdentifier()`, you can pass an identifier straight to the `fs.share()` function. Do note that this identifier will have to be communicated to the receiver of the share, which is not the case when using `fs.assignIdentifier()`. Unless of course the receiver can somehow determine this identifier themselves. + +```ts +// Receiver +const { dataRoot } = await fs.registerExchangeKey('key-id', publicKey) +const receiverDataRoot = dataRoot + +// Sharer +const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot, { + identifier: 'id', +}) +const sharerDataRoot = dataRoot + +// Receiver +const share = await fs.receive( + sharerDataRoot, + { publicKey, privateKey }, + { identifier: 'id' } +) +``` + ## Manage private node using exchange key pair 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. diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index eb20354..ffc9474 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -275,7 +275,7 @@ export class FileSystem { ): Promise<{ path: Path.Partitioned capsuleKey: Uint8Array - shareId: string + shareIndex: number }> async createPrivateNode( node: { @@ -300,7 +300,7 @@ export class FileSystem { ): Promise<{ path: Path.Partitioned capsuleKey: Uint8Array - shareId?: string + shareIndex?: number }> { const { kind, path } = node const absolutePosixPath = Path.toPosix(path, { absolute: true }) @@ -382,14 +382,14 @@ export class FileSystem { // Share to self, pt. 2 if (node.exchangeKeyPair !== undefined) { - const { shareId } = await this.share(pathWithPartition, dataRoot, { + const { shareIndex } = await this.share(pathWithPartition, dataRoot, { mutationOptions, }) return { path: pathWithPartition, capsuleKey: accessKey.toBytes(), - shareId, + shareIndex, } } @@ -418,7 +418,7 @@ export class FileSystem { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } - shareId?: string + shareIndex?: number } ): Promise { await this.mountPrivateNodes([node]) @@ -446,7 +446,7 @@ export class FileSystem { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } - shareId?: string + shareIndex?: number } > ): Promise { @@ -470,7 +470,7 @@ export class FileSystem { await this.calculateDataRoot(), args.exchangeKeyPair, { - shareId: args.shareId, + shareIndex: args.shareIndex, sharerBlockstore: this.#blockstore, } ).then((a) => a.sharedNode) @@ -854,7 +854,8 @@ export class FileSystem { * @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.shareId Specify what shareId to use, otherwise this'll load the last share that was made to the given exchange key. + * @param opts.identifier Specify which identifier was used for the share, by default it looks up the sharer's root identifier. + * @param opts.shareIndex Specify what share index 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. * @@ -867,7 +868,8 @@ export class FileSystem { privateKey: CryptoKey }, opts: { - shareId?: string + identifier?: string + shareIndex?: number sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} @@ -916,6 +918,7 @@ export class FileSystem { * @param path Path to the private file or directory to share (with 'private' prefix) * @param receiverDataRoot Data root CID of the receiver * @param opts Optional overrides + * @param opts.identifier Provide another id to use instead the root identifier * @param opts.receiverBlockstore Specify what blockstore to use to load the receiver's file system * @param opts.receiverRootTreeClass Specify what root tree class was used for the receiver's file system * @param opts.mutationOptions Mutation options @@ -926,29 +929,30 @@ export class FileSystem { path: Partitioned, receiverDataRoot: CID, opts: { + identifier?: string mutationOptions?: MutationOptions receiverBlockstore?: Blockstore receiverRootTreeClass?: typeof RootTree } = {} - ): Promise<{ shareId: string } & MutationResult> { - let shareId: string | undefined + ): Promise<{ shareIndex: number } & MutationResult> { + let shareIndex: number | undefined const result = await this.#infusedTransaction( async (t) => { const shareResult = await t.share(path, receiverDataRoot, opts) - shareId = shareResult.shareId + shareIndex = shareResult.shareIndex }, path, opts.mutationOptions ) - if (shareId === undefined) { - throw new Error('`shareId` was not set') + if (shareIndex === undefined) { + throw new Error('`shareIndex` was not set') } return { ...result, - shareId, + shareIndex, } } diff --git a/packages/nest/src/share.ts b/packages/nest/src/share.ts index 8286e39..7129e01 100644 --- a/packages/nest/src/share.ts +++ b/packages/nest/src/share.ts @@ -23,7 +23,7 @@ import { dataFromBytes } from './data.js' // CLASS export class Share { - readonly id: string + readonly index: number readonly #blockstore: Blockstore readonly #privateNodes: MountedPrivateNodes @@ -31,7 +31,7 @@ export class Share { readonly #rng: Rng /** - * @param id + * @param index * @param blockstore * @param privateNodes * @param rng @@ -39,13 +39,13 @@ export class Share { * @internal */ constructor( - id: string, + index: number, blockstore: Blockstore, privateNodes: MountedPrivateNodes, rng: Rng, rootTree: RootTree ) { - this.id = id + this.index = index this.#blockstore = blockstore this.#privateNodes = privateNodes this.#rng = rng diff --git a/packages/nest/src/sharing.ts b/packages/nest/src/sharing.ts index 985af41..a16d79c 100644 --- a/packages/nest/src/sharing.ts +++ b/packages/nest/src/sharing.ts @@ -24,7 +24,8 @@ import { ExchangeKey } from './exchange-key.js' * @param exchangeKeyPair.publicKey * @param exchangeKeyPair.privateKey * @param opts - * @param opts.shareId + * @param opts.identifier + * @param opts.shareIndex * @param opts.sharerBlockstore * @param opts.sharerRootTreeClass */ @@ -35,12 +36,13 @@ export async function loadShare( privateKey: CryptoKey }, opts: { - shareId?: string + identifier?: string + shareIndex?: number sharerBlockstore: Blockstore sharerRootTreeClass?: typeof RootTree } ): Promise<{ - shareId: string + shareIndex: number sharedNode: PrivateNode sharerRootTree: RootTree }> { @@ -58,30 +60,30 @@ export async function loadShare( const sharerForest = sharerRootTree.privateForest() const sharerCounter = sharerRootTree.shareCounter() - const sharerIdentifier = sharerRootTree.did() - if (sharerIdentifier === undefined) + const identifier = opts.identifier ?? sharerRootTree.did() + + if (identifier === undefined) throw new Error("The sharer's file system is missing an identifier") // Find the share number 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) + opts.shareIndex ?? + (await findLatestShareCounter( + 0, + sharerCounter < 1 ? 1 : sharerCounter, + publicKeyResult, + identifier, + sharerForest, + Store.wnfs(sharerBlockstore) + )) if (shareNumber === undefined) - throw new Error('Failed to determine share number') + throw new Error('Cannot find any share with these parameters') // Determine share name const shareLabel = createShareName( Number(shareNumber), - sharerIdentifier, + identifier, publicKeyResult, sharerForest ) @@ -105,7 +107,7 @@ export async function loadShare( ) return { - shareId: Number(shareNumber).toString(), + shareIndex: Number(shareNumber), sharedNode, sharerRootTree, } diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index 8213192..5b8eedf 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -552,6 +552,31 @@ export class TransactionContext { // SHARING + // TODO: Missing a piece in wnfs-wasm + // + // async hasShare(opts: { + // exchangePublicKey: CryptoKey | Uint8Array + // identifier?: string + // shareIndex: number + // }) { + // const publicKeyResult = + // opts.exchangePublicKey instanceof CryptoKey + // ? await new ExchangeKey(opts.exchangePublicKey).publicKeyModulus() + // : opts.exchangePublicKey + + // const identifier = opts.identifier ?? this.identifier() + // if (identifier === undefined) return false + + // const shareLabel = createShareName( + // opts.shareIndex, + // identifier, + // publicKeyResult, + // this.#rootTree.privateForest() + // ) + + // this.#rootTree.privateForest().has() + // } + /** * Check if an exchange key was already registered. * @@ -600,7 +625,8 @@ export class TransactionContext { * @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.shareId Specify what shareId to use, otherwise this'll load the last share that was made to the given exchange key. + * @param opts.identifier Specify which identifier was used for the share, by default it looks up the sharer's root identifier. + * @param opts.shareIndex Specify what share index 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. * @@ -613,25 +639,27 @@ export class TransactionContext { privateKey: CryptoKey }, opts: { - shareId?: string + identifier?: string + shareIndex?: number sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} ): Promise { const sharerBlockstore = opts.sharerBlockstore ?? this.#blockstore - const { shareId, sharedNode, sharerRootTree } = await loadShare( + const { shareIndex, sharedNode, sharerRootTree } = await loadShare( sharerDataRoot, exchangeKeyPair, { + identifier: opts.identifier, sharerBlockstore, - shareId: opts.shareId, + shareIndex: opts.shareIndex, sharerRootTreeClass: opts.sharerRootTreeClass, } ) // Create share context return new Share( - shareId, + shareIndex, sharerBlockstore, { '/': { path: Path.root(), node: sharedNode } }, this.#rng, @@ -681,6 +709,7 @@ export class TransactionContext { * @param path Path to the private file or directory to share (with 'private' prefix) * @param receiverDataRoot Data root CID of the receiver * @param opts Optional overrides + * @param opts.identifier Provide another id to use instead the root identifier * @param opts.receiverBlockstore Specify what blockstore to use to load the receiver's file system * @param opts.receiverRootTreeClass Specify what root tree class was used for the receiver's file system * @@ -690,15 +719,16 @@ export class TransactionContext { path: Partitioned, receiverDataRoot: CID, opts: { + identifier?: string receiverBlockstore?: Blockstore receiverRootTreeClass?: typeof RootTree } = {} - ): Promise<{ shareId: string }> { - const did = this.identifier() + ): Promise<{ shareIndex: number }> { + const id = opts.identifier ?? this.identifier() - if (did === undefined) + if (id === undefined) throw new Error( - "Identifier wasn't set yet. Set one first using `assignIdentifier`." + "Identifier wasn't set yet. Set one first using `assignIdentifier` or provide one using the options object." ) // Access key @@ -744,7 +774,7 @@ export class TransactionContext { const forest: PrivateForest = await share( AccessKey.fromBytes(key), counter, - did, + id, exchangeRoot, this.#rootTree.privateForest(), mergedBlockstore @@ -766,7 +796,7 @@ export class TransactionContext { // Fin return { - shareId: counter.toString(), + shareIndex: counter, } } diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 3aa7aa7..f7dbc53 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -1063,6 +1063,53 @@ describe('File System Class', () => { assert.equal(content, '🔒') }) + it('can share and receive a file with a custom identifier', async () => { + const keypair = await ExchangeKey.generate() + const identifierA = Date.now().toString() + const identifierB = `${identifierA}-2` + + const receiverFs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + // Register exchange key + const a = await receiverFs.registerExchangeKey('device', keypair.publicKey) + const receiverDataRoot = a.dataRoot + + // Make shares + await fs.write(Path.priv('a'), 'utf8', '🚀') + await fs.write(Path.priv('b'), 'utf8', 'ðŸŠī') + + await fs.share(Path.priv('a'), receiverDataRoot, { + identifier: identifierA, + }) + + await fs.share(Path.priv('b'), receiverDataRoot, { + identifier: identifierB, + }) + + const sharerDataRoot = await fs.calculateDataRoot() + + // Receive share A + const shareA = await receiverFs.receive(sharerDataRoot, keypair, { + identifier: identifierA, + }) + + assert.equal(await shareA.read('utf8'), '🚀') + + // Receive share B + const shareB = await receiverFs.receive(sharerDataRoot, keypair, { + identifier: identifierB, + }) + + assert.equal(await shareB.read('utf8'), 'ðŸŠī') + }) + + it('can load the file system of the sharer and see it they have a particular share', async () => { + // TODO: Missing a piece in wnfs-wasm + }) + it('can mount a share made to self', async () => { const keypair = await ExchangeKey.generate() @@ -1214,7 +1261,7 @@ describe('File System Class', () => { await fs.write(Path.priv('file 1'), 'utf8', '🔐 1') await fs.write(Path.priv('file 2'), 'utf8', '🔒 2') - const { shareId } = await fs.share(Path.priv('file 1'), receiverDataRoot) + const { shareIndex } = await fs.share(Path.priv('file 1'), receiverDataRoot) await fs.share(Path.priv('file 2'), receiverDataRoot) @@ -1222,7 +1269,7 @@ describe('File System Class', () => { const sharerDataRoot = await fs.calculateDataRoot() const content1 = await receiverFs - .receive(sharerDataRoot, keypairA, { shareId }) + .receive(sharerDataRoot, keypairA, { shareIndex }) .then(async (share) => await share.read('utf8')) const content2 = await receiverFs @@ -1266,7 +1313,7 @@ describe('File System Class', () => { await fsInstance.mountPrivateNode({ path: ['first'], exchangeKeyPair: keypair, - shareId: first.shareId, + shareIndex: first.shareIndex, }) await fsInstance.mountPrivateNode({