mirror of
https://github.com/fluencelabs/js-libp2p-crypto
synced 2025-03-15 19:50:58 +00:00
fix: circular circular dep -> DI
This commit is contained in:
parent
8401154102
commit
0dcf1a6f52
@ -28,7 +28,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^2.5.0",
|
"async": "^2.5.0",
|
||||||
"libp2p-crypto": "~0.9.2",
|
|
||||||
"multihashing-async": "~0.4.6",
|
"multihashing-async": "~0.4.6",
|
||||||
"nodeify": "^1.0.1",
|
"nodeify": "^1.0.1",
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
@ -37,6 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aegir": "^11.0.2",
|
"aegir": "^11.0.2",
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
|
"libp2p-crypto": "~0.9.4",
|
||||||
"chai": "^4.1.0",
|
"chai": "^4.1.0",
|
||||||
"dirty-chai": "^2.0.1",
|
"dirty-chai": "^2.0.1",
|
||||||
"pre-commit": "^1.2.2"
|
"pre-commit": "^1.2.2"
|
||||||
|
89
src/crypto.js
Normal file
89
src/crypto.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const secp256k1 = require('secp256k1')
|
||||||
|
const multihashing = require('multihashing-async')
|
||||||
|
const setImmediate = require('async/setImmediate')
|
||||||
|
|
||||||
|
const HASH_ALGORITHM = 'sha2-256'
|
||||||
|
|
||||||
|
module.exports = (randomBytes) => {
|
||||||
|
const privateKeyLength = 32
|
||||||
|
|
||||||
|
function generateKey (callback) {
|
||||||
|
const done = (err, res) => setImmediate(() => callback(err, res))
|
||||||
|
|
||||||
|
let privateKey
|
||||||
|
do {
|
||||||
|
privateKey = randomBytes(32)
|
||||||
|
} while (!secp256k1.privateKeyVerify(privateKey))
|
||||||
|
|
||||||
|
done(null, privateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashAndSign (key, msg, callback) {
|
||||||
|
const done = (err, res) => setImmediate(() => callback(err, res))
|
||||||
|
|
||||||
|
multihashing.digest(msg, HASH_ALGORITHM, (err, digest) => {
|
||||||
|
if (err) { return done(err) }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sig = secp256k1.sign(digest, key)
|
||||||
|
const sigDER = secp256k1.signatureExport(sig.signature)
|
||||||
|
return done(null, sigDER)
|
||||||
|
} catch (err) { done(err) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashAndVerify (key, sig, msg, callback) {
|
||||||
|
const done = (err, res) => setImmediate(() => callback(err, res))
|
||||||
|
|
||||||
|
multihashing.digest(msg, HASH_ALGORITHM, (err, digest) => {
|
||||||
|
if (err) { return done(err) }
|
||||||
|
try {
|
||||||
|
sig = secp256k1.signatureImport(sig)
|
||||||
|
const valid = secp256k1.verify(digest, sig, key)
|
||||||
|
return done(null, valid)
|
||||||
|
} catch (err) { done(err) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressPublicKey (key) {
|
||||||
|
if (!secp256k1.publicKeyVerify(key)) {
|
||||||
|
throw new Error('Invalid public key')
|
||||||
|
}
|
||||||
|
return secp256k1.publicKeyConvert(key, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decompressPublicKey (key) {
|
||||||
|
return secp256k1.publicKeyConvert(key, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePrivateKey (key) {
|
||||||
|
if (!secp256k1.privateKeyVerify(key)) {
|
||||||
|
throw new Error('Invalid private key')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePublicKey (key) {
|
||||||
|
if (!secp256k1.publicKeyVerify(key)) {
|
||||||
|
throw new Error('Invalid public key')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePublicKey (privateKey) {
|
||||||
|
validatePrivateKey(privateKey)
|
||||||
|
return secp256k1.publicKeyCreate(privateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateKey: generateKey,
|
||||||
|
privateKeyLength: privateKeyLength,
|
||||||
|
hashAndSign: hashAndSign,
|
||||||
|
hashAndVerify: hashAndVerify,
|
||||||
|
compressPublicKey: compressPublicKey,
|
||||||
|
decompressPublicKey: decompressPublicKey,
|
||||||
|
validatePrivateKey: validatePrivateKey,
|
||||||
|
validatePublicKey: validatePublicKey,
|
||||||
|
computePublicKey: computePublicKey
|
||||||
|
}
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const secp256k1 = require('secp256k1')
|
|
||||||
const multihashing = require('multihashing-async')
|
|
||||||
const setImmediate = require('async/setImmediate')
|
|
||||||
const randomBytes = require('libp2p-crypto').randomBytes
|
|
||||||
|
|
||||||
const HASH_ALGORITHM = 'sha2-256'
|
|
||||||
|
|
||||||
exports.privateKeyLength = 32
|
|
||||||
|
|
||||||
exports.generateKey = function (callback) {
|
|
||||||
const done = (err, res) => setImmediate(() => {
|
|
||||||
callback(err, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
let privateKey
|
|
||||||
do {
|
|
||||||
privateKey = randomBytes(32)
|
|
||||||
} while (!secp256k1.privateKeyVerify(privateKey))
|
|
||||||
|
|
||||||
done(null, privateKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.hashAndSign = function (key, msg, callback) {
|
|
||||||
const done = (err, res) => setImmediate(() => {
|
|
||||||
callback(err, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
multihashing.digest(msg, HASH_ALGORITHM, (err, digest) => {
|
|
||||||
if (err) { return done(err) }
|
|
||||||
try {
|
|
||||||
const sig = secp256k1.sign(digest, key)
|
|
||||||
const sigDER = secp256k1.signatureExport(sig.signature)
|
|
||||||
return done(null, sigDER)
|
|
||||||
} catch (err) {
|
|
||||||
done(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.hashAndVerify = function (key, sig, msg, callback) {
|
|
||||||
const done = (err, res) => setImmediate(() => {
|
|
||||||
callback(err, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
multihashing.digest(msg, HASH_ALGORITHM, (err, digest) => {
|
|
||||||
if (err) { return done(err) }
|
|
||||||
try {
|
|
||||||
sig = secp256k1.signatureImport(sig)
|
|
||||||
const valid = secp256k1.verify(digest, sig, key)
|
|
||||||
return done(null, valid)
|
|
||||||
} catch (err) {
|
|
||||||
done(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.compressPublicKey = function compressPublicKey (key) {
|
|
||||||
if (!secp256k1.publicKeyVerify(key)) {
|
|
||||||
throw new Error('Invalid public key')
|
|
||||||
}
|
|
||||||
return secp256k1.publicKeyConvert(key, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.decompressPublicKey = function decompressPublicKey (key) {
|
|
||||||
return secp256k1.publicKeyConvert(key, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.validatePrivateKey = function validatePrivateKey (key) {
|
|
||||||
if (!secp256k1.privateKeyVerify(key)) {
|
|
||||||
throw new Error('Invalid private key')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.validatePublicKey = function validatePublicKey (key) {
|
|
||||||
if (!secp256k1.publicKeyVerify(key)) {
|
|
||||||
throw new Error('Invalid public key')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.computePublicKey = function computePublicKey (privateKey) {
|
|
||||||
exports.validatePrivateKey(privateKey)
|
|
||||||
return secp256k1.publicKeyCreate(privateKey)
|
|
||||||
}
|
|
205
src/index.js
205
src/index.js
@ -1,119 +1,118 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const multihashing = require('multihashing-async')
|
const multihashing = require('multihashing-async')
|
||||||
const crypto = require('./crypto')
|
|
||||||
const pbm = require('libp2p-crypto').keys.pbm
|
|
||||||
|
|
||||||
class Secp256k1PublicKey {
|
module.exports = (keysProtobuf, randomBytes, crypto) => {
|
||||||
constructor (key) {
|
crypto = crypto || require('./crypto')(randomBytes)
|
||||||
crypto.validatePublicKey(key)
|
|
||||||
this._key = key
|
class Secp256k1PublicKey {
|
||||||
|
constructor (key) {
|
||||||
|
crypto.validatePublicKey(key)
|
||||||
|
this._key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
verify (data, sig, callback) {
|
||||||
|
ensure(callback)
|
||||||
|
crypto.hashAndVerify(this._key, sig, data, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal () {
|
||||||
|
return crypto.compressPublicKey(this._key)
|
||||||
|
}
|
||||||
|
|
||||||
|
get bytes () {
|
||||||
|
return keysProtobuf.PublicKey.encode({
|
||||||
|
Type: keysProtobuf.KeyType.Secp256k1,
|
||||||
|
Data: this.marshal()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
equals (key) {
|
||||||
|
return this.bytes.equals(key.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash (callback) {
|
||||||
|
ensure(callback)
|
||||||
|
multihashing(this.bytes, 'sha2-256', callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify (data, sig, callback) {
|
class Secp256k1PrivateKey {
|
||||||
|
constructor (key, publicKey) {
|
||||||
|
this._key = key
|
||||||
|
this._publicKey = publicKey || crypto.computePublicKey(key)
|
||||||
|
crypto.validatePrivateKey(this._key)
|
||||||
|
crypto.validatePublicKey(this._publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
sign (message, callback) {
|
||||||
|
ensure(callback)
|
||||||
|
crypto.hashAndSign(this._key, message, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
get public () {
|
||||||
|
return new Secp256k1PublicKey(this._publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal () {
|
||||||
|
return this._key
|
||||||
|
}
|
||||||
|
|
||||||
|
get bytes () {
|
||||||
|
return keysProtobuf.PrivateKey.encode({
|
||||||
|
Type: keysProtobuf.KeyType.Secp256k1,
|
||||||
|
Data: this.marshal()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
equals (key) {
|
||||||
|
return this.bytes.equals(key.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash (callback) {
|
||||||
|
ensure(callback)
|
||||||
|
multihashing(this.bytes, 'sha2-256', callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmarshalSecp256k1PrivateKey (bytes, callback) {
|
||||||
|
callback(null, new Secp256k1PrivateKey(bytes), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmarshalSecp256k1PublicKey (bytes) {
|
||||||
|
return new Secp256k1PublicKey(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateKeyPair (_bits, callback) {
|
||||||
|
if (callback === undefined && typeof _bits === 'function') {
|
||||||
|
callback = _bits
|
||||||
|
}
|
||||||
|
|
||||||
ensure(callback)
|
ensure(callback)
|
||||||
crypto.hashAndVerify(this._key, sig, data, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
marshal () {
|
crypto.generateKey((err, privateKeyBytes) => {
|
||||||
return crypto.compressPublicKey(this._key)
|
if (err) { return callback(err) }
|
||||||
}
|
|
||||||
|
|
||||||
get bytes () {
|
let privkey
|
||||||
return pbm.PublicKey.encode({
|
try {
|
||||||
Type: pbm.KeyType.Secp256k1,
|
privkey = new Secp256k1PrivateKey(privateKeyBytes)
|
||||||
Data: this.marshal()
|
} catch (err) { return callback(err) }
|
||||||
|
|
||||||
|
callback(null, privkey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
equals (key) {
|
function ensure (callback) {
|
||||||
return this.bytes.equals(key.bytes)
|
if (typeof callback !== 'function') {
|
||||||
}
|
throw new Error('callback is required')
|
||||||
|
|
||||||
hash (callback) {
|
|
||||||
ensure(callback)
|
|
||||||
multihashing(this.bytes, 'sha2-256', callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Secp256k1PrivateKey {
|
|
||||||
constructor (key, publicKey) {
|
|
||||||
this._key = key
|
|
||||||
this._publicKey = publicKey || crypto.computePublicKey(key)
|
|
||||||
crypto.validatePrivateKey(this._key)
|
|
||||||
crypto.validatePublicKey(this._publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
sign (message, callback) {
|
|
||||||
ensure(callback)
|
|
||||||
crypto.hashAndSign(this._key, message, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
get public () {
|
|
||||||
return new Secp256k1PublicKey(this._publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
marshal () {
|
|
||||||
return this._key
|
|
||||||
}
|
|
||||||
|
|
||||||
get bytes () {
|
|
||||||
return pbm.PrivateKey.encode({
|
|
||||||
Type: pbm.KeyType.Secp256k1,
|
|
||||||
Data: this.marshal()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
equals (key) {
|
|
||||||
return this.bytes.equals(key.bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash (callback) {
|
|
||||||
ensure(callback)
|
|
||||||
multihashing(this.bytes, 'sha2-256', callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unmarshalSecp256k1PrivateKey (bytes, callback) {
|
|
||||||
callback(null, new Secp256k1PrivateKey(bytes), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function unmarshalSecp256k1PublicKey (bytes) {
|
|
||||||
return new Secp256k1PublicKey(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateKeyPair (_bits, cb) {
|
|
||||||
if (cb === undefined && typeof _bits === 'function') {
|
|
||||||
cb = _bits
|
|
||||||
}
|
|
||||||
ensure(cb)
|
|
||||||
|
|
||||||
crypto.generateKey((err, privateKeyBytes) => {
|
|
||||||
if (err) {
|
|
||||||
return cb(err)
|
|
||||||
}
|
|
||||||
let privkey
|
|
||||||
try {
|
|
||||||
privkey = new Secp256k1PrivateKey(privateKeyBytes)
|
|
||||||
} catch (err) {
|
|
||||||
cb(err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, privkey)
|
return {
|
||||||
})
|
Secp256k1PublicKey,
|
||||||
}
|
Secp256k1PrivateKey,
|
||||||
|
unmarshalSecp256k1PrivateKey,
|
||||||
function ensure (cb) {
|
unmarshalSecp256k1PublicKey,
|
||||||
if (typeof cb !== 'function') {
|
generateKeyPair
|
||||||
throw new Error('callback is required')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Secp256k1PublicKey,
|
|
||||||
Secp256k1PrivateKey,
|
|
||||||
unmarshalSecp256k1PrivateKey,
|
|
||||||
unmarshalSecp256k1PublicKey,
|
|
||||||
generateKeyPair
|
|
||||||
}
|
|
||||||
|
@ -8,14 +8,15 @@ chai.use(dirtyChai)
|
|||||||
|
|
||||||
const Buffer = require('safe-buffer').Buffer
|
const Buffer = require('safe-buffer').Buffer
|
||||||
|
|
||||||
const secp256k1 = require('../src')
|
|
||||||
const crypto = require('../src/crypto')
|
|
||||||
const libp2pCrypto = require('libp2p-crypto')
|
const libp2pCrypto = require('libp2p-crypto')
|
||||||
const pbm = libp2pCrypto.keys.pbm
|
const keysPBM = libp2pCrypto.keys.keysPBM
|
||||||
const randomBytes = libp2pCrypto.randomBytes
|
const randomBytes = libp2pCrypto.randomBytes
|
||||||
|
const crypto = require('../src/crypto')(randomBytes)
|
||||||
|
|
||||||
describe('secp256k1 keys', () => {
|
describe('secp256k1 keys', () => {
|
||||||
let key
|
let key
|
||||||
|
const secp256k1 = require('../src')(keysPBM, randomBytes)
|
||||||
|
|
||||||
before((done) => {
|
before((done) => {
|
||||||
secp256k1.generateKeyPair((err, _key) => {
|
secp256k1.generateKeyPair((err, _key) => {
|
||||||
expect(err).to.not.exist()
|
expect(err).to.not.exist()
|
||||||
@ -133,10 +134,13 @@ describe('secp256k1 keys', () => {
|
|||||||
|
|
||||||
describe('key generation error', () => {
|
describe('key generation error', () => {
|
||||||
let generateKey
|
let generateKey
|
||||||
|
let secp256k1
|
||||||
|
|
||||||
before((done) => {
|
before((done) => {
|
||||||
generateKey = crypto.generateKey
|
generateKey = crypto.generateKey
|
||||||
crypto.generateKey = (callback) => { callback(new Error('Error generating key')) }
|
crypto.generateKey = (callback) => callback(new Error('Error generating key'))
|
||||||
|
secp256k1 = require('../src')(keysPBM, randomBytes, crypto)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -156,10 +160,13 @@ describe('key generation error', () => {
|
|||||||
|
|
||||||
describe('handles generation of invalid key', () => {
|
describe('handles generation of invalid key', () => {
|
||||||
let generateKey
|
let generateKey
|
||||||
|
let secp256k1
|
||||||
|
|
||||||
before((done) => {
|
before((done) => {
|
||||||
generateKey = crypto.generateKey
|
generateKey = crypto.generateKey
|
||||||
crypto.generateKey = (callback) => { callback(null, Buffer.from('not a real key')) }
|
crypto.generateKey = (callback) => { callback(null, Buffer.from('not a real key')) }
|
||||||
|
secp256k1 = require('../src')(keysPBM, randomBytes, crypto)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -280,13 +287,14 @@ describe('crypto functions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('go interop', () => {
|
describe('go interop', () => {
|
||||||
|
const secp256k1 = require('../src')(keysPBM, randomBytes)
|
||||||
const fixtures = require('./fixtures/go-interop')
|
const fixtures = require('./fixtures/go-interop')
|
||||||
|
|
||||||
it('loads a private key marshaled by go-libp2p-crypto', (done) => {
|
it('loads a private key marshaled by go-libp2p-crypto', (done) => {
|
||||||
// we need to first extract the key data from the protobuf, which is
|
// we need to first extract the key data from the protobuf, which is
|
||||||
// normally handled by js-libp2p-crypto
|
// normally handled by js-libp2p-crypto
|
||||||
const decoded = pbm.PrivateKey.decode(fixtures.privateKey)
|
const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey)
|
||||||
expect(decoded.Type).to.eql(pbm.KeyType.Secp256k1)
|
expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1)
|
||||||
|
|
||||||
secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data, (err, key) => {
|
secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data, (err, key) => {
|
||||||
expect(err).to.not.exist()
|
expect(err).to.not.exist()
|
||||||
@ -298,8 +306,8 @@ describe('go interop', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('loads a public key marshaled by go-libp2p-crypto', (done) => {
|
it('loads a public key marshaled by go-libp2p-crypto', (done) => {
|
||||||
const decoded = pbm.PublicKey.decode(fixtures.publicKey)
|
const decoded = keysPBM.PublicKey.decode(fixtures.publicKey)
|
||||||
expect(decoded.Type).to.be.eql(pbm.KeyType.Secp256k1)
|
expect(decoded.Type).to.be.eql(keysPBM.KeyType.Secp256k1)
|
||||||
|
|
||||||
const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data)
|
const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data)
|
||||||
expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey)
|
expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey)
|
||||||
@ -308,8 +316,8 @@ describe('go interop', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('generates the same signature as go-libp2p-crypto', (done) => {
|
it('generates the same signature as go-libp2p-crypto', (done) => {
|
||||||
const decoded = pbm.PrivateKey.decode(fixtures.privateKey)
|
const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey)
|
||||||
expect(decoded.Type).to.eql(pbm.KeyType.Secp256k1)
|
expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1)
|
||||||
|
|
||||||
secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data, (err, key) => {
|
secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data, (err, key) => {
|
||||||
expect(err).to.not.exist()
|
expect(err).to.not.exist()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user