const base58 = require('bs58') const ed25519 = require('ssb-keys/sodium') const stringify = require('fast-json-stable-stringify') function validateShape(msg) { if (!msg || typeof msg !== 'object') { return new Error('invalid message: not an object') } if (!msg.metadata || typeof msg.metadata !== 'object') { return new Error('invalid message: must have metadata') } if (typeof msg.metadata.who === 'undefined') { return new Error('invalid message: must have metadata.who') } if (typeof msg.metadata.depth === 'undefined') { return new Error('invalid message: must have metadata.depth') } if (typeof msg.metadata.prev === 'undefined') { return new Error('invalid message: must have metadata.prev') } if (typeof msg.metadata.proof === 'undefined') { return new Error('invalid message: must have metadata.proof') } if (typeof msg.metadata.size === 'undefined') { return new Error('invalid message: must have metadata.size') } if (typeof msg.content === 'undefined') { return new Error('invalid message: must have content') } if (typeof msg.sig === 'undefined') { return new Error('invalid message: must have sig') } } function validateWho(msg) { try { base58.decode(msg.metadata.who) } catch (err) { return new Error('invalid message: must have "who" as base58 string') } // FIXME: if there are prev, then `who` must match } function validateSignature(msg) { const { sig } = msg if (typeof sig !== 'string') { return new Error('invalid message: must have sig as a string') } try { base58.decode(sig) } catch (err) { return new Error('invalid message: sig must be a base58 string') } const sigBuf = Buffer.from(base58.decode(sig)) if (sigBuf.length !== 64) { // prettier-ignore return new Error('invalid message: sig should be 64 bytes but was ' + sigBuf.length + ', on feed: ' + msg.metadata.who); } const publicKeyBuf = Buffer.from(base58.decode(msg.metadata.who)) const signableBuf = Buffer.from(stringify(msg.metadata), 'utf8') const verified = ed25519.verify(publicKeyBuf, sigBuf, signableBuf) if (!verified) { // prettier-ignore return new Error('invalid message: sig does not match, on feed: ' + msg.metadata.who); } } function validatePrev(msg, existingMsgs) { if (!msg.metadata.prev || !msg.metadata.prev[Symbol.iterator]) { // prettier-ignore return new Error('invalid message: prev must be an iterator, on feed: ' + msg.metadata.who); } for (const p of msg.metadata.prev) { if (typeof p !== 'string') { // prettier-ignore return new Error('invalid message: prev must contain strings but found ' + p + ', on feed: ' + msg.metadata.who); } if (p.startsWith('ppppp:')) { // prettier-ignore return new Error('invalid message: prev must not contain URIs, on feed: ' + msg.metadata.who); } if (!existingMsgs.has(p)) { // prettier-ignore return new Error('invalid message: prev ' + p + ' is not locally known, on feed: ' + msg.metadata.who); } const existingMsg = existingMsgs.get(p) if (existingMsg.metadata.who !== msg.metadata.who) { // prettier-ignore return new Error('invalid message: prev ' + p + ' is not from the same who, on feed: ' + msg.metadata.who); } if (existingMsg.metadata.type !== msg.metadata.type) { // prettier-ignore return new Error('invalid message: prev ' + p + ' is not from the same type, on feed: ' + msg.metadata.who); } if (existingMsg.metadata.depth >= msg.metadata.depth) { // prettier-ignore return new Error('invalid message: depth of prev ' + p + ' is not lower, on feed: ' + msg.metadata.who); } } } function validateFirstPrev(msg) { if (!Array.isArray(msg.metadata.prev)) { // prettier-ignore return new Error('invalid message: prev must be an array, on feed: ' + msg.metadata.who); } if (msg.metadata.prev.length !== 0) { // prettier-ignore return new Error('invalid message: prev of 1st msg must be an empty array, on feed: ' + msg.metadata.who); } } function validateWhen(msg) { if (msg.metadata.when && typeof msg.metadata.when !== 'number') { // prettier-ignore return new Error('invalid message: `when` is not a number, on feed: ' + msg.metadata.who); } } function validateType(type) { if (!type || typeof type !== 'string') { // prettier-ignore return new Error('type is not a string'); } if (type.length > 100) { // prettier-ignore return new Error('invalid type ' + type + ' is 100+ characters long'); } if (type.length < 3) { // prettier-ignore return new Error('invalid type ' + type + ' is shorter than 3 characters'); } if (/[^a-zA-Z0-9_]/.test(type)) { // prettier-ignore return new Error('invalid type ' + type + ' contains characters other than a-z, A-Z, 0-9, or _'); } } function validateContent(msg) { // FIXME: if content exists, check it against `proof` and `size` // FIXME: if content does not exist, do nothing const { content } = msg if (!content) { return new Error('invalid message: must have content') } if (Array.isArray(content)) { return new Error('invalid message: content must not be an array') } if (typeof content !== 'object' && typeof content !== 'string') { // prettier-ignore return new Error('invalid message: content must be an object or string, on feed: ' + msg.metadata.who); } } // FIXME: validateDepth should be +1 of the max of prev depth function validateSync(msg, existingMsgs) { let err if ((err = validateShape(msg))) return err if ((err = validateWho(msg))) return err if ((err = validateWhen(msg))) return err if (msg.metadata.depth === 0) { if ((err = validateFirstPrev(msg))) return err } else { if ((err = validatePrev(msg, existingMsgs))) return err } if ((err = validateContent(msg))) return err if ((err = validateSignature(msg))) return err } // function validateOOOSync(nativeMsg, hmacKey) { // let err // if ((err = validateShape(nativeMsg))) return err // if ((err = validateHmac(hmacKey))) return err // if ((err = validateAuthor(nativeMsg))) return err // if ((err = validateHash(nativeMsg))) return err // if ((err = validateOrder(nativeMsg))) return err // if ((err = validateContent(nativeMsg))) return err // if ((err = validateAsJSON(nativeMsg))) return err // if ((err = validateSignature(nativeMsg, hmacKey))) return err // } function validate(msg, existingMsgs, cb) { let err if ((err = validateSync(msg, existingMsgs))) { return cb(err) } cb() } // function validateOOO(nativeMsg, hmacKey, cb) { // let err // if ((err = validateOOOSync(nativeMsg, hmacKey))) { // return cb(err) // } // cb() // } // function validateBatch(nativeMsgs, prevNativeMsg, hmacKey, cb) { // let err // let prev = prevNativeMsg // for (const nativeMsg of nativeMsgs) { // err = validateSync(nativeMsg, prev, hmacKey) // if (err) return cb(err) // prev = nativeMsg // } // cb() // } // function validateOOOBatch(nativeMsgs, hmacKey, cb) { // let err // for (const nativeMsg of nativeMsgs) { // err = validateOOOSync(nativeMsg, hmacKey) // if (err) return cb(err) // } // cb() // } module.exports = { validateType, validateContent, validate, // validateBatch, // validateOOO, // validateOOOBatch, }