mirror of https://codeberg.org/pzp/pzp-db.git
231 lines
7.2 KiB
JavaScript
231 lines
7.2 KiB
JavaScript
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,
|
|
}
|