Convey service
Convey service - Public transport layer for JWTs using IPFS
Large contents don't fit in QR codes so this service is designed to receive an encrypted file, store it in IPFS and also store the file in memory. It returns the cid of the stored file and a convey-uri that indicates that this kind of files should be gotten from a convey service implementation
Read the running guide to run the whole project or browse the open-source repo to run locally (please use tag v0.1.0
)
Find the protocol specification here.
Main flows
The Convey service has been built to act as a content relayer in front of IPFS. Let's imagine that Bob wants to share a JWT with Alice through a QR code. If the content is too large, it will not fit in a QR code, so there is where the Convey service acts. It allows to transport and caché files that can be accessed via HTTPS, and provides a tiny URL that can fit in any QR code. The service exposes two main endpoints: POST and GET files. Please refer to the protocol description here for more details about it.
Authentication flow
- Bob requests to login to the Convey service by sending it's DID
- The Convey service responds with a challenge
- Bob signs a VC with that challenge and send it to the Convey service to authenticate
- Convey service verifies the received challenge and validates that the signer is the same that Bob provided in step 1. If it is ok, it responds with an authentication token
- Bob saves that authentication token to be used on each interaction with the Convey service
See the DID Authentication protocol for more details.
Example
import axios from 'axios'
import { createVerifiableCredentialJwt } from 'did-jwt-vc'
import RSKEthrDID from '@rsksmart/ethr-did'
const identity = new RSKEthrDID({
address: '0xDe9D2B98E1c23E2765c06C5057723a6c1c453147',
privateKey: '2f86e57652ee906707d4415105228b4fda7b1b900cfd0871cd5d17277ad084b8'
})
axios.post(`${serviceUrl}/request-auth`, { did: identity.did })
.then(res => res.status === 200 && !!res.data && res.data.challenge)
.then(challenge => createVerifiableCredentialJwt({
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential'],
credentialSubject: {
claims: [
{ claimType: 'challenge', claimValue: challenge }
]
}
}
}, identity))
.then(jwt => axios.post(`${serviceUrl}/auth`, { jwt }))
.then(res => res.status === 200 && !!res.data && res.data.token)
Post and share content
- Bob does the authentication flow
- Bob posts some content (ideally encrypted) to the Convey service
- Convey service puts it in IPFS and store its CID (associated to the original file) in memory. It responds with the CID of the saved content and the convey formatted uri (see the protocol)
- Bob builds a QR code with the received convey formatted uri (and the encryption key in case the content needs to be decrypted) with the format
convey://{CID}#{key}
Example
import axios from 'axios'
const content = 'myLargeContent'
const { encrypted, encriptionKey } = encrypt(content)
login()
.then(token => axios.post(`${serviceUrl}/file`, { file: encrypted }, { headers: { 'Authorization': token }))
.then(res => res.status === 200 && !!res.data && res.data)
.then({ cid, url } => generateQr(`${url}#${encriptionKey}`))
Receive shared content
- Alice does the authentication flow
- Alice scans the QR and receive the convey formatted uri
- Alice gets the CID and encryption key (if provided) from the received uri and builds and HTTP url as the following:
http://{conveyService}/file/{CID}
- Alice tries to get the content from the Convey service
- If found, the Convey service will return a JSON with the file. If not found,
- the Convey service will return an HTTP 404.
- Alice will get the file from the IPFS Gateway (
https://{ipfsGateway}/ipfs/{CID}
) - The content is encrypted, Alice decrypts it with the key and gets the original file
Example
import axios from 'axios'
const qrContent = `convey://${cid}#${ecKey}`
const index = 'convey://'.length
const identifier = uri.substring(index)
const [cid, encryptionKey] = identifier.split('#')
login()
.then(token => axios.get(`${serviceUrl}/file/${cid}`, { headers: { 'Authorization': token }))
.then(res => res.status === 200 && !!res.data && res.data.file)
.then(file => decrypt(file, encryptionKey))
.then(content => console.log(`Decrypted content: ${content}`))
API
Authentication
This service uses a simplified version of DID Auth protocol
POST /request-auth
Generates a random 64 bytes challenge that is associated to the received DID. It is part of the implementation of the DID Authentication protocol
Parameter
did
-string
DID that will be associated with the generated challenge.
Returns
{ challenge: generatedChallenge }
Example with Axios
import axios from 'axios'
const did = 'did:ethr:rsk:testnet:0x....1234'
axios.post(`${serviceUrl}/request-auth`, { did })
.then(res => res.status === 200 && !!res.data && res.data.challenge)
.then(challenge => console.log(`The challenge is: ${challenge}`))
POST /auth
Verifies challenge and did of the received VC, if it is ok, it emits another VC and responds with the JWT representation of it. It will act as the authentication token. It is part of the implementation of the DID Authentication protocol.
Parameter
jwt
-jwt
representation of a VC signed by the client with the received challenge. That VC should be signed by the DID sent before and follow this format:
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential'],
credentialSubject: {
claims: [
{ claimType: 'challenge', claimValue: RECEIVED_CHALLENGE }
]
}
}
Returns
{ token: generatedToken }
Example with Axios
and did-jwt-vc
import axios from 'axios'
import { createVerifiableCredentialJwt } from 'did-jwt-vc'
const jwt = await createVerifiableCredentialJwt({
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential'],
credentialSubject: {
claims: [
{ claimType: 'challenge', claimValue: theReceivedChallenge }
]
}
}
}, identity) // this identity should be associated to the did sent in /request_auth
axios.post(`${serviceUrl}/auth`, { jwt }))
.then(res => res.status === 200 && !!res.data && res.data.token)
.then(token => console.log(`The authentication token is: ${token}`))
Convey files
Use this API to send and receive files using the convey
POST /file
It stores the file in IPFS and then saves in the service memory the original content associated to its IPFS hash (cid)
Parameter
file
- string content to be saved
Returns
{
url: `convey://${cid}`,
cid: generatedCid
}
Example with Axios
import axios from 'axios'
const content = 'myLargeContent'
const { encrypted, encriptionKey } = encrypt(content)
const token = thePreviouslyReceivedToken
axios.post(`${serviceUrl}/file`, { file: encrypted }, { headers: { 'Authorization': token }))
.then(res => res.status === 200 && !!res.data && res.data)
.then({ cid, url } => console.log(`The Convey uri: ${url}. The CID: ${cid}`))
GET /file
It looks for the given CID in the service memory and responds with it in case it is found. If not, returns an HTTP 404.
Parameter
cid
- CID of the desired content
Returns
{
file: retrievedFileFromMemory
}
Example with Axios
import axios from 'axios'
const token = thePreviouslyReceivedToken
const cid = theOneGottenFromTheQr
axios.get(`${serviceUrl}/file/${cid}`, { headers: { 'Authorization': token }))
.then(res => res.status === 200 && !!res.data && res.data.file)
.then(file => console.log(`The retrieved file is: ${file}.`))
Possible extensions
- Limit the amount of time the content is saved in memory
- Limit file size
Run for development
The service source code is hosted in Github, so please refer directly to the README and check there the detailed guide to install and test the service locally or with a Docker container.