mirror of https://codeberg.org/pzp/pzp-db.git
implement powers in identity.add()
This commit is contained in:
parent
e6aa33d3f0
commit
557ea2252c
99
lib/index.js
99
lib/index.js
|
@ -23,6 +23,7 @@ const { decrypt } = require('./encryption')
|
||||||
* @typedef {import('ppppp-keypair').KeypairPrivateSlice} KeypairPrivateSlice
|
* @typedef {import('ppppp-keypair').KeypairPrivateSlice} KeypairPrivateSlice
|
||||||
* @typedef {import('./msg-v3').Msg} Msg
|
* @typedef {import('./msg-v3').Msg} Msg
|
||||||
* @typedef {import('./msg-v3').IdentityData} IdentityData
|
* @typedef {import('./msg-v3').IdentityData} IdentityData
|
||||||
|
* @typedef {import('./msg-v3').IdentityPower} IdentityPower
|
||||||
* @typedef {import('./encryption').EncryptionFormat} EncryptionFormat
|
* @typedef {import('./encryption').EncryptionFormat} EncryptionFormat
|
||||||
*
|
*
|
||||||
* @typedef {Buffer | Uint8Array} B4A
|
* @typedef {Buffer | Uint8Array} B4A
|
||||||
|
@ -274,18 +275,23 @@ function initDB(peer, config) {
|
||||||
// Or even better, a bloom filter. If you just want to answer no/perhaps.
|
// Or even better, a bloom filter. If you just want to answer no/perhaps.
|
||||||
let rec
|
let rec
|
||||||
if ((rec = getRecord(msgHash))) return cb(null, rec)
|
if ((rec = getRecord(msgHash))) return cb(null, rec)
|
||||||
|
else rec = { msg, hash: msgHash }
|
||||||
|
|
||||||
// TODO: optimize this. This may be slow if you're adding many msgs in a
|
// TODO: optimize this. This may be slow if you're adding many msgs in a
|
||||||
// row, because it creates a new Map() each time. Perhaps with QuickLRU
|
// row, because it creates a new Map() each time. Perhaps with QuickLRU
|
||||||
const tangle = new DBTangle(tangleRootHash, records())
|
const tangle = new DBTangle(tangleRootHash, records())
|
||||||
|
|
||||||
|
// Find which pubkeys are authorized to sign this msg given the identity:
|
||||||
const pubkeys = new Set()
|
const pubkeys = new Set()
|
||||||
if (msg.metadata.identity && msg.metadata.identity !== IDENTITY_SELF) {
|
const identity = getIdentityId(rec)
|
||||||
const identityTangle = new DBTangle(msg.metadata.identity, records())
|
let identityTangle = /** @type {DBTangle | null} */ (null)
|
||||||
if (!identityTangle.has(msg.metadata.identity)) {
|
if (identity && !MsgV3.isRoot(msg)) {
|
||||||
|
identityTangle = new DBTangle(identity, records())
|
||||||
|
if (!identityTangle.has(identity)) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return cb(new Error('add() failed because the identity tangle is unknown'))
|
return cb(new Error('add() failed because the identity tangle is unknown'))
|
||||||
}
|
}
|
||||||
|
// TODO: prune the identityTangle beyond msg.metadata.identityTips
|
||||||
for (const msgHash of identityTangle.topoSort()) {
|
for (const msgHash of identityTangle.topoSort()) {
|
||||||
const msg = get(msgHash)
|
const msg = get(msgHash)
|
||||||
if (!msg?.data) continue
|
if (!msg?.data) continue
|
||||||
|
@ -303,6 +309,22 @@ function initDB(peer, config) {
|
||||||
return cb(new Error('add() failed msg validation', { cause: err }))
|
return cb(new Error('add() failed msg validation', { cause: err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identity tangle related validations
|
||||||
|
if (msg.metadata.identity === IDENTITY_SELF && identityTangle) {
|
||||||
|
/** @type {IdentityData} */
|
||||||
|
const data = msg.data
|
||||||
|
if (data.action === 'add') {
|
||||||
|
// Does this msg.pubkey have the "add" power?
|
||||||
|
const keypair = { curve: 'ed25519', public: msg.pubkey }
|
||||||
|
const powers = getIdentityPowers(identityTangle, keypair)
|
||||||
|
if (!powers.has('add')) {
|
||||||
|
// prettier-ignore
|
||||||
|
return cb(new Error('add() failed because this msg.pubkey does not have "add" power'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO validate 'del'
|
||||||
|
}
|
||||||
|
|
||||||
logAppend(msgHash, msg, (err, rec) => {
|
logAppend(msgHash, msg, (err, rec) => {
|
||||||
if (err) return cb(new Error('add() failed in the log', { cause: err }))
|
if (err) return cb(new Error('add() failed in the log', { cause: err }))
|
||||||
onRecordAdded.set(rec)
|
onRecordAdded.set(rec)
|
||||||
|
@ -478,6 +500,30 @@ function initDB(peer, config) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DBTangle} identityTangle
|
||||||
|
* @param {KeypairPublicSlice} keypair
|
||||||
|
* @returns {Set<IdentityPower>}
|
||||||
|
*/
|
||||||
|
function getIdentityPowers(identityTangle, keypair) {
|
||||||
|
const powers = new Set()
|
||||||
|
for (const msgHash of identityTangle.topoSort()) {
|
||||||
|
const msg = get(msgHash)
|
||||||
|
if (!msg?.data) continue
|
||||||
|
/** @type {IdentityData} */
|
||||||
|
const data = msg.data
|
||||||
|
if (data.action !== 'add') continue
|
||||||
|
if (data.add.key.algorithm !== keypair.curve) continue
|
||||||
|
if (data.add.key.bytes !== keypair.public) continue
|
||||||
|
if (data.add.powers) {
|
||||||
|
for (const power of data.add.powers) {
|
||||||
|
powers.add(power)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return powers
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a consent signature for the given `keypair` (or the implicit
|
* Create a consent signature for the given `keypair` (or the implicit
|
||||||
* config.keypair) to be added to the given `identity`.
|
* config.keypair) to be added to the given `identity`.
|
||||||
|
@ -502,8 +548,15 @@ function initDB(peer, config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Add the given `keypair` (or the implicit config.keypair) to the given
|
||||||
|
* `identity`, authorized by the given `consent` (or implicitly created on the
|
||||||
|
* fly if the `keypair` contains the private key) with the following `powers`
|
||||||
|
* (defaulting to no powers).
|
||||||
|
*
|
||||||
* @param {{
|
* @param {{
|
||||||
* identity: string;
|
* identity: string;
|
||||||
|
* powers?: Array<IdentityPower>;
|
||||||
|
* _disobey?: true;
|
||||||
* } & ({
|
* } & ({
|
||||||
* keypair: KeypairPublicSlice & {private?: never};
|
* keypair: KeypairPublicSlice & {private?: never};
|
||||||
* consent: string;
|
* consent: string;
|
||||||
|
@ -514,12 +567,13 @@ function initDB(peer, config) {
|
||||||
* @param {CB<Rec>} cb
|
* @param {CB<Rec>} cb
|
||||||
*/
|
*/
|
||||||
function addToIdentity(opts, cb) {
|
function addToIdentity(opts, cb) {
|
||||||
|
if (!opts) return cb(new Error('identity.add() requires an `opts`'))
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!opts?.identity) return cb(new Error('identity.add() requires a `identity`'))
|
if (!opts.identity) return cb(new Error('identity.add() requires a `identity`'))
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!opts?.keypair) return cb(new Error('identity.add() requires a `keypair`'))
|
if (!opts.keypair) return cb(new Error('identity.add() requires a `keypair`'))
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!opts?.keypair.public) return cb(new Error('identity.add() requires a `keypair` with `public`'))
|
if (!opts.keypair.public) return cb(new Error('identity.add() requires a `keypair` with `public`'))
|
||||||
let consent = /** @type {string} */ (opts.consent)
|
let consent = /** @type {string} */ (opts.consent)
|
||||||
if (typeof opts.consent === 'undefined') {
|
if (typeof opts.consent === 'undefined') {
|
||||||
if (opts.keypair.private) {
|
if (opts.keypair.private) {
|
||||||
|
@ -528,6 +582,7 @@ function initDB(peer, config) {
|
||||||
return cb(new Error('identity.add() requires a `consent`'))
|
return cb(new Error('identity.add() requires a `consent`'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const obeying = !opts._disobey
|
||||||
const addedKeypair = opts.keypair
|
const addedKeypair = opts.keypair
|
||||||
const signingKeypair = config.keypair
|
const signingKeypair = config.keypair
|
||||||
|
|
||||||
|
@ -535,11 +590,36 @@ function initDB(peer, config) {
|
||||||
const signableBuf = b4a.from(
|
const signableBuf = b4a.from(
|
||||||
SIGNATURE_TAG_IDENTITY_ADD + base58.decode(opts.identity)
|
SIGNATURE_TAG_IDENTITY_ADD + base58.decode(opts.identity)
|
||||||
)
|
)
|
||||||
if (!Keypair.verify(addedKeypair, signableBuf, consent)) {
|
if (obeying && !Keypair.verify(addedKeypair, signableBuf, consent)) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return cb(new Error('identity.add() failed because the consent is invalid'))
|
return cb(new Error('identity.add() failed because the consent is invalid'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify powers of the signingKeypair:
|
||||||
|
const identityTangle = new DBTangle(opts.identity, records())
|
||||||
|
if (obeying) {
|
||||||
|
const signingPowers = getIdentityPowers(identityTangle, signingKeypair)
|
||||||
|
if (!signingPowers.has('add')) {
|
||||||
|
// prettier-ignore
|
||||||
|
return cb(new Error('identity.add() failed because the signing keypair does not have the "add" power'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify input powers for the addedKeypair:
|
||||||
|
if (obeying && opts.powers) {
|
||||||
|
if (!Array.isArray(opts.powers)) {
|
||||||
|
// prettier-ignore
|
||||||
|
return cb(new Error('identity.add() failed because opts.powers is not an array'))
|
||||||
|
}
|
||||||
|
for (const power of opts.powers) {
|
||||||
|
if (power !== 'add' && power !== 'del' && power !== 'box') {
|
||||||
|
// prettier-ignore
|
||||||
|
return cb(new Error(`identity.add() failed because opts.powers contains an unknown power "${power}"`))
|
||||||
|
}
|
||||||
|
// TODO check against duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const identityRoot = get(opts.identity)
|
const identityRoot = get(opts.identity)
|
||||||
if (!identityRoot) {
|
if (!identityRoot) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
|
@ -558,12 +638,15 @@ function initDB(peer, config) {
|
||||||
consent,
|
consent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if (opts.powers) data.add.powers = opts.powers
|
||||||
|
|
||||||
// Fill-in tangle opts:
|
// Fill-in tangle opts:
|
||||||
const fullOpts = {
|
const fullOpts = {
|
||||||
identity: IDENTITY_SELF,
|
identity: IDENTITY_SELF,
|
||||||
identityTips: null,
|
identityTips: null,
|
||||||
tangles: populateTangles([opts.identity]),
|
tangles: {
|
||||||
|
[opts.identity]: identityTangle,
|
||||||
|
},
|
||||||
keypair: signingKeypair,
|
keypair: signingKeypair,
|
||||||
data,
|
data,
|
||||||
domain: identityRoot.metadata.domain,
|
domain: identityRoot.metadata.domain,
|
||||||
|
|
|
@ -2,6 +2,9 @@ module.exports = {
|
||||||
/** @type {'self'} */
|
/** @type {'self'} */
|
||||||
IDENTITY_SELF: 'self',
|
IDENTITY_SELF: 'self',
|
||||||
|
|
||||||
|
/** @type {'any'} */
|
||||||
|
IDENTITY_ANY: 'any',
|
||||||
|
|
||||||
SIGNATURE_TAG_MSG_V3: ':msg-v3:',
|
SIGNATURE_TAG_MSG_V3: ':msg-v3:',
|
||||||
SIGNATURE_TAG_IDENTITY_ADD: ':identity-add:',
|
SIGNATURE_TAG_IDENTITY_ADD: ':identity-add:',
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,12 @@ const {
|
||||||
validateMsgHash,
|
validateMsgHash,
|
||||||
} = require('./validation')
|
} = require('./validation')
|
||||||
const Tangle = require('./tangle')
|
const Tangle = require('./tangle')
|
||||||
const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants')
|
const {
|
||||||
|
IDENTITY_SELF,
|
||||||
|
IDENTITY_ANY,
|
||||||
|
SIGNATURE_TAG_MSG_V3,
|
||||||
|
} = require('./constants')
|
||||||
|
const { isEmptyObject } = require('./util')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('ppppp-keypair').Keypair} Keypair
|
* @typedef {import('ppppp-keypair').Keypair} Keypair
|
||||||
|
@ -38,7 +43,7 @@ const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants')
|
||||||
* metadata: {
|
* metadata: {
|
||||||
* dataHash: string | null;
|
* dataHash: string | null;
|
||||||
* dataSize: number;
|
* dataSize: number;
|
||||||
* identity: string | (typeof IDENTITY_SELF) | null;
|
* identity: string | (typeof IDENTITY_SELF) | (typeof IDENTITY_ANY);
|
||||||
* identityTips: Array<string> | null;
|
* identityTips: Array<string> | null;
|
||||||
* tangles: Record<string, TangleMetadata>;
|
* tangles: Record<string, TangleMetadata>;
|
||||||
* domain: string;
|
* domain: string;
|
||||||
|
@ -54,10 +59,13 @@ const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants')
|
||||||
* action: 'del', del: IdentityDel
|
* action: 'del', del: IdentityDel
|
||||||
* }} IdentityData
|
* }} IdentityData
|
||||||
*
|
*
|
||||||
|
* @typedef {'add' | 'del' | 'box'} IdentityPower
|
||||||
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* key: IdentityKey;
|
* key: IdentityKey;
|
||||||
* nonce?: string;
|
* nonce?: string;
|
||||||
* consent?: string;
|
* consent?: string;
|
||||||
|
* powers?: Array<IdentityPower>;
|
||||||
* }} IdentityAdd
|
* }} IdentityAdd
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
|
@ -71,24 +79,18 @@ const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants')
|
||||||
* }} SigKey
|
* }} SigKey
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* purpose: 'subidentity';
|
|
||||||
* algorithm: 'tangle';
|
|
||||||
* bytes: string;
|
|
||||||
* }} SubidentityKey;
|
|
||||||
*
|
|
||||||
* @typedef {{
|
|
||||||
* purpose: 'box';
|
* purpose: 'box';
|
||||||
* algorithm: 'x25519-xsalsa20-poly1305';
|
* algorithm: 'x25519-xsalsa20-poly1305';
|
||||||
* bytes: string;
|
* bytes: string;
|
||||||
* }} BoxKey;
|
* }} BoxKey;
|
||||||
*
|
*
|
||||||
* @typedef {SigKey | SubidentityKey | BoxKey} IdentityKey
|
* @typedef {SigKey | BoxKey} IdentityKey
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* data: any;
|
* data: any;
|
||||||
* domain: string;
|
* domain: string;
|
||||||
* keypair: Keypair;
|
* keypair: Keypair;
|
||||||
* identity: string | null;
|
* identity: string | (typeof IDENTITY_SELF) | (typeof IDENTITY_ANY);
|
||||||
* identityTips: Array<string> | null;
|
* identityTips: Array<string> | null;
|
||||||
* tangles: Record<string, Tangle>;
|
* tangles: Record<string, Tangle>;
|
||||||
* }} CreateOpts
|
* }} CreateOpts
|
||||||
|
@ -137,7 +139,7 @@ function create(opts) {
|
||||||
if (!opts.tangles) throw new Error('opts.tangles is required')
|
if (!opts.tangles) throw new Error('opts.tangles is required')
|
||||||
|
|
||||||
const [dataHash, dataSize] = representData(opts.data)
|
const [dataHash, dataSize] = representData(opts.data)
|
||||||
const identity = opts.identity ? stripIdentity(opts.identity) : null
|
const identity = opts.identity
|
||||||
const identityTips = opts.identityTips ? opts.identityTips.sort() : null
|
const identityTips = opts.identityTips ? opts.identityTips.sort() : null
|
||||||
|
|
||||||
const tangles = /** @type {Msg['metadata']['tangles']} */ ({})
|
const tangles = /** @type {Msg['metadata']['tangles']} */ ({})
|
||||||
|
@ -238,6 +240,7 @@ function createIdentity(keypair, domain, nonce = getRandomNonce) {
|
||||||
bytes: keypair.public,
|
bytes: keypair.public,
|
||||||
},
|
},
|
||||||
nonce: typeof nonce === 'function' ? nonce() : nonce,
|
nonce: typeof nonce === 'function' ? nonce() : nonce,
|
||||||
|
powers: ['add', 'del', 'box'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +271,13 @@ function fromPlaintextBuffer(plaintextBuf, msg) {
|
||||||
return { ...msg, data: JSON.parse(plaintextBuf.toString('utf-8')) }
|
return { ...msg, data: JSON.parse(plaintextBuf.toString('utf-8')) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Msg} msg
|
||||||
|
*/
|
||||||
|
function isRoot(msg) {
|
||||||
|
return isEmptyObject(msg.metadata.tangles)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getMsgHash,
|
getMsgHash,
|
||||||
getMsgId,
|
getMsgId,
|
||||||
|
@ -280,6 +290,7 @@ module.exports = {
|
||||||
stripIdentity,
|
stripIdentity,
|
||||||
toPlaintextBuffer,
|
toPlaintextBuffer,
|
||||||
fromPlaintextBuffer,
|
fromPlaintextBuffer,
|
||||||
|
isRoot,
|
||||||
Tangle,
|
Tangle,
|
||||||
validate,
|
validate,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
const { stripIdentity } = require('./strip')
|
const { stripIdentity } = require('./strip')
|
||||||
|
const { isEmptyObject } = require('./util')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('.').Msg} Msg
|
* @typedef {import('.').Msg} Msg
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} obj
|
|
||||||
*/
|
|
||||||
function isEmptyObject(obj) {
|
|
||||||
for (const _key in obj) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Msg} msg
|
* @param {Msg} msg
|
||||||
* @param {string | 0} id
|
* @param {string | 0} id
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} obj
|
||||||
|
*/
|
||||||
|
function isEmptyObject(obj) {
|
||||||
|
for (const _key in obj) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isEmptyObject,
|
||||||
|
}
|
15
protospec.md
15
protospec.md
|
@ -24,7 +24,7 @@ interface Msg {
|
||||||
metadata: {
|
metadata: {
|
||||||
dataHash: ContentHash | null // blake3 hash of the `content` object serialized
|
dataHash: ContentHash | null // blake3 hash of the `content` object serialized
|
||||||
dataSize: number // byte size (unsigned integer) of the `content` object serialized
|
dataSize: number // byte size (unsigned integer) of the `content` object serialized
|
||||||
identity: string | 'self' | null // blake3 hash of an identity tangle root msg, or the string 'self', or null
|
identity: string | 'self' | 'any' // blake3 hash of an identity tangle root msg, or the string 'self', or 'any'
|
||||||
identityTips: Array<string> | null // list of blake3 hashes of identity tangle tips, or null
|
identityTips: Array<string> | null // list of blake3 hashes of identity tangle tips, or null
|
||||||
tangles: {
|
tangles: {
|
||||||
// for each tangle this msg belongs to, identified by the tangle's root
|
// for each tangle this msg belongs to, identified by the tangle's root
|
||||||
|
@ -67,13 +67,19 @@ interface Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityData =
|
type IdentityData =
|
||||||
| { action: 'add' add: IdentityAdd }
|
| { action: 'add', add: IdentityAdd }
|
||||||
| { action: 'del' del: IdentityDel }
|
| { action: 'del', del: IdentityDel }
|
||||||
|
|
||||||
|
// "add" means this keypair can validly add more keypairs to the identity tangle
|
||||||
|
// "del" means this keypair can validly revoke other keypairs from the identity
|
||||||
|
// "box" means the peer with this keypair should get access to the box keypair
|
||||||
|
type IdentityPower = 'add' | 'del' | 'box'
|
||||||
|
|
||||||
type IdentityAdd = {
|
type IdentityAdd = {
|
||||||
key: Key
|
key: Key
|
||||||
nonce?: string // nonce required only on the identity tangle's root
|
nonce?: string // nonce required only on the identity tangle's root
|
||||||
consent?: string // base58 encoded signature of the string `:identity-add:<ID>` where `<ID>` is the identity's ID, required only on non-root msgs
|
consent?: string // base58 encoded signature of the string `:identity-add:<ID>` where `<ID>` is the identity's ID, required only on non-root msgs
|
||||||
|
identityPowers?: Array<IdentityPower> // list of powers granted to this key, defaults to []
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityDel = {
|
type IdentityDel = {
|
||||||
|
@ -84,10 +90,9 @@ type Key =
|
||||||
| {
|
| {
|
||||||
purpose: 'sig' // digital signatures
|
purpose: 'sig' // digital signatures
|
||||||
algorithm: 'ed25519' // libsodium crypto_sign_detached
|
algorithm: 'ed25519' // libsodium crypto_sign_detached
|
||||||
bytes: string // base58 encoded string for the public key being added
|
bytes: string // base58 encoded string for the public key
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// WIP!!
|
|
||||||
purpose: 'box' // asymmetric encryption
|
purpose: 'box' // asymmetric encryption
|
||||||
algorithm: 'x25519-xsalsa20-poly1305' // libsodium crypto_box_easy
|
algorithm: 'x25519-xsalsa20-poly1305' // libsodium crypto_box_easy
|
||||||
bytes: string // base58 encoded string of the public key
|
bytes: string // base58 encoded string of the public key
|
||||||
|
|
|
@ -34,6 +34,7 @@ test('identity.add()', async (t) => {
|
||||||
identity: id,
|
identity: id,
|
||||||
keypair: keypair2,
|
keypair: keypair2,
|
||||||
consent,
|
consent,
|
||||||
|
powers: ['box'],
|
||||||
})
|
})
|
||||||
assert.ok(identityRec1, 'identityRec1 exists')
|
assert.ok(identityRec1, 'identityRec1 exists')
|
||||||
const { hash, msg } = identityRec1
|
const { hash, msg } = identityRec1
|
||||||
|
@ -49,6 +50,7 @@ test('identity.add()', async (t) => {
|
||||||
bytes: keypair2.public,
|
bytes: keypair2.public,
|
||||||
},
|
},
|
||||||
consent,
|
consent,
|
||||||
|
powers: ['box'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'msg.data.add NEW KEY'
|
'msg.data.add NEW KEY'
|
||||||
|
@ -68,6 +70,86 @@ test('identity.add()', async (t) => {
|
||||||
await p(peer.close)()
|
await p(peer.close)()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('keypair with no "add" powers cannot identity.add()', async (t) => {
|
||||||
|
rimraf.sync(DIR)
|
||||||
|
const keypair1 = Keypair.generate('ed25519', 'alice')
|
||||||
|
const keypair2 = Keypair.generate('ed25519', 'bob')
|
||||||
|
const keypair3 = Keypair.generate('ed25519', 'carol')
|
||||||
|
|
||||||
|
const peer1 = SecretStack({ appKey: caps.shse })
|
||||||
|
.use(require('../lib'))
|
||||||
|
.use(require('ssb-box'))
|
||||||
|
.call(null, { keypair: keypair1, path: DIR })
|
||||||
|
|
||||||
|
await peer1.db.loaded()
|
||||||
|
const id = await p(peer1.db.identity.create)({
|
||||||
|
keypair: keypair1,
|
||||||
|
domain: 'account',
|
||||||
|
})
|
||||||
|
const msg1 = peer1.db.get(id)
|
||||||
|
|
||||||
|
const { msg: msg2 } = await p(peer1.db.identity.add)({
|
||||||
|
identity: id,
|
||||||
|
keypair: keypair2,
|
||||||
|
powers: [],
|
||||||
|
})
|
||||||
|
assert.equal(msg2.data.add.key.bytes, keypair2.public)
|
||||||
|
|
||||||
|
assert.equal(peer1.db.identity.has({ identity: id, keypair: keypair2 }), true)
|
||||||
|
|
||||||
|
await p(peer1.close)()
|
||||||
|
rimraf.sync(DIR)
|
||||||
|
|
||||||
|
const peer2 = SecretStack({ appKey: caps.shse })
|
||||||
|
.use(require('../lib'))
|
||||||
|
.use(require('ssb-box'))
|
||||||
|
.call(null, { keypair: keypair2, path: DIR })
|
||||||
|
|
||||||
|
await peer2.db.loaded()
|
||||||
|
await p(peer2.db.add)(msg1, id)
|
||||||
|
await p(peer2.db.add)(msg2, id)
|
||||||
|
|
||||||
|
// Test author-side power validation
|
||||||
|
assert.rejects(
|
||||||
|
p(peer2.db.identity.add)({
|
||||||
|
identity: id,
|
||||||
|
keypair: keypair3,
|
||||||
|
powers: [],
|
||||||
|
}),
|
||||||
|
/signing keypair does not have the "add" power/
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make the author disobey power validation
|
||||||
|
const { msg: msg3 } = await p(peer2.db.identity.add)({
|
||||||
|
identity: id,
|
||||||
|
keypair: keypair3,
|
||||||
|
powers: [],
|
||||||
|
_disobey: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(msg3.data.add.key.bytes, keypair3.public)
|
||||||
|
|
||||||
|
await p(peer2.close)()
|
||||||
|
rimraf.sync(DIR)
|
||||||
|
|
||||||
|
const peer1again = SecretStack({ appKey: caps.shse })
|
||||||
|
.use(require('../lib'))
|
||||||
|
.use(require('ssb-box'))
|
||||||
|
.call(null, { keypair: keypair1, path: DIR })
|
||||||
|
|
||||||
|
await peer1again.db.loaded()
|
||||||
|
await p(peer1again.db.add)(msg1, id) // re-add because lost during rimraf
|
||||||
|
await p(peer1again.db.add)(msg2, id) // re-add because lost during rimraf
|
||||||
|
|
||||||
|
// Test replicator-side power validation
|
||||||
|
assert.rejects(
|
||||||
|
p(peer1again.db.add)(msg3, id),
|
||||||
|
/msg\.pubkey does not have "add" power/
|
||||||
|
)
|
||||||
|
|
||||||
|
await p(peer1again.close)()
|
||||||
|
})
|
||||||
|
|
||||||
test('publish with a key in the identity', async (t) => {
|
test('publish with a key in the identity', async (t) => {
|
||||||
rimraf.sync(DIR)
|
rimraf.sync(DIR)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ test('identity.create() with just "domain"', async (t) => {
|
||||||
bytes: keypair.public,
|
bytes: keypair.public,
|
||||||
},
|
},
|
||||||
nonce: 'MYNONCE',
|
nonce: 'MYNONCE',
|
||||||
|
powers: ['add', 'del', 'box'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'msg.data.add'
|
'msg.data.add'
|
||||||
|
|
|
@ -21,12 +21,13 @@ test('MsgV3.createIdentity()', (t) => {
|
||||||
bytes: keypair.public,
|
bytes: keypair.public,
|
||||||
},
|
},
|
||||||
nonce: 'MYNONCE',
|
nonce: 'MYNONCE',
|
||||||
|
powers: ['add', 'del', 'box'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'data'
|
'data'
|
||||||
)
|
)
|
||||||
assert.equal(identityMsg0.metadata.dataHash, 'C9XZXiZV4kxD6MtVqNkuBw', 'hash')
|
assert.equal(identityMsg0.metadata.dataHash, 'R5az9nC1CB3Afd5Q57HYRQ', 'hash')
|
||||||
assert.equal(identityMsg0.metadata.dataSize, 143, 'size')
|
assert.equal(identityMsg0.metadata.dataSize, 172, 'size')
|
||||||
assert.equal(identityMsg0.metadata.identity, 'self', 'identity')
|
assert.equal(identityMsg0.metadata.identity, 'self', 'identity')
|
||||||
assert.equal(identityMsg0.metadata.identityTips, null, 'identityTips')
|
assert.equal(identityMsg0.metadata.identityTips, null, 'identityTips')
|
||||||
assert.deepEqual(identityMsg0.metadata.tangles, {}, 'tangles')
|
assert.deepEqual(identityMsg0.metadata.tangles, {}, 'tangles')
|
||||||
|
@ -35,7 +36,7 @@ test('MsgV3.createIdentity()', (t) => {
|
||||||
assert.equal(identityMsg0.pubkey, keypair.public, 'pubkey')
|
assert.equal(identityMsg0.pubkey, keypair.public, 'pubkey')
|
||||||
|
|
||||||
identity = MsgV3.getMsgHash(identityMsg0)
|
identity = MsgV3.getMsgHash(identityMsg0)
|
||||||
assert.equal(identity, 'NwZbYERYSrShDwcKrrLVYe', 'identity ID')
|
assert.equal(identity, 'GZJ1T864pFVHKJ2mRS2c5q', 'identity ID')
|
||||||
})
|
})
|
||||||
|
|
||||||
let rootMsg = null
|
let rootMsg = null
|
||||||
|
@ -57,7 +58,7 @@ test('MsgV3.createRoot()', (t) => {
|
||||||
assert.equal(rootMsg.pubkey, keypair.public, 'pubkey')
|
assert.equal(rootMsg.pubkey, keypair.public, 'pubkey')
|
||||||
|
|
||||||
rootHash = MsgV3.getMsgHash(rootMsg)
|
rootHash = MsgV3.getMsgHash(rootMsg)
|
||||||
assert.equal(rootHash, '2yqmqJLffAeomD93izwjx9', 'root hash')
|
assert.equal(rootHash, '4VfVj9DQArX5Vk6PVz5s5J', 'root hash')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('MsgV3.create()', (t) => {
|
test('MsgV3.create()', (t) => {
|
||||||
|
@ -125,11 +126,11 @@ test('MsgV3.create()', (t) => {
|
||||||
)
|
)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
msg1.sig,
|
msg1.sig,
|
||||||
'2GsXePCkaJk1emgjRmwTrn9qqA5GozG8BrDa9je4SeCNJX8KVYr45MyZGmfkJsGBoMocZCMhP4jiFgdL1PqURhx6',
|
'23CPZzKBAeRa6gb2ijwUJAd4VrYmokLSbQTmWEFMCiSogjViwqvms6ShyPq1UCzNWKAggmmJP4qETnVrY4iEMQ5J',
|
||||||
'sig'
|
'sig'
|
||||||
)
|
)
|
||||||
|
|
||||||
const msgHash1 = 'BTybPnRjVjVZMdWBr52kfz'
|
const msgHash1 = 'kF6XHyi1LtJdttRDp54VM'
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
MsgV3.getMsgId(msg1),
|
MsgV3.getMsgId(msg1),
|
||||||
|
@ -201,13 +202,13 @@ test('MsgV3.create()', (t) => {
|
||||||
)
|
)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
msg2.sig,
|
msg2.sig,
|
||||||
'EA8PUp44ZBdgsXa4DRioejc4W5NMapoA9oUbgJwQSsvKDj9nh3oAHn7ScnZo2Hfw67JzHeZXeXVwgLCWzsKCFYw',
|
'tpMaMqV7t4hhYtLPZu7nFmUZej3pXVAYWf3pwXChThsQ8qT9Zxxym2TDDTUrT9VF7CNXRnLNoLMgYuZKAQrZ5bR',
|
||||||
'sig'
|
'sig'
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
MsgV3.getMsgId(msg2),
|
MsgV3.getMsgId(msg2),
|
||||||
`ppppp:message/v3/${identity}/post/3bwFna3PvkSNxssPd9QDYe`,
|
`ppppp:message/v3/${identity}/post/7W2nJCdpMeco7D8BYvRq7A`,
|
||||||
'getMsgId'
|
'getMsgId'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,7 +43,7 @@ test('validate identity tangle', (t) => {
|
||||||
const keypair2 = Keypair.generate('ed25519', 'bob')
|
const keypair2 = Keypair.generate('ed25519', 'bob')
|
||||||
|
|
||||||
const identityMsg1 = MsgV3.create({
|
const identityMsg1 = MsgV3.create({
|
||||||
identity: null,
|
identity: 'self',
|
||||||
identityTips: null,
|
identityTips: null,
|
||||||
domain: 'identity',
|
domain: 'identity',
|
||||||
data: { add: keypair2.public },
|
data: { add: keypair2.public },
|
||||||
|
|
Loading…
Reference in New Issue