Skip to content

Commit

Permalink
sdk/encryption/stress initial commit (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
texuf authored May 20, 2024
1 parent 803db82 commit 86fe548
Show file tree
Hide file tree
Showing 177 changed files with 29,267 additions and 0 deletions.
68 changes: 68 additions & 0 deletions core/encryption/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:prettier/recommended",
"plugin:jest/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./tsconfig.eslint.json"] },
"plugins": ["@typescript-eslint", "import"],
"ignorePatterns": ["dist/**", ".turbo/**", "node_modules/**", "jest.config.ts"],
"rules": {
"no-console": "error",
"import/no-extraneous-dependencies": [
"error",
{
"packageDir": "."
}
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
],
"@typescript-eslint/restrict-template-expressions": [
"error",
{
"allowNever": true,
"allowBoolean": true,
"allowNumber": true,
"allowAny": true,
"allowNullish": true
}
],
"@typescript-eslint/no-empty-function": [
"error",
{
"allow": ["arrowFunctions"]
}
],

"@typescript-eslint/ban-ts-comment": "off"
},
"overrides": [
{
"files": ["**/*.test.*"],
"rules": {
"jest/no-standalone-expect": "off",
"jest/expect-expect": "off",
"jest/no-conditional-expect": "off",
"jest/no-disabled-tests": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unsafe-argument": "off"
}
}
]
}
5 changes: 5 additions & 0 deletions core/encryption/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Group Encryption protocol

Documentation is checked in here: <https://github.com/HereNotThere/docs-river-build.git>

Published here: <https://docs.river.build/build/encryption>
36 changes: 36 additions & 0 deletions core/encryption/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { JestConfigWithTsJest } from 'ts-jest'

const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: './../jest.env.ts',
testEnvironmentOptions: {
browsers: ['chrome', 'firefox', 'safari'],
url: 'https://localhost:5158',
},
verbose: true,
testTimeout: 60000,
modulePathIgnorePatterns: ['/dist/'],
testPathIgnorePatterns: ['/dist/', '/node_modules/', 'util.test.ts'],
setupFilesAfterEnv: ['jest-extended/all'],
setupFiles: ['fake-indexeddb/auto'],
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
moduleNameMapper: {
'bn.js': 'bn.js',
'hash.js': 'hash.js',
'(.+)\\.js': '$1',
'\\.(wasm)$': require.resolve('./src/mock-wasm-file.js'),
},
collectCoverage: true,
coverageProvider: 'v8',
coverageReporters: ['json', 'html'],
}

export default config
52 changes: 52 additions & 0 deletions core/encryption/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@river-build/encryption",
"packageManager": "[email protected]",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"cb": "yarn clean && yarn build",
"clean": "rm -rf dist",
"lint": "yarn eslint --format unix ./src",
"lint:fix": "yarn lint --fix",
"my-jest": "yarn node -r ../../scripts/node-no-warn.js --experimental-vm-modules $(yarn bin jest)",
"test": "yarn my-jest",
"test:ci": "yarn test",
"test:debug": "DEBUG=test yarn my-jest",
"watch": "tsc --watch"
},
"dependencies": {
"@bufbuild/protobuf": "^1.4.1",
"@ethereumjs/util": "^8.0.1",
"@matrix-org/olm": "^3.2.15",
"@river-build/dlog": "workspace:^",
"@river-build/proto": "workspace:^",
"@river-build/web3": "workspace:^",
"debug": "^4.3.4",
"dexie": "^3.2.4",
"ethers": "^5.7.2",
"nanoid": "^4.0.0",
"typed-emitter": "^2.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/lodash": "^4.14.186",
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.3",
"fake-indexeddb": "^4.0.1",
"jest": "^29.6.2",
"jest-extended": "^4.0.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"files": [
"dist"
]
}
71 changes: 71 additions & 0 deletions core/encryption/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { GroupEncryptionSession, UserDeviceCollection } from './olmLib'

import { EncryptionDevice } from './encryptionDevice'

export interface IGroupEncryptionClient {
downloadUserDeviceInfo(userIds: string[], forceDownload: boolean): Promise<UserDeviceCollection>
encryptAndShareGroupSessions(
streamId: string,
sessions: GroupEncryptionSession[],
devicesInRoom: UserDeviceCollection,
): Promise<void>
getDevicesInStream(streamId: string): Promise<UserDeviceCollection>
}

export interface IDecryptionParams {
/** olm.js wrapper */
device: EncryptionDevice
}

export interface IEncryptionParams {
client: IGroupEncryptionClient
/** olm.js wrapper */
device: EncryptionDevice
}

/**
* base type for encryption implementations
*/
export abstract class EncryptionAlgorithm implements IEncryptionParams {
public readonly device: EncryptionDevice
public readonly client: IGroupEncryptionClient

/**
* @param params - parameters
*/
public constructor(params: IEncryptionParams) {
this.device = params.device
this.client = params.client
}
}

