Skip to content

Commit

Permalink
Merge branch 'tim/2fa' into 'master'
Browse files Browse the repository at this point in the history
2FA

See merge request TankerHQ/sdk-js!602
  • Loading branch information
Jeremy T committed Mar 30, 2021
2 parents 814ce3c + 38810e0 commit 167b12d
Show file tree
Hide file tree
Showing 16 changed files with 500 additions and 38 deletions.
24 changes: 24 additions & 0 deletions packages/benchmarks/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let appId;
before(async () => {
appHelper = await AppHelper.newApp();
appId = utils.toBase64(appHelper.appId);
await appHelper.set2FA();
});

after(async () => {
Expand Down Expand Up @@ -151,6 +152,29 @@ benchmark('verifyIdentity_passphrase', async (state) => {
}
});

// What: starts and verifies an identity with a passphrase and asks for a session token
// PreCond: an identity was registered with another device
// PostCond: the session is open and we have a session token
benchmark('verifyIdentity_passphrase_withToken', async (state) => {
const tanker = makeTanker();
const identity = await appHelper.generateIdentity();
await tanker.start(identity);
await tanker.registerIdentity({ passphrase: 'passphrase' });
await tanker.stop();

while (state.iter()) {
state.pause();
const tanker2 = makeTanker();
state.unpause();
await tanker2.start(identity);
const token = await tanker2.verifyIdentity({ passphrase: 'passphrase' }, { withSessionToken: true });
if (!token)
throw new Error("no session token received, this benchmark isn't benchmarking what we thought it would");
state.pause();
await tanker2.stop();
}
});

// What: encrypts data
// PreCond: a session is open
// PostCond: the buffer is encrypted
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/Blocks/Nature.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const NATURE = Object.freeze({
user_group_addition_v2: 16,
user_group_creation_v3: 17,
user_group_addition_v3: 18,
session_certificate: 19,
});

const NATURE_INT = Object.values(NATURE);
Expand All @@ -40,6 +41,7 @@ export const NATURE_KIND = Object.freeze({
user_group_addition: 7,
key_publish_to_provisional_user: 8,
provisional_identity_claim: 9,
session_certificate: 10,
});

export type NatureKind = $Values<typeof NATURE_KIND>;
Expand All @@ -56,6 +58,7 @@ export function preferredNature(kind: NatureKind): Nature {
case NATURE_KIND.user_group_creation: return NATURE.user_group_creation_v3;
case NATURE_KIND.user_group_addition: return NATURE.user_group_addition_v3;
case NATURE_KIND.provisional_identity_claim: return NATURE.provisional_identity_claim;
case NATURE_KIND.session_certificate: return NATURE.session_certificate;
default: throw new InternalError(`invalid kind: ${kind}`);
}
}
Expand All @@ -79,6 +82,7 @@ export function natureKind(val: Nature): NatureKind {
case NATURE.user_group_addition_v2: return NATURE_KIND.user_group_addition;
case NATURE.user_group_addition_v3: return NATURE_KIND.user_group_addition;
case NATURE.provisional_identity_claim: return NATURE_KIND.provisional_identity_claim;
case NATURE.session_certificate: return NATURE_KIND.session_certificate;
default: throw new InternalError(`invalid nature: ${val}`);
}
}
6 changes: 6 additions & 0 deletions packages/core/src/Blocks/Serialize.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import varint from 'varint';
import { InternalError } from '@tanker/errors';
import { utils } from '@tanker/crypto';

type Unserializer = (src: Uint8Array, offset: number) => { newOffset: number, [name: string]: any };

Expand All @@ -13,6 +14,11 @@ export function getArray(src: Uint8Array, offset: number, name: string = 'value'
return { [name]: buffer, newOffset: pos };
}

export function getString(src: Uint8Array, offset: number, name: string = 'value'): Object {
const result = getArray(src, offset, name);
return { [name]: utils.toString(result[name]), newOffset: result.newOffset };
}

