Skip to content

Share encrypted data

Share encrypted data by giving the recipient a key, not by re-encrypting the data. Keysmith wraps the key with a grant that only the recipient’s private key can open. Nothing is written on chain or passes through servers: your application delivers and stores it. A tree of keys explains how the keys relate.

Key shared What the recipient can decrypt
Keyspace key Every piece in the keyspace.
Scope key Pieces in that scope and its descendants, including future pieces.
Piece key One piece, effectively read-only.
Share link One piece.

Share a keyspace or scope with a session key

Section titled “Share a keyspace or scope with a session key”

A session key cannot sign as the owner, so it cannot derive encryption keys itself. Instead, the owner wraps a key to the session key’s public key. Share a scope rather than the whole keyspace when the session needs only one branch of your data.

// The session key publishes its key-agreement public key.
const const sessionPublicKey: `0x${string}`sessionPublicKey = import KeysmithKeysmith.
function publicKeyOf(privateKey: Hex): Hex
export publicKeyOf
publicKeyOf
(const sessionPrivateKey: `0x${string}`sessionPrivateKey)
// Share the whole keyspace... const const keyspaceGrant: Keysmith.GrantkeyspaceGrant = await import KeysmithKeysmith.
function wrapTo(recipientPublicKey: Hex, key: Uint8Array, input: Keysmith.GrantDescriptor): Promise<Keysmith.Grant>
export wrapTo
wrapTo
(const sessionPublicKey: `0x${string}`sessionPublicKey, const keyspaceKey: Uint8Array<ArrayBufferLike>keyspaceKey, import KeysmithKeysmith.
function grantDescriptor(ref: Keysmith.KeyspaceRef, node: Keysmith.GrantNode): Keysmith.GrantDescriptor
export grantDescriptor
grantDescriptor
(const ref: Keysmith.KeyspaceRefref, 'keyspace'))
// ...or one scope and the scopes beneath it. const const scopeGrant: Keysmith.GrantscopeGrant = await import KeysmithKeysmith.
function wrapTo(recipientPublicKey: Hex, key: Uint8Array, input: Keysmith.GrantDescriptor): Promise<Keysmith.Grant>
export wrapTo
wrapTo
(
const sessionPublicKey: `0x${string}`sessionPublicKey, import KeysmithKeysmith.
function scopeKey(kk: Uint8Array, scope: string): Uint8Array
export scopeKey
scopeKey
(const keyspaceKey: Uint8Array<ArrayBufferLike>keyspaceKey, 'agent-memory'),
import KeysmithKeysmith.
function grantDescriptor(ref: Keysmith.KeyspaceRef, node: Keysmith.GrantNode): Keysmith.GrantDescriptor
export grantDescriptor
grantDescriptor
(const ref: Keysmith.KeyspaceRefref, 'scope:agent-memory'),
) // Bytes to send or store. Use Base64 or similar over a text channel. const const bytes: Uint8Array<ArrayBufferLike>bytes = import KeysmithKeysmith.
encodeGrant<Keysmith.Grant>(grant: Keysmith.Grant): Uint8Array
export encodeGrant
encodeGrant
(const scopeGrant: Keysmith.GrantscopeGrant)

The grant covers encryption only. To store the encrypted data the session key also needs the CreateDataSet and AddPieces permissions. Always keep the two in step: the Filecoin Pay Console does this for you on filecoin-pin login, but if you do your own session key management or use the owner key directly, you’ll need to take care of this yourself.

The recipient opens the grant with its private key. writeTarget() then picks the key for a new piece: it takes the keyspace from the grant and refuses a scope the grant does not cover.

Anyone can send anyone a grant, so check that it names the owner and keyspace you expect before writing with it.

