@@ -18,11 +18,6 @@ import ajvFormatsModule from 'ajv-formats';
1818import debounceFn from 'debounce-fn' ;
1919import semver from 'semver' ;
2020import { type JSONSchema } from 'json-schema-typed' ;
21- import {
22- concatUint8Arrays ,
23- stringToUint8Array ,
24- uint8ArrayToString ,
25- } from 'uint8array-extras' ;
2621import {
2722 type Deserialize ,
2823 type Migrations ,
@@ -38,8 +33,6 @@ import {
3833 type Schema ,
3934} from './types.js' ;
4035
41- const encryptionAlgorithm = 'aes-256-cbc' ;
42-
4336const createPlainObject = < T = Record < string , unknown > > ( ) : T => Object . create ( null ) ;
4437
4538// Minimal wrapper: clone and assign to null-prototype object
@@ -69,7 +62,6 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
6962 readonly path : string ;
7063 readonly events : EventTarget ;
7164 #validator?: AjvValidateFunction ;
72- readonly #encryptionKey?: string | Uint8Array | NodeJS . TypedArray | DataView ;
7365 readonly #options: Readonly < Partial < Options < T > > > ;
7466 readonly #defaultValues: Partial < T > = createPlainObject ( ) ;
7567 #isInMigration = false ;
@@ -78,6 +70,7 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
7870 #debouncedChangeHandler?: ( ) => void ;
7971
8072 #cache?: T ;
73+ #internalBackup?: Record < string , unknown > ;
8174 #writePending = false ;
8275 #writeTimer?: NodeJS . Timeout ;
8376 #atomicChangeLock?: PromiseWithResolvers < void > ;
@@ -89,7 +82,6 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
8982 this . #applyDefaultValues( options ) ;
9083 this . #configureSerialization( options ) ;
9184 this . events = new EventTarget ( ) ;
92- this . #encryptionKey = options . encryptionKey ;
9385 this . path = this . #resolvePath( options ) ;
9486 this . #initializeStore( options ) ;
9587
@@ -107,7 +99,6 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
10799 return ;
108100 }
109101
110- this . #cache &&= undefined ;
111102 this . _read ( ) ;
112103 }
113104
@@ -297,6 +288,10 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
297288 delete < Key extends keyof T > ( key : Key ) : void ;
298289 delete < Key extends DotNotationKeyOf < T > > ( key : Key ) : void ;
299290 delete ( key : string ) : void {
291+ if ( this . _isReservedKeyPath ( key ) ) {
292+ throw new Error ( `The key \`${ key } \` is reserved and cannot be deleted` ) ;
293+ }
294+
300295 const { store} = this ;
301296 if ( this . #options. accessPropertiesByDotNotation ) {
302297 deleteProperty ( store , key ) ;
@@ -395,25 +390,21 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
395390 }
396391
397392 set store ( value : T ) {
398- this . _ensureDirectory ( ) ;
399-
400393 // Preserve existing internal data if it exists and the new value doesn't contain it
401- if ( ! hasProperty ( value , INTERNAL_KEY ) ) {
394+ if ( hasProperty ( value , INTERNAL_KEY ) ) {
395+ this . #internalBackup = getProperty ( value , INTERNAL_KEY ) ;
396+ } else if ( this . #internalBackup) {
402397 try {
403398 // Read directly from file to avoid recursion during migration
404- const data = fs . readFileSync ( this . path , this . #encryptionKey ? null : 'utf8' ) ;
405- const dataString = this . _decryptData ( data ) ;
406- const currentStore = this . _deserialize ( dataString ) ;
407- if ( hasProperty ( currentStore , INTERNAL_KEY ) ) {
408- setProperty ( value , INTERNAL_KEY , getProperty ( currentStore , INTERNAL_KEY ) ) ;
409- }
399+ setProperty ( value , INTERNAL_KEY , this . #internalBackup) ;
410400 } catch {
411401 // Silently ignore errors when trying to preserve internal data
412402 // This could happen if the file doesn't exist yet or is corrupted
413403 // In these cases, we just proceed without preserving internal data
414404 }
415405 }
416406
407+ // Validate before updating cache to ensure cache is never left in invalid state
417408 if ( ! this . #isInMigration) {
418409 this . _validate ( value ) ;
419410 }
@@ -487,32 +478,24 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
487478 }
488479 }
489480
490- private _decryptData ( data : string | Uint8Array ) : string {
491- if ( ! this . #encryptionKey) {
492- return typeof data === 'string' ? data : uint8ArrayToString ( data ) ;
481+ private _decryptData ( data : string | Buffer ) : string {
482+ const { encryption} = this . #options;
483+
484+ if ( ! encryption ) {
485+ return data . toString ( ) ;
493486 }
494487
495- // Check if an initialization vector has been used to encrypt the data.
496- try {
497- const initializationVector = data . slice ( 0 , 16 ) ;
498- const password = crypto . pbkdf2Sync ( this . #encryptionKey, initializationVector , 10_000 , 32 , 'sha512' ) ;
499- const decipher = crypto . createDecipheriv ( encryptionAlgorithm , password , initializationVector ) ;
500- const slice = data . slice ( 17 ) ;
501- const dataUpdate = typeof slice === 'string' ? stringToUint8Array ( slice ) : slice ;
502- return uint8ArrayToString ( concatUint8Arrays ( [ decipher . update ( dataUpdate ) , decipher . final ( ) ] ) ) ;
503- } catch {
504- try {
505- // Fallback to legacy scheme (iv.toString() as salt)
506- const initializationVector = data . slice ( 0 , 16 ) ;
507- const password = crypto . pbkdf2Sync ( this . #encryptionKey, initializationVector . toString ( ) , 10_000 , 32 , 'sha512' ) ;
508- const decipher = crypto . createDecipheriv ( encryptionAlgorithm , password , initializationVector ) ;
509- const slice = data . slice ( 17 ) ;
510- const dataUpdate = typeof slice === 'string' ? stringToUint8Array ( slice ) : slice ;
511- return uint8ArrayToString ( concatUint8Arrays ( [ decipher . update ( dataUpdate ) , decipher . final ( ) ] ) ) ;
512- } catch { }
488+ return encryption . decrypt ( typeof data === 'string' ? Buffer . from ( data ) : data ) ;
489+ }
490+
491+ private _encryptData ( data : string ) : Buffer {
492+ const { encryption} = this . #options;
493+
494+ if ( ! encryption ) {
495+ return Buffer . from ( data ) ;
513496 }
514497
515- return typeof data === 'string' ? data : uint8ArrayToString ( data ) ;
498+ return encryption . encrypt ( data ) ;
516499 }
517500
518501 private _handleStoreChange ( callback : OnDidAnyChangeCallback < T > ) : Unsubscribe {
@@ -563,18 +546,15 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
563546 }
564547
565548 private readonly _deserialize : Deserialize < T > = value => {
566- const { deserialize, encryption} = this . #options;
567- const data = encryption ? encryption . decrypt ( Uint8Array . from ( value ) ) : value ;
549+ const { deserialize} = this . #options;
568550
569- return deserialize ? deserialize ( data ) : JSON . parse ( data ) ;
551+ return deserialize ? deserialize ( value ) : JSON . parse ( value ) ;
570552 } ;
571553
572554 private readonly _serialize : Serialize < T > = value => {
573- const { serialize, encryption } = this . #options;
555+ const { serialize} = this . #options;
574556
575- const data = serialize ? serialize ( value ) : JSON . stringify ( value , undefined , '\t' ) ;
576-
577- return encryption ? encryption . encrypt ( data ) . toString ( ) : data ;
557+ return serialize ? serialize ( value ) : JSON . stringify ( value , undefined , '\t' ) ;
578558 } ;
579559
580560 private _validate ( data : T | unknown ) : void {
@@ -598,8 +578,10 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
598578 }
599579
600580 private _read ( ) : void {
581+ this . #internalBackup = undefined ;
582+
601583 try {
602- const data = fs . readFileSync ( this . path , this . #encryptionKey ? null : 'utf8' ) ;
584+ const data = fs . readFileSync ( this . path ) ;
603585 const dataString = this . _decryptData ( data ) ;
604586 const deserializedData = this . _deserialize ( dataString ) ;
605587
@@ -608,6 +590,8 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
608590 }
609591
610592 this . #cache = Object . assign ( createPlainObject ( ) , deserializedData ) ;
593+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
594+ this . #internalBackup = this . #cache[ INTERNAL_KEY ] ?? undefined ;
611595 } catch ( error : unknown ) {
612596 if ( ( error as any ) ?. code === 'ENOENT' ) {
613597 this . _ensureDirectory ( ) ;
@@ -642,22 +626,18 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
642626 if ( this . #writeTimer) {
643627 this . #writePending = true ;
644628 } else {
645- this . _forceWrite ( ) ;
629+ this . writeToDisk ( ) ;
646630 this . _startWriteTimeout ( ) ;
647631 }
648632 }
649633
650- private _forceWrite ( ) : void {
634+ writeToDisk ( ) : void {
651635 this . _cancelWriteTimeout ( ) ;
652636
653- let data : string | Uint8Array = this . _serialize ( this . #cache! ) ;
637+ // Validation already done in _write(), no need to validate again here
638+ const data : string | Uint8Array = this . _encryptData ( this . _serialize ( this . #cache ?? createPlainObject < T > ( ) ) ) ;
654639
655- if ( this . #encryptionKey) {
656- const initializationVector = crypto . randomBytes ( 16 ) ;
657- const password = crypto . pbkdf2Sync ( this . #encryptionKey, initializationVector , 10_000 , 32 , 'sha512' ) ;
658- const cipher = crypto . createCipheriv ( encryptionAlgorithm , password , initializationVector ) ;
659- data = concatUint8Arrays ( [ initializationVector , stringToUint8Array ( ':' ) , cipher . update ( stringToUint8Array ( data ) ) , cipher . final ( ) ] ) ;
660- }
640+ this . _ensureDirectory ( ) ;
661641
662642 // Temporary workaround for Conf being packaged in a Ubuntu Snap app.
663643 // See https://github.com/sindresorhus/conf/pull/82
@@ -689,7 +669,7 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
689669
690670 if ( this . #writePending) {
691671 this . #writePending = false ;
692- this . _forceWrite ( ) ;
672+ this . writeToDisk ( ) ;
693673 }
694674 } , this . #options. writeTimeout ) ;
695675 }
@@ -775,11 +755,6 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
775755 } catch ( error : unknown ) {
776756 // Restore backup (validation is skipped during migration)
777757 this . store = storeBackup ;
778- // Try to write the restored state to disk to ensure rollback persists
779- // If write fails (e.g., read-only file), we still throw the original error
780- try {
781- this . _write ( storeBackup ) ;
782- } catch { }
783758
784759 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
785760 throw new Error ( `Something went wrong during the migration! Changes applied to the store until this failed migration will be restored. ${ errorMessage } ` ) ;
0 commit comments