/**
* base type for decryption implementations
*/
export abstract class DecryptionAlgorithm implements IDecryptionParams {
public readonly device: EncryptionDevice

public constructor(params: IDecryptionParams) {
this.device = params.device
}
}

/**
* Exception thrown when decryption fails
*
* @param msg - user-visible message describing the problem
*
* @param details - key/value pairs reported in the logs but not shown
* to the user.
*/
export class DecryptionError extends Error {
public constructor(public readonly code: string, msg: string) {
super(msg)
this.code = code
this.name = 'DecryptionError'
}
}

export function isDecryptionError(e: Error): e is DecryptionError {
return e.name === 'DecryptionError'
}
147 changes: 147 additions & 0 deletions core/encryption/src/cryptoStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { AccountRecord, GroupSessionRecord, UserDeviceRecord } from './storeTypes'
import Dexie, { Table } from 'dexie'

import { InboundGroupSessionData } from './encryptionDevice'
import { UserDevice } from './olmLib'

const DEFAULT_USER_DEVICE_EXPIRATION_TIME_MS = 15 * 60 * 1000 // 15 minutes todo increase to like 10 days or something https://github.com/HereNotThere/harmony/pull/4222#issuecomment-1822935596

export class CryptoStore extends Dexie {
account!: Table<AccountRecord>
outboundGroupSessions!: Table<GroupSessionRecord>
inboundGroupSessions!: Table<InboundGroupSessionData & { streamId: string; sessionId: string }>
devices!: Table<UserDeviceRecord>
userId: string

constructor(databaseName: string, userId: string) {
super(databaseName)
this.userId = userId
this.version(6).stores({
account: 'id',
inboundGroupSessions: '[streamId+sessionId]',
outboundGroupSessions: 'streamId',
devices: '[userId+deviceKey],expirationTimestamp',
})
}

async initialize() {
await this.devices.where('expirationTimestamp').below(Date.now()).delete()
}

deleteAllData() {
throw new Error('Method not implemented.')
}

async deleteInboundGroupSessions(streamId: string, sessionId: string): Promise<void> {
await this.inboundGroupSessions.where({ streamId, sessionId }).delete()
}

async deleteAccount(userId: string): Promise<void> {
await this.account.where({ id: userId }).delete()
}

async getAccount(): Promise<string> {
const account = await this.account.get({ id: this.userId })
if (!account) {
throw new Error('account not found')
}
return account.accountPickle
}

async storeAccount(accountPickle: string): Promise<void> {
await this.account.put({ id: this.userId, accountPickle })
}

async storeEndToEndOutboundGroupSession(
sessionId: string,
sessionData: string,
streamId: string,
): Promise<void> {
await this.outboundGroupSessions.put({ sessionId, session: sessionData, streamId })
}

async getEndToEndOutboundGroupSession(streamId: string): Promise<string> {
const session = await this.outboundGroupSessions.get({ streamId })
if (!session) {
throw new Error('session not found')
}
return session.session
}

async getEndToEndInboundGroupSession(
streamId: string,
sessionId: string,
): Promise<InboundGroupSessionData | undefined> {
return await this.inboundGroupSessions.get({ sessionId, streamId })
}

async storeEndToEndInboundGroupSession(
streamId: string,
sessionId: string,
sessionData: InboundGroupSessionData,
): Promise<void> {
await this.inboundGroupSessions.put({ streamId, sessionId, ...sessionData })
}

async getInboundGroupSessionIds(streamId: string): Promise<string[]> {
const sessions = await this.inboundGroupSessions.where({ streamId }).toArray()
return sessions.map((s) => s.sessionId)
}

async withAccountTx<T>(fn: () => Promise<T>): Promise<T> {
return await this.transaction('rw', this.account, fn)
}

async withGroupSessions<T>(fn: () => Promise<T>): Promise<T> {
return await this.transaction(
'rw',
this.outboundGroupSessions,
this.inboundGroupSessions,
fn,
)
}

/**
* Only used for testing
* @returns total number of devices in the store
*/
async deviceRecordCount() {
return await this.devices.count()
}

/**
* Store a list of devices for a given userId
* @param userId string
* @param devices UserDeviceInfo[]
* @param expirationMs Expiration time in milliseconds
*/
async saveUserDevices(
userId: string,
devices: UserDevice[],
expirationMs: number = DEFAULT_USER_DEVICE_EXPIRATION_TIME_MS,
) {
const expirationTimestamp = Date.now() + expirationMs
for (const device of devices) {
await this.devices.put({ userId, expirationTimestamp, ...device })
}
}

/**
* Get all stored devices for a given userId
* @param userId string
* @returns UserDevice[], a list of devices
*/
async getUserDevices(userId: string): Promise<UserDevice[]> {
const expirationTimestamp = Date.now()
return (
await this.devices
.where('userId')
.equals(userId)
.and((record) => record.expirationTimestamp > expirationTimestamp)
.toArray()
).map((record) => ({
deviceKey: record.deviceKey,
fallbackKey: record.fallbackKey,
}))
}
}
Loading

0 comments on commit 86fe548

Please sign in to comment.