Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(do not merge) sketch of encrypted primary key #96

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions illustrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const Corestore = require('.')
const b4a = require('b4a')

async function main () {
const loc = 'test-store'
const password = b4a.from('some-dummy-password')

const store = new Corestore(loc, { useEncryptedPrimaryKey: true, password })
await store.ready()
console.log('Loaded store with primary key', store.primaryKey.toString('hex'))

console.log('\nIllustration that we cannot create an unencrypted core:')
try {
store.get({ name: 'my unencrypted core' })
} catch (e) {
console.log(e.message)
}

console.log('\nIllustration that we can create an encrypted core:')
const encryptionKey = b4a.from('a'.repeat(64, 'hex'))
const core = store.get({ name: 'encrypted-core', encryptionKey })
await core.ready()
if (core.length > 0) {
console.log('Loaded existing core--last entry:', (await core.get(core.length - 1)).toString())
} else {
await core.append('block 0')
await core.append('block 1')
console.log('Created a core. Rerun to show it can be reloaded')
}
}

main()
76 changes: 68 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Xache = require('xache')
const b4a = require('b4a')
const ReadyResource = require('ready-resource')
const RW = require('read-write-mutexify')
const { crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE, crypto_pwhash_ALG_DEFAULT, crypto_pwhash_SALTBYTES } = require('sodium-universal') // eslint-disable-line

const [NS] = crypto.namespace('corestore', 1)
const DEFAULT_NAMESPACE = b4a.alloc(32) // This is meant to be 32 0-bytes
Expand Down Expand Up @@ -34,6 +35,11 @@ module.exports = class Corestore extends ReadyResource {
this.compat = typeof opts.compat === 'boolean' ? opts.compat : (root ? root.compat : DEFAULT_COMPAT)
this.inflightRange = opts.inflightRange || null
this.globalCache = opts.globalCache || null
this.useEncryptedPrimaryKey = opts.useEncryptedPrimaryKey === true
this.password = opts.password || null
if (this.password === null && this.useEncryptedPrimaryKey) {
throw new Error('A password must be specified when useEncryptedPrimaryKey is true')
}

this._keyStorage = null
this._bootstrap = opts._bootstrap || null
Expand Down Expand Up @@ -155,18 +161,50 @@ module.exports = class Corestore extends ReadyResource {
this.primaryKey = await new Promise((resolve, reject) => {
this._keyStorage.stat((err, st) => {
if (err && err.code !== 'ENOENT') return reject(err)
if (err || st.size < 32 || this._overwrite) {
const key = this.primaryKey || crypto.randomBytes(32)
return this._keyStorage.write(0, key, err => {
if (this.useEncryptedPrimaryKey) {
if (st !== undefined) {
const expectedSize = 16 + sodium.crypto_generichash_BYTES
if (st.size !== expectedSize) {
reject(new Error('Encrypted-primary-key mode expects a 16-byte salt followed by a checksum'))
}
this._keyStorage.read(0, expectedSize, (err, saltAndCheckSum) => {
if (err) return reject(err)
// TODO: figure out why the following line exists for the other path
// if (this.primaryKey) return resolve(this.primaryKey)
const salt = b4a.alloc(16)
const checkSum = b4a.alloc(sodium.crypto_generichash_BYTES)
b4a.copy(saltAndCheckSum, salt, 0, 0, 16)
b4a.copy(saltAndCheckSum, checkSum, 0, 16, 16 + sodium.crypto_generichash_BYTES)

const primaryKey = getPrimaryKeyFromPassword(this.password, salt)
verifyAgainstChecksum(primaryKey, checkSum)

return resolve(primaryKey)
})
} else {
const salt = crypto.randomBytes(16)
const primaryKey = getPrimaryKeyFromPassword(this.password, salt)
const checkSum = deriveChecksum(primaryKey)
const saltWithChecksum = b4a.concat([salt, checkSum])
this._keyStorage.write(0, saltWithChecksum, err => {
if (err) return reject(err)
return resolve(primaryKey)
})
}
} else {
if (err || st.size < 32 || this._overwrite) {
const key = this.primaryKey || crypto.randomBytes(32)
return this._keyStorage.write(0, key, err => {
if (err) return reject(err)
return resolve(key)
})
}
this._keyStorage.read(0, 32, (err, key) => {
if (err) return reject(err)
if (this.primaryKey) return resolve(this.primaryKey)
return resolve(key)
})
}
this._keyStorage.read(0, 32, (err, key) => {
if (err) return reject(err)
if (this.primaryKey) return resolve(this.primaryKey)
return resolve(key)
})
})
})

Expand Down Expand Up @@ -381,6 +419,9 @@ module.exports = class Corestore extends ReadyResource {
get (opts = {}) {
if (this.closing || this._root.closing) throw new Error('The corestore is closed')
opts = validateGetOptions(opts)
if (this.useEncryptedPrimaryKey && !opts.encryptionKey) {
throw new Error('You must create encrypted hypercores when the corestore operates with an encrypted primary key')
}

if (opts.cache !== false) {
opts.cache = opts.cache === true || (this.cache && !opts.cache) ? defaultCache() : opts.cache
Expand Down Expand Up @@ -589,3 +630,22 @@ async function forceClose (core) {
function getStorageRoot (id) {
return CORES_DIR + '/' + id.slice(0, 2) + '/' + id.slice(2, 4) + '/' + id
}

function getPrimaryKeyFromPassword (passwd, salt, expectedChecksum) {
const primaryKey = b4a.alloc(32)
sodium.crypto_pwhash(primaryKey, passwd, salt, crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE, crypto_pwhash_ALG_DEFAULT)

return primaryKey
}

function deriveChecksum (buffer) {
const checkSum = b4a.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(checkSum, buffer)
return checkSum
}

function verifyAgainstChecksum (buffer, expectedChecksum) {
const checkSum = deriveChecksum(buffer)
if (b4a.equals(checkSum, expectedChecksum)) return true
throw new Error('Checksum mismatch (invalid password?)')
}
Loading