Elfin Recryptor

A new function in content delivery

CDN providers run recryptors to serve the authors and audience.

The authors and audience connect to recryptor enclaves through https links directly. Even the CDN providers do not know what are transferred through the TLS tunnel.

The recryptor acts as a gRPC client to get IPFS data from a RPCX server run by the same CDN provider.

Serve the author

Start an encryption task

A GET request should be sent to such a URL:

/eg_getEncryptTaskToken?fileId=<hex-string>&sig=<hex-encoded-signature>

It returns a json string with three fields:

  1. encrypt_task_token, an encryption task token (base58-encoded)
  2. recryptorsalt: a recryptorsalt (base64-encoded)
  3. pubkey: the recryptor’s pubkey (base64-encoded)

The fileId is a unique id the author assigned to his files. It can be sha256sum or IPFS CID, or anything the author likes.

The author endorses the fileId with a signature sig, which is generated using MetaMask’s personal_sign. The signed text is:

To Recryptor: fileId=<hex-string-with-0x-prefix>

The recryptor salt is a true random number generated by the recryptor enclave. It will be used in decryption.

Get encrypted parts from authorizers

A POST request should be sent to such a URL:

/eg_getEncryptedParts?token=<base58-string>

The encryption task token is given as the token parameter. The body of the request is a json string with following schema:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "ElfinGuard Encryption Guide",
    "description": "Instructions Used by Recryptors to Decrypt Files",
    "type": "object",
    "properties": {
         "chainid": {
             "type": "string",
             "description": "a hex string indicating the target chain's ID"
         },
         "contract": {
             "type": "string",
             "description": "the EVM address of the authorization contract"
         },
         "function": {
             "type": "string",
             "description": "the signature of the function to be called"
         },
         "threshold": {
             "type": "integer"
             "description": "the minimum number of authorizers required to decrypt this file",
             "minimum": 1,
             "exclusiveMinimum": false
         },
         "authorizerlist": {
             "type": "array",
             "items": {
                 "type": "string",
                 "description": "the domain name of an authorizer"
             },
             "minItems": 1,
             "uniqueItems": true
         },
         "outdata": {
             "type": "string"
             "description": "the expected outdata from eth_call",
         }
    }
}

It returns json-encoded byte string list. Each entry of the list is an encrypted parts from an authorizer.

Encrypt file chunks

A POST request should be sent to follow URLs:

/eg_encryptChunk?token=<base58-string>&index=<chunk-index>
/eg_encryptChunkOnServer?token=<base58-string>&index=<chunk-index>

A file is looked as a list of 256KB chunks. It must be encrypted chunk by chunk.

The body of the request is the bytes of a chunk. The encryption task token is given as the token parameter, and index shows the index of this chunk in the file’s chunk list.

The encryptChunk RPC returns the encrypted chunk. The encryptChunkOnServer RPC writes the encrypted chunk to the server’s local file at a proper offset indicated by the index parameter.

Using repeatly requests, you can fill the fully-encrypted file at the client side (encryptChunk) or at the server side (encryptChunkOnServer).

Serve the audience

Start a decryption task

The RPC endpiont’s URL is like below:

/eg_getDecryptTaskToken

The body of the request is a json string with the follow schema:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "ElfinGuard Decryption Guide",
    "description": "Instructions Used by Recryptors to Decrypt Files",
    "type": "object",
    "properties": {
         "chainid": {
             "type": "string",
             "description": "a hex string indicating the target chain's ID"
         },
         "contract": {
             "type": "string",
             "description": "the EVM address of the authorization contract"
         },
         "function": {
             "type": "string",
             "description": "the signature of the function to be called"
         },
         "threshold": {
             "type": "integer"
             "description": "the minimum number of authorizers required to decrypt this file",
             "minimum": 1,
             "exclusiveMinimum": false
         },
         "authorizerlist": {
             "type": "array",
             "items": {
                 "type": "string",
                 "description": "the domain name of an authorizer"
             },
             "minItems": 1,
             "uniqueItems": true
         },
         "outdata": {
             "type": "string"
             "description": "the expected outdata from eth_call",
         }
         "encryptedparts": {
             "type": "array",
             "items": {
                 "type": "string",
                 "description": "base64-encoded shamir part encrypted with the grantcode from the authorizer"
             },
             "minItems": 1,
             "uniqueItems": true
         }
         "calldatalist": {
             "type": "array",
             "items": {
                 "type": "string",
                 "description": "the calldata sent to the authorizer as the calldata to call the contract address. calldata[36:68] must equal the fileid"
             },
             "minItems": 1,
             "uniqueItems": true
         }
         "signature": {
             "type": "string"
             "description": "a signature signed by the requestor",
         },
         "timestamp": {
             "type": "integer"
             "description": "the UNIX timestamp when the requestor signs the signature",
         },
         "recryptorsalt": {
             "type": "string"
             "description": "random bytes generated by the recryptor",
         },
         "fileid": {
             "type": "string"
             "description": "a unique id for this file",
         }
     }
}

