Skip to content

Commit

Permalink
Create @celo/encrypted-backup backup for ODIS hardened password encry…
Browse files Browse the repository at this point in the history
…pted backups (#8896)

* create encrypted-backup package

* basic backup and open functions working without any key hardening

* basic backup and open functions working without any key hardening

* use io-ts to create serialization and deserialization functions

* extract the encryption and decryption logic from the main functions

* add io-ts schema definitions for Domain types

* add domain field to backup type and schema

* mix in the domain hash to the key as a simple proxy for ODIS

* add a stub to hold the place of circuit breaker functions

* update the dependency graph

* swap order of circuit breaker and odis stubs

* [broken] move domains source from @celo/identity to @celo/phone-number-privacy

* [broken] remove @celo/identity dependency from @celo/phone-number-privacy-common

* finish removing @celo/identity and a dependency of @celo/phone-number-privacy-common

* remove duplication and inconsistencies betweem @celo/identity and @celo/phone-number-privacy-common

* fix linter errors

* clean up domain state and response types

* [checkpoint] partially implemented key hardening through ODIS

* refactor phone-number-privacy a little

* initial implementaion of odis key hardening logic

* wire in odis key hardening

* fix issues occuring during package initialization

* tests now working against a mock implementation of ODIS

* add (untested) circuit breaker client implementation

* add comments and pipe in circuit breaker config values

* fix issues founds in manual testing

* add a mock and tests for circuit breaker client

* add a NO_GANACHE env variable to disable starting ganache for testing

* round out tests for the circuit breaker client

* refactor the mock circuit breaker to be ready to export

* use the circuit breaker for key hardening

* bump package versions

* add more information to error handling and debug messages

* add doc strings to create and open backup functions

* add wrapper function createPinEncryptedBackup with documentation

* refactor mock odis

* handle 429 status from ODIS and add some error condition tests to backup lib

* seperate handling of fetch errors and add more error case tests

* add tests for error cases in openBackup

* remove DO NOT MERGE note

* update dependency graph

* add more information to comments

* add links to the new documentation

* fix build error in signer

* change request type definitions and checkSequentialDelay function

* fix linter errors

* fix linter errors

* but like really, fix linter errors

* ok I was only joking before about fixing the linter errors. this time for sure.

* add support for computational key hardening [lacks schema or tests]

* extract odis mock to new file

* add computational hardening to test config

* add failure case tests for mutated backups

* add computational hardening to schema

* Update packages/sdk/identity/src/odis/circuit-breaker.ts

Co-authored-by: Alec Schaefer <alec@cLabs.co>

* consolidate imports

* address review comments

* add odis verification error type

* add safety gate to prevent use of OPRF function for key hardening in prod

* bump dep versions in phone-number-privacy-common

* remove new code for encrypted backup

* Revert "remove new code for encrypted backup"

This reverts commit e6b4c62.

* populate index.ts file

* fix import

* fix import again

* update dep graphy

* fix lint errors

* fix lint error

* update dependency graph

* remove outdated DO NOT MERGE

* add phone-number-privacy-common to package list and sort the list

* add @types/express as dev dependency

* update circuit breaker keys to production values

* fix typos and add DO NOT MERGE comments for changes to be made

* bump development version of phone-number-privacy-common package

* address most of the DO NOT MERGE comments

* add createPasswordEncryptedBackup function

* add safety measure to prevent accidental usage of the createBackup API with an empty hardening config

* fix usages of renamed Endpoint enum

* fix enum types

* remove accidentally added walletconnect package

* fix lint error

* fix another lint error

* fix dangling reference to CustomSigner

* remove dangling refernce to signWithRawKey

Co-authored-by: Alec Schaefer <alec@cLabs.co>
  • Loading branch information
2 people authored and martinvol committed May 13, 2022
1 parent 1b66483 commit b3db564
Show file tree
Hide file tree
Showing 38 changed files with 2,898 additions and 119 deletions.
11 changes: 11 additions & 0 deletions dependency-graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@
"@celo/utils"
]
},
"@celo/encrypted-backup": {
"location": "packages/sdk/encrypted-backup",
"dependencies": [
"@celo/base",
"@celo/dev-utils",
"@celo/flake-tracker",
"@celo/identity",
"@celo/phone-number-privacy-common",
"@celo/utils"
]
},
"@celo/explorer": {
"location": "packages/sdk/explorer",
"dependencies": [
Expand Down
4 changes: 2 additions & 2 deletions packages/phone-number-privacy/combiner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"dependencies": {
"@celo/contractkit": "1.5.3-dev",
"@celo/identity": "1.5.3-dev",
"@celo/phone-number-privacy-common": "1.0.36-dev",
"@celo/phone-number-privacy-common": "1.0.40-dev",
"@celo/utils": "1.5.3-dev",
"blind-threshold-bls": "https://github.com/celo-org/blind-threshold-bls-wasm#e1e2f8a",
"firebase-admin": "^9.12.0",
Expand All @@ -55,4 +55,4 @@
"engines": {
"node": "12"
}
}
}
2 changes: 1 addition & 1 deletion packages/phone-number-privacy/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@celo/phone-number-privacy-common",
"version": "1.0.36-dev",
"version": "1.0.40-dev",
"description": "Common library for the combiner and signer libraries",
"author": "Celo",
"license": "Apache-2.0",
Expand Down
19 changes: 15 additions & 4 deletions packages/phone-number-privacy/common/src/interfaces/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,33 @@ import {
SequentialDelayDomain,
} from '../domains'

export enum Endpoints {
export enum PhoneNumberPrivacyEndpoint {
STATUS = '/status',
METRICS = '/metrics',
GET_BLINDED_MESSAGE_PARTIAL_SIG = '/getBlindedMessagePartialSig',
GET_QUOTA = '/getQuota',
}

export enum DomainEndpoint {
DISABLE_DOMAIN = '/domain/disable',
DOMAIN_SIGN = '/domain/sign/',
DOMAIN_QUOTA_STATUS = '/domain/quotaStatus',
}

export type SignerEndpoint = PhoneNumberPrivacyEndpoint | DomainEndpoint
export const SignerEndpoint = { ...PhoneNumberPrivacyEndpoint, ...DomainEndpoint }

export enum CombinerEndpoint {
SIGN_MESSAGE = '/getBlindedMessageSig',
MATCHMAKING = '/getContactMatches',
}

export type Endpoint = SignerEndpoint | CombinerEndpoint
export const Endpoint = { ...SignerEndpoint, ...CombinerEndpoint }

export enum AuthenticationMethod {
NONE = 'none',
WALLET_KEY = 'wallet_key',
ENCRYPTION_KEY = 'encryption_key',
CUSTOM_SIGNER = 'custom_signer',
}

export interface GetBlindedMessageSigRequest {
Expand Down Expand Up @@ -275,7 +286,7 @@ function verifyRequestSignature<R extends DomainRequest<SequentialDelayDomain>>(
request: R
): boolean {
// If the address field is undefined, then this domain is unauthenticated.
// Return false as the signature cannot need to be checked.
// Return false as the signature cannot be checked.
if (!request.domain.address.defined) {
return false
}
Expand Down
16 changes: 16 additions & 0 deletions packages/phone-number-privacy/common/src/interfaces/responses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { KnownDomainState } from '../domains'
import {
DisableDomainRequest,
DomainQuotaStatusRequest,
DomainRequest,
DomainRestrictedSignatureRequest,
} from './requests'

export interface SignMessageResponse {
success: boolean
Expand Down Expand Up @@ -77,3 +83,13 @@ export interface DisableDomainResponseFailure {
}

export type DisableDomainResponse = DisableDomainResponseSuccess | DisableDomainResponseFailure

export type DomainResponse<
R extends DomainRequest = DomainRequest
> = R extends DomainRestrictedSignatureRequest
? DomainRestrictedSignatureResponse
: never | R extends DomainQuotaStatusRequest
? DomainQuotaStatusResponse
: never | R extends DisableDomainRequest
? DisableDomainResponse
: never
4 changes: 2 additions & 2 deletions packages/phone-number-privacy/monitor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
"@celo/contractkit": "1.5.1",
"@celo/identity": "1.5.1",
"@celo/wallet-local": "1.5.1",
"@celo/phone-number-privacy-common": "1.0.36-dev",
"@celo/phone-number-privacy-common": "1.0.40-dev",
"@celo/utils": "1.5.1",
"firebase-admin": "^8.10.0",
"firebase-functions": "^3.6.0"
},
"engines": {
"node": ">=10"
}
}
}
4 changes: 2 additions & 2 deletions packages/phone-number-privacy/signer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@celo/base": "1.5.3-dev",
"@celo/contractkit": "1.5.3-dev",
"@celo/phone-number-privacy-common": "1.0.36-dev",
"@celo/phone-number-privacy-common": "1.0.40-dev",
"@celo/identity": "1.5.3-dev",
"@celo/utils": "1.5.3-dev",
"@celo/wallet-hsm-azure": "1.5.3-dev",
Expand Down Expand Up @@ -54,4 +54,4 @@
"engines": {
"node": ">=10"
}
}
}
29 changes: 12 additions & 17 deletions packages/phone-number-privacy/signer/src/domain/domain.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DomainRestrictedSignatureRequest,
DomainRestrictedSignatureResponse,
DomainRestrictedSignatureResponseSuccess,
Endpoints,
Endpoint,
ErrorMessage,
isKnownDomain,
KnownDomain,
Expand Down Expand Up @@ -45,11 +45,11 @@ export class DomainService implements IDomainService {
request: Request<{}, {}, DisableDomainRequest>,
response: Response<DisableDomainResponse>
): Promise<void> {
Counters.requests.labels(Endpoints.DISABLE_DOMAIN).inc()
Counters.requests.labels(Endpoint.DISABLE_DOMAIN).inc()

const logger = response.locals.logger
const domain = request.body.domain
if (!this.inputValidation(domain, response, Endpoints.DISABLE_DOMAIN, logger)) {
if (!this.inputValidation(domain, response, Endpoint.DISABLE_DOMAIN, logger)) {
// inputValidation returns a response to the user internally. Nothing left to do.
return
}
Expand All @@ -74,24 +74,19 @@ export class DomainService implements IDomainService {
return
} catch (error) {
logger.error('Error while disabling domain', error)
respondWithError(
Endpoints.DISABLE_DOMAIN,
response,
500,
ErrorMessage.DATABASE_UPDATE_FAILURE
)
respondWithError(Endpoint.DISABLE_DOMAIN, response, 500, ErrorMessage.DATABASE_UPDATE_FAILURE)
}
}

public async handleGetDomainQuotaStatus(
request: Request<{}, {}, DomainQuotaStatusRequest>,
response: Response<DomainQuotaStatusResponse>
): Promise<void> {
Counters.requests.labels(Endpoints.DOMAIN_QUOTA_STATUS).inc()
Counters.requests.labels(Endpoint.DOMAIN_QUOTA_STATUS).inc()

const logger = response.locals.logger
const domain = request.body.domain
if (!this.inputValidation(domain, response, Endpoints.DOMAIN_QUOTA_STATUS, logger)) {
if (!this.inputValidation(domain, response, Endpoint.DOMAIN_QUOTA_STATUS, logger)) {
// inputValidation returns a response to the user internally. Nothing left to do.
return
}
Expand Down Expand Up @@ -127,7 +122,7 @@ export class DomainService implements IDomainService {
} catch (error) {
logger.error('Error while getting domain status', error)
respondWithError(
Endpoints.DOMAIN_QUOTA_STATUS,
Endpoint.DOMAIN_QUOTA_STATUS,
response,
500,
ErrorMessage.DATABASE_GET_FAILURE
Expand All @@ -139,11 +134,11 @@ export class DomainService implements IDomainService {
request: Request<{}, {}, DomainRestrictedSignatureRequest>,
response: Response<DomainRestrictedSignatureResponse>
): Promise<void> {
Counters.requests.labels(Endpoints.DOMAIN_SIGN).inc()
Counters.requests.labels(Endpoint.DOMAIN_SIGN).inc()

const logger = response.locals.logger
const domain = request.body.domain
if (!this.inputValidation(domain, response, Endpoints.DOMAIN_SIGN, logger)) {
if (!this.inputValidation(domain, response, Endpoint.DOMAIN_SIGN, logger)) {
// inputValidation returns a response to the user internally. Nothing left to do.
return
}
Expand Down Expand Up @@ -177,7 +172,7 @@ export class DomainService implements IDomainService {
name: domain.name,
version: domain.version,
})
respondWithError(Endpoints.DOMAIN_SIGN, response, 403, WarningMessage.EXCEEDED_QUOTA)
respondWithError(Endpoint.DOMAIN_SIGN, response, 403, WarningMessage.EXCEEDED_QUOTA)
return
}

Expand Down Expand Up @@ -206,14 +201,14 @@ export class DomainService implements IDomainService {
} catch (err) {
logger.error('Failed to get signature for a domain')
logger.error(err)
respondWithError(Endpoints.DOMAIN_SIGN, response, 500, ErrorMessage.UNKNOWN_ERROR)
respondWithError(Endpoint.DOMAIN_SIGN, response, 500, ErrorMessage.UNKNOWN_ERROR)
}
}

private inputValidation(
domain: Domain,
response: Response,
endpoint: Endpoints,
endpoint: Endpoint,
logger: Logger
): domain is KnownDomain {
if (!this.authService.authCheck()) {
Expand Down
24 changes: 10 additions & 14 deletions packages/phone-number-privacy/signer/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { timeout } from '@celo/base'
import {
Endpoints,
loggerMiddleware,
rootLogger as logger,
} from '@celo/phone-number-privacy-common'
import { Endpoint, loggerMiddleware, rootLogger as logger } from '@celo/phone-number-privacy-common'
import Logger from 'bunyan'
import express, { Request, Response } from 'express'
import fs from 'fs'
Expand All @@ -26,18 +22,18 @@ export function createServer() {
const app = express()
app.use(express.json({ limit: '0.2mb' }), loggerMiddleware)

app.get(Endpoints.STATUS, (_req, res) => {
app.get(Endpoint.STATUS, (_req, res) => {
res.status(200).json({
version: getVersion(),
})
})

app.get(Endpoints.METRICS, (_req, res) => {
app.get(Endpoint.METRICS, (_req, res) => {
res.send(PromClient.register.metrics())
})

const addMeteredEndpoint = (
endpoint: Endpoints,
endpoint: Endpoint,
handler: (req: Request, res: Response) => Promise<void>,
method: 'post' | 'get' = 'post'
) =>
Expand All @@ -46,11 +42,11 @@ export function createServer() {
})

// EG. curl -v "http://localhost:8080/getBlindedMessagePartialSig" -H "Authorization: 0xdaf63ea42a092e69b2001db3826bc81dc859bffa4d51ce8943fddc8ccfcf6b2b1f55d64e4612e7c028791528796f5a62c1d2865b184b664589696a08c83fc62a00" -d '{"hashedPhoneNumber":"0x5f6e88c3f724b3a09d3194c0514426494955eff7127c29654e48a361a19b4b96","blindedQueryPhoneNumber":"n/I9srniwEHm5o6t3y0tTUB5fn7xjxRrLP1F/i8ORCdqV++WWiaAzUo3GA2UNHiB","account":"0x588e4b68193001e4d10928660aB4165b813717C0"}' -H 'Content-Type: application/json'
addMeteredEndpoint(Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG, handleGetBlindedMessagePartialSig)
addMeteredEndpoint(Endpoints.GET_QUOTA, handleGetQuota)
addMeteredEndpoint(Endpoints.DOMAIN_QUOTA_STATUS, domainService.handleGetDomainQuotaStatus)
addMeteredEndpoint(Endpoints.DOMAIN_SIGN, domainService.handleGetDomainRestrictedSignature)
addMeteredEndpoint(Endpoints.DISABLE_DOMAIN, domainService.handleDisableDomain)
addMeteredEndpoint(Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG, handleGetBlindedMessagePartialSig)
addMeteredEndpoint(Endpoint.GET_QUOTA, handleGetQuota)
addMeteredEndpoint(Endpoint.DOMAIN_QUOTA_STATUS, domainService.handleGetDomainQuotaStatus)
addMeteredEndpoint(Endpoint.DOMAIN_SIGN, domainService.handleGetDomainRestrictedSignature)
addMeteredEndpoint(Endpoint.DISABLE_DOMAIN, domainService.handleDisableDomain)

const sslOptions = getSslOptions()
if (sslOptions) {
Expand All @@ -61,7 +57,7 @@ export function createServer() {
}

async function callAndMeterLatency(
endpoint: Endpoints,
endpoint: Endpoint,
handler: (req: Request, res: Response) => Promise<void>,
req: Request,
res: Response
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
authenticateUser,
Endpoints,
Endpoint,
ErrorMessage,
GetBlindedMessageSigRequest,
hasValidAccountParam,
Expand Down Expand Up @@ -32,7 +32,7 @@ export async function handleGetBlindedMessagePartialSig(
request: Request<{}, {}, GetBlindedMessagePartialSigRequest>,
response: Response
) {
Counters.requests.labels(Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG).inc()
Counters.requests.labels(Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG).inc()

const logger: Logger = response.locals.logger
logger.info({ request: request.body }, 'Request received')
Expand All @@ -41,7 +41,7 @@ export async function handleGetBlindedMessagePartialSig(
try {
if (!isValidGetSignatureInput(request.body)) {
respondWithError(
Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG,
Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG,
response,
400,
WarningMessage.INVALID_INPUT
Expand All @@ -56,7 +56,7 @@ export async function handleGetBlindedMessagePartialSig(
!(await authenticateUser(request, getContractKit(), logger).finally(meterAuthenticateUser))
) {
respondWithError(
Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG,
Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG,
response,
401,
WarningMessage.UNAUTHENTICATED_USER
Expand Down Expand Up @@ -109,7 +109,7 @@ export async function handleGetBlindedMessagePartialSig(
logger.info({ request: request.body }, 'Request will bypass quota check for testing')
} else {
respondWithError(
Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG,
Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG,
response,
403,
WarningMessage.EXCEEDED_QUOTA,
Expand Down Expand Up @@ -179,14 +179,14 @@ export async function handleGetBlindedMessagePartialSig(
} else {
signMessageResponse = signMessageResponseSuccess
}
Counters.responses.labels(Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG, '200').inc()
Counters.responses.labels(Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG, '200').inc()
logger.info({ response: signMessageResponse }, 'Signature retrieval success')
response.json(signMessageResponse)
} catch (err) {
logger.error('Failed to get signature')
logger.error(err)
respondWithError(
Endpoints.GET_BLINDED_MESSAGE_PARTIAL_SIG,
Endpoint.GET_BLINDED_MESSAGE_PARTIAL_SIG,
response,
500,
ErrorMessage.UNKNOWN_ERROR
Expand Down
Loading

0 comments on commit b3db564

Please sign in to comment.