export function setStaticArray(src: Uint8Array, dest: Uint8Array, offset: number = 0): number {
dest.set(src, offset);
return offset + src.length;
Expand Down
41 changes: 32 additions & 9 deletions packages/core/src/LocalUser/Manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import type { ProvisionalUserKeyPairs, IndexedProvisionalUserKeyPairs } from './
import type KeyStore from './KeyStore';
import LocalUser from './LocalUser';
import { formatVerificationRequest } from './requests';
import type { Verification, VerificationMethod, RemoteVerification } from './types';
import type {
VerificationMethod,
VerificationWithToken,
RemoteVerificationWithToken
} from './types';
import { generateUserCreation, generateDeviceFromGhostDevice, makeDeviceRevocation } from './UserCreation';
import type { UserData, DelegationToken } from './UserData';

import type { Client, PullOptions } from '../Network/Client';
import { statuses, type Status } from '../Session/status';
import type { Device } from '../Users/types';
import { makeSessionCertificate } from './SessionCertificate';

export type PrivateProvisionalKeys = {|
appEncryptionKeyPair: tcrypto.SodiumKeyPair,
Expand Down Expand Up @@ -85,9 +90,13 @@ export class LocalUserManager extends EventEmitter {
});
}

setVerificationMethod = (verification: RemoteVerification): Promise<void> => this._client.setVerificationMethod({
verification: formatVerificationRequest(verification, this._localUser),
});
setVerificationMethod = (verification: RemoteVerificationWithToken): Promise<void> => {
const requestVerification = formatVerificationRequest(verification, this._localUser);
requestVerification.with_token = verification.withToken; // May be undefined
return this._client.setVerificationMethod({
verification: requestVerification,
});
}

