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.tangles !== 'object') { return new Error('invalid message: must have metadata.tangles') } 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 validateMsgHash(str) { try { base58.decode(str) } catch (err) { return new Error( `invalid message: msgHash ${str} should have been a base58 string` ) } } 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 validateTangle(msg, existingMsgs, tangleId) { const tangle = msg.metadata.tangles[tangleId] if (!tangle?.prev || !Array.isArray(tangle.prev)) { // prettier-ignore return new Error('invalid message: prev must be an array, on feed: ' + msg.metadata.who); } for (const p of tangle.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.type !== msg.metadata.type) { // prettier-ignore return new Error('invalid message: prev ' + p + ' is not from the same type, on feed: ' + msg.metadata.who); } const existingDepth = existingMsg.metadata.tangles[tangleId]?.depth ?? 0 if (existingDepth >= tangle.depth) { // prettier-ignore return new Error('invalid message: depth of prev ' + p + ' is not lower, on feed: ' + msg.metadata.who); } } } function validateTangleRoot(msg, tangleId) { if (msg.metadata.tangles[tangleId]) { // prettier-ignore return new Error('invalid message: tangle root must not have self tangle data, 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 } 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, msgHash, rootHash) { let err if ((err = validateShape(msg))) return err if ((err = validateWho(msg))) return err if ((err = validateWhen(msg))) return err if (msgHash === rootHash) { if ((err = validateTangleRoot(msg))) return err } else { if ((err = validateTangle(msg, existingMsgs, rootHash))) return err } if ((err = validateContent(msg))) return err if ((err = validateSignature(msg))) return err } function validate(msg, existingMsgs, msgHash, rootHash, cb) { let err if ((err = validateSync(msg, existingMsgs, msgHash, rootHash))) { 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() // } module.exports = { validateType, validateContent, validate, validateMsgHash, // validateBatch, }