Skip to content

Commit

Permalink
WIP wiring up KEK, MK, CEK keygen & encrypting/storing the MK
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Feb 28, 2024
1 parent 25a78b6 commit 21d5085
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 14 deletions.
3 changes: 3 additions & 0 deletions apps/policy-engine/.env.default
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ PORT=3010

POLICY_ENGINE_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/engine?schema=public"

ENGINE_UID="local-dev-engine-instance-1"

MASTER_PASSWORD="unsafe-local-dev-master-password"
4 changes: 3 additions & 1 deletion apps/policy-engine/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HttpModule } from '@nestjs/axios'
import { Module, ValidationPipe } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_PIPE } from '@nestjs/core'
import { KeyringModule } from '../keyring/keyring.module'
import { load } from '../policy-engine.config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
Expand All @@ -16,7 +17,8 @@ import { EntityRepository } from './persistence/repository/entity.repository'
load: [load],
isGlobal: true
}),
HttpModule
HttpModule,
KeyringModule
],
controllers: [AppController, AdminController],
providers: [
Expand Down
113 changes: 113 additions & 0 deletions apps/policy-engine/src/keyring/core/keyring.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import crypto from 'crypto'
import { Config } from '../../policy-engine.config'
import { KeyringRepository } from '../persistence/repository/keyring.repository'

const IV_LENGTH = 16
const AUTH_TAG_LENGTH = 16

@Injectable()
export class KeyringService implements OnApplicationBootstrap {
private logger = new Logger(KeyringService.name)

private engineUid: string

private kek: Buffer

private masterPassword: string

private masterKey: Buffer

private adminApiKey: Buffer

constructor(
private keyringRepository: KeyringRepository,
@Inject(ConfigService) configService: ConfigService<Config, true>
) {
this.engineUid = configService.get('engineUid', { infer: true })
this.masterPassword = configService.get('masterPassword', { infer: true })
}

async onApplicationBootstrap(): Promise<void> {
this.logger.log('Keyring Service boot')
let engine = await this.keyringRepository.getEngine(this.engineUid)

// Derive the Key Encryption Key (KEK) from the master password using PBKDF2
this.kek = this.deriveKek(this.masterPassword)

if (!engine) {
// New Engine, set it up
engine = await this.firstTimeSetup()
}

this.masterKey = this.decryptWithKey(engine.masterKey, this.kek)
this.adminApiKey = this.decryptWithKey(engine.adminApiKey, this.kek)
}

private deriveKek(password: string): Buffer {
// Derive the Key Encryption Key (KEK) from the master password using PBKDF2
const kek = crypto.pbkdf2Sync(password.normalize(), this.engineUid.normalize(), 1000000, 32, 'sha256')
this.logger.log('Derived KEK', { kek: kek.toString('hex') })
return kek
}

decryptWithKey(encryptedString: string, key: Buffer): Buffer {
const encryptedBuffer = Buffer.from(encryptedString, 'hex')
// IV and AuthTag are prepend/appended, so slice them off
const iv = encryptedBuffer.subarray(0, IV_LENGTH)
const authTag = encryptedBuffer.subarray(encryptedBuffer.length - AUTH_TAG_LENGTH)
const encrypted = encryptedBuffer.subarray(IV_LENGTH, encryptedBuffer.length - AUTH_TAG_LENGTH)

// Decrypt the data with the key
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv, { authTagLength: AUTH_TAG_LENGTH })
decipher.setAuthTag(authTag)
let decrypted = decipher.update(encrypted)
decrypted = Buffer.concat([decrypted, decipher.final()])

return decrypted
}

encryptWithKey(data: Buffer, key: Buffer): string {
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv, { authTagLength: AUTH_TAG_LENGTH })
let encrypted = cipher.update(data)
encrypted = Buffer.concat([encrypted, cipher.final()])
const authTag = cipher.getAuthTag()
// Concatenate the IV, encrypted key, and auth tag since those are not-secret and needed for decryption
const result = Buffer.concat([iv, encrypted, authTag])
return result.toString('hex')
}

private async firstTimeSetup() {
// Generate a new Master Key (MK) with AES256
const mk = crypto.generateKeySync('aes', { length: 256 })
const mkBuffer = mk.export()

// Generate an Admin API Key, just a random 32-byte string
const adminApiKeyBuffer = crypto.randomBytes(32)

// Encrypt the Master Key (MK) with the Key Encryption Key (KEK)
const encryptedMk = this.encryptWithKey(mkBuffer, this.kek)
const encryptedApiKey = this.encryptWithKey(adminApiKeyBuffer, this.kek)

// Save the Result.
const engine = await this.keyringRepository.createEngine(this.engineUid, encryptedMk, encryptedApiKey)

this.logger.log('Engine Initial Setup Complete')
this.logger.log('Admin API Key -- DO NOT LOSE THIS', adminApiKeyBuffer.toString('hex'))
return engine
}

