const base58 = require('bs58') const ed25519 = require('ssb-keys/sodium') const stringify = require('fast-json-stable-stringify') const { stripMsgKey } = require('./strip') const { getMsgHash } = require('./get-msg-id') function validateShape(nativeMsg) { if (!nativeMsg || typeof nativeMsg !== 'object') { return new Error('invalid message: not a dag msg') } if (!nativeMsg.metadata || typeof nativeMsg.metadata !== 'object') { return new Error('invalid message: must have metadata') } if (typeof nativeMsg.metadata.author === 'undefined') { return new Error('invalid message: must have metadata.author') } if (typeof nativeMsg.metadata.type === 'undefined') { return new Error('invalid message: must have metadata.sequence') } if (typeof nativeMsg.metadata.previous === 'undefined') { return new Error('invalid message: must have metadata.previous') } if (typeof nativeMsg.metadata.timestamp === 'undefined') { return new Error('invalid message: must have metadata.timestamp') } if (typeof nativeMsg.metadata.contentHash === 'undefined') { return new Error('invalid message: must have metadata.contentHash') } if (typeof nativeMsg.metadata.contentSize === 'undefined') { return new Error('invalid message: must have metadata.contentSize') } if (typeof nativeMsg.content === 'undefined') { return new Error('invalid message: must have content') } if (typeof nativeMsg.signature === 'undefined') { return new Error('invalid message: must have signature') } } function validateAuthor(nativeMsg) { try { base58.decode(nativeMsg.metadata.author) } catch (err) { return new Error('invalid message: must have author as base58 string') } } function validateSignature(nativeMsg, hmacKey) { const { signature } = nativeMsg if (typeof signature !== 'string') { return new Error('invalid message: must have signature as a string') } try { base58.decode(signature) } catch (err) { return new Error('invalid message: signature must be a base58 string') } const signatureBuf = Buffer.from(base58.decode(signature)) if (signatureBuf.length !== 64) { // prettier-ignore return new Error('invalid message: signature should be 64 bytes but was ' + signatureBuf.length + ', on feed: ' + nativeMsg.metadata.author); } const publicKeyBuf = Buffer.from(base58.decode(nativeMsg.metadata.author)) const signableBuf = Buffer.from(stringify(nativeMsg.metadata), 'utf8') const verified = ed25519.verify(publicKeyBuf, signatureBuf, signableBuf) if (!verified) { // prettier-ignore return new Error('invalid message: signature does not match, on feed: ' + nativeMsg.metadata.author); } } function validatePrevious(nativeMsg, existingNativeMsgs) { if (!Array.isArray(nativeMsg.metadata.previous)) { // prettier-ignore return new Error('invalid message: previous must be an array, on feed: ' + nativeMsg.metadata.author); } for (const prevId of nativeMsg.metadata.previous) { if (typeof prevId !== 'string') { // prettier-ignore return new Error('invalid message: previous must contain strings but found ' + prevId + ', on feed: ' + nativeMsg.metadata.author); } if (prevId.startsWith('ssb:')) { // prettier-ignore return new Error('invalid message: previous must not contain SSB URIs, on feed: ' + nativeMsg.metadata.author); } if (existingNativeMsgs instanceof Set) { if (!existingNativeMsgs.has(prevId)) { // prettier-ignore return new Error('invalid message: previous ' + prevId + ' is not a known message ID, on feed: ' + nativeMsg.metadata.author); } continue } else { let found = false for (const nmsg of existingNativeMsgs) { const existingId = nmsg.key ? stripMsgKey(nmsg.key) : typeof nmsg === 'string' ? stripMsgKey(nmsg) : getMsgHash(nmsg) if (existingId === prevId) { found = true break } } if (!found) { // prettier-ignore return new Error('invalid message: previous ' + prevId + ' is not a known message ID, on feed: ' + nativeMsg.metadata.author); } } } } function validateFirstPrevious(nativeMsg) { if (!Array.isArray(nativeMsg.metadata.previous)) { // prettier-ignore return new Error('invalid message: previous must be an array, on feed: ' + nativeMsg.metadata.author); } if (nativeMsg.metadata.previous.length !== 0) { // prettier-ignore return new Error('initial message: previous must be an empty array, on feed: ' + nativeMsg.metadata.author); } } function validateTimestamp(nativeMsg) { if (typeof nativeMsg.metadata.timestamp !== 'number') { // prettier-ignore return new Error('initial message must have timestamp, on feed: ' + nativeMsg.metadata.author); } } 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(nativeMsg) { const { content } = nativeMsg 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: ' + nativeMsg.metadata.author); } } function validateHmac(hmacKey) { if (!hmacKey) return if (typeof hmacKey !== 'string' && !Buffer.isBuffer(hmacKey)) { return new Error('invalid hmac key: must be a string or buffer') } const bytes = Buffer.isBuffer(hmacKey) ? hmacKey : Buffer.from(hmacKey, 'base64') if (typeof hmacKey === 'string' && bytes.toString('base64') !== hmacKey) { return new Error('invalid hmac') } if (bytes.length !== 32) { return new Error('invalid hmac, it should have 32 bytes') } } function emptyExisting(existingNativeMsgs) { if (existingNativeMsgs instanceof Set) { return existingNativeMsgs.size === 0 } else if (Array.isArray(existingNativeMsgs)) { return existingNativeMsgs.length === 0 } else { return !existingNativeMsgs } } function validateSync(nativeMsg, existingNativeMsgs, hmacKey) { let err if ((err = validateShape(nativeMsg))) return err if ((err = validateHmac(hmacKey))) return err if ((err = validateAuthor(nativeMsg))) return err if ((err = validateTimestamp(nativeMsg))) return err if (emptyExisting(existingNativeMsgs)) { if ((err = validateFirstPrevious(nativeMsg))) return err } else { if ((err = validatePrevious(nativeMsg, existingNativeMsgs))) return err } if ((err = validateContent(nativeMsg))) return err if ((err = validateSignature(nativeMsg, hmacKey))) 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 = validateTimestamp(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(nativeMsg, prevNativeMsg, hmacKey, cb) { let err if ((err = validateSync(nativeMsg, prevNativeMsg, hmacKey))) { 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, }