Building Zero Knowledge

A ZKA Backend w/ Node.js

@m4d_z
alwaysdata
Our Mission: to protect the Data Flow

We Need Encryption

Backend has to be agnostic

Stick to the Venerables HTTP Verbs

What ZKA is?

Design pattern that
enforces Users Privacy

E2E Encryption

Asymetric Keys
at each Ends (for sharing)

Symetric Key Wraping
(IDEA, BlowFish, etc.)

Backend Roles

  • Stores/Returns Encrypted Data
  • Manages Public Keys and Tokens
  • Delivers Certificates
  • Invalidates Expired Payloads

Client ←→ Server Workflow

  1. Client Registers, GET ting the ROOT CA
  2. Client Generates Keys-pairs, POST ing Pubkeys and Hashes
  3. Client Encrypts Data w/ wrapped Key and Signs it, PUT ing it on the Server
  4. Server Validates timestamped signature before returning the payload

Backend Behaviors

Mission Auth

  • Identify users
  • Creates accounts for storage
  • Link shares between recipients

Mission Key Management

  • Certificates Generation
  • Keys Storages
  • (opt) HSM

How to Securing the data:
JSON Web Tokens

  • UUID
  • Keys, Salts, Params
  • Data Chunks

How to Persist

Document Oriented Database

How to Transmit:
Distributed / Federated

  • Queue messaging solution
  • ActivityPub

Backend Workflow

  1. Creates User
  2. Returns User’s JWT
  3. Handles POST ed Data Chunks
  4. Handles PATCH ed Data Chunks
  5. Checks Signature and Expiration
  6. Returns Data (or Error code)

Build it w/ Node.js!

Why Node.js?

  • Fast
  • Simple/Elegant
  • All about Modules (and NPM 😍)
  • JSON Native
  • Resilient
  • Frontends compatible

Server Part: restify

  • Full REST compatible
  • Handlers chains
  • Pre-handlers
  • Hypermedia
  • Versioned
  • Smart at failing

restify setup

const restify = require('restify')

const server = restify.createServer()

server.get('/', (req, res) => {
  res.send(200)
})

server.listen(8080, () => {
  console.log('%s listening at %s', server.name, server.url)
})

Security: node-forge

  • Full TLS native implementation
  • X.509 compatible
const { pki } = require('node-forge')

const caStore = pki.createCaStore([ AppRootCertPEM ])

server.post('/users', (req, res, next) => {
    // CREATE USER
    //
    res.send(201, { cert: pki.certificateToPen(caStore[0]), uuid })
})

server.put('/users/:uuid', (req, res) => {
  const { uuid } = req.params
  const { pubkey, certHash, privkeyhash,
          salt, iterations, digest } = req.body
  // UPDATE USER
  //
  res.send(204)
})

Storage

MongoDB/CouchDB

Document Tree Structure

  • UUID
  • Username
  • Password Hash / Sign / Salt
  • Enc Keys: Pubkey / Privkey hash
  • Auth Keys (ZKP): Pubkey / Privkey hash
  • Certificate hash
  • Items
    • UUID
    • Salt / Params / Sign
    • Data
    • Recipient UUID

Auth

  • Create account = POST username / pwd
    • Username
    • Password hash
    • Certificate hash
    • Pubkeys
    • Privkey hashes
    • Salt
  • Returns JWT
const users = db.collection('users')

passport.use(new LocalStrategy((username, password, done) => {
  users.find({ username }, (err, user) => {
    if (!user) return done(null, false)
    if (!user.verifyPassword(password)) return done(null, false)
    return done(null, user)
  })
}))

server.post('/users/:name/login', passport.authenticate('local'), (req, res) => {
    res.redirect(`/users/${req.params.name}`)
})

server.get('/users/:name', passport.authenticate('local'), (req, res) => {
  // RETURN JWT
})

Protect Requests

  • TLS → Built-in
  • CORS
  • JWT
const restifyCors = require('restify-cors')

const cors = restifyCors({
  origins: [
    'https://api.example.org',
    'https://web.example.org'
  ],
  allowHeaders: ['API-Token', 'Authorization'],
  exposeHeaders: ['API-Token-Expiry']
})