const const grant: Keysmith.Grant & Record<string, unknown>grant = import KeysmithKeysmith.
function decodeGrant(bytes: Uint8Array): Keysmith.Grant & Record<string, unknown>
export decodeGrant
decodeGrant
(const bytes: Uint8Array<ArrayBufferLike>bytes)
if (const grant: Keysmith.Grant & Record<string, unknown>grant.GrantDescriptor.owner: `0x${string}`owner !== const expected: Keysmith.KeyspaceRefexpected.KeyspaceRef.owner: `0x${string}`owner.String.toLowerCase(): string
Converts all the alphabetic characters in a string to lowercase.
toLowerCase
() || const grant: Keysmith.Grant & Record<string, unknown>grant.GrantDescriptor.keyspace: `0x${string}`keyspace !== const expected: Keysmith.KeyspaceRefexpected.KeyspaceRef.keyspace: `0x${string}`keyspace) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('This grant is for another keyspace')
} const const key: Uint8Array<ArrayBufferLike>key = await import KeysmithKeysmith.
function unwrapWith(privateKey: Hex, input: Keysmith.Grant | Uint8Array): Promise<Uint8Array>
export unwrapWith
unwrapWith
(const sessionPrivateKey: `0x${string}`sessionPrivateKey, const grant: Keysmith.Grant & Record<string, unknown>grant)
// Encrypt a new piece in the agent-memory scope. const const target: Keysmith.WriteTargettarget = import KeysmithKeysmith.
function writeTarget(grant: Keysmith.GrantDescriptor, nodeKey: Uint8Array, scope?: string): Keysmith.WriteTarget
export writeTarget
writeTarget
(const grant: Keysmith.Grant & Record<string, unknown>grant, const key: Uint8Array<ArrayBufferLike>key, 'agent-memory')
const const salt: `0x${string}`salt = import KeysmithKeysmith.
function newSalt(): Hex
export newSalt
newSalt
()
const const encrypted: Uint8Array<ArrayBufferLike>encrypted = await function through(bytes: Uint8Array, transform: ReadableWritablePair<Uint8Array, Uint8Array>): Promise<Uint8Array>through(const original: Uint8Array<ArrayBufferLike>original, import feefee.
function encrypt(options: fee.ChunkedEncryptOptions): ReadableWritablePair<Uint8Array, Uint8Array>
export encrypt
encrypt
({
ChunkedEncryptOptions.cek: Uint8Array<ArrayBufferLike>cek: import KeysmithKeysmith.
function pieceKey(node: Uint8Array, salt: Hex): Uint8Array
export pieceKey
pieceKey
(const target: Keysmith.WriteTargettarget.WriteTarget.key: Uint8Array<ArrayBufferLike>key, const salt: `0x${string}`salt),
ChunkedEncryptOptions.appMetadata?: fee.AppMetadata | undefinedappMetadata: import KeysmithKeysmith.
function pieceMetadata(ref: Keysmith.KeyspaceRef, options: {
    salt: Hex;
    scope?: string;
}): Keysmith.PieceMetadata
export pieceMetadata
pieceMetadata
(const target: Keysmith.WriteTargettarget.WriteTarget.ref: Keysmith.KeyspaceRefref, { salt: `0x${string}`salt, scope?: string | undefinedscope: const target: Keysmith.WriteTargettarget.WriteTarget.scope?: string | undefinedscope }),
ChunkedEncryptOptions.contentLength?: number | undefinedcontentLength: const original: Uint8Array<ArrayBufferLike>original.Uint8Array<ArrayBufferLike>.byteLength: number
The length in bytes of the array.
byteLength
,
})) // Decrypt any piece the grant covers. const const restored: Uint8Array<ArrayBufferLike>restored = await function through(bytes: Uint8Array, transform: ReadableWritablePair<Uint8Array, Uint8Array>): Promise<Uint8Array>through(const downloaded: Uint8Array<ArrayBufferLike>downloaded, import feefee.
function decrypt(key: Uint8Array | fee.KeyResolver): ReadableWritablePair<Uint8Array, Uint8Array>
export decrypt
decrypt
(import KeysmithKeysmith.
function keyResolver(node: Uint8Array, holding?: Keysmith.Holding | Keysmith.GrantDescriptor): (info: {
    appMetadata?: Readonly<Record<string, unknown>>;
}) => Uint8Array
export keyResolver
keyResolver
(const key: Uint8Array<ArrayBufferLike>key, const grant: Keysmith.Grant & Record<string, unknown>grant)))

A recipient holding a scope can share a scope beneath it, for example with a sub-agent, without involving the owner’s wallet. Keys derive only downward, so the sub-agent cannot reach data outside of the narrow area allowed.

