diff --git a/README.md b/README.md
index 3c6e4446f..f4289297f 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,18 @@
-Narval is the most advanced and secure authorization stack for web3.
-
-
-
-
-
-
+Armory is the most advanced and secure authorization stack for web3.
-## Description
+## Project
-TBD
+| Project | Status |
+|---------------------------------------------------------------------------------------|--------|
+| [@app/armory](./apps/armory/README.md) | |
+| [@app/policy-engine](./apps/policy-engine/README.md) | |
+| [@narval/encryption-module](./packages/encryption-module/README.md) | N/A |
+| [@narval/policy-engine-shared](./packages/policy-engine-shared/README.md) | |
+| [@narval/signature](./packages/signature/README.md) | N/A |
+| [@narval/transaction-request-intent](./packages/transaction-request-intent/README.md) | |
## Getting started
@@ -25,13 +26,6 @@ make setup
At the end, you must have a working environment ready to run any application.
-## Project
-
-- [@app/armory](./apps/armory/README.md)
-- [@app/policy-engine](./apps/policy-engine/README.md)
-- [@narval/policy-engine-shared](./packages/policy-engine-shared/README.md)
-- [@narval/transaction-request-intent](./packages/transaction-request-intent/README.md)
-
## Docker
We use Docker & `docker-compose` to run the application's dependencies.
diff --git a/apps/policy-engine/Makefile b/apps/policy-engine/Makefile
index 1f5329c5d..d242aff36 100644
--- a/apps/policy-engine/Makefile
+++ b/apps/policy-engine/Makefile
@@ -14,6 +14,7 @@ policy-engine/setup:
make policy-engine/rego/build
make policy-engine/db/setup
make policy-engine/test/db/setup
+ make policy-engine/cli CMD=provision
policy-engine/copy-default-env:
cp ${POLICY_ENGINE_PROJECT_DIR}/.env.default ${POLICY_ENGINE_PROJECT_DIR}/.env
@@ -119,6 +120,14 @@ policy-engine/test:
make policy-engine/test/integration
make policy-engine/test/e2e
+# === CLI ===
+
+policy-engine/cli:
+ npx dotenv -e ${POLICY_ENGINE_PROJECT_DIR}/.env -- \
+ ts-node -r tsconfig-paths/register \
+ --project ${POLICY_ENGINE_PROJECT_DIR}/tsconfig.app.json \
+ ${POLICY_ENGINE_PROJECT_DIR}/src/cli.ts ${CMD}
+
# === Open Policy Agent & Rego ===
policy-engine/rego/build:
diff --git a/apps/policy-engine/README.md b/apps/policy-engine/README.md
index 6561686ec..06464fafb 100644
--- a/apps/policy-engine/README.md
+++ b/apps/policy-engine/README.md
@@ -38,3 +38,9 @@ make policy-engine/lint
make policy-engine/format/check
make policy-engine/lint/check
```
+
+## CLI
+
+```bash
+make policy-engine/cli CMD=help
+```
diff --git a/apps/policy-engine/src/app/__test__/e2e/tenant.spec.ts b/apps/policy-engine/src/app/__test__/e2e/tenant.spec.ts
index dea950706..9c3e64331 100644
--- a/apps/policy-engine/src/app/__test__/e2e/tenant.spec.ts
+++ b/apps/policy-engine/src/app/__test__/e2e/tenant.spec.ts
@@ -1,15 +1,16 @@
+import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import request from 'supertest'
import { v4 as uuid } from 'uuid'
import { AppModule } from '../../../app/app.module'
-import { EncryptionService } from '../../../encryption/core/encryption.service'
import { Config, load } from '../../../policy-engine.config'
import { REQUEST_HEADER_API_KEY } from '../../../policy-engine.constant'
import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository'
import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
+import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing'
import { EngineService } from '../../core/service/engine.service'
import { CreateTenantDto } from '../../http/rest/dto/create-tenant.dto'
import { TenantRepository } from '../../persistence/repository/tenant.repository'
@@ -19,7 +20,6 @@ describe('Tenant', () => {
let module: TestingModule
let testPrismaService: TestPrismaService
let tenantRepository: TenantRepository
- let encryptionService: EncryptionService
let engineService: EngineService
let configService: ConfigService
@@ -37,6 +37,10 @@ describe('Tenant', () => {
})
.overrideProvider(KeyValueRepository)
.useValue(new InMemoryKeyValueRepository())
+ .overrideProvider(EncryptionModuleOptionProvider)
+ .useValue({
+ keyring: getTestRawAesKeyring()
+ })
.compile()
app = module.createNestApplication()
@@ -44,13 +48,11 @@ describe('Tenant', () => {
engineService = module.get(EngineService)
tenantRepository = module.get(TenantRepository)
testPrismaService = module.get(TestPrismaService)
- encryptionService = module.get(EncryptionService)
configService = module.get>(ConfigService)
await testPrismaService.truncateAll()
- await encryptionService.setup()
- await engineService.create({
+ await engineService.save({
id: configService.get('engine.id', { infer: true }),
masterKey: 'unsafe-test-master-key',
adminApiKey
diff --git a/apps/policy-engine/src/app/app.module.ts b/apps/policy-engine/src/app/app.module.ts
index 56628bbbb..6c4a6101f 100644
--- a/apps/policy-engine/src/app/app.module.ts
+++ b/apps/policy-engine/src/app/app.module.ts
@@ -1,9 +1,10 @@
+import { EncryptionModule } from '@narval/encryption-module'
import { HttpModule } from '@nestjs/axios'
import { Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common'
-import { ConfigModule } from '@nestjs/config'
+import { ConfigModule, ConfigService } from '@nestjs/config'
import { APP_PIPE } from '@nestjs/core'
-import { EncryptionModule } from '../encryption/encryption.module'
import { load } from '../policy-engine.config'
+import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory'
import { KeyValueModule } from '../shared/module/key-value/key-value.module'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@@ -11,6 +12,7 @@ import { DataStoreRepositoryFactory } from './core/factory/data-store-repository
import { BootstrapService } from './core/service/bootstrap.service'
import { DataStoreService } from './core/service/data-store.service'
import { EngineService } from './core/service/engine.service'
+import { ProvisionService } from './core/service/provision.service'
import { SigningService } from './core/service/signing.service'
import { TenantService } from './core/service/tenant.service'
import { TenantController } from './http/rest/controller/tenant.controller'
@@ -28,8 +30,12 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
isGlobal: true
}),
HttpModule,
- EncryptionModule,
- KeyValueModule
+ KeyValueModule,
+ EncryptionModule.registerAsync({
+ imports: [AppModule],
+ inject: [ConfigService, EngineService],
+ useClass: EncryptionModuleOptionFactory
+ })
],
controllers: [AppController, TenantController],
providers: [
@@ -39,18 +45,22 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
DataStoreService,
EngineRepository,
EngineService,
- SigningService,
EntityRepository,
FileSystemDataStoreRepository,
HttpDataStoreRepository,
OpaService,
+ ProvisionService,
+ SigningService,
TenantRepository,
TenantService,
{
provide: APP_PIPE,
useClass: ValidationPipe
}
- ]
+ ],
+ // - The EngineService is required by the EncryptionModule async registration.
+ // - The ProvisionService is required by the CliModule.
+ exports: [EngineService, ProvisionService]
})
export class AppModule implements OnApplicationBootstrap {
constructor(private bootstrapService: BootstrapService) {}
diff --git a/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts b/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts
index 8fbf87f52..15a5e6533 100644
--- a/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts
+++ b/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts
@@ -1,6 +1,13 @@
+import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
+import { EngineRepository } from '../../../../../app/persistence/repository/engine.repository'
+import { load } from '../../../../../policy-engine.config'
+import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
+import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
+import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { BootstrapService } from '../../bootstrap.service'
+import { EngineService } from '../../engine.service'
import { TenantService } from '../../tenant.service'
describe(BootstrapService.name, () => {
@@ -41,8 +48,21 @@ describe(BootstrapService.name, () => {
tenantServiceMock.findAll.mockResolvedValue([tenantOne, tenantTwo])
const module = await Test.createTestingModule({
+ imports: [
+ ConfigModule.forRoot({
+ load: [load],
+ isGlobal: true
+ })
+ ],
providers: [
BootstrapService,
+ EngineService,
+ EngineRepository,
+ KeyValueService,
+ {
+ provide: KeyValueRepository,
+ useClass: InMemoryKeyValueRepository
+ },
{
provide: TenantService,
useValue: tenantServiceMock
diff --git a/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts b/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts
index 8d26e1b62..d22d93f6e 100644
--- a/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts
+++ b/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts
@@ -1,8 +1,11 @@
+import { EncryptionModule } from '@narval/encryption-module'
import { DataStoreConfiguration, FIXTURE } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
-import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
+import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
+import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service'
import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
+import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing'
import { Tenant } from '../../../../../shared/type/domain.type'
import { TenantRepository } from '../../../../persistence/repository/tenant.repository'
import { DataStoreService } from '../../data-store.service'
@@ -48,15 +51,21 @@ describe(TenantService.name, () => {
dataStoreServiceMock.fetch.mockResolvedValue(stores)
const module = await Test.createTestingModule({
+ imports: [
+ EncryptionModule.register({
+ keyring: getTestRawAesKeyring()
+ })
+ ],
providers: [
TenantService,
TenantRepository,
+ EncryptKeyValueService,
{
provide: DataStoreService,
useValue: dataStoreServiceMock
},
{
- provide: KeyValueService,
+ provide: KeyValueRepository,
useClass: InMemoryKeyValueRepository
}
]
diff --git a/apps/policy-engine/src/app/core/service/bootstrap.service.ts b/apps/policy-engine/src/app/core/service/bootstrap.service.ts
index c361a40a4..0a7a8cf64 100644
--- a/apps/policy-engine/src/app/core/service/bootstrap.service.ts
+++ b/apps/policy-engine/src/app/core/service/bootstrap.service.ts
@@ -8,30 +8,7 @@ export class BootstrapService {
constructor(private tenantService: TenantService) {}
async boot(): Promise {
- this.logger.log('Start application bootstrap procedure')
-
- await this.tenantService.onboard(
- {
- clientId: '012553b0-34e9-4b48-b217-ced3c906cd39',
- clientSecret: 'unsafe-dev-secret',
- dataStore: {
- entity: {
- dataUrl: 'http://127.0.0.1:4200/api/data-store',
- signatureUrl: 'http://127.0.0.1:4200/api/data-store',
- keys: []
- },
- policy: {
- dataUrl: 'http://127.0.0.1:4200/api/data-store',
- signatureUrl: 'http://127.0.0.1:4200/api/data-store',
- keys: []
- }
- },
- createdAt: new Date(),
- updatedAt: new Date()
- },
- // Disable sync after the onboard because we'll sync it as part of the boot.
- { syncAfter: false }
- )
+ this.logger.log('Start engine bootstrap')
await this.syncTenants()
}
diff --git a/apps/policy-engine/src/app/core/service/engine.service.ts b/apps/policy-engine/src/app/core/service/engine.service.ts
index 398e9cca6..5d002b1ae 100644
--- a/apps/policy-engine/src/app/core/service/engine.service.ts
+++ b/apps/policy-engine/src/app/core/service/engine.service.ts
@@ -12,8 +12,8 @@ export class EngineService {
private engineRepository: EngineRepository
) {}
- async getEngine(): Promise {
- const engine = await this.engineRepository.findById(this.getId())
+ async getEngineOrThrow(): Promise {
+ const engine = await this.getEngine()
if (engine) {
return engine
@@ -22,8 +22,18 @@ export class EngineService {
throw new EngineNotProvisionedException()
}
- async create(engine: Engine): Promise {
- return this.engineRepository.create(engine)
+ async getEngine(): Promise {
+ const engine = await this.engineRepository.findById(this.getId())
+
+ if (engine) {
+ return engine
+ }
+
+ return null
+ }
+
+ async save(engine: Engine): Promise {
+ return this.engineRepository.save(engine)
}
private getId(): string {
diff --git a/apps/policy-engine/src/app/core/service/provision.service.ts b/apps/policy-engine/src/app/core/service/provision.service.ts
new file mode 100644
index 000000000..13edc28b3
--- /dev/null
+++ b/apps/policy-engine/src/app/core/service/provision.service.ts
@@ -0,0 +1,65 @@
+import { generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module'
+import { Injectable, Logger } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { randomBytes } from 'crypto'
+import { Config } from '../../../policy-engine.config'
+import { EngineService } from './engine.service'
+
+@Injectable()
+export class ProvisionService {
+ private logger = new Logger(ProvisionService.name)
+
+ constructor(
+ private configService: ConfigService,
+ private engineService: EngineService
+ ) {}
+
+ async provision(): Promise {
+ this.logger.log('Start engine provision')
+
+ const engine = await this.engineService.getEngine()
+
+ const isFirstTime = engine === null
+
+ // IMPORTANT: The order of internal methods call matters.
+
+ if (isFirstTime) {
+ await this.createEngine()
+ await this.maybeSetupEncryption()
+ }
+ }
+
+ private async createEngine(): Promise {
+ this.logger.log('Generate admin API key and save engine')
+
+ await this.engineService.save({
+ id: this.getEngineId(),
+ adminApiKey: randomBytes(20).toString('hex')
+ })
+ }
+
+ private async maybeSetupEncryption(): Promise {
+ // Get the engine's latest state.
+ const engine = await this.engineService.getEngineOrThrow()
+
+ if (engine.masterKey) {
+ return this.logger.log('Skip master key set up because it already exists')
+ }
+
+ const keyring = this.configService.get('keyring', { infer: true })
+
+ if (keyring.type === 'raw') {
+ this.logger.log('Generate and save engine master key')
+
+ const { masterPassword } = keyring
+ const kek = generateKeyEncryptionKey(masterPassword, this.getEngineId())
+ const masterKey = await generateMasterKey(kek)
+
+ await this.engineService.save({ ...engine, masterKey })
+ }
+ }
+
+ private getEngineId(): string {
+ return this.configService.get('engine.id', { infer: true })
+ }
+}
diff --git a/apps/policy-engine/src/app/persistence/repository/__test__/unit/engine.repository.spec.ts b/apps/policy-engine/src/app/persistence/repository/__test__/unit/engine.repository.spec.ts
index 354bf17cb..24f8e6abf 100644
--- a/apps/policy-engine/src/app/persistence/repository/__test__/unit/engine.repository.spec.ts
+++ b/apps/policy-engine/src/app/persistence/repository/__test__/unit/engine.repository.spec.ts
@@ -1,9 +1,9 @@
+import { EncryptionModule } from '@narval/encryption-module'
import { Test } from '@nestjs/testing'
-import { EncryptionModule } from '../../../../../encryption/encryption.module'
-import { ApplicationException } from '../../../../../shared/exception/application.exception'
import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
+import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing'
import { Engine } from '../../../../../shared/type/domain.type'
import { EngineRepository } from '../../engine.repository'
@@ -15,7 +15,11 @@ describe(EngineRepository.name, () => {
inMemoryKeyValueRepository = new InMemoryKeyValueRepository()
const module = await Test.createTestingModule({
- imports: [EncryptionModule],
+ imports: [
+ EncryptionModule.register({
+ keyring: getTestRawAesKeyring()
+ })
+ ],
providers: [
KeyValueService,
EngineRepository,
@@ -29,15 +33,15 @@ describe(EngineRepository.name, () => {
repository = module.get(EngineRepository)
})
- describe('create', () => {
+ describe('save', () => {
const engine: Engine = {
id: 'test-engine-id',
adminApiKey: 'unsafe-test-admin-api-key',
masterKey: 'unsafe-test-master-key'
}
- it('creates a new engine', async () => {
- await repository.create(engine)
+ it('saves a new engine', async () => {
+ await repository.save(engine)
const value = await inMemoryKeyValueRepository.get(repository.getKey(engine.id))
const actualEngine = await repository.findById(engine.id)
@@ -45,11 +49,5 @@ describe(EngineRepository.name, () => {
expect(value).not.toEqual(null)
expect(engine).toEqual(actualEngine)
})
-
- it('throws an error when engine is duplicate', async () => {
- await repository.create(engine)
-
- await expect(repository.create(engine)).rejects.toThrow(ApplicationException)
- })
})
})
diff --git a/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts b/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts
index b14cccdd8..c778b9b37 100644
--- a/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts
+++ b/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts
@@ -1,3 +1,4 @@
+import { EncryptionModule } from '@narval/encryption-module'
import {
Action,
Criterion,
@@ -8,13 +9,11 @@ import {
Then
} from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
-import { mock } from 'jest-mock-extended'
-import { EncryptionService } from '../../../../../encryption/core/encryption.service'
-import { EncryptionModule } from '../../../../../encryption/encryption.module'
-import { EncryptionRepository } from '../../../../../encryption/persistence/repository/encryption.repository'
import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
+import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service'
import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
+import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing'
import { Tenant } from '../../../../../shared/type/domain.type'
import { TenantRepository } from '../../../repository/tenant.repository'
@@ -27,22 +26,16 @@ describe(TenantRepository.name, () => {
beforeEach(async () => {
inMemoryKeyValueRepository = new InMemoryKeyValueRepository()
- const encryptionRepository = mock()
- encryptionRepository.getEngine.mockResolvedValue({
- id: 'test-engine',
- masterKey: 'unsafe-test-master-key',
- adminApiKey: 'unsafe-test-api-key'
- })
-
const module = await Test.createTestingModule({
- imports: [EncryptionModule],
+ imports: [
+ EncryptionModule.register({
+ keyring: getTestRawAesKeyring()
+ })
+ ],
providers: [
KeyValueService,
TenantRepository,
- {
- provide: EncryptionRepository,
- useValue: encryptionRepository
- },
+ EncryptKeyValueService,
{
provide: KeyValueRepository,
useValue: inMemoryKeyValueRepository
@@ -50,13 +43,6 @@ describe(TenantRepository.name, () => {
]
}).compile()
- // IMPORTANT: The onApplicationBootstrap performs several side-effects to
- // set up the encryption.
- //
- // TODO: Refactor the encryption service. It MUST be ready for usage given
- // its arguments rather than depending on a set up step.
- await module.get(EncryptionService).setup()
-
repository = module.get(TenantRepository)
})
diff --git a/apps/policy-engine/src/app/persistence/repository/engine.repository.ts b/apps/policy-engine/src/app/persistence/repository/engine.repository.ts
index d53ae4949..c2d63117e 100644
--- a/apps/policy-engine/src/app/persistence/repository/engine.repository.ts
+++ b/apps/policy-engine/src/app/persistence/repository/engine.repository.ts
@@ -1,5 +1,4 @@
-import { HttpStatus, Injectable } from '@nestjs/common'
-import { ApplicationException } from '../../../shared/exception/application.exception'
+import { Injectable } from '@nestjs/common'
import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service'
import { engineSchema } from '../../../shared/schema/engine.schema'
import { Engine } from '../../../shared/type/domain.type'
@@ -18,14 +17,7 @@ export class EngineRepository {
return null
}
- async create(engine: Engine): Promise {
- if (await this.keyValueService.get(this.getKey(engine.id))) {
- throw new ApplicationException({
- message: 'Engine already exist',
- suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR
- })
- }
-
+ async save(engine: Engine): Promise {
await this.keyValueService.set(this.getKey(engine.id), this.encode(engine))
return engine
diff --git a/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts b/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts
index da36c809a..5f655da14 100644
--- a/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts
+++ b/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts
@@ -1,16 +1,16 @@
import { EntityStore, PolicyStore, entityStoreSchema, policyStoreSchema } from '@narval/policy-engine-shared'
import { Injectable } from '@nestjs/common'
import { compact } from 'lodash/fp'
-import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service'
+import { EncryptKeyValueService } from '../../../shared/module/key-value/core/service/encrypt-key-value.service'
import { tenantIndexSchema, tenantSchema } from '../../../shared/schema/tenant.schema'
import { Tenant } from '../../../shared/type/domain.type'
@Injectable()
export class TenantRepository {
- constructor(private keyValueService: KeyValueService) {}
+ constructor(private encryptKeyValueService: EncryptKeyValueService) {}
async findByClientId(clientId: string): Promise {
- const value = await this.keyValueService.get(this.getKey(clientId))
+ const value = await this.encryptKeyValueService.get(this.getKey(clientId))
if (value) {
return this.decode(value)
@@ -20,14 +20,14 @@ export class TenantRepository {
}
async save(tenant: Tenant): Promise {
- await this.keyValueService.set(this.getKey(tenant.clientId), this.encode(tenant))
+ await this.encryptKeyValueService.set(this.getKey(tenant.clientId), this.encode(tenant))
await this.index(tenant)
return tenant
}
async getTenantIndex(): Promise {
- const index = await this.keyValueService.get(this.getIndexKey())
+ const index = await this.encryptKeyValueService.get(this.getIndexKey())
if (index) {
return this.decodeIndex(index)
@@ -37,11 +37,11 @@ export class TenantRepository {
}
async saveEntityStore(clientId: string, store: EntityStore): Promise {
- return this.keyValueService.set(this.getEntityStoreKey(clientId), this.encodeEntityStore(store))
+ return this.encryptKeyValueService.set(this.getEntityStoreKey(clientId), this.encodeEntityStore(store))
}
async findEntityStore(clientId: string): Promise {
- const value = await this.keyValueService.get(this.getEntityStoreKey(clientId))
+ const value = await this.encryptKeyValueService.get(this.getEntityStoreKey(clientId))
if (value) {
return this.decodeEntityStore(value)
@@ -51,11 +51,11 @@ export class TenantRepository {
}
async savePolicyStore(clientId: string, store: PolicyStore): Promise {
- return this.keyValueService.set(this.getPolicyStoreKey(clientId), this.encodePolicyStore(store))
+ return this.encryptKeyValueService.set(this.getPolicyStoreKey(clientId), this.encodePolicyStore(store))
}
async findPolicyStore(clientId: string): Promise {
- const value = await this.keyValueService.get(this.getPolicyStoreKey(clientId))
+ const value = await this.encryptKeyValueService.get(this.getPolicyStoreKey(clientId))
if (value) {
return this.decodePolicyStore(value)
@@ -97,13 +97,13 @@ export class TenantRepository {
private async index(tenant: Tenant): Promise {
const currentIndex = await this.getTenantIndex()
- await this.keyValueService.set(this.getIndexKey(), this.encodeIndex([...currentIndex, tenant.clientId]))
+ await this.encryptKeyValueService.set(this.getIndexKey(), this.encodeIndex([...currentIndex, tenant.clientId]))
return true
}
private encode(tenant: Tenant): string {
- return KeyValueService.encode(tenantSchema.parse(tenant))
+ return EncryptKeyValueService.encode(tenantSchema.parse(tenant))
}
private decode(value: string): Tenant {
@@ -111,7 +111,7 @@ export class TenantRepository {
}
private encodeIndex(value: string[]): string {
- return KeyValueService.encode(tenantIndexSchema.parse(value))
+ return EncryptKeyValueService.encode(tenantIndexSchema.parse(value))
}
private decodeIndex(value: string): string[] {
@@ -119,7 +119,7 @@ export class TenantRepository {
}
private encodeEntityStore(value: EntityStore): string {
- return KeyValueService.encode(entityStoreSchema.parse(value))
+ return EncryptKeyValueService.encode(entityStoreSchema.parse(value))
}
private decodeEntityStore(value: string): EntityStore {
@@ -127,7 +127,7 @@ export class TenantRepository {
}
private encodePolicyStore(value: PolicyStore): string {
- return KeyValueService.encode(policyStoreSchema.parse(value))
+ return EncryptKeyValueService.encode(policyStoreSchema.parse(value))
}
private decodePolicyStore(value: string): PolicyStore {
diff --git a/apps/policy-engine/src/cli.ts b/apps/policy-engine/src/cli.ts
new file mode 100644
index 000000000..f1262d231
--- /dev/null
+++ b/apps/policy-engine/src/cli.ts
@@ -0,0 +1,8 @@
+import { CommandFactory } from 'nest-commander'
+import { CliModule } from './cli/cli.module'
+
+async function bootstrap() {
+ await CommandFactory.run(CliModule, ['error'])
+}
+
+bootstrap()
diff --git a/apps/policy-engine/src/cli/cli.module.ts b/apps/policy-engine/src/cli/cli.module.ts
new file mode 100644
index 000000000..7c769353d
--- /dev/null
+++ b/apps/policy-engine/src/cli/cli.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common'
+import { AppModule } from '../app/app.module'
+import { ProvisionCommand } from './command/provision.command'
+
+@Module({
+ imports: [AppModule],
+ providers: [ProvisionCommand]
+})
+export class CliModule {}
diff --git a/apps/policy-engine/src/cli/command/provision.command.ts b/apps/policy-engine/src/cli/command/provision.command.ts
new file mode 100644
index 000000000..38869c325
--- /dev/null
+++ b/apps/policy-engine/src/cli/command/provision.command.ts
@@ -0,0 +1,49 @@
+import { ConfigService } from '@nestjs/config'
+import { Command, CommandRunner } from 'nest-commander'
+import { EngineService } from '../../app/core/service/engine.service'
+import { ProvisionService } from '../../app/core/service/provision.service'
+import { Config } from '../../policy-engine.config'
+
+@Command({
+ name: 'provision',
+ description: 'Provision the policy engine for the first time'
+})
+export class ProvisionCommand extends CommandRunner {
+ constructor(
+ private provisionService: ProvisionService,
+ private engineService: EngineService,
+ private configService: ConfigService
+ ) {
+ super()
+ }
+
+ async run(): Promise {
+ const engine = await this.engineService.getEngine()
+
+ if (engine && engine.masterKey) {
+ return console.log('Engine already provisioned')
+ }
+
+ await this.provisionService.provision()
+
+ try {
+ const keyring = this.configService.get('keyring', { infer: true })
+ const engine = await this.engineService.getEngineOrThrow()
+
+ console.log('Engine ID:', engine.id)
+ console.log('Engine admin API key:', engine.adminApiKey)
+ console.log('Encryption type:', keyring.type)
+
+ if (keyring.type === 'raw') {
+ console.log(`Is encryption master password set? ${keyring.masterPassword ? '✅' : '❌'}`)
+ console.log(`Is encryption master key set? ${engine.masterKey ? '✅' : '❌'}`)
+ }
+
+ if (keyring.type === 'awskms') {
+ console.log(`Is encryption master KMS ARN set? ${keyring.masterAwsKmsArn ? '✅' : '❌'}`)
+ }
+ } catch (error) {
+ console.log('Something went wrong provisioning the engine', error)
+ }
+ }
+}
diff --git a/apps/policy-engine/src/encryption/core/__test__/integration/encryption.service.spec.ts b/apps/policy-engine/src/encryption/core/__test__/integration/encryption.service.spec.ts
deleted file mode 100644
index 49c1389ea..000000000
--- a/apps/policy-engine/src/encryption/core/__test__/integration/encryption.service.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { ConfigModule, ConfigService } from '@nestjs/config'
-import { Test, TestingModule } from '@nestjs/testing'
-import { mock } from 'jest-mock-extended'
-import nock from 'nock'
-import { load } from '../../../../policy-engine.config'
-import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module'
-import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service'
-import { EncryptionRepository } from '../../../persistence/repository/encryption.repository'
-import { EncryptionService } from '../../encryption.service'
-
-describe('EncryptionService', () => {
- let module: TestingModule
- let service: EncryptionService
- let testPrismaService: TestPrismaService
-
- nock.enableNetConnect('kms.us-east-2.amazonaws.com:443')
-
- beforeEach(async () => {
- // These mocked config values matter; they're specifically tied to the mocked masterKey below
- // If you change these, the decryption won't work & tests will fail
- const configServiceMock = mock({
- get: jest.fn().mockImplementation((key: string) => {
- if (key === 'keyring') {
- return {
- type: 'raw',
- masterPassword: 'unsafe-local-dev-master-password'
- }
- }
- if (key === 'engine.id') {
- return 'local-dev-engine-instance-1'
- }
- })
- })
-
- module = await Test.createTestingModule({
- imports: [
- ConfigModule.forRoot({
- load: [load],
- isGlobal: true
- }),
- PersistenceModule
- ],
- providers: [
- EncryptionService,
- EncryptionRepository,
- {
- provide: ConfigService,
- useValue: configServiceMock // use the mock ConfigService
- }
- ]
- }).compile()
-
- service = module.get(EncryptionService)
- testPrismaService = module.get(TestPrismaService)
-
- await testPrismaService.truncateAll()
-
- if (service.setup) {
- await service.setup()
- }
- })
-
- afterEach(async () => {
- await testPrismaService.truncateAll()
- await module.close()
- })
-
- it('should create & encrypt a master key on application bootstrap', async () => {
- await service.setup()
-
- const engine = await testPrismaService.getClient().engine.findFirst({
- where: {
- id: 'local-dev-engine-instance-1'
- }
- })
-
- expect(engine?.masterKey).toBeDefined()
- })
-})
diff --git a/apps/policy-engine/src/encryption/core/__test__/unit/encryption.service.spec.ts b/apps/policy-engine/src/encryption/core/__test__/unit/encryption.service.spec.ts
deleted file mode 100644
index d5cb6d1ba..000000000
--- a/apps/policy-engine/src/encryption/core/__test__/unit/encryption.service.spec.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { toBytes, toHex } from '@narval/policy-engine-shared'
-import { ConfigModule, ConfigService } from '@nestjs/config'
-import { Test } from '@nestjs/testing'
-import { mock } from 'jest-mock-extended'
-import { load } from '../../../../policy-engine.config'
-import { EncryptionRepository } from '../../../persistence/repository/encryption.repository'
-import { EncryptionService } from '../../encryption.service'
-
-describe('EncryptionService', () => {
- let service: EncryptionService
-
- beforeEach(async () => {
- // These mocked config values matter; they're specifically tied to the mocked masterKey below
- // If you change these, the decryption won't work & tests will fail
- const configServiceMock = mock({
- get: jest.fn().mockImplementation((key: string) => {
- if (key === 'keyring') {
- return {
- type: 'raw',
- masterPassword: 'unsafe-local-dev-master-password'
- }
- }
- if (key === 'engine.id') {
- return 'local-dev-engine-instance-1'
- }
- })
- })
-
- const encryptionRepositoryMock = {
- getEngine: jest.fn().mockImplementation(() =>
- Promise.resolve({
- // unencryptedMasterKey: dfd9cc70f1ad02d19e0efa020d82f557022f59ca6bedbec1df38e8fd37ae3bb9
- masterKey:
- '0x0205785d67737fa3bae8eb249cf8d3baed5942f1677d8c98b4cdeef55560a3bcf510bd008d00030003617070000d61726d6f72792d656e67696e6500156177732d63727970746f2d7075626c69632d6b657900444177336764324b6e58646f512f2b76745347367031444442384d65766d61434b324c7861426e65476a315531537777526b376b4d366868752f707a446f48724c77773d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000c8a92a7c9deb43316f6c29e8d0030132d63c7337c9888a06b638966e83056a0575958b42588b7aed999b9659e6d4bc5bed4664d91fae0b14d48917e00cdbb02000010000749ed0ed3616b7990f9e73f5a42eb46dc182002612e33dcb8e3c7d4759184c46ce3f0893a87ac15257d53097ac5d74affffffff00000001000000000000000000000001000000205d7209b51db8cf8264b9065add71a8514dc26baa6987d8a0a3acb1c4a2503b0f3b7c974a35ed234c1b94668736cd8bfa00673065023100a5d8d192e9802649dab86af6e00ab6d7472533e85dfe1006cb8bd9ef2472d15096fa42e742d18cb92530c762c3bd44d40230350299b42feaa1149c6ad1b25add24c30b3bf1c08263b96df0d43e2ad3e19802872e792040f1faf3d0a73bca6fb067ca',
- id: 'test-engine-id'
- })
- )
- }
- const moduleRef = await Test.createTestingModule({
- imports: [
- ConfigModule.forRoot({
- load: [load],
- isGlobal: true
- })
- ],
- providers: [
- EncryptionService,
- {
- provide: EncryptionRepository,
- useValue: encryptionRepositoryMock
- },
- {
- provide: ConfigService,
- useValue: configServiceMock // use the mock ConfigService
- }
- ]
- }).compile()
-
- service = moduleRef.get(EncryptionService)
-
- if (service.setup) {
- await service.setup()
- }
- })
-
- it('should encrypt then decrypt successfully, with a string', async () => {
- const data = 'Hello World'
- const encrypted = await service.encrypt(data)
- const decrypted = await service.decrypt(encrypted)
-
- expect(decrypted.toString('utf-8')).toBe(data)
- })
-
- it('should encrypt then decrypt successfully, with a buffer from a hexstring', async () => {
- const data = '0xdfd9cc70f1ad02d19e0efa020d82f557022f59ca6bedbec1df38e8fd37ae3bb9'
- const encrypted = await service.encrypt(toBytes(data))
- const decrypted = await service.decrypt(encrypted)
-
- expect(toHex(decrypted)).toBe(data)
- })
-
- it('should decrypt a hex-encoded string', async () => {
- const data = 'Hello World'
- const encryptedBuffer = await service.encrypt(data)
- const encryptedHex = toHex(encryptedBuffer)
- const decrypted = await service.decrypt(encryptedHex)
-
- expect(decrypted.toString('utf-8')).toBe(data)
- })
-})
diff --git a/apps/policy-engine/src/encryption/core/encryption.service.ts b/apps/policy-engine/src/encryption/core/encryption.service.ts
deleted file mode 100644
index cc9e0f2b6..000000000
--- a/apps/policy-engine/src/encryption/core/encryption.service.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import {
- CommitmentPolicy,
- KmsKeyringNode,
- RawAesKeyringNode,
- RawAesWrappingSuiteIdentifier,
- buildClient
-} from '@aws-crypto/client-node'
-import { Hex, toBytes, toHex } from '@narval/policy-engine-shared'
-import { Inject, Injectable, Logger } from '@nestjs/common'
-import { ConfigService } from '@nestjs/config'
-import crypto from 'crypto'
-import { Config } from '../../policy-engine.config'
-import { EncryptionRepository } from '../persistence/repository/encryption.repository'
-
-const keyNamespace = 'narval.armory.engine'
-const commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT
-const wrappingSuite = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
-const defaultEncryptionContext = {
- purpose: 'data-encryption',
- app: 'armory-engine'
-}
-
-const { encrypt, decrypt } = buildClient(commitmentPolicy)
-@Injectable()
-export class EncryptionService {
- private logger = new Logger(EncryptionService.name)
-
- private configService: ConfigService
-
- private engineId: string
-
- private keyring: RawAesKeyringNode | KmsKeyringNode | undefined
-
- constructor(
- private encryptionRepository: EncryptionRepository,
- @Inject(ConfigService) configService: ConfigService
- ) {
- this.configService = configService
- this.engineId = configService.get('engine.id', { infer: true })
- }
-
- async setup(): Promise {
- this.logger.log('Set up keyring')
- const keyringConfig = this.configService.get('keyring', { infer: true })
-
- // We have a Raw Keyring, so we are using a MasterPassword/KEK+MasterKey for encryption
- if (keyringConfig.type === 'raw') {
- const engine = await this.encryptionRepository.getEngine(this.engineId)
- let encryptedMasterKey = engine?.masterKey
-
- // Derive the Key Encryption Key (KEK) from the master password using PBKDF2
- const masterPassword = keyringConfig.masterPassword
- const kek = this.deriveKeyEncryptionKey(masterPassword)
-
- if (!encryptedMasterKey) {
- // No MK yet, so create it & encrypt w/ the KEK
- encryptedMasterKey = await this.generateMasterKey(kek)
- }
-
- const decryptedMasterKey = await this.decryptMasterKey(kek, toBytes(encryptedMasterKey))
- const isolatedMasterKey = Buffer.alloc(decryptedMasterKey.length)
- decryptedMasterKey.copy(isolatedMasterKey, 0, 0, decryptedMasterKey.length)
-
- /* Configure the Raw AES keyring. */
- const keyring = new RawAesKeyringNode({
- keyName: 'armory.engine.wrapping-key',
- keyNamespace,
- unencryptedMasterKey: isolatedMasterKey,
- wrappingSuite
- })
-
- this.keyring = keyring
- }
- // We have AWS KMS config so we'll use that instead as the MasterKey, which means we don't need a KEK separately
- else if (keyringConfig.type === 'awskms') {
- const keyring = new KmsKeyringNode({ generatorKeyId: keyringConfig.masterAwsKmsArn })
- this.keyring = keyring
- } else {
- throw new Error('Invalid Keyring Configuration found')
- }
- }
-
- private getKeyEncryptionKeyring(kek: Buffer) {
- // Allocate a new isolated buffer to ensure we don't manipulate the kek
- const isolatedKek = Buffer.alloc(kek.length)
- kek.copy(isolatedKek, 0, 0, kek.length)
-
- const keyring = new RawAesKeyringNode({
- keyName: 'armory.engine.kek',
- keyNamespace,
- unencryptedMasterKey: isolatedKek,
- wrappingSuite
- })
-
- return keyring
- }
-
- private deriveKeyEncryptionKey(password: string): Buffer {
- // Derive the Key Encryption Key (KEK) from the master password using PBKDF2
- const kek = crypto.pbkdf2Sync(password.normalize(), this.engineId.normalize(), 1000000, 32, 'sha256')
- return kek
- }
-
- private async encryptMaterKey(kek: Buffer, cleartext: Buffer): Promise {
- // Encrypt the Master Key (MK) with the Key Encryption Key (KEK)
- const keyring = this.getKeyEncryptionKeyring(kek)
- const { result } = await encrypt(keyring, cleartext, {
- encryptionContext: defaultEncryptionContext
- })
-
- return result
- }
-
- private async decryptMasterKey(kek: Buffer, ciphertext: Uint8Array): Promise {
- const keyring = this.getKeyEncryptionKeyring(kek)
- const { plaintext, messageHeader } = await decrypt(keyring, ciphertext)
-
- // Verify the context wasn't changed
- const { encryptionContext } = messageHeader
-
- Object.entries(defaultEncryptionContext).forEach(([key, value]) => {
- if (encryptionContext[key] !== value) throw new Error('Encryption Context does not match expected values')
- })
-
- return plaintext
- }
-
- async encrypt(cleartext: string | Buffer | Uint8Array): Promise {
- const keyring = this.keyring
- if (!keyring) throw new Error('Keyring not set')
-
- const { result } = await encrypt(keyring, cleartext, {
- encryptionContext: defaultEncryptionContext
- })
-
- return result
- }
-
- async decrypt(ciphertext: Buffer | Uint8Array | Hex): Promise {
- const keyring = this.keyring
- if (!keyring) throw new Error('Keyring not set')
-
- let ciphertextBuffer = ciphertext
- if (typeof ciphertext === 'string') {
- ciphertextBuffer = toBytes(ciphertext)
- }
-
- const { plaintext, messageHeader } = await decrypt(keyring, ciphertextBuffer)
-
- // Verify the context wasn't changed
- const { encryptionContext } = messageHeader
-
- Object.entries(defaultEncryptionContext).forEach(([key, value]) => {
- if (encryptionContext[key] !== value) throw new Error('Encryption Context does not match expected values')
- })
-
- return plaintext
- }
-
- private async generateMasterKey(kek: Buffer): Promise {
- // Generate a new Master Key (MK) with AES256
- const mk = crypto.generateKeySync('aes', { length: 256 })
- const mkBuffer = mk.export()
-
- // Encrypt it with the Key Encryption Key (KEK) that was derived from the MP
- const encryptedMk = await this.encryptMaterKey(kek, mkBuffer)
- const encryptedMkString = toHex(encryptedMk)
-
- // Save the Result.
- const existingEngine = await this.encryptionRepository.getEngine(this.engineId)
- const engine = existingEngine
- ? await this.encryptionRepository.saveMasterKey(this.engineId, encryptedMkString)
- : await this.encryptionRepository.createEngine(this.engineId, encryptedMkString)
-
- if (!engine?.masterKey) {
- throw new Error('Master Key was not saved')
- }
-
- this.logger.log('Engine Master Key Setup Complete')
- return encryptedMkString
- }
-}
diff --git a/apps/policy-engine/src/encryption/encryption.module.ts b/apps/policy-engine/src/encryption/encryption.module.ts
deleted file mode 100644
index 20d234a5e..000000000
--- a/apps/policy-engine/src/encryption/encryption.module.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { HttpModule } from '@nestjs/axios'
-import { Module, ValidationPipe } from '@nestjs/common'
-import { ConfigModule } from '@nestjs/config'
-import { APP_PIPE } from '@nestjs/core'
-import { PersistenceModule } from '../shared/module/persistence/persistence.module'
-import { EncryptionService } from './core/encryption.service'
-import { EncryptionRepository } from './persistence/repository/encryption.repository'
-
-@Module({
- imports: [ConfigModule.forRoot(), HttpModule, PersistenceModule],
- controllers: [],
- providers: [
- EncryptionService,
- EncryptionRepository,
- {
- provide: APP_PIPE,
- useClass: ValidationPipe
- }
- ],
- exports: [EncryptionService]
-})
-export class EncryptionModule {}
diff --git a/apps/policy-engine/src/encryption/persistence/repository/encryption.repository.ts b/apps/policy-engine/src/encryption/persistence/repository/encryption.repository.ts
deleted file mode 100644
index 5d8c0bdf6..000000000
--- a/apps/policy-engine/src/encryption/persistence/repository/encryption.repository.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Injectable, Logger } from '@nestjs/common'
-import { PrismaService } from '../../../shared/module/persistence/service/prisma.service'
-
-@Injectable()
-export class EncryptionRepository {
- private logger = new Logger(EncryptionRepository.name)
-
- constructor(private prismaService: PrismaService) {}
-
- async getEngine(engineId: string) {
- return this.prismaService.engine.findUnique({
- where: {
- id: engineId
- }
- })
- }
-
- async createEngine(engineId: string, masterKey: string, adminApiKey?: string) {
- return this.prismaService.engine.create({
- data: {
- id: engineId,
- masterKey,
- adminApiKey
- }
- })
- }
-
- async saveMasterKey(engineId: string, masterKey: string) {
- return this.prismaService.engine.update({
- where: {
- id: engineId,
- masterKey: null // ONLY allow updating it if already null. We don't want to accidentally overwrite it!
- },
- data: {
- masterKey
- }
- })
- }
-}
diff --git a/apps/policy-engine/src/policy-engine.config.ts b/apps/policy-engine/src/policy-engine.config.ts
index 72c6175bb..2cffdeefb 100644
--- a/apps/policy-engine/src/policy-engine.config.ts
+++ b/apps/policy-engine/src/policy-engine.config.ts
@@ -13,7 +13,8 @@ const configSchema = z.object({
url: z.string().startsWith('postgresql:')
}),
engine: z.object({
- id: z.string()
+ id: z.string(),
+ masterKey: z.string().optional()
}),
keyring: z.union([
z.object({
@@ -37,7 +38,8 @@ export const load = (): Config => {
url: process.env.POLICY_ENGINE_DATABASE_URL
},
engine: {
- id: process.env.ENGINE_UID
+ id: process.env.ENGINE_UID,
+ masterKey: process.env.MASTER_KEY
},
keyring: {
type: process.env.KEYRING_TYPE,
diff --git a/apps/policy-engine/src/policy-engine.constant.ts b/apps/policy-engine/src/policy-engine.constant.ts
index 206027506..446431eba 100644
--- a/apps/policy-engine/src/policy-engine.constant.ts
+++ b/apps/policy-engine/src/policy-engine.constant.ts
@@ -1 +1,7 @@
+import { RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node'
+
export const REQUEST_HEADER_API_KEY = 'x-api-key'
+
+export const ENCRYPTION_KEY_NAMESPACE = 'armory.policy-engine'
+export const ENCRYPTION_KEY_NAME = 'storage-encryption'
+export const ENCRYPTION_WRAPPING_SUITE = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
diff --git a/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts
new file mode 100644
index 000000000..d211f0678
--- /dev/null
+++ b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts
@@ -0,0 +1,58 @@
+import { RawAesKeyringNode } from '@aws-crypto/client-node'
+import {
+ EncryptionModuleOption,
+ decryptMasterKey,
+ generateKeyEncryptionKey,
+ isolateBuffer
+} from '@narval/encryption-module'
+import { toBytes } from '@narval/policy-engine-shared'
+import { Injectable, Logger } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { EngineService } from '../../app/core/service/engine.service'
+import { Config } from '../../policy-engine.config'
+import { ENCRYPTION_KEY_NAME, ENCRYPTION_KEY_NAMESPACE, ENCRYPTION_WRAPPING_SUITE } from '../../policy-engine.constant'
+
+@Injectable()
+export class EncryptionModuleOptionFactory {
+ private logger = new Logger(EncryptionModuleOptionFactory.name)
+
+ constructor(
+ private engineService: EngineService,
+ private configService: ConfigService
+ ) {}
+
+ async create(): Promise {
+ const keyring = this.configService.get('keyring', { infer: true })
+ const engine = await this.engineService.getEngine()
+
+ // NOTE: An undefined engine at boot time only happens during the
+ // provisioning.
+ if (!engine) {
+ this.logger.warn('Booting the encryption module without a keyring. Please, provision the engine.')
+
+ return {
+ keyring: undefined
+ }
+ }
+
+ if (keyring.type === 'raw') {
+ if (!engine.masterKey) {
+ throw new Error('Master key not set')
+ }
+
+ const kek = generateKeyEncryptionKey(keyring.masterPassword, engine.id)
+ const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(engine.masterKey))
+
+ return {
+ keyring: new RawAesKeyringNode({
+ unencryptedMasterKey: isolateBuffer(unencryptedMasterKey),
+ keyName: ENCRYPTION_KEY_NAME,
+ keyNamespace: ENCRYPTION_KEY_NAMESPACE,
+ wrappingSuite: ENCRYPTION_WRAPPING_SUITE
+ })
+ }
+ }
+
+ throw new Error('Unsupported keyring type')
+ }
+}
diff --git a/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts b/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts
index 977d3abb0..018cfc16e 100644
--- a/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts
+++ b/apps/policy-engine/src/shared/guard/__test__/unit/admin-api-key.guard.spec.ts
@@ -20,14 +20,17 @@ describe(AdminApiKeyGuard.name, () => {
}
const mockEngineService = (adminApiKey: string = 'test-admin-api-key') => {
- const engineService = mock()
- engineService.getEngine.mockResolvedValue({
+ const engine = {
adminApiKey,
id: 'test-engine-id',
masterKey: 'test-master-key'
- })
+ }
+
+ const serviceMock = mock()
+ serviceMock.getEngine.mockResolvedValue(engine)
+ serviceMock.getEngineOrThrow.mockResolvedValue(engine)
- return engineService
+ return serviceMock
}
it(`throws an error when ${REQUEST_HEADER_API_KEY} header is missing`, async () => {
diff --git a/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts b/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts
index 9ef97d01a..bee1f3b7b 100644
--- a/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts
+++ b/apps/policy-engine/src/shared/guard/admin-api-key.guard.ts
@@ -18,7 +18,7 @@ export class AdminApiKeyGuard implements CanActivate {
})
}
- const engine = await this.engineService.getEngine()
+ const engine = await this.engineService.getEngineOrThrow()
return engine.adminApiKey === apiKey
}
diff --git a/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/encrypt-key-value.service.spec.ts b/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/encrypt-key-value.service.spec.ts
new file mode 100644
index 000000000..fbc8e6412
--- /dev/null
+++ b/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/encrypt-key-value.service.spec.ts
@@ -0,0 +1,52 @@
+import { EncryptionModule } from '@narval/encryption-module'
+import { ConfigModule } from '@nestjs/config'
+import { Test } from '@nestjs/testing'
+import { load } from '../../../../../../../policy-engine.config'
+import { getTestRawAesKeyring } from '../../../../../../../shared/testing/encryption.testing'
+import { InMemoryKeyValueRepository } from '../../../../persistence/repository/in-memory-key-value.repository'
+import { KeyValueRepository } from '../../../repository/key-value.repository'
+import { EncryptKeyValueService } from '../../encrypt-key-value.service'
+
+describe(EncryptKeyValueService.name, () => {
+ let service: EncryptKeyValueService
+ let keyValueRepository: KeyValueRepository
+ let inMemoryKeyValueRepository: InMemoryKeyValueRepository
+
+ beforeEach(async () => {
+ inMemoryKeyValueRepository = new InMemoryKeyValueRepository()
+
+ const module = await Test.createTestingModule({
+ imports: [
+ ConfigModule.forRoot({
+ load: [load],
+ isGlobal: true
+ }),
+ EncryptionModule.register({
+ keyring: getTestRawAesKeyring()
+ })
+ ],
+ providers: [
+ EncryptKeyValueService,
+ {
+ provide: KeyValueRepository,
+ useValue: inMemoryKeyValueRepository
+ }
+ ]
+ }).compile()
+
+ service = module.get(EncryptKeyValueService)
+ keyValueRepository = module.get(KeyValueRepository)
+ })
+
+ describe('set', () => {
+ it('sets encrypt value in the key-value storage', async () => {
+ const key = 'test-key'
+ const value = 'plain value'
+
+ await service.set(key, value)
+
+ expect(await keyValueRepository.get(key)).not.toEqual(value)
+ expect(await service.get(key)).toEqual(value)
+ })
+ })
+})
diff --git a/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/key-value.service.spec.ts b/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/key-value.service.spec.ts
index 6611c7106..67185523f 100644
--- a/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/key-value.service.spec.ts
+++ b/apps/policy-engine/src/shared/module/key-value/core/service/__test__/integration/key-value.service.spec.ts
@@ -1,17 +1,12 @@
import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
-import { EncryptionModule } from '../../../../../../../encryption/encryption.module'
import { load } from '../../../../../../../policy-engine.config'
-import { TestPrismaService } from '../../../../../../../shared/module/persistence/service/test-prisma.service'
import { InMemoryKeyValueRepository } from '../../../../persistence/repository/in-memory-key-value.repository'
import { KeyValueRepository } from '../../../repository/key-value.repository'
import { KeyValueService } from '../../key-value.service'
describe(KeyValueService.name, () => {
let service: KeyValueService
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- let keyValueRepository: KeyValueRepository
- let testPrismaService: TestPrismaService
let inMemoryKeyValueRepository: InMemoryKeyValueRepository
beforeEach(async () => {
@@ -22,8 +17,7 @@ describe(KeyValueService.name, () => {
ConfigModule.forRoot({
load: [load],
isGlobal: true
- }),
- EncryptionModule
+ })
],
providers: [
KeyValueService,
@@ -35,24 +29,15 @@ describe(KeyValueService.name, () => {
}).compile()
service = module.get(KeyValueService)
- keyValueRepository = module.get(KeyValueRepository)
- testPrismaService = module.get(TestPrismaService)
-
- await testPrismaService.truncateAll()
- })
-
- afterAll(async () => {
- await testPrismaService.truncateAll()
})
describe('set', () => {
- it('sets encrypted value in the key-value storage', async () => {
+ it('sets dencrypted value in the key-value storage', async () => {
const key = 'test-key'
- const value = 'not encrypted value'
+ const value = 'plain value'
await service.set(key, value)
- // expect(await keyValueRepository.get(key)).not.toEqual(value)
expect(await service.get(key)).toEqual(value)
})
})
diff --git a/apps/policy-engine/src/shared/module/key-value/core/service/encrypt-key-value.service.ts b/apps/policy-engine/src/shared/module/key-value/core/service/encrypt-key-value.service.ts
new file mode 100644
index 000000000..9bbd32acb
--- /dev/null
+++ b/apps/policy-engine/src/shared/module/key-value/core/service/encrypt-key-value.service.ts
@@ -0,0 +1,43 @@
+import { EncryptionService } from '@narval/encryption-module'
+import { Inject, Injectable } from '@nestjs/common'
+import { KeyValueRepository } from '../repository/key-value.repository'
+
+/**
+ * The key-value service is the main interface to interact with any storage
+ * back-end. Since the storage backend lives outside the engine, it's considered
+ * untrusted so the engine will encrypt the data before it sends them to the
+ * storage.
+ */
+@Injectable()
+export class EncryptKeyValueService {
+ constructor(
+ @Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository,
+ private encryptionService: EncryptionService
+ ) {}
+
+ async get(key: string): Promise {
+ const encryptedValue = await this.keyValueRepository.get(key)
+
+ if (encryptedValue) {
+ const value = await this.encryptionService.decrypt(Buffer.from(encryptedValue, 'hex'))
+
+ return value.toString()
+ }
+
+ return null
+ }
+
+ async set(key: string, value: string): Promise {
+ const encryptedValue = await this.encryptionService.encrypt(value)
+
+ return this.keyValueRepository.set(key, encryptedValue.toString('hex'))
+ }
+
+ async delete(key: string): Promise {
+ return this.keyValueRepository.delete(key)
+ }
+
+ static encode(value: unknown): string {
+ return JSON.stringify(value)
+ }
+}
diff --git a/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts b/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts
index 96b88008e..8215aad17 100644
--- a/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts
+++ b/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts
@@ -1,38 +1,15 @@
import { Inject, Injectable } from '@nestjs/common'
import { KeyValueRepository } from '../repository/key-value.repository'
-/**
- * The key-value service is the main interface to interact with any storage
- * back-end. Since the storage backend lives outside the engine, it's considered
- * untrusted so the engine will encrypt the data before it sends them to the
- * storage.
- *
- * It's because of that the key-value service assumes data is always encrypted.
- * If you need non-encrypted data, you can use the key-value repository.
- */
@Injectable()
export class KeyValueService {
constructor(@Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository) {}
async get(key: string): Promise {
- // const encryptedValue = await this.keyValueRepository.get(key)
-
- // if (encryptedValue) {
- // const value = await this.encryptionService.decrypt(Buffer.from(encryptedValue, 'hex'))
-
- // return value.toString()
- // }
-
- // return null
-
return this.keyValueRepository.get(key)
}
async set(key: string, value: string): Promise {
- // const encryptedValue = await this.encryptionService.encrypt(value)
-
- // return this.keyValueRepository.set(key, encryptedValue.toString('hex'))
-
return this.keyValueRepository.set(key, value)
}
diff --git a/apps/policy-engine/src/shared/module/key-value/key-value.module.ts b/apps/policy-engine/src/shared/module/key-value/key-value.module.ts
index cb08f8a85..c0333a31e 100644
--- a/apps/policy-engine/src/shared/module/key-value/key-value.module.ts
+++ b/apps/policy-engine/src/shared/module/key-value/key-value.module.ts
@@ -1,18 +1,35 @@
-import { Module } from '@nestjs/common'
-import { EncryptionModule } from '../../../encryption/encryption.module'
+import { EncryptionModule } from '@narval/encryption-module'
+import { Module, forwardRef } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { AppModule } from '../../../app/app.module'
+import { EngineService } from '../../../app/core/service/engine.service'
+import { EncryptionModuleOptionFactory } from '../../factory/encryption-module-option.factory'
+import { PersistenceModule } from '../persistence/persistence.module'
import { KeyValueRepository } from './core/repository/key-value.repository'
+import { EncryptKeyValueService } from './core/service/encrypt-key-value.service'
import { KeyValueService } from './core/service/key-value.service'
import { InMemoryKeyValueRepository } from './persistence/repository/in-memory-key-value.repository'
+import { PrismaKeyValueRepository } from './persistence/repository/prisma-key-value.repository'
@Module({
- imports: [EncryptionModule],
+ imports: [
+ PersistenceModule,
+ EncryptionModule.registerAsync({
+ imports: [forwardRef(() => AppModule)],
+ inject: [ConfigService, EngineService],
+ useClass: EncryptionModuleOptionFactory
+ })
+ ],
providers: [
KeyValueService,
+ EncryptKeyValueService,
+ InMemoryKeyValueRepository,
+ PrismaKeyValueRepository,
{
provide: KeyValueRepository,
- useClass: InMemoryKeyValueRepository
+ useExisting: PrismaKeyValueRepository
}
],
- exports: [KeyValueService, KeyValueRepository]
+ exports: [KeyValueService, EncryptKeyValueService]
})
export class KeyValueModule {}
diff --git a/apps/policy-engine/src/shared/module/key-value/persistence/repository/prisma-key-value.repository.ts b/apps/policy-engine/src/shared/module/key-value/persistence/repository/prisma-key-value.repository.ts
new file mode 100644
index 000000000..630f236a6
--- /dev/null
+++ b/apps/policy-engine/src/shared/module/key-value/persistence/repository/prisma-key-value.repository.ts
@@ -0,0 +1,46 @@
+import { Injectable } from '@nestjs/common'
+import { PrismaService } from '../../../persistence/service/prisma.service'
+import { KeyValueRepository } from '../../core/repository/key-value.repository'
+
+@Injectable()
+export class PrismaKeyValueRepository implements KeyValueRepository {
+ constructor(private prismaService: PrismaService) {}
+
+ async get(key: string): Promise {
+ const model = await this.prismaService.keyValue.findUnique({
+ where: { key }
+ })
+
+ if (model) {
+ return model.value
+ }
+
+ return null
+ }
+
+ async set(key: string, value: string): Promise {
+ try {
+ await this.prismaService.keyValue.upsert({
+ where: { key },
+ create: { key, value },
+ update: { value }
+ })
+
+ return true
+ } catch (error) {
+ return false
+ }
+ }
+
+ async delete(key: string): Promise {
+ try {
+ await this.prismaService.keyValue.delete({
+ where: { key }
+ })
+
+ return true
+ } catch (error) {
+ return false
+ }
+ }
+}
diff --git a/apps/policy-engine/src/shared/module/persistence/schema/migrations/20240301204146_init/migration.sql b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20240312112602_init/migration.sql
similarity index 52%
rename from apps/policy-engine/src/shared/module/persistence/schema/migrations/20240301204146_init/migration.sql
rename to apps/policy-engine/src/shared/module/persistence/schema/migrations/20240312112602_init/migration.sql
index a57f19a4e..61f90609d 100644
--- a/apps/policy-engine/src/shared/module/persistence/schema/migrations/20240301204146_init/migration.sql
+++ b/apps/policy-engine/src/shared/module/persistence/schema/migrations/20240312112602_init/migration.sql
@@ -6,3 +6,11 @@ CREATE TABLE "engine" (
CONSTRAINT "engine_pkey" PRIMARY KEY ("id")
);
+
+-- CreateTable
+CREATE TABLE "key_value" (
+ "key" TEXT NOT NULL,
+ "value" TEXT NOT NULL,
+
+ CONSTRAINT "key_value_pkey" PRIMARY KEY ("key")
+);
diff --git a/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma b/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma
index 9ea1412f7..f202242c1 100644
--- a/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma
+++ b/apps/policy-engine/src/shared/module/persistence/schema/schema.prisma
@@ -19,3 +19,12 @@ model Engine {
@@map("engine")
}
+
+// TODO: (@wcalderipe, 12/03/23) use hstore extension for better performance.
+// See https://www.postgresql.org/docs/9.1/hstore.html
+model KeyValue {
+ key String @id
+ value String
+
+ @@map("key_value")
+}
diff --git a/apps/policy-engine/src/shared/schema/engine.schema.ts b/apps/policy-engine/src/shared/schema/engine.schema.ts
index 7a5ce78cc..51415b9b0 100644
--- a/apps/policy-engine/src/shared/schema/engine.schema.ts
+++ b/apps/policy-engine/src/shared/schema/engine.schema.ts
@@ -1,7 +1,7 @@
import { z } from 'zod'
export const engineSchema = z.object({
- id: z.string(),
- masterKey: z.string(),
- adminApiKey: z.string()
+ id: z.string().min(1),
+ adminApiKey: z.string().min(1),
+ masterKey: z.string().min(1).optional()
})
diff --git a/apps/policy-engine/src/shared/testing/encryption.testing.ts b/apps/policy-engine/src/shared/testing/encryption.testing.ts
new file mode 100644
index 000000000..252e1a8a1
--- /dev/null
+++ b/apps/policy-engine/src/shared/testing/encryption.testing.ts
@@ -0,0 +1,14 @@
+import { RawAesKeyringNode } from '@aws-crypto/client-node'
+import { DEFAULT_WRAPPING_SUITE, generateKeyEncryptionKey } from '@narval/encryption-module'
+
+export const getTestRawAesKeyring = (options?: { password: string; salt: string }) => {
+ const password = options?.password || 'test-encryption-password'
+ const salt = options?.salt || 'test-encryption-salt'
+
+ return new RawAesKeyringNode({
+ keyName: 'test.key.name',
+ keyNamespace: 'test.key.namespace',
+ unencryptedMasterKey: generateKeyEncryptionKey(password, salt),
+ wrappingSuite: DEFAULT_WRAPPING_SUITE
+ })
+}
diff --git a/package-lock.json b/package-lock.json
index ee42869b1..2416a7db2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
"jose": "^5.2.2",
"lodash": "^4.17.21",
"lowdb": "^7.0.1",
+ "nest-commander": "^3.12.5",
"next": "14.0.4",
"prism-react-renderer": "^2.3.1",
"react": "18.2.0",
@@ -7474,6 +7475,18 @@
"npm": ">=6.14.13"
}
},
+ "node_modules/@golevelup/nestjs-discovery": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz",
+ "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==",
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^10.x",
+ "@nestjs/core": "^10.x"
+ }
+ },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -16210,6 +16223,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/inquirer": {
+ "version": "8.2.10",
+ "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.10.tgz",
+ "integrity": "sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==",
+ "peer": true,
+ "dependencies": {
+ "@types/through": "*",
+ "rxjs": "^7.2.0"
+ }
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -16504,6 +16527,15 @@
"@types/superagent": "^8.1.0"
}
},
+ "node_modules/@types/through": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz",
+ "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -18008,7 +18040,6 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
- "dev": true,
"dependencies": {
"type-fest": "^0.21.3"
},
@@ -18023,7 +18054,6 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -19466,6 +19496,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+ },
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -19798,6 +19833,14 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -24228,6 +24271,30 @@
"node": ">=12.0.0"
}
},
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/external-editor/node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -24361,7 +24428,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
- "dev": true,
"dependencies": {
"escape-string-regexp": "^1.0.5"
},
@@ -26296,6 +26362,66 @@
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
+ "node_modules/inquirer": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
+ "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==",
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.1",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "0.0.8",
+ "ora": "^5.4.1",
+ "run-async": "^2.4.0",
+ "rxjs": "^7.5.5",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6",
+ "wrap-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/inquirer/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/internal-slot": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
@@ -32350,6 +32476,11 @@
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
},
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
+ },
"node_modules/mylas": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",
@@ -32451,6 +32582,67 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
+ "node_modules/nest-commander": {
+ "version": "3.12.5",
+ "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.12.5.tgz",
+ "integrity": "sha512-UDzUvCG59ma84/7uUUWGltXr7gGtG3smr7ILg+Guia5wFzQNhxNLtlqapzI3woFr5kuuWtVcLRL/4+diLefZrA==",
+ "dependencies": {
+ "@fig/complete-commander": "^3.0.0",
+ "@golevelup/nestjs-discovery": "4.0.0",
+ "commander": "11.1.0",
+ "cosmiconfig": "8.3.6",
+ "inquirer": "8.2.6"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "@types/inquirer": "^8.1.3"
+ }
+ },
+ "node_modules/nest-commander/node_modules/@fig/complete-commander": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@fig/complete-commander/-/complete-commander-3.0.0.tgz",
+ "integrity": "sha512-jxiF1O+xiqdM7jECmTTrSO5w35iKsVRcSCz9mu20R4bFgLJS+61VNHw2A3EY7gU1kKlLJye0TmkyTfAoPhIq7A==",
+ "dependencies": {
+ "prettier": "^3.1.0"
+ },
+ "peerDependencies": {
+ "commander": "^11.1.0"
+ }
+ },
+ "node_modules/nest-commander/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/nest-commander/node_modules/cosmiconfig": {
+ "version": "8.3.6",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
+ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
+ "dependencies": {
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0",
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next": {
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz",
@@ -33144,6 +33336,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/p-cancelable": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
@@ -34582,7 +34782,6 @@
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
- "dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -36457,6 +36656,14 @@
"node": ">=12.0.0"
}
},
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -38504,8 +38711,7 @@
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
- "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
- "dev": true
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/through2": {
"version": "2.0.5",
diff --git a/package.json b/package.json
index 5b2ad96a2..73cafb5f8 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
"jose": "^5.2.2",
"lodash": "^4.17.21",
"lowdb": "^7.0.1",
+ "nest-commander": "^3.12.5",
"next": "14.0.4",
"prism-react-renderer": "^2.3.1",
"react": "18.2.0",
diff --git a/packages/encryption-module/.eslintrc.json b/packages/encryption-module/.eslintrc.json
new file mode 100644
index 000000000..9d9c0db55
--- /dev/null
+++ b/packages/encryption-module/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/encryption-module/README.md b/packages/encryption-module/README.md
new file mode 100644
index 000000000..9d562d32e
--- /dev/null
+++ b/packages/encryption-module/README.md
@@ -0,0 +1,38 @@
+# Encryption Module
+
+This is a NestJS module for encryption on top of `@aws-crypto/client-node`
+tailored to meet the needs of the Armory server.
+
+## Getting started
+
+```typescript
+import { RawAesKeyringNode, RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node'
+import { EncryptionModule, decryptMasterKey, generateKeyEncryptionKey, isolateBuffer } from '@narval/encryption-module'
+import { toBytes } from '@narval/policy-engine-shared'
+
+@Module({
+ imports: [
+ EncryptionModule.registerAsync({
+ inject: [ConfigService],
+ useFactory: async (configService: ConfigService) => {
+ const salt = configService.get('ENCRYPTION_SALT')
+ const password = configService.get('ENCRYPTION_PASSWORD')
+ const masterKey = configService.get('ENCRYPTION_MASTER_KEY')
+ const kek = generateKeyEncryptionKey(password, salt)
+ const unencryptedMasterKey = await decryptMasterKey(kek, toBytes(masterKey))
+
+ return {
+ keyring: new RawAesKeyringNode({
+ unencryptedMasterKey: isolateBuffer(unencryptedMasterKey),
+ keyName: 'arbitrary.key.name',
+ keyNamespace: 'arbitrary.key.namespace',
+ wrappingSuite: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
+ })
+ }
+ }
+ })
+ ]
+})
+```
+
+> Note: the module also exposes `.register` method for sync registration.
diff --git a/packages/encryption-module/jest.config.ts b/packages/encryption-module/jest.config.ts
new file mode 100644
index 000000000..af6893aa7
--- /dev/null
+++ b/packages/encryption-module/jest.config.ts
@@ -0,0 +1,11 @@
+/* eslint-disable */
+export default {
+ displayName: 'encryption-module',
+ preset: '../../jest.preset.js',
+ testEnvironment: 'node',
+ transform: {
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }]
+ },
+ moduleFileExtensions: ['ts', 'js', 'html'],
+ coverageDirectory: '../../coverage/packages/encryption-module'
+}
diff --git a/packages/encryption-module/project.json b/packages/encryption-module/project.json
new file mode 100644
index 000000000..5c802b971
--- /dev/null
+++ b/packages/encryption-module/project.json
@@ -0,0 +1,23 @@
+{
+ "name": "encryption-module",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/encryption-module/src",
+ "projectType": "library",
+ "targets": {
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["packages/encryption-module/**/*.ts"]
+ }
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "packages/encryption-module/jest.config.ts"
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/packages/encryption-module/src/index.ts b/packages/encryption-module/src/index.ts
new file mode 100644
index 000000000..2e58ae550
--- /dev/null
+++ b/packages/encryption-module/src/index.ts
@@ -0,0 +1,6 @@
+export * from './lib/encryption.constant'
+export * from './lib/encryption.exception'
+export * from './lib/encryption.module'
+export * from './lib/encryption.service'
+export * from './lib/encryption.type'
+export * from './lib/encryption.util'
diff --git a/packages/encryption-module/src/lib/__test__/unit/encryption.module.spec.ts b/packages/encryption-module/src/lib/__test__/unit/encryption.module.spec.ts
new file mode 100644
index 000000000..d4fe0f5ae
--- /dev/null
+++ b/packages/encryption-module/src/lib/__test__/unit/encryption.module.spec.ts
@@ -0,0 +1,22 @@
+import { RawAesKeyringNode, RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node'
+import { EncryptionModule } from '../../encryption.module'
+import { generateKeyEncryptionKey } from '../../encryption.util'
+
+describe(EncryptionModule.name, () => {
+ describe('registerAsync', () => {
+ it('creates a dynamic module with a custom keyring', async () => {
+ const module = EncryptionModule.registerAsync({
+ useFactory: () => ({
+ keyring: new RawAesKeyringNode({
+ keyName: 'test.key.name',
+ keyNamespace: 'test.key.namespace',
+ unencryptedMasterKey: generateKeyEncryptionKey('test-password', 'test-salt'),
+ wrappingSuite: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
+ })
+ })
+ })
+
+ expect(module).toBeDefined()
+ })
+ })
+})
diff --git a/packages/encryption-module/src/lib/__test__/unit/encryption.service.spec.ts b/packages/encryption-module/src/lib/__test__/unit/encryption.service.spec.ts
new file mode 100644
index 000000000..3ebc4aaa4
--- /dev/null
+++ b/packages/encryption-module/src/lib/__test__/unit/encryption.service.spec.ts
@@ -0,0 +1,45 @@
+import { RawAesKeyringNode, RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node'
+import { Test } from '@nestjs/testing'
+import { MODULE_OPTIONS_TOKEN } from '../../encryption.module-definition'
+import { EncryptionService } from '../../encryption.service'
+import { generateKeyEncryptionKey } from '../../encryption.util'
+
+describe(EncryptionService.name, () => {
+ let encryptionService: EncryptionService
+
+ beforeEach(async () => {
+ const keyring = new RawAesKeyringNode({
+ keyName: 'test.key.name',
+ keyNamespace: 'test.key.namespace',
+ unencryptedMasterKey: generateKeyEncryptionKey('test-password', 'test-salt'),
+ wrappingSuite: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
+ })
+
+ const module = await Test.createTestingModule({
+ providers: [
+ EncryptionService,
+ {
+ provide: MODULE_OPTIONS_TOKEN,
+ useValue: { keyring }
+ }
+ ]
+ }).compile()
+
+ encryptionService = module.get(EncryptionService)
+ })
+
+ it('encrypts given string', async () => {
+ const value = 'shh... this is a secret'
+ const cipher = await encryptionService.encrypt(value)
+
+ expect(cipher).not.toEqual(value)
+ })
+
+ it('decrypts given cipher', async () => {
+ const value = 'shh... this is a secret'
+ const cipher = await encryptionService.encrypt(value)
+ const decrypted = await encryptionService.decrypt(cipher)
+
+ expect(decrypted.toString()).toEqual(value)
+ })
+})
diff --git a/packages/encryption-module/src/lib/__test__/unit/encryption.util.spec.ts b/packages/encryption-module/src/lib/__test__/unit/encryption.util.spec.ts
new file mode 100644
index 000000000..2d6cc0801
--- /dev/null
+++ b/packages/encryption-module/src/lib/__test__/unit/encryption.util.spec.ts
@@ -0,0 +1,20 @@
+import { generateKeyEncryptionKey } from '../../encryption.util'
+
+describe('generateKeyEncryptionKey', () => {
+ const password = 'test-password'
+ const salt = 'test-salt'
+
+ it('generates a standard kek from password and salt', () => {
+ const kekOne = generateKeyEncryptionKey(password, salt)
+ const kekTwo = generateKeyEncryptionKey(password, salt)
+
+ expect(kekOne).toEqual(kekTwo)
+ expect(kekOne.length).toEqual(32)
+ })
+
+ it('generates a kek with a custom length', () => {
+ const kek = generateKeyEncryptionKey(password, salt, { lenght: 64 })
+
+ expect(kek.length).toEqual(64)
+ })
+})
diff --git a/packages/encryption-module/src/lib/encryption.constant.ts b/packages/encryption-module/src/lib/encryption.constant.ts
new file mode 100644
index 000000000..afdf1a6e5
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.constant.ts
@@ -0,0 +1,10 @@
+import { RawAesWrappingSuiteIdentifier } from '@aws-crypto/client-node'
+
+export const DEFAULT_ENCRYPTION_CONTEXT = {
+ purpose: 'data-encryption',
+ app: 'armory.encryption-module'
+}
+
+export const DEFAULT_KEY_NAMESPACE = 'narval.armory.engine'
+
+export const DEFAULT_WRAPPING_SUITE = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
diff --git a/packages/encryption-module/src/lib/encryption.exception.ts b/packages/encryption-module/src/lib/encryption.exception.ts
new file mode 100644
index 000000000..62c45c22c
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.exception.ts
@@ -0,0 +1 @@
+export class EncryptionException extends Error {}
diff --git a/packages/encryption-module/src/lib/encryption.module-definition.ts b/packages/encryption-module/src/lib/encryption.module-definition.ts
new file mode 100644
index 000000000..007857e6c
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.module-definition.ts
@@ -0,0 +1,6 @@
+import { ConfigurableModuleBuilder } from '@nestjs/common'
+import { EncryptionModuleOption } from './encryption.type'
+
+export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder()
+ .setFactoryMethodName('create')
+ .build()
diff --git a/packages/encryption-module/src/lib/encryption.module.ts b/packages/encryption-module/src/lib/encryption.module.ts
new file mode 100644
index 000000000..1876ef86f
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.module.ts
@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common'
+import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './encryption.module-definition'
+import { EncryptionService } from './encryption.service'
+
+export const EncryptionModuleOptionProvider = MODULE_OPTIONS_TOKEN
+
+@Module({
+ providers: [EncryptionService],
+ exports: [EncryptionService]
+})
+export class EncryptionModule extends ConfigurableModuleClass {}
diff --git a/packages/encryption-module/src/lib/encryption.service.ts b/packages/encryption-module/src/lib/encryption.service.ts
new file mode 100644
index 000000000..ed9181d78
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.service.ts
@@ -0,0 +1,49 @@
+import { Hex, toBytes } from '@narval/policy-engine-shared'
+import { Inject, Injectable } from '@nestjs/common'
+import { DEFAULT_ENCRYPTION_CONTEXT } from './encryption.constant'
+import { EncryptionException } from './encryption.exception'
+import { MODULE_OPTIONS_TOKEN } from './encryption.module-definition'
+import { EncryptionModuleOption, Keyring } from './encryption.type'
+import { getClient } from './encryption.util'
+
+@Injectable()
+export class EncryptionService {
+ constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: EncryptionModuleOption) {}
+
+ async encrypt(value: string | Buffer | Uint8Array): Promise {
+ const { encrypt } = getClient()
+ const { result } = await encrypt(this.getKeyring(), value, {
+ encryptionContext: DEFAULT_ENCRYPTION_CONTEXT
+ })
+
+ return result
+ }
+
+ async decrypt(ciphertext: Buffer | Uint8Array | Hex): Promise {
+ let ciphertextBuffer = ciphertext
+ if (typeof ciphertext === 'string') {
+ ciphertextBuffer = toBytes(ciphertext)
+ }
+
+ const { decrypt } = getClient()
+ const { plaintext, messageHeader } = await decrypt(this.getKeyring(), ciphertextBuffer)
+
+ // Verify the context wasn't changed.
+ const { encryptionContext } = messageHeader
+ Object.entries(DEFAULT_ENCRYPTION_CONTEXT).forEach(([key, value]) => {
+ if (encryptionContext[key] !== value) {
+ throw new EncryptionException('Encryption context does not match expected values')
+ }
+ })
+
+ return plaintext
+ }
+
+ getKeyring(): Keyring {
+ if (this.options.keyring) {
+ return this.options.keyring
+ }
+
+ throw new EncryptionException('Missing keyring. It seems the encryption module was not properly registered')
+ }
+}
diff --git a/packages/encryption-module/src/lib/encryption.type.ts b/packages/encryption-module/src/lib/encryption.type.ts
new file mode 100644
index 000000000..106e4d1a4
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.type.ts
@@ -0,0 +1,7 @@
+import { KmsKeyringNode, RawAesKeyringNode } from '@aws-crypto/client-node'
+
+export type Keyring = RawAesKeyringNode | KmsKeyringNode
+
+export type EncryptionModuleOption = {
+ keyring?: Keyring
+}
diff --git a/packages/encryption-module/src/lib/encryption.util.ts b/packages/encryption-module/src/lib/encryption.util.ts
new file mode 100644
index 000000000..97445bf54
--- /dev/null
+++ b/packages/encryption-module/src/lib/encryption.util.ts
@@ -0,0 +1,74 @@
+import { CommitmentPolicy, RawAesKeyringNode, buildClient as buildAwsClient } from '@aws-crypto/client-node'
+import { toHex } from '@narval/policy-engine-shared'
+import { generateKeySync, pbkdf2Sync } from 'crypto'
+import { DEFAULT_ENCRYPTION_CONTEXT, DEFAULT_KEY_NAMESPACE, DEFAULT_WRAPPING_SUITE } from './encryption.constant'
+
+export const isolateBuffer = (buffer: Buffer): Buffer => {
+ const newBuffer = Buffer.alloc(buffer.length)
+ buffer.copy(newBuffer, 0, 0, buffer.length)
+
+ return newBuffer
+}
+
+export const generateKeyEncryptionKey = (
+ password: string,
+ salt: string,
+ options?: { iterations?: number; lenght: number }
+): Buffer => {
+ const iterations = options?.lenght || 1_000_000
+ const length = options?.lenght || 32
+
+ const kek = pbkdf2Sync(password.normalize(), salt.normalize(), iterations, length, 'sha256')
+
+ return kek
+}
+
+const buildKeyEncryptionKeyring = (kek: Buffer) => {
+ return new RawAesKeyringNode({
+ keyName: 'armory.engine.kek',
+ unencryptedMasterKey: isolateBuffer(kek),
+ keyNamespace: DEFAULT_KEY_NAMESPACE,
+ wrappingSuite: DEFAULT_WRAPPING_SUITE
+ })
+}
+
+export const getClient = () => {
+ return buildAwsClient(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
+}
+
+export const encryptMaterKey = async (kek: Buffer, cleartext: Buffer): Promise => {
+ // Encrypt the Master Key with the Key Encryption Key.
+ const keyring = buildKeyEncryptionKeyring(kek)
+ const { result } = await getClient().encrypt(keyring, cleartext, {
+ encryptionContext: DEFAULT_ENCRYPTION_CONTEXT
+ })
+
+ return result
+}
+
+export const decryptMasterKey = async (kek: Buffer, ciphertext: Uint8Array): Promise => {
+ const keyring = buildKeyEncryptionKeyring(kek)
+ const { plaintext, messageHeader } = await getClient().decrypt(keyring, ciphertext)
+ const { encryptionContext } = messageHeader
+
+ // Verify the context wasn't changed.
+ Object.entries(DEFAULT_ENCRYPTION_CONTEXT).forEach(([key, value]) => {
+ if (encryptionContext[key] !== value) {
+ throw new Error('Encryption Context does not match expected values')
+ }
+ })
+
+ return plaintext
+}
+
+export const generateMasterKey = async (kek: Buffer): Promise => {
+ const mk = generateKeySync('aes', { length: 256 })
+ const mkBuffer = mk.export()
+
+ // Encrypt it with the Key Encryption Key (KEK) that was derived from
+ // the a password and salt.
+ const encryptedMk = await encryptMaterKey(kek, mkBuffer)
+ const encryptedMkString = toHex(encryptedMk)
+
+ return encryptedMkString
+}
diff --git a/packages/encryption-module/tsconfig.json b/packages/encryption-module/tsconfig.json
new file mode 100644
index 000000000..9f7af61b1
--- /dev/null
+++ b/packages/encryption-module/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "forceConsistentCasingInFileNames": true,
+ "module": "commonjs",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "strict": true,
+ "strictPropertyInitialization": false
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/packages/encryption-module/tsconfig.lib.json b/packages/encryption-module/tsconfig.lib.json
new file mode 100644
index 000000000..c297a2487
--- /dev/null
+++ b/packages/encryption-module/tsconfig.lib.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "types": ["node"],
+ "target": "es2021",
+ "strictNullChecks": true,
+ "noImplicitAny": true,
+ "strictBindCallApply": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/packages/encryption-module/tsconfig.spec.json b/packages/encryption-module/tsconfig.spec.json
new file mode 100644
index 000000000..f6d8ffcc9
--- /dev/null
+++ b/packages/encryption-module/tsconfig.spec.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 2d39e0afc..8b6ad853b 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -13,6 +13,7 @@
"noImplicitAny": true,
"noImplicitThis": true,
"paths": {
+ "@narval/encryption-module": ["packages/encryption-module/src/index.ts"],
"@narval/policy-engine-shared": ["packages/policy-engine-shared/src/index.ts"],
"@narval/signature": ["packages/signature/src/index.ts"],
"@narval/transaction-engine-module": ["packages/transaction-engine-module/src/index.ts"],