UNPKG

ssb-classic

Version:

The original SSB feed format, for ssb-db2

296 lines (270 loc) 9.26 kB
// SPDX-FileCopyrightText: 2022 Andre 'Staltz' Medeiros // // SPDX-License-Identifier: LGPL-3.0-only const Ref = require('ssb-ref') const ssbKeys = require('ssb-keys') const isCanonicalBase64 = require('is-canonical-base64') const getMsgId = require('./get-msg-id') const isSignatureRx = isCanonicalBase64('', '\\.sig.\\w+') const SIZE_LIMIT = 8192 // max "encoded character" size limit of classic message function validateShape(msgVal) { if (!msgVal || typeof msgVal !== 'object') { return new Error('invalid message: not a classic msg') } if (typeof msgVal.author === 'undefined') { return new Error('invalid message: must have author') } if (typeof msgVal.previous === 'undefined') { return new Error('invalid message: must have previous') } if (typeof msgVal.sequence === 'undefined') { return new Error('invalid message: must have sequence') } if (typeof msgVal.timestamp === 'undefined') { return new Error('invalid message: must have timestamp') } if (typeof msgVal.hash === 'undefined') { return new Error('invalid message: must have hash') } if (typeof msgVal.content === 'undefined') { return new Error('invalid message: must have content') } if (typeof msgVal.signature === 'undefined') { return new Error('invalid message: must have signature') } } function validateAuthor(msgVal) { if (!Ref.isFeedId(msgVal.author)) { return new Error('invalid message: must have author as a sigil ID') } } function validateSignature(msgVal, hmacKey) { const { signature } = msgVal if (typeof signature !== 'string') { return new Error('invalid message: must have signature as a string') } if (!signature.endsWith('.sig.ed25519')) { // prettier-ignore return new Error('invalid message: signature must end with .sig.ed25519') } if (!isSignatureRx.test(signature)) { // prettier-ignore return new Error('invalid message: signature must be a canonical base64 string') } if (signature.length !== 100) { // prettier-ignore return new Error('invalid message: signature must be 64 bytes, on feed: ' + msgVal.author); } const keys = { public: msgVal.author.substring(1) } if (!ssbKeys.verifyObj(keys, hmacKey, msgVal)) { // prettier-ignore return new Error('invalid message: signature does not match, on feed: ' + msgVal.author); } } function validateOrder(msgVal) { const keys = Object.keys(msgVal) if (keys.length !== 7) { return new Error('invalid message: wrong number of object fields') } if ( keys[0] !== 'previous' || keys[3] !== 'timestamp' || keys[4] !== 'hash' || keys[5] !== 'content' || keys[6] !== 'signature' ) { return new Error('invalid message: wrong order of object fields') } // author and sequence may be swapped. if ( !( (keys[1] === 'sequence' && keys[2] === 'author') || (keys[1] === 'author' && keys[2] === 'sequence') ) ) { return new Error('invalid message: wrong order of object fields') } } function validatePrevious(msgVal, prevMsgVal) { const prevMsgId = prevMsgVal.id ? prevMsgVal.id : getMsgId(prevMsgVal) if (msgVal.previous !== prevMsgId) { // prettier-ignore return new Error('invalid message: expected different previous message, on feed: ' + msgVal.author); } } function validateFirstPrevious(msgVal) { if (msgVal.previous !== null) { // prettier-ignore return new Error('initial message must have previous: null, on feed: ' + msgVal.author); } } function validateFirstSequence(msgVal) { if (msgVal.sequence !== 1) { // prettier-ignore return new Error('initial message must have sequence: 1, on feed: ' + msgVal.author); } } function validateSequence(msgVal, prevMsgVal) { const { sequence } = msgVal if (!Number.isInteger(sequence)) { // prettier-ignore return new Error('invalid message: sequence must be a number on feed: ' + msgVal.author); } const next = prevMsgVal.sequence + 1 if (sequence !== next) { // prettier-ignore return new Error('invalid message: expected sequence ' + next + ' but got: ' + sequence + ' on feed: ' + msgVal.author); } } function validateTimestamp(msgVal) { if (typeof msgVal.timestamp !== 'number') { // prettier-ignore return new Error('initial message must have timestamp, on feed: ' + msgVal.author); } } function validateHash(msgVal) { if (msgVal.hash !== 'sha256') { // prettier-ignore return new Error('invalid message: hash must be sha256, on feed: ' + msgVal.author); } } function validateContent(msgVal) { const { content } = msgVal 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: ' + msgVal.author); } if (typeof content === 'string') { if (!content.endsWith('.box') && !content.endsWith('.box2')) { return new Error( 'invalid message: string content must end with .box or .box2, on feed: ' + msgVal.author ) } if (content.length > SIZE_LIMIT) { return new Error( 'invalid message: string content must be at most 8192 characters. Current size is ' + content.length ) } } if (typeof content === 'object') { if (!content.type || typeof content.type !== 'string') { // prettier-ignore return new Error('invalid message: content must have type, on feed: ' + msgVal.author); } if (content.type.length > 52) { // prettier-ignore return new Error('invalid message: content type must be shorter than 52 characters, on feed: ' + msgVal.author); } if (content.type.length < 3) { // prettier-ignore return new Error('invalid message: content type must be longer than 2 characters, on feed: ' + msgVal.author); } const size = JSON.stringify(content, null, 2).length if (size > SIZE_LIMIT) { return new Error( 'invalid message: encoded content must be at most 8192 characters. Current size is ' + size ) } } } 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 validateAsJSON(msgVal) { const asJson = JSON.stringify(msgVal, null, 2) if (asJson.length > 8192) { // prettier-ignore return new Error('invalid message: message is longer than 8192 latin1 codepoints'); } } function validateSync(nativeMsg, prevNativeMsg, 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 (prevNativeMsg) { if ((err = validatePrevious(nativeMsg, prevNativeMsg))) return err if ((err = validateSequence(nativeMsg, prevNativeMsg))) return err } else { if ((err = validateFirstPrevious(nativeMsg))) return err if ((err = validateFirstSequence(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 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 = { validate, validateBatch, validateOOO, validateOOOBatch, validateContent, }