Skip to content

Commit

Permalink
Merge pull request #2 from caru-ini/feat/sign-in
Browse files Browse the repository at this point in the history
ログイン機能の実装
  • Loading branch information
solufa authored Jun 18, 2024
2 parents 4d23e55 + ab17fd7 commit 6799e0e
Show file tree
Hide file tree
Showing 16 changed files with 365 additions and 51 deletions.
8 changes: 8 additions & 0 deletions server/api/@types/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { EntityId } from './brandedId';

export type ChallengeVal = {
secretBlock: string;
pubA: string;
pubB: string;
secB: string;
};

export type UserEntity = {
id: EntityId['user'];
name: string;
Expand All @@ -11,4 +18,5 @@ export type UserEntity = {
refreshToken: string;
userPoolId: EntityId['userPool'];
createdTime: number;
challenge?: ChallengeVal;
};
75 changes: 73 additions & 2 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import type { Jwks, UserSrpAuthTarget } from 'api/@types/auth';
import type { EntityId } from 'api/@types/brandedId';
import type { UserEntity } from 'api/@types/user';
import type { ChallengeVal, UserEntity } from 'api/@types/user';
import type { UserPoolClientEntity, UserPoolEntity } from 'api/@types/userPool';
import assert from 'assert';
import crypto from 'crypto';
import { genConfirmationCode } from 'domain/user/service/genConfirmationCode';
import { genTokens } from 'domain/user/service/genTokens';
import {
calculateScramblingParameter,
calculateSessionKey,
} from 'domain/user/service/srp/calcSessionKey';
import { calculateSignature } from 'domain/user/service/srp/calcSignature';
import { calculateSrpB } from 'domain/user/service/srp/calcSrpB';
import { getPoolName } from 'domain/user/service/srp/util';
import { brandedId } from 'service/brandedId';
import { ulid } from 'ulid';
import { genConfirmationCode } from '../service/genConfirmationCode';

export const userMethod = {
createUser: (val: {
Expand All @@ -29,4 +40,64 @@ export const userMethod = {

return { ...user, verified: true };
},
createChallenge: (
user: UserEntity,
params: UserSrpAuthTarget['reqBody']['AuthParameters'],
): {
userWithChallenge: UserEntity;
ChallengeParameters: UserSrpAuthTarget['resBody']['ChallengeParameters'];
} => {
const { B, b } = calculateSrpB(user.verifier);
const secretBlock = crypto.randomBytes(64).toString('base64');

const challenge: ChallengeVal = {
pubB: B,
secB: b,
pubA: params.SRP_A,
secretBlock,
};
return {
userWithChallenge: { ...user, challenge },
ChallengeParameters: {
SALT: user.salt,
SECRET_BLOCK: secretBlock,
SRP_B: B,
USERNAME: user.name,
USER_ID_FOR_SRP: user.name,
},
};
},
srpAuth: (params: {
user: UserEntity;
timestamp: string;
clientSignature: string;
jwks: Jwks;
pool: UserPoolEntity;
poolClient: UserPoolClientEntity;
}): {
AccessToken: string;
IdToken: string;
} => {
assert(params.user.challenge);
const { pubA: A, pubB: B, secB: b } = params.user.challenge;
const poolname = getPoolName(params.user.userPoolId);
const scramblingParameter = calculateScramblingParameter(A, B);
const sessionKey = calculateSessionKey({ A, B, b, v: params.user.verifier });
const signature = calculateSignature({
poolname,
username: params.user.name,
secretBlock: params.user.challenge.secretBlock,
timestamp: params.timestamp,
scramblingParameter,
key: sessionKey,
});
assert(signature === params.clientSignature);

return genTokens({
privateKey: params.pool.privateKey,
userPoolClientId: params.poolClient.id,
jwks: params.jwks,
user: params.user,
});
},
};
9 changes: 9 additions & 0 deletions server/domain/user/repository/toUserEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export const toUserEntity = (prismaUser: User): UserEntity => {
refreshToken: prismaUser.refreshToken,
verified: prismaUser.verified,
confirmationCode: prismaUser.confirmationCode,
challenge:
prismaUser.secretBlock && prismaUser.pubA && prismaUser.pubB && prismaUser.secB
? {
secretBlock: prismaUser.secretBlock,
pubA: prismaUser.pubA,
pubB: prismaUser.pubB,
secB: prismaUser.secB,
}
: undefined,
userPoolId: brandedId.userPool.entity.parse(prismaUser.userPoolId),
createdTime: prismaUser.createdAt.getTime(),
};
Expand Down
4 changes: 4 additions & 0 deletions server/domain/user/repository/userCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const userCommand = {
verified: user.verified,
refreshToken: user.refreshToken,
confirmationCode: user.confirmationCode,
secretBlock: user.challenge?.secretBlock,
pubA: user.challenge?.pubA,
pubB: user.challenge?.pubB,
secB: user.challenge?.secB,
},
create: {
id: user.id,
Expand Down
26 changes: 17 additions & 9 deletions server/domain/user/service/genCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import type { EntityId } from 'api/@types/brandedId';
import assert from 'assert';
import crypto from 'crypto';
import { g, N } from './srp/constants';
import { calculatePrivateKey, toBuffer } from './srp/util';
import { g, N, Nbytes } from './srp/constants';
import { calculatePrivateKey, getPoolName, toBufferWithLength } from './srp/util';

export const genVerifier = (params: {
poolId: EntityId['userPool'];
username: string;
password: string;
salt: string;
}): string => {
// extract pool name from poolId (poolId format: userPoolId_poolName)
const poolName = getPoolName(params.poolId);
const privateKey = calculatePrivateKey(poolName, params.username, params.password, params.salt);
// verifier = g ^ privateKey % N
const verifier = toBufferWithLength(g.modPow(privateKey, N), Nbytes).toString('hex');
return verifier;
};

export const genCredentials = (params: {
poolId: EntityId['userPool'];
username: string;
password: string;
}): { salt: string; verifier: string } => {
const salt = crypto.randomBytes(16).toString('hex');
// extract pool name from poolId (poolId format: userPoolId_poolName)
const poolName = params.poolId.split('_')[1];
assert(poolName, 'Invalid poolId');
const privateKey = calculatePrivateKey(poolName, params.username, params.password, salt);
// verifier = g^privateKey % N
const verifier = toBuffer(g.modPow(privateKey, N)).toString('hex');
const verifier = genVerifier({ ...params, salt });
return { salt, verifier };
};
51 changes: 51 additions & 0 deletions server/domain/user/service/srp/calcClientSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from 'assert';
import { calculateScramblingParameter } from 'domain/user/service/srp/calcSessionKey';
import { calculateSignature } from 'domain/user/service/srp/calcSignature';
import { N, Nbytes, g, multiplierParam } from 'domain/user/service/srp/constants';
import {
calculatePrivateKey,
fromBuffer,
getPoolName,
padHex,
toBufferWithLength,
} from 'domain/user/service/srp/util';
import { BigInteger } from 'jsbn';
import { DEFAULT_USER_POOL_ID } from 'service/envValues';

export const calcClientSignature = (params: {
A: string;
a: BigInteger;
B: string;
username: string;
password: string;
salt: string;
timestamp: string;
secretBlock: string;
}): string => {
const poolname = getPoolName(DEFAULT_USER_POOL_ID);
const Bint = new BigInteger(padHex(params.B), 16);

assert(Bint.compareTo(BigInteger.ZERO) > 0, 'B should be greater than 0');
assert(Bint.compareTo(N) < 0, 'A should be less than N');

const privateKey = calculatePrivateKey(poolname, params.username, params.password, params.salt);

const scramblingParameter = calculateScramblingParameter(params.A, params.B);

const S = toBufferWithLength(
// S = (B - k * (g ^ x)) ^ (a + u * x)
Bint.subtract(multiplierParam.multiply(g.modPow(privateKey, N)))
.modPow(params.a.add(fromBuffer(scramblingParameter).multiply(privateKey)), N)
.mod(N),
Nbytes,
);

return calculateSignature({
poolname,
username: params.username,
secretBlock: params.secretBlock,
timestamp: params.timestamp,
scramblingParameter,
key: S,
});
};
39 changes: 39 additions & 0 deletions server/domain/user/service/srp/calcSessionKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from 'assert';
import crypto from 'crypto';
import { BigInteger } from 'jsbn';
import { N, Nbytes } from './constants';
import { fromBuffer, padHex, toBufferWithLength } from './util';

export const calculateScramblingParameter = (A: string, B: string): Buffer => {
// H(A | B)
const ABuffer = Buffer.from(padHex(A), 'hex');
const BBuffer = Buffer.from(padHex(B), 'hex');
const hash = crypto.createHash('sha256').update(ABuffer).update(BBuffer).digest();
return hash;
};

export const calculateSessionKey = (params: {
A: string;
B: string;
b: string;
v: string;
}): Buffer => {
const Aint = new BigInteger(padHex(params.A), 16);
const Bint = new BigInteger(padHex(params.B), 16);
const bInt = new BigInteger(padHex(params.b), 16);
const vInt = new BigInteger(params.v, 16);

assert(Aint.compareTo(BigInteger.ZERO) > 0, 'A should be greater than 0');
assert(Aint.compareTo(N) < 0, 'A should be less than N');
assert(Bint.compareTo(BigInteger.ZERO) > 0, 'B should be greater than 0');
assert(Bint.compareTo(N) < 0, 'A should be less than N');

const scramblingParameter = calculateScramblingParameter(params.A, params.B);

// u = H(A,B) % N
const u = vInt.modPow(fromBuffer(scramblingParameter), N);
// S = (B - k * g^x) ^ (a + u * x) % N
const S = Aint.multiply(u).modPow(bInt, N).mod(N);

return toBufferWithLength(S, Nbytes);
};
30 changes: 30 additions & 0 deletions server/domain/user/service/srp/calcSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import crypto from 'crypto';
import { infoBits } from './constants';
import { padBufferToHex } from './util';

export const calculateSignature = (params: {
poolname: string;
username: string;
secretBlock: string;
timestamp: string;
scramblingParameter: Buffer;
key: Buffer;
}): string => {
const secretBlockBuffer = Buffer.from(params.secretBlock, 'base64');

const prk = crypto
.createHmac('sha256', Buffer.from(padBufferToHex(params.scramblingParameter), 'hex'))
.update(Buffer.from(padBufferToHex(params.key), 'hex'))
.digest();

const hmac = crypto.createHmac('sha256', prk).update(infoBits).digest();
const hkdf = hmac.subarray(0, 16);

return crypto
.createHmac('sha256', hkdf)
.update(params.poolname)
.update(params.username)
.update(secretBlockBuffer)
.update(params.timestamp)
.digest('base64');
};
18 changes: 18 additions & 0 deletions server/domain/user/service/srp/calcSrpB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import crypto from 'crypto';
import { BigInteger } from 'jsbn';
import { N, g, multiplierParam } from './constants';
import { fromBuffer, toBuffer } from './util';

export const calculateSrpB = (
v: string,
): {
b: string;
B: string;
} => {
const b = crypto.randomBytes(32);
const bInt = fromBuffer(b);
const vInt = new BigInteger(v, 16);
// kv + g^b
const B = toBuffer(multiplierParam.multiply(vInt).add(g.modPow(bInt, N)).mod(N));
return { b: b.toString('hex'), B: B.toString('hex') };
};
11 changes: 10 additions & 1 deletion server/domain/user/service/srp/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { BigInteger } from 'jsbn';
import { getHash } from './util';

const Nhex = `FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF`;
export const N = new BigInteger(Nhex, 16);
export const Nbits = N.bitLength();
export const Nbytes = Nbits / 8;
export const g = new BigInteger('2');
export const infoBits = 'Caldera Derived Key\x01';

export const HASH_TYPE = 'sha256';
const getMultiplierParam = (): BigInteger => {
const Nhex = `00${N.toString(16)}`;
const ghex = `0${g.toString(16)}`;
const hash = getHash(Buffer.from(Nhex + ghex, 'hex'), 32);
return new BigInteger(hash, 16);
};

export const multiplierParam = getMultiplierParam();
33 changes: 29 additions & 4 deletions server/domain/user/service/srp/util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import assert from 'assert';
import crypto from 'crypto';
import { BigInteger } from 'jsbn';
import { HASH_TYPE, Nbytes } from './constants';

export const getHash = (data: Buffer | string, length: number): string => {
const hash = crypto.createHash(HASH_TYPE).update(data).digest('hex');
const hash = crypto.createHash('sha256').update(data).digest('hex');

return hash.padStart(length * 2, '0');
};
Expand All @@ -14,11 +14,36 @@ export const calculatePrivateKey = (
password: string,
salt: string,
): BigInteger => {
// x = H(s,p)
const hash = getHash(`${poolname}${username}:${password}`, 32);
const buffer = Buffer.from(salt + hash, 'hex');
const buffer = Buffer.from(padHex(salt) + hash, 'hex');
return new BigInteger(getHash(buffer, 32), 16);
};

export const padHex = (hex: string): string => {
return hex.replace(/^([89A-Fa-f])/, '00$1');
};

export const padBufferToHex = (buffer: Buffer): string => {
return padHex(buffer.toString('hex'));
};

export const toBuffer = (bigInt: BigInteger): Buffer => {
return Buffer.from(bigInt.toString(16).padStart(Nbytes * 2, '0'), 'hex');
const str = bigInt.toString(16);
return Buffer.from(str, 'hex');
};

export const toBufferWithLength = (bigInt: BigInteger, length: number): Buffer => {
const str = bigInt.toString(16).padStart(length * 2, '0');
return Buffer.from(str, 'hex');
};

export const fromBuffer = (buffer: Buffer): BigInteger => {
return new BigInteger(buffer.toString('hex'), 16);
};

export const getPoolName = (poolId: string): string => {
const name = poolId.split('_')[1];
assert(name, 'Invalid poolId');
return name;
};
Loading

0 comments on commit 6799e0e

Please sign in to comment.