const const notes: Keysmith.WriteTargetnotes = import KeysmithKeysmith.
function writeTarget(grant: Keysmith.GrantDescriptor, nodeKey: Uint8Array, scope?: string): Keysmith.WriteTarget
export writeTarget
writeTarget
(const grant: Keysmith.Grantgrant, const key: Uint8Array<ArrayBufferLike>key, 'agent-memory/notes')
const const subGrant: Keysmith.GrantsubGrant = await import KeysmithKeysmith.
function wrapTo(recipientPublicKey: Hex, key: Uint8Array, input: Keysmith.GrantDescriptor): Promise<Keysmith.Grant>
export wrapTo
wrapTo
(
const subAgentPublicKey: `0x${string}`subAgentPublicKey, const notes: Keysmith.WriteTargetnotes.WriteTarget.key: Uint8Array<ArrayBufferLike>key, import KeysmithKeysmith.
function grantDescriptor(ref: Keysmith.KeyspaceRef, node: Keysmith.GrantNode): Keysmith.GrantDescriptor
export grantDescriptor
grantDescriptor
(const notes: Keysmith.WriteTargetnotes.WriteTarget.ref: Keysmith.KeyspaceRefref, 'scope:agent-memory/notes'),
)

There are 2 supported ways to share a single piece:

  • Using a piece grant: securely wraps the piece decryption key to a known recipient (eg another agent running under a different session in your account), the same as any other Keysmith key operation.
  • Using Share links: creates a bearer credential with all necessary key material inside. Less secure, but enables sharing with anyone, even if they have no wallet, special tools, or indeed any relationship with Filecoin at all.

To share one piece and nothing else, wrap that piece’s key. The grant names the piece by its salt; add its PieceCID so the recipient knows what to fetch. This key is effectively decrypt-only because a new upload (eg ‘editing a file’) would change the contents and salt, so a new piece key would be derived on upload.

Anyone holding a key in the hierarchy above the piece can share it. Read the piece’s envelope metadata with parse():

const { const appMetadata: fee.AppMetadata | undefinedappMetadata } = await import feefee.
function parse(source: Uint8Array | fee.RandomAccessSource): Promise<fee.EnvelopeInfo>
export parse
parse
(const encrypted: Uint8Array<ArrayBufferLike>encrypted)
const const metadata: Keysmith.PieceMetadatametadata = const appMetadata: fee.AppMetadata | undefinedappMetadata as import KeysmithKeysmith.
type PieceMetadata = {
    'foc/v': 1;
    'foc/own': Address;
    'foc/ks': Hex;
    'foc/scope'?: string;
    'foc/salt': Hex;
}
export PieceMetadata
PieceMetadata
const const grant: Keysmith.Grantgrant = await import KeysmithKeysmith.
function wrapTo(recipientPublicKey: Hex, key: Uint8Array, input: Keysmith.GrantDescriptor): Promise<Keysmith.Grant>
export wrapTo
wrapTo
(
const recipientPublicKey: `0x${string}`recipientPublicKey, import KeysmithKeysmith.
function keyForEnvelope(node: Uint8Array, envelopeMetadata: Keysmith.PieceMetadata | Readonly<Record<string, unknown>> | undefined, holder?: Keysmith.Holding | Keysmith.GrantDescriptor): Uint8Array
export keyForEnvelope
keyForEnvelope
(const keyspaceKey: Uint8Array<ArrayBufferLike>keyspaceKey, const metadata: Keysmith.PieceMetadatametadata),
import KeysmithKeysmith.
function grantDescriptor(ref: Keysmith.KeyspaceRef, node: Keysmith.GrantNode): Keysmith.GrantDescriptor
export grantDescriptor
grantDescriptor
(const ref: Keysmith.KeyspaceRefref, import KeysmithKeysmith.
function pieceNode(metadata: Pick<Keysmith.PieceMetadata, "foc/salt">): Keysmith.GrantNode
export pieceNode
pieceNode
(const metadata: Keysmith.PieceMetadatametadata)),
) // The PieceCID is informational: it is not part of what the grant authenticates. const const bytes: Uint8Array<ArrayBufferLike>bytes = import KeysmithKeysmith.
encodeGrant<{
    pieceCid: string;
    alg: "ECDH-ES+A256GCM/secp256k1";
    epk: Hex;
    iv: Hex;
    ct: Hex;
    v: 1;
    node: string;
    owner: Address;
    keyspace: Hex;
}>(grant: {
    pieceCid: string;
    alg: "ECDH-ES+A256GCM/secp256k1";
    epk: Hex;
    iv: Hex;
    ct: Hex;
    v: 1;
    node: string;
    owner: Address;
    keyspace: Hex;
}): Uint8Array
export encodeGrant
encodeGrant
({ ...const grant: Keysmith.Grantgrant, pieceCid: stringpieceCid: const pieceCid: PieceCIDpieceCid.CID<unknown, 85, 4113, 1>.toString(base?: MultibaseEncoder<string>): string
Returns a string representation of an object.
toString
() })