updateDeviceInfo = async (id: Uint8Array, encryptionKeyPair: tcrypto.SodiumKeyPair, signatureKeyPair: tcrypto.SodiumKeyPair): Promise<void> => {
this._localUser.deviceId = id;
Expand All @@ -97,7 +106,7 @@ export class LocalUserManager extends EventEmitter {
await this.updateLocalUser({ isLight: true });
}

createUser = async (verification: Verification): Promise<void> => {
createUser = async (verification: VerificationWithToken): Promise<void> => {
let ghostDeviceKeys;
if (verification.verificationKey) {
try {
Expand All @@ -124,15 +133,16 @@ export class LocalUserManager extends EventEmitter {
if (verification.email || verification.passphrase || verification.oidcIdToken) {
request.v2_encrypted_verification_key = ghostDeviceToEncryptedVerificationKey(ghostDevice, this._localUser.userSecret);
request.verification = formatVerificationRequest(verification, this._localUser);
request.verification.with_token = verification.withToken; // May be undefined
}

await this._client.createUser(firstDeviceId, firstDeviceSignatureKeyPair, request);
await this.updateDeviceInfo(firstDeviceId, firstDeviceEncryptionKeyPair, firstDeviceSignatureKeyPair);
}

createNewDevice = async (verification: Verification): Promise<void> => {
createNewDevice = async (verification: VerificationWithToken): Promise<void> => {
try {
const verificationKey = await this._getVerificationKey(verification);
const verificationKey = await this.getVerificationKey(verification);
const ghostDevice = extractGhostDevice(verificationKey);

const ghostSignatureKeyPair = tcrypto.getSignatureKeyPairFromPrivateKey(ghostDevice.privateSignatureKey);
Expand Down Expand Up @@ -172,6 +182,18 @@ export class LocalUserManager extends EventEmitter {
return devices.filter(d => !d.isGhostDevice);
}

getSessionToken = async (verification: VerificationWithToken): Promise<string> => {
await this.updateLocalUser();

const { payload, nature } = makeSessionCertificate(verification);
const block = this._localUser.makeBlock(payload, nature);

if (!verification.withToken)
throw new InternalError('Assertion error: Cannot get a session certificate without withToken');

return this._client.getSessionToken({ session_certificate: block, nonce: verification.withToken.nonce });
}

findUserKey = async (publicKey: Uint8Array): Promise<tcrypto.SodiumKeyPair> => {
const userKey = this._localUser.findUserKey(publicKey);
if (!userKey) {
Expand Down Expand Up @@ -228,12 +250,13 @@ export class LocalUserManager extends EventEmitter {
});
}

_getVerificationKey = async (verification: Verification) => {
getVerificationKey = async (verification: VerificationWithToken) => {
if (verification.verificationKey) {
return verification.verificationKey;
}
const remoteVerification: RemoteVerification = (verification: any);
const remoteVerification: RemoteVerificationWithToken = (verification: any);
const request = { verification: formatVerificationRequest(remoteVerification, this._localUser) };
request.verification.with_token = verification.withToken; // May be undefined
const encryptedVerificationKey = await this._client.getVerificationKey(request);
return decryptVerificationKey(encryptedVerificationKey, this._localUser.userSecret);
}
Expand Down
98 changes: 98 additions & 0 deletions packages/core/src/LocalUser/SessionCertificate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @flow
import { InternalError, InvalidArgument } from '@tanker/errors';
import { generichash, utils, tcrypto, number } from '@tanker/crypto';
import varint from 'varint';
import type {
VerificationMethod, VerificationWithToken,
} from './types';
import { getStaticArray, unserializeGeneric } from '../Blocks/Serialize';
import { NATURE_KIND, preferredNature } from '../Blocks/Nature';

export const VERIFICATION_METHOD_TYPES = Object.freeze({
email: 1,
passphrase: 2,
verificationKey: 3,
oidcIdToken: 4,
});
const VERIFICATION_METHOD_TYPES_INT = Object.values(VERIFICATION_METHOD_TYPES);
export type VerificationMethodType = $Values<typeof VERIFICATION_METHOD_TYPES>;

export type SessionCertificateRecord = {|
timestamp: number,
verification_method_type: VerificationMethodType,
verification_method_target: Uint8Array,
// If you're wondering, this one is currently unused (future compat)
session_public_signature_key: Uint8Array,
|};

function verificationToVerificationMethod(verification: VerificationWithToken): VerificationMethod {
if ('email' in verification)
return {
type: 'email',
// $FlowIgnore[prop-missing]
email: verification.email
};
if ('passphrase' in verification)
return { type: 'passphrase' };
if ('verificationKey' in verification)
return { type: 'verificationKey' };
if ('oidcIdToken' in verification)
return { type: 'oidcIdToken' };
throw new InvalidArgument('verification', 'unknown verification method used in verification', verification);
}

export const serializeSessionCertificate = (sessionCertificate: SessionCertificateRecord): Uint8Array => {
if (!(sessionCertificate.verification_method_type in VERIFICATION_METHOD_TYPES_INT))
throw new InternalError('Assertion error: invalid session certificate method type');
if (sessionCertificate.verification_method_target.length !== tcrypto.HASH_SIZE)
throw new InternalError('Assertion error: invalid session certificate method target size');
if (sessionCertificate.session_public_signature_key.length !== tcrypto.SIGNATURE_PUBLIC_KEY_SIZE)
throw new InternalError('Assertion error: invalid session public signature key size');

return utils.concatArrays(
sessionCertificate.session_public_signature_key,
number.toUint64le(sessionCertificate.timestamp),
new Uint8Array(varint.encode(sessionCertificate.verification_method_type)),
sessionCertificate.verification_method_target,
);
};

export const unserializeSessionCertificate = (payload: Uint8Array): SessionCertificateRecord => unserializeGeneric(payload, [
(d, o) => getStaticArray(d, tcrypto.SIGNATURE_PUBLIC_KEY_SIZE, o, 'session_public_signature_key'),
(d, o) => ({
timestamp: number.fromUint64le(d.subarray(o, o + 8)),
newOffset: o + 8
}),
(d, o) => ({
verification_method_type: varint.decode(d, o),
newOffset: o + varint.decode.bytes
}),
(d, o) => getStaticArray(d, tcrypto.HASH_SIZE, o, 'verification_method_target'),
]);

export const makeSessionCertificate = (
verification: VerificationWithToken
) => {
const verifMethod = verificationToVerificationMethod(verification);
let verifTarget;
if (verifMethod.type === 'email') {
verifTarget = generichash(utils.fromString(verifMethod.email));
} else {
verifTarget = new Uint8Array(tcrypto.HASH_SIZE);
}

// Note: We don't currently _do_ anything with this one, but we added it to the block format for future compat...
const signatureKeyPair = tcrypto.makeSignKeyPair();

const payload = serializeSessionCertificate({
timestamp: Math.floor(Date.now() / 1000),
verification_method_type: VERIFICATION_METHOD_TYPES[verifMethod.type],
verification_method_target: verifTarget,
session_public_signature_key: signatureKeyPair.publicKey,
});

return {
payload,
nature: preferredNature(NATURE_KIND.session_certificate)
};
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// @flow

import { tcrypto, random } from '@tanker/crypto';
import { tcrypto, random, generichash, utils } from '@tanker/crypto';
import { expect } from '@tanker/test-utils';

import { serializeTrustchainCreation, unserializeTrustchainCreation } from '../Serialize';
import {
serializeSessionCertificate,
unserializeSessionCertificate,
VERIFICATION_METHOD_TYPES
} from '../SessionCertificate';
import type { SessionCertificateRecord } from '../SessionCertificate';

// NOTE: If you ever have to change something here, change it in the Go code too!
// The test vectors should stay the same
Expand Down Expand Up @@ -38,3 +44,28 @@ describe('TrustchainCreation', () => {
expect(unserializeTrustchainCreation(serializeTrustchainCreation(trustchainCreation))).to.deep.equal(trustchainCreation);
});
});

describe('SessionCertificate', () => {
it('should throw when serializing an invalid SessionCertificate', async () => {
const badSessionCertificate = {
timestamp: Math.floor(Date.now() / 1000),
verification_method_type: 999,
verification_method_target: new Uint8Array(0),
session_public_signature_key: new Uint8Array(0),
};
// Flow noticed that our method type is invalid =)
const sessionCertificate = ((badSessionCertificate: any): SessionCertificateRecord);
expect(() => serializeSessionCertificate(sessionCertificate)).to.throw();
});

it('should serialize/unserialize a SessionCertificate', async () => {
const sessionCertificate = {
timestamp: Math.floor(Date.now() / 1000),
verification_method_type: VERIFICATION_METHOD_TYPES.email,
verification_method_target: generichash(utils.fromString('[email protected]')),
session_public_signature_key: tcrypto.makeSignKeyPair().publicKey,
};

expect(unserializeSessionCertificate(serializeSessionCertificate(sessionCertificate))).to.deep.equal(sessionCertificate);
});
});
7 changes: 5 additions & 2 deletions packages/core/src/LocalUser/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import { encryptionV2, generichash, utils } from '@tanker/crypto';
import { InternalError } from '@tanker/errors';

import type LocalUser from './LocalUser';
import type { RemoteVerification } from './types';
import type { RemoteVerification, RemoteVerificationWithToken } from './types';

type VerificationRequest = $Exact<{
hashed_passphrase: Uint8Array,
with_token?: {| nonce: string |}
}> | $Exact<{
hashed_email: Uint8Array,
v2_encrypted_email: Uint8Array,
verification_code: string,
with_token?: {| nonce: string |}
}> | $Exact<{
oidc_id_token: string,
with_token?: {| nonce: string |}
}>;

export const formatVerificationRequest = (verification: RemoteVerification, localUser: LocalUser): VerificationRequest => {
export const formatVerificationRequest = (verification: RemoteVerification | RemoteVerificationWithToken, localUser: LocalUser): VerificationRequest => {
if (verification.email) {
return {
hashed_email: generichash(utils.fromString(verification.email)),
Expand Down
Loading

0 comments on commit 167b12d

Please sign in to comment.