From 404143214b36ecb01353de49b68d1ac95c3b9c62 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Wed, 7 Dec 2022 21:11:06 -0500 Subject: [PATCH] Update ODIS SDK to accept any type of identifier (#9985) * wip * general identifier odis sdk * update tests * add fn back in for backwards compatability * no salt only pepper * fix tests * rename identifiers * fix tests * PR comments * Merge branch 'master' of github.com:celo-org/celo-monorepo into isabellewei/allIdentifiers * update test * get rid of erroneously committed file * add prefix when blinding * add backwards compatibility test and fix existing tests * update dependency graph * add comment * PR comments * PR comments * ensure backwards compatibility * update dep graph * hardcode expected values in backwards compatibility test * nit: remove newline Co-authored-by: Victor Graf * make comments TSDoc compatible Co-authored-by: Victor Graf * PR comments * update docstrings and enforce prefix type * add yarn.lock * revert identity pkg version in attestation-service * use latest identity pkg version pre identifier change in attestation-service * yarn.lock * update test value for nested hashing Co-authored-by: Victor Graf Co-authored-by: Alec Schaefer --- packages/attestation-service/package.json | 2 +- packages/sdk/identity/package.json | 5 +- ...identifier-backwards-compatibility.test.ts | 117 +++++++++ .../sdk/identity/src/odis/identifier.test.ts | 156 ++++++++++++ packages/sdk/identity/src/odis/identifier.ts | 231 ++++++++++++++++++ .../src/odis/phone-number-identifier.test.ts | 9 - .../src/odis/phone-number-identifier.ts | 137 +++++------ yarn.lock | 173 ++++++++++++- 8 files changed, 736 insertions(+), 94 deletions(-) create mode 100644 packages/sdk/identity/src/odis/identifier-backwards-compatibility.test.ts create mode 100644 packages/sdk/identity/src/odis/identifier.test.ts create mode 100644 packages/sdk/identity/src/odis/identifier.ts diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index 5cc95740b72..4977af6c42e 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@celo/contractkit": "3.0.1-dev", - "@celo/identity": "3.0.1-dev", + "@celo/identity": "3.0.0", "@celo/keystores": "3.0.1-dev", "@celo/phone-utils": "3.0.1-dev", "bignumber.js": "^9.0.0", diff --git a/packages/sdk/identity/package.json b/packages/sdk/identity/package.json index 09cb4001ffe..8a4b7c56bcd 100644 --- a/packages/sdk/identity/package.json +++ b/packages/sdk/identity/package.json @@ -20,7 +20,7 @@ "docs": "typedoc", "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 28", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", - "test": "jest --runInBand", + "test": "jest --runInBand --testPathIgnorePatterns src/odis/identifier-backwards-compatibility.test.ts", "lint": "tslint -c tslint.json --project .", "prepublishOnly": "yarn build" }, @@ -44,7 +44,8 @@ "fetch-mock": "9.10.4", "@types/elliptic": "^6.4.12", "@celo/flake-tracker": "0.0.1-dev", - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#21652da" + "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#21652da", + "old-identity-sdk": "npm:@celo/identity@1.5.2" }, "engines": { "node": ">=12.9.0" diff --git a/packages/sdk/identity/src/odis/identifier-backwards-compatibility.test.ts b/packages/sdk/identity/src/odis/identifier-backwards-compatibility.test.ts new file mode 100644 index 00000000000..c2bcff92918 --- /dev/null +++ b/packages/sdk/identity/src/odis/identifier-backwards-compatibility.test.ts @@ -0,0 +1,117 @@ +import { getPhoneHash } from '@celo/base' +import { soliditySha3 } from '@celo/utils/lib/solidity' +import { OdisUtils } from 'old-identity-sdk' +import { WasmBlsBlindingClient } from './bls-blinding-client' +import { + getBlindedIdentifier, + getIdentifierHash, + getObfuscatedIdentifier, + IdentifierPrefix, +} from './identifier' +import { AuthenticationMethod, AuthSigner, getServiceContext } from './query' + +const mockE164Number = '+14155550000' +const mockAccount = '0x755dB5fF7B82e9a96e0dDDD143293dc2ADeC0050' +// const mockPrivateKey = '0x2cacaf965ae80da49d5b1fc4b4c9b08ffc35971a584aedcc1cb8322b9d5fd9c9' + +// this DEK has been registered to the above account on alfajores +const dekPrivateKey = '0xc2bbdabb440141efed205497a41d5fb6114e0435fd541e368dc628a8e086bfee' +// const dekPublicKey = '0xc2bbdabb440141efed205497a41d5fb6114e0435fd541e368dc628a8e086bfee' + +const authSigner: AuthSigner = { + authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, + rawKey: dekPrivateKey, +} +const oldServiceContext = OdisUtils.Query.getServiceContext('alfajores') +const currentServiceContext = getServiceContext('alfajores') + +const expectedObfuscatedIdentifier = + '0xf82c6272fd57d3e5d4e291be16b3ebac5c616084a5e6f3730c73f62efd39c6ae' +const expectedPepper = 'Pi4Z1NQnfsdvJ' + +describe('backwards compatibility of phone number identifiers', () => { + beforeAll(() => { + fetchMock.reset() + // disables the mock, lets all calls fall through to the actual network + fetchMock.spy() + }) + + it('should match when using EncryptionSigner', async () => { + const oldRes = await OdisUtils.PhoneNumberIdentifier.getPhoneNumberIdentifier( + mockE164Number, + mockAccount, + authSigner, + oldServiceContext + ) + + const currRes = await getObfuscatedIdentifier( + mockE164Number, + IdentifierPrefix.PHONE_NUMBER, + mockAccount, + authSigner, + currentServiceContext + ) + + expect(oldRes.e164Number).toEqual(currRes.plaintextIdentifier) + expect(oldRes.phoneHash).toEqual(expectedObfuscatedIdentifier) + expect(currRes.obfuscatedIdentifier).toEqual(expectedObfuscatedIdentifier) + expect(oldRes.pepper).toEqual(expectedPepper) + expect(currRes.pepper).toEqual(expectedPepper) + }, 20000) + + it('blinded identifier should match', async () => { + const blsBlindingClient = new WasmBlsBlindingClient('') + const seed = Buffer.from( + '44714c0a2b2bacec757a67822a4fbbdfe043cca8c6ae936545ef992f246df1a9', + 'hex' + ) + const oldRes = await OdisUtils.PhoneNumberIdentifier.getBlindedPhoneNumber( + mockE164Number, + blsBlindingClient, + seed + ) + const currentRes = await getBlindedIdentifier( + mockE164Number, + IdentifierPrefix.PHONE_NUMBER, + blsBlindingClient, + seed + ) + + const expectedBlindedIdentifier = + 'fuN6SmbxkYBqVbKZu4SizdyDjavNLK/XguIlwsWUhsWA0hQDoZtsZjQCbXqTnUiA' + + expect(oldRes).toEqual(expectedBlindedIdentifier) + expect(currentRes).toEqual(expectedBlindedIdentifier) + }) + + it('obfuscated identifier should match', async () => { + const sha3 = (v: string) => soliditySha3({ type: 'string', value: v }) + const oldRes = getPhoneHash(sha3, mockE164Number, expectedPepper) + + const currRes = getIdentifierHash(mockE164Number, IdentifierPrefix.PHONE_NUMBER, expectedPepper) + + expect(oldRes).toEqual(expectedObfuscatedIdentifier) + expect(currRes).toEqual(expectedObfuscatedIdentifier) + }) + + it('should not match when different prefix used', async () => { + const oldRes = await OdisUtils.PhoneNumberIdentifier.getPhoneNumberIdentifier( + mockE164Number, + mockAccount, + authSigner, + oldServiceContext + ) + + const currRes = await getObfuscatedIdentifier( + mockE164Number, + 'badPrefix', + mockAccount, + authSigner, + currentServiceContext + ) + + expect(oldRes.e164Number).toEqual(currRes.plaintextIdentifier) + expect(oldRes.phoneHash).not.toEqual(currRes.obfuscatedIdentifier) + expect(oldRes.pepper).not.toEqual(currRes.pepper) + }) +}) diff --git a/packages/sdk/identity/src/odis/identifier.test.ts b/packages/sdk/identity/src/odis/identifier.test.ts new file mode 100644 index 00000000000..30cb631c174 --- /dev/null +++ b/packages/sdk/identity/src/odis/identifier.test.ts @@ -0,0 +1,156 @@ +import { CombinerEndpoint } from '@celo/phone-number-privacy-common' +import { WasmBlsBlindingClient } from './bls-blinding-client' +import { + getBlindedIdentifier, + getBlindedIdentifierSignature, + getObfuscatedIdentifier, + getObfuscatedIdentifierFromSignature, + getPepperFromThresholdSignature, + IdentifierPrefix, +} from './identifier' +import { AuthenticationMethod, EncryptionKeySigner, ErrorMessages, ServiceContext } from './query' + +jest.mock('./bls-blinding-client', () => { + // tslint:disable-next-line:no-shadowed-variable + class WasmBlsBlindingClient { + blindMessage = (m: string) => m + unblindAndVerifyMessage = (m: string) => m + } + return { + WasmBlsBlindingClient, + } +}) + +const mockOffchainIdentifier = 'twitterHandle' +const mockAccount = '0x0000000000000000000000000000000000007E57' +const expectedIdentifierHash = '0x8d1f580d4e49568883df9092285c0f8336e50d592b944607a613aff804e0b48f' +const expectedPepper = 'nHIvMC9B4j2+H' + +const serviceContext: ServiceContext = { + odisUrl: 'https://mockodis.com', + odisPubKey: + '7FsWGsFnmVvRfMDpzz95Np76wf/1sPaK0Og9yiB+P8QbjiC8FV67NBans9hzZEkBaQMhiapzgMR6CkZIZPvgwQboAxl65JWRZecGe5V3XO4sdKeNemdAZ2TzQuWkuZoA', +} +const endpoint = serviceContext.odisUrl + CombinerEndpoint.PNP_SIGN +const rawKey = '41e8e8593108eeedcbded883b8af34d2f028710355c57f4c10a056b72486aa04' + +const authSigner: EncryptionKeySigner = { + authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, + rawKey, +} + +describe(getObfuscatedIdentifier, () => { + afterEach(() => { + fetchMock.reset() + }) + + describe('Retrieves a pepper correctly', () => { + it('Using EncryptionKeySigner', async () => { + fetchMock.mock(endpoint, { + success: true, + signature: '0Uj+qoAu7ASMVvm6hvcUGx2eO/cmNdyEgGn0mSoZH8/dujrC1++SZ1N6IP6v2I8A', + performedQueryCount: 5, + totalQuota: 10, + version: '', + }) + + const blsBlindingClient = new WasmBlsBlindingClient(serviceContext.odisPubKey) + const base64BlindedMessage = await getBlindedIdentifier( + mockOffchainIdentifier, + IdentifierPrefix.TWITTER, + blsBlindingClient + ) + const base64BlindSig = await getBlindedIdentifierSignature( + mockAccount, + authSigner, + serviceContext, + base64BlindedMessage + ) + const base64UnblindedSig = await blsBlindingClient.unblindAndVerifyMessage(base64BlindSig) + + await expect( + getObfuscatedIdentifier( + mockOffchainIdentifier, + IdentifierPrefix.TWITTER, + mockAccount, + authSigner, + serviceContext + ) + ).resolves.toMatchObject({ + plaintextIdentifier: mockOffchainIdentifier, + pepper: expectedPepper, + obfuscatedIdentifier: expectedIdentifierHash, + unblindedSignature: base64UnblindedSig, + }) + }) + + it('Preblinding the off-chain identifier', async () => { + fetchMock.mock(endpoint, { + success: true, + signature: '0Uj+qoAu7ASMVvm6hvcUGx2eO/cmNdyEgGn0mSoZH8/dujrC1++SZ1N6IP6v2I8A', + performedQueryCount: 5, + totalQuota: 10, + version: '', + }) + + const blsBlindingClient = new WasmBlsBlindingClient(serviceContext.odisPubKey) + const base64BlindedMessage = await getBlindedIdentifier( + mockOffchainIdentifier, + IdentifierPrefix.TWITTER, + blsBlindingClient + ) + + const base64BlindSig = await getBlindedIdentifierSignature( + mockAccount, + authSigner, + serviceContext, + base64BlindedMessage + ) + + const obfuscatedIdentifierDetails = await getObfuscatedIdentifierFromSignature( + mockOffchainIdentifier, + IdentifierPrefix.TWITTER, + base64BlindSig, + blsBlindingClient + ) + + expect(obfuscatedIdentifierDetails.obfuscatedIdentifier).toEqual(expectedIdentifierHash) + expect(obfuscatedIdentifierDetails.pepper).toEqual(expectedPepper) + }) + }) + + it('Throws quota error', async () => { + fetchMock.mock(endpoint, 403) + + await expect( + getObfuscatedIdentifier( + mockOffchainIdentifier, + IdentifierPrefix.PHONE_NUMBER, + mockAccount, + authSigner, + serviceContext + ) + ).rejects.toThrow(ErrorMessages.ODIS_QUOTA_ERROR) + }) + + it('Throws auth error', async () => { + fetchMock.mock(endpoint, 401) + await expect( + getObfuscatedIdentifier( + mockOffchainIdentifier, + IdentifierPrefix.PHONE_NUMBER, + mockAccount, + authSigner, + serviceContext + ) + ).rejects.toThrow(ErrorMessages.ODIS_AUTH_ERROR) + }) +}) + +describe(getPepperFromThresholdSignature, () => { + it('Hashes sigs correctly', () => { + const base64Sig = 'vJeFZJ3MY5KlpI9+kIIozKkZSR4cMymLPh2GHZUatWIiiLILyOcTiw2uqK/LBReA' + const signature = Buffer.from(base64Sig, 'base64') + expect(getPepperFromThresholdSignature(signature)).toBe('piWqRHHYWtfg9') + }) +}) diff --git a/packages/sdk/identity/src/odis/identifier.ts b/packages/sdk/identity/src/odis/identifier.ts new file mode 100644 index 00000000000..f248e271f9d --- /dev/null +++ b/packages/sdk/identity/src/odis/identifier.ts @@ -0,0 +1,231 @@ +import { + CombinerEndpointPNP, + KEY_VERSION_HEADER, + SignMessageRequest, + SignMessageResponseSchema, +} from '@celo/phone-number-privacy-common' +import { soliditySha3 } from '@celo/utils/lib/solidity' +import { createHash } from 'crypto' +import debugFactory from 'debug' +import { BlsBlindingClient, WasmBlsBlindingClient } from './bls-blinding-client' +import { + AuthenticationMethod, + AuthSigner, + EncryptionKeySigner, + getOdisPnpRequestAuth, + queryOdis, + ServiceContext, +} from './query' + +const debug = debugFactory('kit:odis:identifier') +const sha3 = (v: string) => soliditySha3({ type: 'string', value: v }) + +const PEPPER_CHAR_LENGTH = 13 +const PEPPER_SEPARATOR = '__' + +// using DID methods as prefixes when they exist +// https://w3c.github.io/did-spec-registries/#did-methods +export enum IdentifierPrefix { + PHONE_NUMBER = 'tel', + EMAIL = 'mailto', + TWITTER = 'twit', + // feel free to put up a PR to add more types! +} + +/** + * Steps from the private plaintext identifier to the obfuscated identifier, which can be made public. + * + * plaintext identifier: off-chain information, ex: phone number, twitter handle, email, etc. + * blinded identifier: obtained by blinding the plaintext identifier + * blinded signature: blinded identifier signed by ODIS + * unblinded signatue: obtained by unblinding the blinded signature + * pepper: unique secret, obtained by hashing the unblinded signature + * obfuscated identifier: identifier used for on-chain attestations, obtained by hashing the plaintext identifier and pepper + */ + +export interface IdentifierHashDetails { + // plaintext off-chain phone number, twitter handle, email, etc. + plaintextIdentifier: string + // identifier obtained after hashing, used for on-chain attestations + obfuscatedIdentifier: string + // unique pepper obtained through ODIS + pepper: string + // raw signature from ODIS + unblindedSignature?: string +} + +/** + * Retrieve the obfuscated identifier for the provided plaintext identifier + * Performs blinding, querying, and unblinding + * + * This function will send a request to ODIS, authorized by the provided signer. + * This method consumes ODIS quota on the account provided by the signer. + * You can use the DEK as your signer to decrease quota usage + */ +export async function getObfuscatedIdentifier( + plaintextIdentifier: string, + identifierPrefix: IdentifierPrefix, + account: string, + signer: AuthSigner, + context: ServiceContext, + blindingFactor?: string, + clientVersion?: string, + blsBlindingClient?: BlsBlindingClient, + sessionID?: string, + keyVersion?: number, + endpoint?: CombinerEndpointPNP.LEGACY_PNP_SIGN | CombinerEndpointPNP.PNP_SIGN +): Promise { + debug('Getting identifier pepper') + + let seed: Buffer | undefined + if (blindingFactor) { + seed = Buffer.from(blindingFactor) + } else if (signer.authenticationMethod === AuthenticationMethod.ENCRYPTION_KEY) { + seed = Buffer.from((signer as EncryptionKeySigner).rawKey) + } + + // Fallback to using Wasm version if not specified + if (!blsBlindingClient) { + debug('No BLSBlindingClient found, using WasmBlsBlindingClient') + blsBlindingClient = new WasmBlsBlindingClient(context.odisPubKey) + } + + const base64BlindedMessage = await getBlindedIdentifier( + plaintextIdentifier, + identifierPrefix, + blsBlindingClient, + seed + ) + + const base64BlindSig = await getBlindedIdentifierSignature( + account, + signer, + context, + base64BlindedMessage, + clientVersion, + sessionID, + keyVersion, + endpoint ?? CombinerEndpointPNP.PNP_SIGN + ) + + return getObfuscatedIdentifierFromSignature( + plaintextIdentifier, + identifierPrefix, + base64BlindSig, + blsBlindingClient + ) +} + +/** + * Blinds the plaintext identifier in preparation for the ODIS request + * Caller should use the same blsBlindingClient instance for unblinding + */ +export async function getBlindedIdentifier( + identifier: string, + identifierPrefix: IdentifierPrefix, + blsBlindingClient: BlsBlindingClient, + seed?: Buffer +): Promise { + debug('Retrieving blinded message') + // phone number identifiers don't have prefixes in the blinding stage + // to maintain backwards compatibility wih ASv1 + const base64Identifier = + identifierPrefix === IdentifierPrefix.PHONE_NUMBER + ? Buffer.from(identifier).toString('base64') + : Buffer.from(getPrefixedIdentifier(identifier, identifierPrefix)).toString('base64') + return blsBlindingClient.blindMessage(base64Identifier, seed) +} + +/** + * Query ODIS for the blinded signature + * Response can be passed into getObfuscatedIdentifierFromSignature + * to retrieve the obfuscated identifier + */ +export async function getBlindedIdentifierSignature( + account: string, + signer: AuthSigner, + context: ServiceContext, + base64BlindedMessage: string, + clientVersion?: string, + sessionID?: string, + keyVersion?: number, + endpoint?: CombinerEndpointPNP.LEGACY_PNP_SIGN | CombinerEndpointPNP.PNP_SIGN +): Promise { + const body: SignMessageRequest = { + account, + blindedQueryPhoneNumber: base64BlindedMessage, + version: clientVersion, + authenticationMethod: signer.authenticationMethod, + sessionID, + } + + const response = await queryOdis( + body, + context, + endpoint ?? CombinerEndpointPNP.PNP_SIGN, + SignMessageResponseSchema, + { + [KEY_VERSION_HEADER]: keyVersion?.toString(), + Authorization: await getOdisPnpRequestAuth(body, signer), + } + ) + + if (!response.success) { + throw new Error(response.error) + } + + return response.signature +} + +/** + * Unblind the response and return the obfuscated identifier + */ +export async function getObfuscatedIdentifierFromSignature( + plaintextIdentifier: string, + identifierType: IdentifierPrefix, + base64BlindedSignature: string, + blsBlindingClient: BlsBlindingClient +): Promise { + debug('Retrieving unblinded signature') + const base64UnblindedSig = await blsBlindingClient.unblindAndVerifyMessage(base64BlindedSignature) + const sigBuf = Buffer.from(base64UnblindedSig, 'base64') + + debug('Converting sig to pepper') + const pepper = getPepperFromThresholdSignature(sigBuf) + const obfuscatedIdentifier = getIdentifierHash(plaintextIdentifier, identifierType, pepper) + return { + plaintextIdentifier, + obfuscatedIdentifier, + pepper, + unblindedSignature: base64UnblindedSig, + } +} + +export const getPrefixedIdentifier = ( + plaintextIdentifier: string, + identifierPrefix: IdentifierPrefix +): string => identifierPrefix + '://' + plaintextIdentifier + +export const getIdentifierHash = ( + plaintextIdentifier: string, + identifierPrefix: IdentifierPrefix, + pepper: string +): string => { + // hashing the identifier before appending the pepper to avoid domain collisions where the + // identifier may contain underscores + // not doing this for phone numbers to maintain backwards compatibility + const value = + identifierPrefix === IdentifierPrefix.PHONE_NUMBER + ? getPrefixedIdentifier(plaintextIdentifier, identifierPrefix) + PEPPER_SEPARATOR + pepper + : (sha3(getPrefixedIdentifier(plaintextIdentifier, identifierPrefix)) as string) + + PEPPER_SEPARATOR + + pepper + return sha3(value) as string +} + +// This is the algorithm that creates a pepper from the unblinded message signatures +// It simply hashes it with sha256 and encodes it to hex +export function getPepperFromThresholdSignature(sigBuf: Buffer) { + // Currently uses 13 chars for a 78 bit pepper + return createHash('sha256').update(sigBuf).digest('base64').slice(0, PEPPER_CHAR_LENGTH) +} diff --git a/packages/sdk/identity/src/odis/phone-number-identifier.test.ts b/packages/sdk/identity/src/odis/phone-number-identifier.test.ts index dfc999ea770..52774707502 100644 --- a/packages/sdk/identity/src/odis/phone-number-identifier.test.ts +++ b/packages/sdk/identity/src/odis/phone-number-identifier.test.ts @@ -3,7 +3,6 @@ import { WasmBlsBlindingClient } from './bls-blinding-client' import { getBlindedPhoneNumber, getBlindedPhoneNumberSignature, - getPepperFromThresholdSignature, getPhoneNumberIdentifier, getPhoneNumberIdentifierFromSignature, isBalanceSufficientForSigRetrieval, @@ -127,11 +126,3 @@ describe(getPhoneNumberIdentifier, () => { ).rejects.toThrow(ErrorMessages.ODIS_AUTH_ERROR) }) }) - -describe(getPepperFromThresholdSignature, () => { - it('Hashes sigs correctly', () => { - const base64Sig = 'vJeFZJ3MY5KlpI9+kIIozKkZSR4cMymLPh2GHZUatWIiiLILyOcTiw2uqK/LBReA' - const signature = Buffer.from(base64Sig, 'base64') - expect(getPepperFromThresholdSignature(signature)).toBe('piWqRHHYWtfg9') - }) -}) diff --git a/packages/sdk/identity/src/odis/phone-number-identifier.ts b/packages/sdk/identity/src/odis/phone-number-identifier.ts index e02638cf13a..9ab7d451339 100644 --- a/packages/sdk/identity/src/odis/phone-number-identifier.ts +++ b/packages/sdk/identity/src/odis/phone-number-identifier.ts @@ -1,23 +1,16 @@ -import { getPhoneHash, isE164Number } from '@celo/base/lib/phoneNumbers' -import { - CombinerEndpointPNP, - KEY_VERSION_HEADER, - SignMessageRequest, - SignMessageResponseSchema, -} from '@celo/phone-number-privacy-common' -import { soliditySha3 } from '@celo/utils/lib/solidity' +import { isE164Number } from '@celo/base/lib/phoneNumbers' +import { CombinerEndpointPNP } from '@celo/phone-number-privacy-common' import BigNumber from 'bignumber.js' -import { createHash } from 'crypto' import debugFactory from 'debug' -import { BlsBlindingClient, WasmBlsBlindingClient } from './bls-blinding-client' +import { BlsBlindingClient } from './bls-blinding-client' import { - AuthenticationMethod, - AuthSigner, - EncryptionKeySigner, - getOdisPnpRequestAuth, - queryOdis, - ServiceContext, -} from './query' + getBlindedIdentifier, + getBlindedIdentifierSignature, + getObfuscatedIdentifier, + getObfuscatedIdentifierFromSignature, + IdentifierPrefix, +} from './identifier' +import { AuthSigner, ServiceContext } from './query' // ODIS minimum dollar balance for sig retrieval export const ODIS_MINIMUM_DOLLAR_BALANCE = 0.01 @@ -25,9 +18,6 @@ export const ODIS_MINIMUM_DOLLAR_BALANCE = 0.01 export const ODIS_MINIMUM_CELO_BALANCE = 0.005 const debug = debugFactory('kit:odis:phone-number-identifier') -const sha3 = (v: string) => soliditySha3({ type: 'string', value: v }) - -const PEPPER_CHAR_LENGTH = 13 export interface PhoneNumberHashDetails { e164Number: string @@ -39,6 +29,7 @@ export interface PhoneNumberHashDetails { /** * Retrieve the on-chain identifier for the provided phone number * Performs blinding, querying, and unblinding + * @deprecated use getObfuscatedIdentifier instead */ export async function getPhoneNumberIdentifier( e164Number: string, @@ -58,54 +49,50 @@ export async function getPhoneNumberIdentifier( throw new Error(`Invalid phone number: ${e164Number}`) } - let seed: Buffer | undefined - if (blindingFactor) { - seed = Buffer.from(blindingFactor) - } else if (signer.authenticationMethod === AuthenticationMethod.ENCRYPTION_KEY) { - seed = Buffer.from((signer as EncryptionKeySigner).rawKey) - } - - // Fallback to using Wasm version if not specified - - if (!blsBlindingClient) { - debug('No BLSBlindingClient found, using WasmBlsBlindingClient') - blsBlindingClient = new WasmBlsBlindingClient(context.odisPubKey) - } - - const base64BlindedMessage = await getBlindedPhoneNumber(e164Number, blsBlindingClient, seed) - - const base64BlindSig = await getBlindedPhoneNumberSignature( + const { + plaintextIdentifier, + obfuscatedIdentifier, + pepper, + unblindedSignature, + } = await getObfuscatedIdentifier( + e164Number, + IdentifierPrefix.PHONE_NUMBER, account, signer, context, - base64BlindedMessage, + blindingFactor, clientVersion, + blsBlindingClient, sessionID, keyVersion, - endpoint ?? CombinerEndpointPNP.PNP_SIGN + endpoint ) - - return getPhoneNumberIdentifierFromSignature(e164Number, base64BlindSig, blsBlindingClient) + return { + e164Number: plaintextIdentifier, + phoneHash: obfuscatedIdentifier, + pepper, + unblindedSignature, + } } /** * Blinds the phone number in preparation for the ODIS request * Caller should use the same blsBlindingClient instance for unblinding + * @deprecated use getBlindedIdentifier instead */ export async function getBlindedPhoneNumber( e164Number: string, blsBlindingClient: BlsBlindingClient, seed?: Buffer ): Promise { - debug('Retrieving blinded message') - const base64PhoneNumber = Buffer.from(e164Number).toString('base64') - return blsBlindingClient.blindMessage(base64PhoneNumber, seed) + return getBlindedIdentifier(e164Number, IdentifierPrefix.PHONE_NUMBER, blsBlindingClient, seed) } /** * Query ODIS for the blinded signature * Response can be passed into getPhoneNumberIdentifierFromSignature * to retrieve the on-chain identifier + * @deprecated use getBlindedIdentifierSignature instead */ export async function getBlindedPhoneNumberSignature( account: string, @@ -117,59 +104,49 @@ export async function getBlindedPhoneNumberSignature( keyVersion?: number, endpoint?: CombinerEndpointPNP.LEGACY_PNP_SIGN | CombinerEndpointPNP.PNP_SIGN ): Promise { - const body: SignMessageRequest = { + return getBlindedIdentifierSignature( account, - blindedQueryPhoneNumber: base64BlindedMessage, - version: clientVersion, - authenticationMethod: signer.authenticationMethod, - sessionID, - } - - const response = await queryOdis( - body, + signer, context, - endpoint ?? CombinerEndpointPNP.PNP_SIGN, - SignMessageResponseSchema, - { - [KEY_VERSION_HEADER]: keyVersion?.toString(), - Authorization: await getOdisPnpRequestAuth(body, signer), - } + base64BlindedMessage, + clientVersion, + sessionID, + keyVersion, + endpoint ) - - if (!response.success) { - throw new Error(response.error) - } - - return response.signature } /** * Unblind the response and return the on-chain identifier + * @deprecated use getObfuscatedIdentifieriFromSignature instead */ export async function getPhoneNumberIdentifierFromSignature( e164Number: string, base64BlindedSignature: string, blsBlindingClient: BlsBlindingClient ): Promise { - debug('Retrieving unblinded signature') - const base64UnblindedSig = await blsBlindingClient.unblindAndVerifyMessage(base64BlindedSignature) - const sigBuf = Buffer.from(base64UnblindedSig, 'base64') - - debug('Converting sig to pepper') - const pepper = getPepperFromThresholdSignature(sigBuf) - const phoneHash = getPhoneHash(sha3, e164Number, pepper) - return { e164Number, phoneHash, pepper, unblindedSignature: base64UnblindedSig } -} - -// This is the algorithm that creates a pepper from the unblinded message signatures -// It simply hashes it with sha256 and encodes it to hex -export function getPepperFromThresholdSignature(sigBuf: Buffer) { - // Currently uses 13 chars for a 78 bit pepper - return createHash('sha256').update(sigBuf).digest('base64').slice(0, PEPPER_CHAR_LENGTH) + const { + plaintextIdentifier, + obfuscatedIdentifier, + pepper, + unblindedSignature, + } = await getObfuscatedIdentifierFromSignature( + e164Number, + IdentifierPrefix.PHONE_NUMBER, + base64BlindedSignature, + blsBlindingClient + ) + return { + e164Number: plaintextIdentifier, + phoneHash: obfuscatedIdentifier, + pepper, + unblindedSignature, + } } /** * Check if balance is sufficient for quota retrieval + * @deprecated use getPnpQuotaStatus instead */ export function isBalanceSufficientForSigRetrieval( dollarBalance: BigNumber.Value, diff --git a/yarn.lock b/yarn.lock index 26f2bbad288..d4f96dbe1f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1329,6 +1329,11 @@ resolved "https://registry.yarnpkg.com/@celo/base/-/base-1.5.2.tgz#168ab5e4e30b374079d8d139fafc52ca6bfd4100" integrity sha512-KGf6Dl9E6D01vAfkgkjL2sG+zqAjspAogILIpWstljWdG5ifyA75jihrnDEHaMCoQS0KxHvTdP1XYS/GS6BEyQ== +"@celo/base@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/base/-/base-3.0.0.tgz#698b2c3da99f3f76e1b5c1936bfa4fa37d37bb7c" + integrity sha512-SBVaruEw4QLLGB8a0+J79O6GuG4gEtXmw9jV+2tqMO1Dc7v6vu0TwochgIA7QPGzFo6Z/FTXcf/dBnJgSRSIJA== + "@celo/bls12377js@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@celo/bls12377js/-/bls12377js-0.1.1.tgz#ba3574f41697cdba96c10ae96bb1aac057285798" @@ -1349,7 +1354,20 @@ debug "^4.1.1" utf8 "3.0.0" -"@celo/contractkit@^1.2.0": +"@celo/connect@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/connect/-/connect-3.0.0.tgz#575ae86315ae31356fe0a01ce586edd8a7f52ece" + integrity sha512-WpB7iDPos61IpmfV2QURaMQgNBAtAjR5jhfxJM7t70VAZyjbYhIiac3yNfXyTPuX0oFAQ+3pIjJmiKScQm1wvQ== + dependencies: + "@celo/base" "3.0.0" + "@celo/utils" "3.0.0" + "@types/debug" "^4.1.5" + "@types/utf8" "^2.1.6" + bignumber.js "^9.0.0" + debug "^4.1.1" + utf8 "3.0.0" + +"@celo/contractkit@1.5.2", "@celo/contractkit@^1.2.0": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-1.5.2.tgz#be15d570f3044a190dabb6bbe53d5081c78ea605" integrity sha512-b0r5TlfYDEscxze1Ai2jyJayiVElA9jvEehMD6aOSNtVhfP8oirjFIIffRe0Wzw1MSDGkw+q1c4m0Yw5sEOlvA== @@ -1367,6 +1385,25 @@ semver "^7.3.5" web3 "1.3.6" +"@celo/contractkit@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-3.0.0.tgz#9eda45ff9f424b1f0601f725a4c4ce4d7cd64785" + integrity sha512-I5Y66C/jIzOipPu+db1awfEdSPmTII5q3DqaojoO8AFytYOxe1B1yMDMGnhKwvLqRMDYiGw4cdNXNncFeZLhtA== + dependencies: + "@celo/base" "3.0.0" + "@celo/connect" "3.0.0" + "@celo/utils" "3.0.0" + "@celo/wallet-local" "3.0.0" + "@types/bn.js" "^5.1.0" + "@types/debug" "^4.1.5" + bignumber.js "^9.0.0" + cross-fetch "^3.0.6" + debug "^4.1.1" + fp-ts "2.1.1" + io-ts "2.0.1" + semver "^7.3.5" + web3 "1.3.6" + "@celo/ganache-cli@git+https://github.com/celo-org/ganache-cli.git#21652da": version "6.6.1" resolved "git+https://github.com/celo-org/ganache-cli.git#21652da953de1a7217c0cb86b596e94d22c2cc76" @@ -1434,6 +1471,76 @@ fp-ts "2.1.1" io-ts "2.0.1" +"@celo/identity@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/identity/-/identity-3.0.0.tgz#ba32e8ffb03dba72f435572b8c617bff11175f15" + integrity sha512-Mmxi4eH+iW+kaWFAr0BAdAszCLEkC3yM9fOAq12S+N0ry8N146f/UihJm3KvaIF+RyWeDepXQrT2EVncLSW0Mw== + dependencies: + "@celo/base" "3.0.0" + "@celo/contractkit" "3.0.0" + "@celo/phone-number-privacy-common" "2.0.0" + "@celo/utils" "3.0.0" + "@types/debug" "^4.1.5" + bignumber.js "^9.0.0" + blind-threshold-bls "https://github.com/celo-org/blind-threshold-bls-wasm#e1e2f8a" + cross-fetch "3.0.4" + debug "^4.1.1" + elliptic "^6.5.4" + fp-ts "2.1.1" + io-ts "2.0.1" + +"@celo/phone-number-privacy-common@1.0.39": + version "1.0.39" + resolved "https://registry.yarnpkg.com/@celo/phone-number-privacy-common/-/phone-number-privacy-common-1.0.39.tgz#3c9568f70378d24d11afcc4306024c5cf4f8efe9" + integrity sha512-0sbeuoYCN2ZQYO1CryR0Hf9HhOQKuIDZraWFMpUlwrUKk5qKmSMlV16xobG4VL5qUpXHgIRjKPfmcaf0rkrn8A== + dependencies: + "@celo/base" "1.5.2" + "@celo/contractkit" "1.5.2" + "@celo/utils" "1.5.2" + bignumber.js "^9.0.0" + blind-threshold-bls "https://github.com/celo-org/blind-threshold-bls-wasm#e1e2f8a" + btoa "1.2.1" + bunyan "1.8.12" + bunyan-debug-stream "2.0.0" + bunyan-gke-stackdriver "0.1.2" + dotenv "^8.2.0" + elliptic "^6.5.4" + is-base64 "^1.1.0" + +"@celo/phone-number-privacy-common@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@celo/phone-number-privacy-common/-/phone-number-privacy-common-2.0.0.tgz#bf932f3ba3f891c14bf3f587b0d85fae1f24f88d" + integrity sha512-lTKNn+XPiQ6Zl0VSCzRSY8gwTxuCwDv/ufcoh39Uj20JePAaQdlE4JU2DGAaQ3Nlo0FYRtxMxgvWHRxRob2dww== + dependencies: + "@celo/base" "3.0.0" + "@celo/contractkit" "3.0.0" + "@celo/phone-utils" "3.0.0" + "@celo/utils" "3.0.0" + bignumber.js "^9.0.0" + bunyan "1.8.12" + bunyan-debug-stream "2.0.0" + bunyan-gke-stackdriver "0.1.2" + dotenv "^8.2.0" + elliptic "^6.5.4" + io-ts "2.0.1" + is-base64 "^1.1.0" + +"@celo/phone-utils@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/phone-utils/-/phone-utils-3.0.0.tgz#73f87efc81ec3b5c92b3638102703c437d7bc128" + integrity sha512-/3Y65Nq1hSADwZV+7z2bJlny5TiOW8JWYXS0oTYT8HZDEhtEdHtet/oKGmyLNXuCKgmUQQKKr1MGfb5ZTp9vjA== + dependencies: + "@celo/base" "3.0.0" + "@celo/utils" "3.0.0" + "@types/country-data" "^0.0.0" + "@types/ethereumjs-util" "^5.2.0" + "@types/google-libphonenumber" "^7.4.23" + "@types/node" "^10.12.18" + country-data "^0.0.31" + fp-ts "2.1.1" + google-libphonenumber "^3.2.27" + io-ts "2.0.1" + "@celo/poprf@^0.1.9": version "0.1.9" resolved "https://registry.yarnpkg.com/@celo/poprf/-/poprf-0.1.9.tgz#38c514ce0f572b80edeb9dc280b6cf5e9d7c2a75" @@ -1513,6 +1620,23 @@ web3-eth-abi "1.3.6" web3-utils "1.3.6" +"@celo/utils@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/utils/-/utils-3.0.0.tgz#c076738fa24c59936265585187be21bddf9c786c" + integrity sha512-bRe37kzhEBT7svmeDOHIVN9rIiOJiwgu5B+l6noxMr4rcBCoYNnRjxzdfuzAwgHt0mrY5qIUPgO85FobCZBEIQ== + dependencies: + "@celo/base" "3.0.0" + "@types/bn.js" "^5.1.0" + "@types/elliptic" "^6.4.9" + "@types/ethereumjs-util" "^5.2.0" + "@types/node" "^10.12.18" + bignumber.js "^9.0.0" + elliptic "^6.5.4" + ethereumjs-util "^5.2.0" + io-ts "2.0.1" + web3-eth-abi "1.3.6" + web3-utils "1.3.6" + "@celo/wallet-base@1.5.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/wallet-base/-/wallet-base-1.5.2.tgz#ae8df425bf3c702277bb1b63a761a2ec8429e7aa" @@ -1528,6 +1652,21 @@ eth-lib "^0.2.8" ethereumjs-util "^5.2.0" +"@celo/wallet-base@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/wallet-base/-/wallet-base-3.0.0.tgz#1d4f603ee9ab5f89181622fa759234f1c5975cb5" + integrity sha512-vwuMcR3ZL8qj/lofVuAgjA9Gt3gM5/YB5bCvIWEnNLUr7gf9syC6u0PF6azd6WshfCaNsYAtYkdFZdKfQPYCjA== + dependencies: + "@celo/base" "3.0.0" + "@celo/connect" "3.0.0" + "@celo/utils" "3.0.0" + "@types/debug" "^4.1.5" + "@types/ethereumjs-util" "^5.2.0" + bignumber.js "^9.0.0" + debug "^4.1.1" + eth-lib "^0.2.8" + ethereumjs-util "^5.2.0" + "@celo/wallet-local@1.5.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/wallet-local/-/wallet-local-1.5.2.tgz#66ea5fb763e19724309e3d56f312f1a342e12b91" @@ -1540,6 +1679,18 @@ eth-lib "^0.2.8" ethereumjs-util "^5.2.0" +"@celo/wallet-local@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@celo/wallet-local/-/wallet-local-3.0.0.tgz#f6c9065b3462876799a786a5b562923c81a72787" + integrity sha512-l7k47bFkFb86MV8THt9PnIu3YQVrsDsZ552jKCi0KiqpV2EoB8VC2iOLlRpef+bX81GYVfxPfF1wiz4j1XletQ== + dependencies: + "@celo/connect" "3.0.0" + "@celo/utils" "3.0.0" + "@celo/wallet-base" "3.0.0" + "@types/ethereumjs-util" "^5.2.0" + eth-lib "^0.2.8" + ethereumjs-util "^5.2.0" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -7859,7 +8010,7 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -btoa@^1.2.1: +btoa@1.2.1, btoa@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== @@ -20132,6 +20283,24 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +"old-identity-sdk@npm:@celo/identity@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@celo/identity/-/identity-1.5.2.tgz#6401aeefbf7893374f6f940c9e0918d0a75d0411" + integrity sha512-/9JTL5P4xTY37hgu6qh5tU1d2GS3duBjP3QL600Zz1KAQrUVgb8g8JPpiRY21oEK6L7ZoNTukQJIuM3sbi//vg== + dependencies: + "@celo/base" "1.5.2" + "@celo/contractkit" "1.5.2" + "@celo/phone-number-privacy-common" "1.0.39" + "@celo/utils" "1.5.2" + "@types/debug" "^4.1.5" + bignumber.js "^9.0.0" + blind-threshold-bls "https://github.com/celo-org/blind-threshold-bls-wasm#e1e2f8a" + cross-fetch "3.0.4" + debug "^4.1.1" + elliptic "^6.5.4" + fp-ts "2.1.1" + io-ts "2.0.1" + omni-fetch@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/omni-fetch/-/omni-fetch-0.2.3.tgz#56a6f46ad170b6d978982cca5bd1c6b70597a58d"