server.pre(cors.preflight)
server.use(cors.actual)

Actions are
identified and protected
with JWT

const jwt = require('restify-jwt-community')
const secret = server.token

server.use(restify.authorizationParser())

server.get('/items', jwt({ secret }), (req, res) => {
  if (!req.user.admin) return { res.send(401) }
  res.send(200)
})

POST Item

  1. Client derivate key from password (PBKDF2)
  2. POST w/ JWT
    • Salt, Recipient UUID, Params…
    • (opt) Expiration timestamp
  3. Returns
    • Updated JWT
    • Item UUID
server.use(restify.plugins.bodyParser())

server.post('/items', jwt({ secret }), (req, res) => {
  const { salt, recipient, iterations, digest } = req.body
  const { user } = req

  // CREATE A NEW ITEM FOR USER IN DB

  res.send(201, { uuid })
})

PUT Data Chunk

  • Payload
    • encrypted data
    • wrapped key
    • (opt) timestamp
    • signature
  • Exception if Data already exists
const crypto = require('crypto')
const items = db.collection('items')

server.put('/items/:uuid', jwt({ secret }), (req, res) => {
  const { uuid } = req.params
  const item = items.find({ uuid })
  const { timestamp, signature, data } = req.body
  const verify = crypto.createVerify('sha256')
  verify.update(data + '.' + timestamp)

  if (item.data) return res.send(new ConflictError())
  if (!verify.verify(req.user.pubkey, signature, 'base64'))
    return res.send(new PreconditionFailedError())

  // SAVE DATA, SIGNATURE, TIMESTAMP
  res.send(204)
})

GET User

  • Passport
  • Returns JWT
    • UUID
    • Username
  • Hypermedia to belonging items
const jwt = require('jsonwebtoken')

server.get('/users/:name', passport.authenticate('local'), (req, res) => {
  const { name } = req.params
  const user = users.find({ name })

  let token = jwt.sign({
    uuid: user.uuid,
    admin: user.admin
  }, secret, { expiresIn: '1m' })

  res.send({
    token,
    username: user.name,
    pubkey: user.pubkey,
    items: user.items.map(item => server.router.render('items', { uuid: item.uuid }))
  })
})

GET Data Chunk

  1. Check signature w/ pubkey
  2. Check Exp. Timestamp
  3. Returns Data or Exception
const items = db.collection('items')
server.get('/items/:uuid', jwt({ secret }), (req, res) => {
  const { uuid } = req.params
  const item = items.find({ uuid })
  const verify = crypto.createVerify('sha256')
  verify.update(item.data + '.' + item.timestamp)

  if (!item) return res.send(new NotFoundError())
  if (item.recipient.uuid !== req.user.uuid)
    return res.send(new UnauthorizedError())
  if (item.expire < Date.now())
    return res.send(new PreconditionFailedError())
  if (!verify.verify(req.user.pubkey, item.signature, 'base64'))
    return res.send(new PreconditionFailedError())

  res.send(item)
})

Bonus ZKP
Share payload with a user

  1. Recipient PUT users/:name/recipients/:uuid
  2. Returns a JWT
  3. Recipient UPDATE JWT w/ its Signature using Privkey
  4. PUSH to User
  5. User PUT item flagged item w/ the recipient UUID

Just a simple REST Ping Pong!

It’s not that complex
(at least on the backend side)

It’s mostly a REST API
with some Crypto sugar

Your Tools

  • JWT
  • Signatures
  • Datetime Tokens

The complex part
is on the Client
and requires a lot of Crypto

m4dz's avatar
m4dz

Paranoïd Web Dino · Tech Evangelist

alwaysdata logo
https://www.alwaysdata.com

Questions?

Thank You!

Available under licence CC BY-SA 4.0

Illustrations

m4dz, CC BY-SA 4.0

Interleaf images

Courtesy of Unsplash and Pexels contributors

Icons

  • Layout icons are from Entypo+
  • Content icons are from FontAwesome

Fonts

  • Cover Title: Sinzano
  • Titles: Argentoratum
  • Body: Mohave
  • Code: Fira Code

Tools

Powered by Reveal.js

Source code available at
https://git.madslab.net/talks