The recipient fetches the piece and decrypts it:

const const grant: Keysmith.Grant & Record<string, unknown>grant = import KeysmithKeysmith.
function decodeGrant(bytes: Uint8Array): Keysmith.Grant & Record<string, unknown>
export decodeGrant
decodeGrant
(const bytes: Uint8Array<ArrayBufferLike>bytes)
const const key: Uint8Array<ArrayBufferLike>key = await import KeysmithKeysmith.
function unwrapWith(privateKey: Hex, input: Keysmith.Grant | Uint8Array): Promise<Uint8Array>
export unwrapWith
unwrapWith
(const recipientPrivateKey: `0x${string}`recipientPrivateKey, const grant: Keysmith.Grant & Record<string, unknown>grant)
const const downloaded: Uint8Array<ArrayBufferLike>downloaded = await const synapse: Synapsesynapse.Synapse.storage: StorageManagerstorage.StorageManager.download(options: StorageManagerDownloadOptions): Promise<Uint8Array>download({ pieceCid: string | PieceCIDpieceCid:
var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String
(const grant: Keysmith.Grant & Record<string, unknown>grant.unknownpieceCid) })
const const restored: Uint8Array<ArrayBufferLike>restored = await function through(bytes: Uint8Array, transform: ReadableWritablePair<Uint8Array, Uint8Array>): Promise<Uint8Array>through(const downloaded: Uint8Array<ArrayBufferLike>downloaded, import feefee.
function decrypt(key: Uint8Array | fee.KeyResolver): ReadableWritablePair<Uint8Array, Uint8Array>
export decrypt
decrypt
(import KeysmithKeysmith.
function keyResolver(node: Uint8Array, holding?: Keysmith.Holding | Keysmith.GrantDescriptor): (info: {
    appMetadata?: Readonly<Record<string, unknown>>;
}) => Uint8Array
export keyResolver
keyResolver
(const key: Uint8Array<ArrayBufferLike>key, const grant: Keysmith.Grant & Record<string, unknown>grant)))

A share link shares one piece with someone who has no key pair, such as a person opening a link in a browser. It is a piece grant wrapped to a throwaway key pair made for that link. The throwaway private key, the access key, is handed over too, so whoever holds both can read the piece, like a bearer token.

  • Open link: the link carries the access key, so the link alone opens the piece, like “anyone with the link”.
  • Restricted link: the access key is sent separately, by another channel, so the link alone opens nothing.

The key material sits in the URL fragment, which browsers do not send to any server. The link opens in a viewer that fetches the piece and decrypts it in the browser.

Filecoin Pin creates share links from the command line.

Filecoin Pin encrypts, shares and decrypts without any code:

# Log in with a session key that can encrypt. The console sends it a grant.
filecoin-pin login --scopes createDataSet,addPieces,encrypt

# Encrypt and upload, then share one upload.
filecoin-pin add ./report.pdf --encrypt
filecoin-pin get-share-link <encrypted-root-cid>               # open link
filecoin-pin get-share-link <encrypted-root-cid> --restricted  # link and a separate access key

# The recipient downloads and decrypts.
filecoin-pin get ./out --share '<link>'

See Encrypted Uploads and Share Links in the Filecoin Pin README.

  • Grants cannot be revoked or expire. A shared key keeps opening what it covers, including future pieces under a shared scope.
  • Check who a Grant is from. Ensure that the sender’s key commitment matches the sender you expected. If your app accepts Grants from someone else they could send you a key from the wrong keyspace, and make your data unrecoverable. The Keysmith API (and filecoin-pin utility) provide simple functionality to do this and keep you safe.