/**
* Server Identity Manager for Pear-Git P2P
*
* Manages the server's persistent keypair used for:
* - Deriving P2P topic hashes (topic = hash(serverKey + repoName))
* - Identifying the server in pear:// URLs
* - Signing P2P protocol messages (future)
*
* The server key is stored persistently so that pear:// URLs remain stable.
*/
import fs from 'bare-fs'
import path from 'bare-path'
import crypto from 'hypercore-crypto'
export class ServerIdentity {
constructor(options = {}) {
this.storagePath = options.storagePath
this.keyFile = path.join(this.storagePath, 'server-identity.json')
this.publicKey = null
this.secretKey = null
}
/**
* Initialize server identity
* Loads existing keypair or generates new one
*/
async init() {
// Ensure storage directory exists
try {
await fs.promises.mkdir(this.storagePath, { recursive: true })
} catch (err) {
if (err.code !== 'EEXIST') throw err
}
// Try to load existing identity
try {
const data = await fs.promises.readFile(this.keyFile, 'utf-8')
const identity = JSON.parse(data)
this.publicKey = Buffer.from(identity.publicKey, 'hex')
this.secretKey = Buffer.from(identity.secretKey, 'hex')
console.log(`[Identity] Loaded server key: ${this.getPublicKeyHex().slice(0, 8)}...`)
} catch (err) {
if (err.code !== 'ENOENT') throw err
// Generate new identity
await this._generateIdentity()
}
}
/**
* Get public key as hex string (for URLs)
*/
getPublicKeyHex() {
return this.publicKey.toString('hex')
}
/**
* Get public key as buffer (for crypto operations)
*/
getPublicKeyBuffer() {
return this.publicKey
}
/**
* Derive a topic hash for a repository
* topic = hash(serverPublicKey + repoName)
*
* @param {string} repoName - Repository name
* @returns {Buffer} - 32-byte topic hash
*/
deriveTopic(repoName) {
return crypto.hash(Buffer.concat([
this.publicKey,
Buffer.from(repoName, 'utf-8')
])).subarray(0, 32)
}
/**
* Derive topic from any key (for clients)
* @param {string|Buffer} serverKey - Server public key
* @param {string} repoName - Repository name
* @returns {Buffer} - 32-byte topic hash
*/
static deriveTopicFromKey(serverKey, repoName) {
const keyBuffer = typeof serverKey === 'string'
? Buffer.from(serverKey, 'hex')
: serverKey
return crypto.hash(Buffer.concat([
keyBuffer,
Buffer.from(repoName, 'utf-8')
])).subarray(0, 32)
}
/**
* Sign data with server secret key
* @param {Buffer|string} data - Data to sign
* @returns {Buffer} - Signature
*/
sign(data) {
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
return crypto.sign(dataBuffer, this.secretKey)
}
/**
* Verify a signature from this server
* @param {Buffer|string} data - Original data
* @param {Buffer|string} signature - Signature to verify
* @returns {boolean}
*/
verify(data, signature) {
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
const sigBuffer = typeof signature === 'string' ? Buffer.from(signature, 'hex') : signature
return crypto.verify(dataBuffer, sigBuffer, this.publicKey)
}
/**
* Verify a signature from any server
* @param {Buffer|string} data - Original data
* @param {Buffer|string} signature - Signature
* @param {Buffer|string} publicKey - Server public key
* @returns {boolean}
*/
static verifySignature(data, signature, publicKey) {
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
const sigBuffer = typeof signature === 'string' ? Buffer.from(signature, 'hex') : signature
const keyBuffer = typeof publicKey === 'string' ? Buffer.from(publicKey, 'hex') : publicKey
return crypto.verify(dataBuffer, sigBuffer, keyBuffer)
}
/**
* Export identity info (for display, NOT including secret key)
*/
toJSON() {
return {
publicKey: this.getPublicKeyHex(),
shortKey: this.getPublicKeyHex().slice(0, 8) + '...'
}
}
// === Private Methods ===
async _generateIdentity() {
const keypair = crypto.keyPair()
this.publicKey = keypair.publicKey
this.secretKey = keypair.secretKey
// Save to disk
const identity = {
publicKey: this.publicKey.toString('hex'),
secretKey: this.secretKey.toString('hex'),
createdAt: Date.now()
}
await fs.promises.writeFile(this.keyFile, JSON.stringify(identity, null, 2), 'utf-8')
console.log(`[Identity] Generated new server key: ${this.getPublicKeyHex().slice(0, 8)}...`)
}
}
export default ServerIdentity