The fileid parameter was specified by the author and was used in calling the getEncryptTaskToken endpoint. The recryptorsalt parameter was got by calling the getEncryptTaskToken endpoint.

The requestor must properly construct the signature and calldatalist to prove he is qualified to view the file.

This endpiont returns a json string with following fields:

  1. decrypt_task_token: a decryption task token (base58-encoded)
  2. pubkey: the recryptor’s pubkey (base64-encoded)

Get the decrypted file

The RPC endpiont’s URL is like below:

/eg_decryptChunk?token=<base58-string>&index=<unique-integer>
/eg_getDecryptedFile?token=<base58-string>&path=<file-path-on-ipfs>&size=<integer>

The decryptChunk endpoint decrypts the byte string given in the POST body and returns the decrypted plaintext. The getDecryptedFile endpoint decrypts a file on decentralized storage, and it supports resuming breakpoints during downloading, using the Content-Range Header.

A client-side file can be encrypted by encryptChunk and then decrypted by decryptChunk. The index parameter used by decryptChunk must be the same as the one used when calling encryptChunk. The encryptChunk/decryptChunk endpoints are sued in some use cases where files are shared through some traditional methods, such as email and ftp, instead of decentralized storages.

The decryption task token is given as the token parameter. The path pamameter specifies a file on IPFS from the RPCX/gRPC server. The size parameter specifies the size of the returned data. If the decrypted data is larger than it, then the tail is truncated and not returned.

Load Balance and Authentication

Recryptors are decentralized in a geographic way, because CDN vendors run recryptors on edge nodes. The requestor query a CDN vendor for the nearest recryptor node. Or, the requestor send request to the CDN vendor, which will redirect the request to a nearest recryptor node.

Clients must connect directly to an enclave without any HTTP proxy, to ensure the TLS channel can prevent third parties (including the CDN vendor) from stealing the original file.

Recryptors do not support normal ways for authentication (basic auth, API keys, etc). Instead, to start an encryption task or a decryption task, the requestor must sign a signature to prove her identity.

A decryption/encryption task token can only be read by the same enclave that wrote it. So a requestor must stick to the same enclave during the same decryption/encryption task.

Proxy to authorizers

CDN vendors’s recryptors have a high volume of requests sending to the authorizers. Usually, CDN vendors would like to pay the authorizers for better service. And the authorizers will provide dedicated servers (with special domain name) or dedicated API keys to the paid customers.

Recryptors do not know the dedicated servers or API keys. CDN vendor must run a HTTP proxy which forward the recyrptors’ requests to authorizers.

Rate Limit

The recryptor does not support rate limit itself. Instead, it can connect to an external rate limiter run by the CDN vendor as a microservice.

Recryptor Coordinator

A CDN provider runs a coordinator which coordinates all its recryptors and the backend storage engine. A coordinator has the following functions:

Wallet-based login

You can login to the coordinator and get a session id.

First, you get a random hex string through the following RPC endpoint.

/eg_getNonce

Then, you sign this hex string using personal_sign and use the signature to call the following RPC endpoint:

/eg_getSessionID?sig=<hex-encoded-signature>&nonce=<hex-encoded-nonce>

The nonce parameter used to call eg_getSessionID must the returned value of eg_getNonce.

A session ID is returned to you, which can be used in later requests.

Assign a nearby recryptor

You can ask the coordinator to assign a nearby recryptor to you, for an encryption or decryption task.

/eg_getRecryptor?session=<session-id>

The domain name of the assigned recryptor will be returned.

Gateway to decentralized storages

You can get a non-encrypted file from decentralized storage (such as IPFS):

/eg_getFile?path=<path-of-the-file>&session=<session-id>

The format of path depends on the decentralized storage solution. For IPFS, the path is a CID followed by the file’s path in the Elfin directory.

This RPC helps you get the readme.txt file and the config.json file in the Elfin directory. It may limit the size of the returned file and/or download speed.

Upload an immutable directory

You can request the coordinator to upload an immutable directory onto IPFS by posting a FormData (multipart/form-data).

/eg_upload?session=<session-id>&recryptor=<domain-name-of-recryptor>

The format of the FormData is introduced in the elfindirectory:FormData for upload section.

The recryptor parameter gives the domain name of the recryptor who run encryptChunk for the encrypted files in the immutable directory.

This RPC endpoint will return the CID of the immutable directory.

Proxy to Elfin Authorizers

You can request the coordinator to assign a proxy to you, which can forward your request to an Elfin authorizer.

Note that it needs to input chain name

/eg_getProxy?session=<session-id>&chain_name=<chain-name>

Usually, end users pay CDN providers for higher download speed. However, end users do not directly pay authorizers. Instead, CDN providers will pay the authorizers. To better serve its customers, a CDN provider can build a proxy to forward customers’ requests (/tx, /log and /calldata) to Elfin authorizers.