// Verify if a given string matches our internal Admin Api Key
verifyAdminApiKey(apiKey: string): boolean {
return apiKey === this.adminApiKey.toString('hex')
}

deriveContentEncryptionKey(keyId: string) {
// Derive a CEK from the MK+keyId using HKDF
const cek = crypto.hkdfSync('sha256', this.masterKey, keyId, 'content', 32)
this.logger.log('Derived KEK', { cek: Buffer.from(cek).toString('hex') })
return Buffer.from(cek)
}
}
21 changes: 21 additions & 0 deletions apps/policy-engine/src/keyring/keyring.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HttpModule } from '@nestjs/axios'
import { Module, ValidationPipe } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_PIPE } from '@nestjs/core'
import { KeyringRepository } from '../keyring/persistence/repository/keyring.repository'
import { PersistenceModule } from '../shared/module/persistence/persistence.module'
import { KeyringService } from './core/keyring.service'

@Module({
imports: [ConfigModule.forRoot(), HttpModule, PersistenceModule],
controllers: [],
providers: [
KeyringService,
KeyringRepository,
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class KeyringModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
import { PrismaService } from '../../../shared/module/persistence/service/prisma.service'

@Injectable()
export class KeyringRepository implements OnModuleInit {
private logger = new Logger(KeyringRepository.name)

constructor(private prismaService: PrismaService) {}

async onModuleInit() {
this.logger.log('KeyringRepository initialized')
}

async getEngine(engineUid: string) {
return this.prismaService.engine.findUnique({

Check failure on line 15 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.

Check failure on line 15 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.

Check failure on line 15 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.
where: {
uid: engineUid
}
})
}

async createEngine(engineUid: string, masterKey: string, adminApiKey: string) {
return this.prismaService.engine.create({

Check failure on line 23 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.

Check failure on line 23 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.

Check failure on line 23 in apps/policy-engine/src/keyring/persistence/repository/keyring.repository.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property 'engine' does not exist on type 'PrismaService'.
data: {
uid: engineUid,
masterKey,
adminApiKey
}
})
}
}
2 changes: 1 addition & 1 deletion apps/policy-engine/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async function bootstrap() {
)
)

logger.log(`AuthZ is running on port ${port}`)
logger.log(`Policy Engine is running on port ${port}`)
}

bootstrap()
12 changes: 8 additions & 4 deletions apps/policy-engine/src/policy-engine.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ const ConfigSchema = z.object({
env: z.nativeEnum(Env),
port: z.coerce.number(),
database: z.object({
url: z.string().startsWith('file:')
})
url: z.string().startsWith('postgresql:')
}),
engineUid: z.string().optional(),
masterPassword: z.string().optional()
})

export type Config = z.infer<typeof ConfigSchema>
Expand All @@ -21,8 +23,10 @@ export const load = (): Config => {
env: process.env.NODE_ENV,
port: process.env.PORT,
database: {
url: process.env.ENGINE_DATABASE_URL
}
url: process.env.POLICY_ENGINE_DATABASE_URL
},
engineUid: process.env.ENGINE_UID,
masterPassword: process.env.MASTER_PASSWORD
})

if (result.success) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,19 @@ export class TestPrismaService {

async truncateAll(): Promise<void> {
const tablenames = await this.prisma.$queryRaw<

Check failure on line 14 in apps/policy-engine/src/shared/module/persistence/service/test-prisma.service.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property '$queryRaw' does not exist on type 'PrismaService'.

Check failure on line 14 in apps/policy-engine/src/shared/module/persistence/service/test-prisma.service.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property '$queryRaw' does not exist on type 'PrismaService'.

Check failure on line 14 in apps/policy-engine/src/shared/module/persistence/service/test-prisma.service.ts

View workflow job for this annotation

GitHub Actions / Build and test

Property '$queryRaw' does not exist on type 'PrismaService'.
Array<{ name: string }>
>`SELECT name FROM sqlite_master WHERE type='table'`
Array<{ tablename: string }>
>`SELECT tablename FROM pg_tables WHERE schemaname='public'`

for (const { name } of tablenames) {
if (name !== '_prisma_migrations') {
for (const { tablename } of tablenames) {
if (tablename !== '_prisma_migrations') {
try {
await this.prisma.$executeRawUnsafe(
`DELETE FROM ${name}; UPDATE SQLITE_SEQUENCE SET seq = 0 WHERE name = '${name}';`
)
await this.prisma.$executeRawUnsafe(`TRUNCATE TABLE "public"."${tablename}" CASCADE;`)
} catch (error) {
// The logger may be intentionally silented during tests. Thus, we use
// console.log to ensure engineers will see the error in the stdout.
//
// eslint-disable-next-line no-console
console.error('TestPrismaService DELETE error', error)
console.error('TestPrismaService truncateAll error', error)
}
}
}
Expand Down

0 comments on commit 21d5085

Please sign in to comment.