11const dataparty_crypto = require ( '@dataparty/crypto' )
22
33const debug = require ( 'debug' ) ( 'dataparty.config.secure-config' )
4-
54const deepSet = require ( 'lodash' ) . set
5+
66const reach = require ( '../utils/reach' )
77
88const IConfig = require ( './iconfig' )
@@ -11,6 +11,16 @@ const PASSWORD_HASHING_ROUNDS = 1000000
1111const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000 //! 5min
1212
1313class SecureConfig extends IConfig {
14+
15+ /**
16+ * @class module:Config.SecureConfig
17+ * @implements {module:Config.SecureConfig}
18+ * @link module.Config
19+ * @param {string } id The id of this secure config. Multiple secure configs can be stored within a single `IConfig`
20+ * @param {IConfig } config The underlying IConfig to use for storage
21+ * @param {number } timeoutMs Timeout since last unlock, after which the config will be locked. Defaults to 5 minutes.
22+ * @param {boolean } includeActivity When set to `true` the timeout is reset after any read/write activity. Defaults to `true`
23+ */
1424 constructor ( {
1525 id = 'secure-config' ,
1626 config, timeoutMs= DEFAULT_TIMEOUT_MS , includeActivity= true
@@ -24,78 +34,175 @@ class SecureConfig extends IConfig {
2434 this . timer = null
2535 this . lastActivity = null
2636 this . timeoutMs = timeoutMs || DEFAULT_TIMEOUT_MS
27- this . includeActivity = includeActivity || true
37+ this . includeActivity = ( typeof includeActivity === 'boolean' ) ? includeActivity : true
2838
2939 this . blocked = false
3040 }
3141
42+ /**
43+ * Start the secure storage
44+ * @fires module:Config.SecureConfig#setup-required
45+ * @fires module:Config.SecureConfig#ready
46+ */
3247 async start ( ) {
3348 await this . config . start ( )
3449
3550 const isReady = await this . isInitialized ( )
3651 if ( ! isReady ) {
52+ /**
53+ * Setup required event. The secure config has not yet has a password
54+ * or key configured.
55+ * @event module:Config.SecureConfig#setup-required
56+ */
3757 this . emit ( 'setup-required' )
3858 } else {
59+ /**
60+ * Ready event. The secure config is ready to be unlocked and have
61+ * configuration values read or written.
62+ * @event module:Config.SecureConfig#ready
63+ */
3964 this . emit ( 'ready' )
4065 }
4166 }
4267
68+ /**
69+ * Checks if the secure config has initialized with a password or key
70+ * @returns {boolean }
71+ */
4372 async isInitialized ( ) {
44- let salt = await this . config . read ( 'salt' )
45- let rounds = await this . config . read ( 'rounds' )
4673
74+ let keyType = await this . config . read ( this . id + '.settings.type' )
75+
76+ if ( keyType == 'pbkdf2' ) {
4777
48- return ( salt != undefined && salt . length > 16 && rounds > 100000 )
78+ let salt = await this . config . read ( this . id + '.settings.salt' )
79+ let rounds = await this . config . read ( this . id + '.settings.rounds' )
80+
81+ return ( salt != undefined && salt . length > 16 && rounds > 100000 )
82+
83+ } else if ( keyType == 'key' ) {
84+ return true
85+ } else if ( ! keyType ) {
86+ return false
87+ } else {
88+ debug ( 'type' , keyType )
89+ throw new Error ( 'unexpected key type' )
90+ }
4991 }
5092
93+ /**
94+ * Checks if the secure config is locked. If not locked the secure config
95+ * can be used without blocking waiting for user to unlock.
96+ * @returns {boolean }
97+ */
5198 isLocked ( ) {
5299 return this . content == null || this . identity == null
53100 }
54101
55- async setPassword ( password , defaults = { } ) {
102+ /**
103+ * Initialize the secure config with a password
104+ * @fires module:Config.SecureConfig#intialized
105+ * @fires module:Config.SecureConfig#ready
106+ * @param {string } password
107+ * @param {object } defaults
108+ * @param {('pbkdf2') } type
109+ * @async
110+ */
111+ async setPassword ( password , defaults = { } , type = 'pbkdf2' ) {
56112 debug ( 'setPassword' )
57113 if ( await this . isInitialized ( ) ) { throw new Error ( 'already initialized' ) }
58114
59- const salt = await dataparty_crypto . Routines . generateSalt ( )
60- const rounds = PASSWORD_HASHING_ROUNDS
115+ let key = null
116+ let settings = null
61117
118+ if ( type == 'pbkdf2' ) {
119+ const salt = await dataparty_crypto . Routines . generateSalt ( )
120+ const rounds = PASSWORD_HASHING_ROUNDS
62121
63- await this . config . write ( 'salt' , salt . toString ( 'hex' ) )
64- await this . config . write ( 'rounds' , rounds )
122+ settings = {
123+ type : type ,
124+ salt : salt . toString ( 'hex' ) ,
125+ rounds
126+ }
127+
128+ key = await dataparty_crypto . Routines . createKeyFromPassword ( password , salt , rounds )
65129
66- let key = await dataparty_crypto . Routines . createKeyFromPassword ( password , salt , rounds )
130+ } else {
131+ throw new Error ( 'unsupported KDF[' + type + ']' )
132+ }
67133
68- await this . setIdentity ( key )
134+ await this . initialize ( key , defaults , settings )
69135 }
70136
71- async setIdentity ( key ) {
137+ /**
138+ * Initialize the secure config with a key
139+ * @fires module:Config.SecureConfig#intialized
140+ * @fires module:Config.SecureConfig#ready
141+ * @param {dataparty_crypto/IKey } key
142+ * @param {object } defaults
143+ * @async
144+ */
145+ async setIdentity ( key , defaults ) {
72146 debug ( 'setIdentity' )
73147 if ( await this . isInitialized ( ) ) { throw new Error ( 'already initialized' ) }
74148
149+ const settings = {
150+ key_type : 'key'
151+ }
152+
153+ await this . initialize ( key , defaults , settings )
154+ }
155+
156+
157+ async initialize ( key , defaults , settings ) {
158+ debug ( 'initialize - type:' , settings . key_type )
159+ if ( await this . isInitialized ( ) ) { throw new Error ( 'already initialized' ) }
160+
75161 const pwIdentity = new dataparty_crypto . Identity ( {
76162 key,
77163 id : this . id ,
78164 } )
79165
80- const initialContent = new dataparty_crypto . Message ( { msg : defaults } )
166+ const insecureContent = {
167+ ...settings
168+ }
169+
170+ const secureContent = {
171+ created : Date . now ( ) ,
172+ ...defaults
173+ }
174+
175+ const initialContent = new dataparty_crypto . Message ( { msg : secureContent } )
81176
82177 await initialContent . encrypt ( pwIdentity , pwIdentity . toMini ( ) )
83178
84- await this . config . write ( 'content' , initialContent . toJSON ( ) )
179+ await this . config . write ( this . id + '.settings' , insecureContent )
180+ await this . config . write ( this . id + '.content' , initialContent . toJSON ( ) )
85181
86182 debug ( '\t' , 'identity' , pwIdentity )
87183
88- debug ( '\t' , 'content' , initialContent . toJSON ( ) )
184+ debug ( '\t' , 'insecure content' , insecureContent )
185+
186+ debug ( '\t' , 'secure content' , secureContent )
187+
188+ debug ( '\t' , 'encrypted content' , initialContent . toJSON ( ) )
89189
90190 await this . config . save ( )
91191
92192
93193 const contentMsg = new dataparty_crypto . Message ( initialContent )
94194 //! Verify message
95195 await contentMsg . decrypt ( pwIdentity )
196+ /**
197+ * The secure config has been successfully initialized with a passowrd
198+ * or key.
199+ * @event module:Config.SecureConfig#intialized
200+ */
201+ this . emit ( 'initialized' )
96202 this . emit ( 'ready' )
97203 }
98204
205+
99206 async waitForUnlocked ( reason ) {
100207
101208 if ( ! this . isLocked ( ) ) {
@@ -105,6 +212,12 @@ class SecureConfig extends IConfig {
105212 debug ( 'waitForUnlocked' , reason )
106213
107214 if ( ! this . blocked ) {
215+ /**
216+ * An read/write operation has been blocked due to the secure config
217+ * being locked.
218+ * @event module:Config.SecureConfig#blocked
219+ * @type
220+ */
108221 this . emit ( 'blocked' , reason )
109222 }
110223
@@ -124,15 +237,21 @@ class SecureConfig extends IConfig {
124237 await waiting
125238 }
126239
240+ /**
241+ * Unlocks the secure config
242+ * @fires module:Config.SecureConfig#unlocked
243+ * @param {string } password
244+ * @returns {Promise }
245+ */
127246 async unlock ( password ) {
128247
129248 if ( this . timer != null ) {
130249 clearTimeout ( this . timer )
131250 this . timer = null
132251 }
133252
134- let salt = Buffer . from ( await this . config . read ( ' salt') , 'hex' )
135- let rounds = await this . config . read ( ' rounds')
253+ let salt = Buffer . from ( await this . config . read ( this . id + '.settings. salt') , 'hex' )
254+ let rounds = await this . config . read ( this . id + '.settings. rounds')
136255
137256 let key = await dataparty_crypto . Routines . createKeyFromPassword ( password , salt , rounds )
138257
@@ -142,7 +261,7 @@ class SecureConfig extends IConfig {
142261 } )
143262
144263
145- this . content = await this . config . read ( ' content')
264+ this . content = await this . config . read ( this . id + '. content')
146265
147266 const contentMsg = new dataparty_crypto . Message ( this . content )
148267
@@ -155,9 +274,18 @@ class SecureConfig extends IConfig {
155274 this . timer = setTimeout ( this . onTimeout . bind ( this ) , this . timeoutMs )
156275 }
157276
277+ /**
278+ * The secure config has been unlocked
279+ * @event module:Config.SecureConfig#unlocked
280+ */
158281 this . emit ( 'unlocked' )
159282 }
160283
284+ /**
285+ * Locks the secure config
286+ * @fires module:Config.SecureConfig#locked
287+ * @param {string } password
288+ */
161289 lock ( ) {
162290 if ( this . timer != null ) {
163291 clearTimeout ( this . timer )
@@ -170,6 +298,10 @@ class SecureConfig extends IConfig {
170298 this . content = null
171299 this . identity = null
172300
301+ /**
302+ * The secure config has been locked
303+ * @event module:Config.SecureConfig#locked
304+ */
173305 this . emit ( 'locked' )
174306 }
175307
@@ -269,7 +401,7 @@ class SecureConfig extends IConfig {
269401
270402 this . updateTimeout ( )
271403
272- await this . config . write ( ' content', {
404+ await this . config . write ( this . id + '. content', {
273405 enc : this . content . enc ,
274406 sig : this . content . sig
275407 } )
0 commit comments