From cbfdcfdc786cc718c378a3fecb34833626ede587 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 20:52:45 +0200 Subject: [PATCH 1/9] encrypted-string dependency updated to v2 --- mongoose-encrypted-string.js | 2 +- package-lock.json | 12 ++++++------ package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mongoose-encrypted-string.js b/mongoose-encrypted-string.js index 422d703..e0df660 100644 --- a/mongoose-encrypted-string.js +++ b/mongoose-encrypted-string.js @@ -4,7 +4,7 @@ const sc = require('@tsmx/string-crypto'); class EncryptedString extends SchemaType { constructor(key, options) { options.get = (v) => { return sc.decrypt(v, { key: EncryptedString.options.key, passNull: true }); }; - options.set = (v) => { return sc.encrypt(v, { key: EncryptedString.options.key, passNull: true }); }; + options.set = (v) => { return sc.encrypt(v, { key: EncryptedString.options.key, passNull: true, algorithm: 'aes-256-cbc' }); }; super(key, options, 'EncryptedString'); } diff --git a/package-lock.json b/package-lock.json index ae09aea..7c531e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.9", "license": "MIT", "dependencies": { - "@tsmx/string-crypto": "^1.0.4", + "@tsmx/string-crypto": "^2.0.0", "mongoose": "^9.0.0" }, "devDependencies": { @@ -1232,13 +1232,13 @@ } }, "node_modules/@tsmx/string-crypto": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@tsmx/string-crypto/-/string-crypto-1.0.7.tgz", - "integrity": "sha512-lXgcJTOnfR4J4b+aRubB5JPnogtsGSqcj/hXb16J8kerAQhQWO6zDf4H814K5ZxFxqjlixe4x66goBJSet235A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tsmx/string-crypto/-/string-crypto-2.0.0.tgz", + "integrity": "sha512-k+zVoCEtzjLVAEJwxzhctSw4ZT3PXdymMKfIhsv78dXQUYmqasyuuRUTdzpRAJI2532l27vyyjoIjxNeXQLHPQ==", "license": "MIT", "engines": { - "node": ">=10.0.0", - "npm": ">=6.0.0" + "node": ">=16.9.0", + "npm": ">=7.0.0" } }, "node_modules/@types/babel__core": { diff --git a/package.json b/package.json index ab38d93..d8abb2a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "crypto" ], "dependencies": { - "@tsmx/string-crypto": "^1.0.4", + "@tsmx/string-crypto": "^2.0.0", "mongoose": "^9.0.0" }, "devDependencies": { From c4037a3592658622094b68891384f16d368ca577 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 21:14:15 +0200 Subject: [PATCH 2/9] Encryption algorithm added to the library export function, CBC test suite fixed --- mongoose-encrypted-string.js | 23 ++++++++++++++++--- ...est.js => encryptedstring-aes-cbc.test.js} | 4 ++-- 2 files changed, 22 insertions(+), 5 deletions(-) rename test/{mongoose-encryptedstring.test.js => encryptedstring-aes-cbc.test.js} (96%) diff --git a/mongoose-encrypted-string.js b/mongoose-encrypted-string.js index e0df660..ef97cec 100644 --- a/mongoose-encrypted-string.js +++ b/mongoose-encrypted-string.js @@ -1,10 +1,23 @@ const SchemaType = require('mongoose').SchemaType; const sc = require('@tsmx/string-crypto'); +const allowedAlgorithms = ['aes-256-gcm', 'aes-256-cbc']; + class EncryptedString extends SchemaType { constructor(key, options) { - options.get = (v) => { return sc.decrypt(v, { key: EncryptedString.options.key, passNull: true }); }; - options.set = (v) => { return sc.encrypt(v, { key: EncryptedString.options.key, passNull: true, algorithm: 'aes-256-cbc' }); }; + options.get = (v) => { + return sc.decrypt(v, { + key: EncryptedString.options.key, + passNull: true + }); + }; + options.set = (v) => { + return sc.encrypt(v, { + key: EncryptedString.options.key, + passNull: true, + algorithm: EncryptedString.options.algorithm + }); + }; super(key, options, 'EncryptedString'); } @@ -18,7 +31,11 @@ class EncryptedString extends SchemaType { } -module.exports.registerEncryptedString = function (mongoose, key) { +module.exports.registerEncryptedString = function (mongoose, key, algorithm = 'aes-256-gcm') { + if (!allowedAlgorithms.includes(algorithm)) { + throw new Error(`Invalid algorithm '${algorithm}'. Allowed: ${allowedAlgorithms.join(', ')}`); + } EncryptedString.options.key = key; + EncryptedString.options.algorithm = algorithm; mongoose.Schema.Types.EncryptedString = EncryptedString; }; \ No newline at end of file diff --git a/test/mongoose-encryptedstring.test.js b/test/encryptedstring-aes-cbc.test.js similarity index 96% rename from test/mongoose-encryptedstring.test.js rename to test/encryptedstring-aes-cbc.test.js index ed7b087..4395593 100644 --- a/test/mongoose-encryptedstring.test.js +++ b/test/encryptedstring-aes-cbc.test.js @@ -3,7 +3,7 @@ const sc = require('@tsmx/string-crypto'); const { MongoMemoryServer } = require('mongodb-memory-server'); const mes = require('../mongoose-encrypted-string'); -describe('mongoose-encrypted-string test suite', () => { +describe('mongoose-encrypted-string AES-256-CBC test suite', () => { const testKey = '9af7d400be4705147dc724db25bfd2513aa11d6013d7bf7bdb2bfe050593bd0f'; @@ -14,7 +14,7 @@ describe('mongoose-encrypted-string test suite', () => { beforeAll(async () => { mongoServer = await MongoMemoryServer.create({ dbName: 'encryptedstring' }); await mongoose.connect(mongoServer.getUri()); - mes.registerEncryptedString(mongoose, testKey); + mes.registerEncryptedString(mongoose, testKey, 'aes-256-cbc'); Person = mongoose.model('Person', { id: { type: String, required: true }, firstName: { type: mongoose.Schema.Types.EncryptedString }, From af3f42967f049b939c9e37457a51421a3dabeb72 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 21:24:06 +0200 Subject: [PATCH 3/9] CBC unit tests improved --- test/encryptedstring-aes-cbc.test.js | 34 +++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/encryptedstring-aes-cbc.test.js b/test/encryptedstring-aes-cbc.test.js index 4395593..ab2413a 100644 --- a/test/encryptedstring-aes-cbc.test.js +++ b/test/encryptedstring-aes-cbc.test.js @@ -47,36 +47,38 @@ describe('mongoose-encrypted-string AES-256-CBC test suite', () => { let savedPerson = await person.save(); expect(savedPerson).toBeDefined(); expect(savedPerson._id).toBeDefined(); - expect(savedPerson.firstName).toBe('Hans'); - expect(savedPerson.lastName).toBe('Müller'); + expect(savedPerson.firstName).toStrictEqual('Hans'); + expect(savedPerson.lastName).toStrictEqual('Müller'); let savedPersonLean = await Person.findById(savedPerson._id).lean(); - expect(savedPersonLean.firstName).not.toBe('Hans'); + expect(savedPersonLean.firstName).not.toStrictEqual('Hans'); let firstNameParts = savedPersonLean.firstName.split('|'); - expect(firstNameParts.length).toBe(2); - expect(savedPersonLean.lastName).not.toBe('Müller'); + expect(firstNameParts.length).toStrictEqual(2); + expect(savedPersonLean.lastName).not.toStrictEqual('Müller'); let lastNameParts = savedPersonLean.firstName.split('|'); - expect(lastNameParts.length).toBe(2); + expect(lastNameParts.length).toStrictEqual(2); }); it('tests a successful document update', async () => { let person = await Person.findOne({ id: 'id-test' }); expect(person).toBeDefined(); - expect(person.firstName).toBe('FirstNameTest'); - expect(person.lastName).toBe('LastNameTest'); + expect(person.firstName).toStrictEqual('FirstNameTest'); + expect(person.lastName).toStrictEqual('LastNameTest'); person.firstName = 'NewFirstName'; await person.save(); let updatedPerson = await Person.findOne({ id: 'id-test' }); - expect(updatedPerson.firstName).toBe('NewFirstName'); - expect(updatedPerson.lastName).toBe('LastNameTest'); + expect(updatedPerson.firstName).toStrictEqual('NewFirstName'); + expect(updatedPerson.lastName).toStrictEqual('LastNameTest'); }); it('tests a successful manual decryption of a document from a lean query', async () => { let person = await Person.findOne({ id: 'id-test' }).lean(); expect(person).toBeDefined(); - expect(person.firstName).not.toBe('FirstNameTest'); - expect(person.lastName).not.toBe('LastNameTest'); - expect(sc.decrypt(person.firstName, { key: testKey })).toBe('FirstNameTest'); - expect(sc.decrypt(person.lastName, { key: testKey })).toBe('LastNameTest'); + expect(person.firstName).not.toStrictEqual('FirstNameTest'); + expect(person.firstName.split('|').length).toStrictEqual(2); + expect(person.lastName).not.toStrictEqual('LastNameTest'); + expect(person.lastName.split('|').length).toStrictEqual(2); + expect(sc.decrypt(person.firstName, { key: testKey })).toStrictEqual('FirstNameTest'); + expect(sc.decrypt(person.lastName, { key: testKey })).toStrictEqual('LastNameTest'); }); it('tests a successful document creation and retrieval with null values', async () => { @@ -88,12 +90,12 @@ describe('mongoose-encrypted-string AES-256-CBC test suite', () => { expect(savedPerson).toBeDefined(); expect(savedPerson._id).toBeDefined(); expect(savedPerson.firstName).toStrictEqual(null); - expect(savedPerson.lastName).toBe('Müller'); + expect(savedPerson.lastName).toStrictEqual('Müller'); let retrievedPerson = await Person.findOne({ id: 'id-1' }); expect(retrievedPerson).toBeDefined(); expect(retrievedPerson._id).toBeDefined(); expect(retrievedPerson.firstName).toStrictEqual(null); - expect(retrievedPerson.lastName).toBe('Müller'); + expect(retrievedPerson.lastName).toStrictEqual('Müller'); }); }); \ No newline at end of file From 73a072a70de34e1e0644d6ee11d19098c15c5246 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 21:27:09 +0200 Subject: [PATCH 4/9] GCM test suite added --- test/encryptedstring-aes-gcm.test.js | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/encryptedstring-aes-gcm.test.js diff --git a/test/encryptedstring-aes-gcm.test.js b/test/encryptedstring-aes-gcm.test.js new file mode 100644 index 0000000..da6347e --- /dev/null +++ b/test/encryptedstring-aes-gcm.test.js @@ -0,0 +1,101 @@ +const mongoose = require('mongoose'); +const sc = require('@tsmx/string-crypto'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const mes = require('../mongoose-encrypted-string'); + +describe('mongoose-encrypted-string AES-256-GCM test suite', () => { + + const testKey = '9af7d400be4705147dc724db25bfd2513aa11d6013d7bf7bdb2bfe050593bd0f'; + + var mongoServer = null; + var Person = null; + + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create({ dbName: 'encryptedstring' }); + await mongoose.connect(mongoServer.getUri()); + mes.registerEncryptedString(mongoose, testKey, 'aes-256-gcm'); + Person = mongoose.model('Person', { + id: { type: String, required: true }, + firstName: { type: mongoose.Schema.Types.EncryptedString }, + lastName: { type: mongoose.Schema.Types.EncryptedString } + }); + }); + + afterAll(async () => { + await mongoose.connection.close(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + let testPerson = new Person(); + testPerson.id = 'id-test'; + testPerson.firstName = 'FirstNameTest'; + testPerson.lastName = 'LastNameTest'; + await testPerson.save(); + }); + + afterEach(async () => { + await Person.deleteMany(); + }); + + it('tests a successful document creation', async () => { + let person = new Person(); + person.id = 'id-1'; + person.firstName = 'Hans'; + person.lastName = 'Müller'; + let savedPerson = await person.save(); + expect(savedPerson).toBeDefined(); + expect(savedPerson._id).toBeDefined(); + expect(savedPerson.firstName).toStrictEqual('Hans'); + expect(savedPerson.lastName).toStrictEqual('Müller'); + let savedPersonLean = await Person.findById(savedPerson._id).lean(); + expect(savedPersonLean.firstName).not.toStrictEqual('Hans'); + let firstNameParts = savedPersonLean.firstName.split('|'); + expect(firstNameParts.length).toStrictEqual(3); + expect(savedPersonLean.lastName).not.toStrictEqual('Müller'); + let lastNameParts = savedPersonLean.firstName.split('|'); + expect(lastNameParts.length).toStrictEqual(3); + }); + + it('tests a successful document update', async () => { + let person = await Person.findOne({ id: 'id-test' }); + expect(person).toBeDefined(); + expect(person.firstName).toStrictEqual('FirstNameTest'); + expect(person.lastName).toStrictEqual('LastNameTest'); + person.firstName = 'NewFirstName'; + await person.save(); + let updatedPerson = await Person.findOne({ id: 'id-test' }); + expect(updatedPerson.firstName).toStrictEqual('NewFirstName'); + expect(updatedPerson.lastName).toStrictEqual('LastNameTest'); + }); + + it('tests a successful manual decryption of a document from a lean query', async () => { + let person = await Person.findOne({ id: 'id-test' }).lean(); + expect(person).toBeDefined(); + expect(person.firstName).not.toStrictEqual('FirstNameTest'); + expect(person.firstName.split('|').length).toStrictEqual(3); + expect(person.lastName).not.toStrictEqual('LastNameTest'); + expect(person.lastName.split('|').length).toStrictEqual(3); + expect(sc.decrypt(person.firstName, { key: testKey })).toStrictEqual('FirstNameTest'); + expect(sc.decrypt(person.lastName, { key: testKey })).toStrictEqual('LastNameTest'); + }); + + it('tests a successful document creation and retrieval with null values', async () => { + let person = new Person(); + person.id = 'id-1'; + person.firstName = null; + person.lastName = 'Müller'; + let savedPerson = await person.save(); + expect(savedPerson).toBeDefined(); + expect(savedPerson._id).toBeDefined(); + expect(savedPerson.firstName).toStrictEqual(null); + expect(savedPerson.lastName).toStrictEqual('Müller'); + let retrievedPerson = await Person.findOne({ id: 'id-1' }); + expect(retrievedPerson).toBeDefined(); + expect(retrievedPerson._id).toBeDefined(); + expect(retrievedPerson.firstName).toStrictEqual(null); + expect(retrievedPerson.lastName).toStrictEqual('Müller'); + }); + +}); \ No newline at end of file From 5946eebb6c1bf4a412d0bc0b16b4772a4873ca60 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 21:31:17 +0200 Subject: [PATCH 5/9] GCM test for tampered authTag added --- test/encryptedstring-aes-gcm.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/encryptedstring-aes-gcm.test.js b/test/encryptedstring-aes-gcm.test.js index da6347e..056fc8b 100644 --- a/test/encryptedstring-aes-gcm.test.js +++ b/test/encryptedstring-aes-gcm.test.js @@ -98,4 +98,17 @@ describe('mongoose-encrypted-string AES-256-GCM test suite', () => { expect(retrievedPerson.lastName).toStrictEqual('Müller'); }); + it('tests failed decryption due to tampered authTag', async () => { + let personLean = await Person.findOne({ id: 'id-test' }).lean(); + let parts = personLean.firstName.split('|'); + parts[1] = parts[1][0] === 'a' ? 'b' + parts[1].slice(1) : 'a' + parts[1].slice(1); + let tamperedValue = parts.join('|'); + await mongoose.connection.collection('people').updateOne( + { id: 'id-test' }, + { $set: { firstName: tamperedValue } } + ); + let person = await Person.findOne({ id: 'id-test' }); + expect(() => person.firstName).toThrow(); + }); + }); \ No newline at end of file From 614c658bf9eb2ee82bf10070006b4de338a10fef Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 21:53:30 +0200 Subject: [PATCH 6/9] Unit tests for basic cases added --- test/encryptedstring.test.js | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/encryptedstring.test.js diff --git a/test/encryptedstring.test.js b/test/encryptedstring.test.js new file mode 100644 index 0000000..7261488 --- /dev/null +++ b/test/encryptedstring.test.js @@ -0,0 +1,53 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const mes = require('../mongoose-encrypted-string'); + +describe('mongoose-encrypted-string test suite', () => { + + const testKey = '9af7d400be4705147dc724db25bfd2513aa11d6013d7bf7bdb2bfe050593bd0f'; + + var mongoServer = null; + + beforeEach(async () => { + mongoServer = await MongoMemoryServer.create({ dbName: 'encryptedstring' }); + await mongoose.connect(mongoServer.getUri()); + }); + + afterEach(async () => { + await mongoose.connection.close(); + await mongoServer.stop(); + }); + + it('tests a successful document creation with AES-GCM as default algorithm', async () => { + mes.registerEncryptedString(mongoose, testKey); + let Person = mongoose.model('Person', { + id: { type: String, required: true }, + firstName: { type: mongoose.Schema.Types.EncryptedString }, + lastName: { type: mongoose.Schema.Types.EncryptedString } + }); + // test doc creation + let person = new Person(); + person.id = 'id-1'; + person.firstName = 'Hans'; + person.lastName = 'Müller'; + let savedPerson = await person.save(); + expect(savedPerson).toBeDefined(); + expect(savedPerson._id).toBeDefined(); + expect(savedPerson.firstName).toStrictEqual('Hans'); + expect(savedPerson.lastName).toStrictEqual('Müller'); + let savedPersonLean = await Person.findById(savedPerson._id).lean(); + expect(savedPersonLean.firstName).not.toStrictEqual('Hans'); + let firstNameParts = savedPersonLean.firstName.split('|'); + expect(firstNameParts.length).toStrictEqual(3); + expect(savedPersonLean.lastName).not.toStrictEqual('Müller'); + let lastNameParts = savedPersonLean.firstName.split('|'); + expect(lastNameParts.length).toStrictEqual(3); + // tear-down + await Person.deleteMany(); + }); + + it('tests an exception because of an unknown algorithm', async () => { + expect(() => mes.registerEncryptedString(mongoose, testKey, 'fake-algo')).toThrow(); + }); + +}); \ No newline at end of file From adf42861dcbe3729b115213dfaf131aa167f7555 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 22:13:40 +0200 Subject: [PATCH 7/9] README updated --- README.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c771b24..b167544 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ [![Build Status](https://img.shields.io/github/actions/workflow/status/tsmx/mongoose-encrypted-string/git-build.yml?branch=master)](https://img.shields.io/github/actions/workflow/status/tsmx/mongoose-encrypted-string/git-build.yml?branch=master) [![Coverage Status](https://coveralls.io/repos/github/tsmx/mongoose-encrypted-string/badge.svg?branch=master)](https://coveralls.io/github/tsmx/mongoose-encrypted-string?branch=master) -> `EncryptedString` type for Mongoose schemas. Provides AES-256-CBC encryption-at-rest for strings. +> `EncryptedString` type for Mongoose schemas. Provides AES-256-GCM and AES-256-CBC encryption-at-rest for strings. + +The AES-256-GCM algorithm provides a cryptographic tamper-safety of the encrypted data and should be preferred over AES-256-CBC. See also the [migration guide](#migrating-from-aes-cbc-to-aes-gcm) if you are already using AES-256-CBC. ## Usage @@ -27,32 +29,32 @@ Person = mongoose.model('Person', { let testPerson = new Person(); testPerson.id = 'id-test'; -testPerson.firstName = 'Hans'; // stored encrypted -testPerson.lastName = 'Müller'; // stored encrypted +testPerson.firstName = 'Hans'; // stored AES-256-GCM encrypted +testPerson.lastName = 'Müller'; // stored AES-256-GCM encrypted await testPerson.save(); let queriedPerson = await Person.findOne({ id: 'id-test' }); console.log(queriedPerson.firstName); // 'Hans', decrypted automatically -console.log(queriedPerson.lastName); // 'Müller, decrypted automatically +console.log(queriedPerson.lastName); // 'Müller', decrypted automatically ``` -Directly querying the MongoDB will return the encrypted data. +Directly querying the MongoDB will return the encrypted data. With the default AES-256-GCM algorithm, each encrypted field is stored as a 3-part string (`iv|authTag|ciphertext`). AES-256-CBC produces a 2-part string (`iv|ciphertext`). ```bash > db.persons.findOne({ id: 'id-test' }); { "_id" : ObjectId("5f8576cc0a6ca01d8e5c479c"), "id" : "id-test", - "firstName" : "66db1589b5c0de7f98f5260092e6799f|a6cb74bc05a52d1244addb125352bb0d", - "lastName" : "2b85f4ca2d98ad1234da376a6d0d9128|d5b0257d3797da7047bfea6dfa62e19c", + "firstName" : "66db1589b5c0de7f98f5260092e6799f|a3f1c2e4b5d6789012345678abcdef01|a6cb74bc05a52d1244addb125352bb0d", + "lastName" : "2b85f4ca2d98ad1234da376a6d0d9128|9f8e7d6c5b4a3210fedcba9876543210|d5b0257d3797da7047bfea6dfa62e19c", "__v" : 0 } ``` ## API -### registerEncryptedString(mongoose, key) +### registerEncryptedString(mongoose, key[, algorithm]) -Registers the new type `EncryptedString` in the `mongoose` instance's schema types. Encryption/decryption is done with AES-256-CBC using the given `key`. After calling this funtion you can start using the new type via `mongoose.Schema.Types.EncryptedString` in your schemas. +Registers the new type `EncryptedString` in the `mongoose` instance's schema types. Encryption/decryption is done using the given `key` and `algorithm` (default: `aes-256-gcm`). After calling this function you can start using the new type via `mongoose.Schema.Types.EncryptedString` in your schemas. #### mongoose @@ -62,9 +64,13 @@ The mongoose instance where `EncryptedString` should be registered. The key used for encryption/decryption. Length must be 32 bytes. See [notes](#notes) for details. +#### algorithm + +Optional. The encryption algorithm to use. Accepted values: `aes-256-gcm`, `aes-256-cbc`. Default: `aes-256-gcm`. Throws an `Error` if an unsupported value is passed. + ## Use with lean() queries -For performance reasons it maybe useful to use Mongoose's `lean()` queries. Doing so, the query will return the raw JSON objects from the MongoDB database where all properties of type `EncryptedString` are encrypted. +For performance reasons it may be useful to use Mongoose's `lean()` queries. Doing so, the query will return the raw JSON objects from the MongoDB database where all properties of type `EncryptedString` are encrypted. To get the clear text values back you can directly use [@tsmx/string-crypto](https://www.npmjs.com/package/@tsmx/string-crypto) which is also used internally in this package for encryption and decryption. @@ -72,10 +78,10 @@ To get the clear text values back you can directly use [@tsmx/string-crypto](htt const key = 'YOUR KEY HERE'; const sc = require('@tsmx/string-crypto'); -// query raw objects with encrypted string values +// query raw objects with encrypted string values, either AES-256-GCM or AES-256-CBC let person = await Person.findOne({ id: 'id-test' }).lean(); -// decrypt using string-crypto +// decrypt using string-crypto (algorithm is detected automatically) let firstName = sc.decrypt(person.firstName, { key: key }); let lastName = sc.decrypt(person.lastName, { key: key }); ``` @@ -87,3 +93,11 @@ let lastName = sc.decrypt(person.lastName, { key: key }); - a string of 32 characters length, or - a hexadecimal value of 64 characters length (= 32 bytes) - Don't override getters/setter for `EncryptedString` class or schema elements of this type. This would break the encryption. + +## Migrating from AES-CBC to AES-GCM + +Switching the algorithm after data has already been stored will break decryption of existing documents. To safely migrate existing CBC-encrypted data to GCM, follow these steps: + +1. Keep calling `registerEncryptedString(mongoose, key, 'aes-256-cbc')` until migration is complete. +2. Run a one-off migration script: query affected documents via `.lean()` to get raw encrypted values, then for each `EncryptedString` field decrypt with `sc.decrypt(value, { key })` and re-encrypt with `sc.encrypt(value, { key, algorithm: 'aes-256-gcm' })`, and write back using `collection.updateOne(...)` directly (bypassing Mongoose to avoid double-encryption). +3. Once all documents are migrated, switch to `registerEncryptedString(mongoose, key)` (GCM default). From 1cdbc7ffd8eccf7024a27ebed226d5e0449a2425 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 22:18:21 +0200 Subject: [PATCH 8/9] README updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b167544..16f4514 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ > `EncryptedString` type for Mongoose schemas. Provides AES-256-GCM and AES-256-CBC encryption-at-rest for strings. -The AES-256-GCM algorithm provides a cryptographic tamper-safety of the encrypted data and should be preferred over AES-256-CBC. See also the [migration guide](#migrating-from-aes-cbc-to-aes-gcm) if you are already using AES-256-CBC. +**Note:** The AES-256-GCM algorithm provides an additional cryptographic tamper-safety of the encrypted data by adding an authTag and should be preferred over AES-256-CBC. See also the [migration guide](#migrating-from-aes-cbc-to-aes-gcm) if you are already using AES-256-CBC. ## Usage From 4d124f33e552a8c8c15b36ca51d8daa51d84b2d1 Mon Sep 17 00:00:00 2001 From: tsmx Date: Tue, 31 Mar 2026 22:22:50 +0200 Subject: [PATCH 9/9] Update package description and keywords for AES-GCM and AES-CBC support --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d8abb2a..d53fa35 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tsmx/mongoose-encrypted-string", "version": "1.0.9", - "description": "EncryptedString type for Mongoose schemas.", + "description": "EncryptedString type for Mongoose schemas providing AES-GCM and AES-CBC encryption at rest.", "main": "mongoose-encrypted-string.js", "engines": { "node": ">=18.0.0", @@ -25,6 +25,9 @@ "decryption", "string", "AES", + "AES-256-GCM", + "AES-256-CBC", + "encryption-at-rest", "crypto" ], "dependencies": {