barkey/packages/backend/test/e2e/2fa.ts
Kisaragi 31e82fc29a
test(backend): kill many any in backend test (partial) (#14054)
* kill any on utils:api

* kill any on timeline test

* use optional chain to kill TS2532 on timeline test
変更前: 該当ノートが見つからなければundefinedに対するプロパティアクセスとしてテストがクラッシュ
変更後: 該当ノートが見つからなければoptional chainがundefinedとして評価されるが、strictEqualの右辺がnon-nullableなためアサーションに失敗しテストがクラッシュ

* kill `as any` for ApMfmService

* kill argument any for api-visibility

* kill argument any across a few tests

* do not return value that has yielded from `await`-ing `Promise<void>`

* force cast

* runtime non-null assertion to coerce

* rewrite `assert.notEqual(expr, null)` to `assert.ok(expr)`
こうすることでassertion type扱いになり、non-nullableになる

* change return type of `failedApiCall` to `void`
戻り値がどこにも使われていない

* split bindings for exports.ts
型が合わなくて文句を言ってくるので適切に分割

* runtime non-null assertion

* runtime non-null assertion

* 何故かうまく行かないので、とりあえずXORしてみる

* Revert "何故かうまく行かないので、とりあえずXORしてみる"

This reverts commit 48cf32c930924840d0892af92d71b9437acb5844.

* castAsErrorで安全ではないキャストを隠蔽

* 型アサーションの追加

* 型アサーションの追加

* 型アサーションの追加

* voidで値を返さない

* castAsError

* assert.ok => kill nullability

* もはや明示的な型の指定は必要ない

* castAsError

* castAsError

* 型アサーションの追加

* nullableを一旦抑止

* 変数を分離して型エラーを排除

* 不要なプロパティを削除する処理を隠蔽してanyを排除

* Repository type

* simple type

* assert.ok => kill nullability

* revert `as any` drop
reverts fe95c05b3f53266108128680d9358a3796844232 partialy

* test: fix invalid assertion
partially revert b99b7b5392d9d20c81dfee1346ba8b33ff9e1fbb

* test: 52d8a54fc7 により型が合うようになった部分の`as any`を除去

* format

* test: apply https://github.com/misskey-dev/misskey/pull/14054#discussion_r1672369526 (part 1)

* test: use non-null assertion to suppress too many error

* Update packages/backend/test/utils.ts

Co-authored-by: anatawa12 <anatawa12@icloud.com>

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
2024-07-14 09:33:16 +09:00

495 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as crypto from 'node:crypto';
import cbor from 'cbor';
import * as OTPAuth from 'otpauth';
import { loadConfig } from '@/config.js';
import { api, signup } from '../utils.js';
import type {
AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON,
AuthenticatorAttestationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/types';
import type * as misskey from 'misskey-js';
describe('2要素認証', () => {
let alice: misskey.entities.SignupResponse;
const config = loadConfig();
const password = 'test';
const username = 'alice';
// https://datatracker.ietf.org/doc/html/rfc8152
// 各値の定義は上記規格に基づく。鍵ペアは適当に生成したやつ
const coseKtyEc2 = 2;
const coseKid = 'meriadoc.brandybuck@buckland.example';
const coseAlgEs256 = -7;
const coseEc2CrvP256 = 1;
const coseEc2X = '4932eaacc657565705e4287e7870ce3aad55545d99d35a98a472dc52880cfc8f';
const coseEc2Y = '5ca68303bf2c0433473e3d5cb8586bc2c8c43a4945a496fce8dbeda8b23ab0b1';
// private key only for testing
const pemToSign = '-----BEGIN EC PRIVATE KEY-----\n' +
'MHcCAQEEIHqe/keuXyolbXzgLOu+YFJjDBGWVgXc3QCXfyqwDPf2oAoGCCqGSM49\n' +
'AwEHoUQDQgAESTLqrMZXVlcF5Ch+eHDOOq1VVF2Z01qYpHLcUogM/I9cpoMDvywE\n' +
'M0c+PVy4WGvCyMQ6SUWklvzo2+2osjqwsQ==\n' +
'-----END EC PRIVATE KEY-----\n';
const otpToken = (secret: string): string => {
return OTPAuth.TOTP.generate({
secret: OTPAuth.Secret.fromBase32(secret),
digits: 6,
});
};
const rpIdHash = (): Buffer => {
return crypto.createHash('sha256')
.update(Buffer.from(config.host, 'utf-8'))
.digest();
};
const keyDoneParam = (param: {
token: string,
keyName: string,
credentialId: Buffer,
creationOptions: PublicKeyCredentialCreationOptionsJSON,
}): {
token: string,
password: string,
name: string,
credential: RegistrationResponseJSON,
} => {
// A COSE encoded public key
const credentialPublicKey = cbor.encode(new Map<number, unknown>([
[-1, coseEc2CrvP256],
[-2, Buffer.from(coseEc2X, 'hex')],
[-3, Buffer.from(coseEc2Y, 'hex')],
[1, coseKtyEc2],
[2, coseKid],
[3, coseAlgEs256],
]));
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const credentialIdLength = Buffer.allocUnsafe(2);
credentialIdLength.writeUInt16BE(param.credentialId.length, 0);
const authData = Buffer.concat([
rpIdHash(), // rpIdHash(32)
Buffer.from([0x45]), // flags(1)
Buffer.from([0x00, 0x00, 0x00, 0x00]), // signCount(4)
Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), // AAGUID(16)
credentialIdLength,
param.credentialId,
credentialPublicKey,
]);
return {
password,
token: param.token,
name: param.keyName,
credential: <RegistrationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAttestationResponseJSON>{
clientDataJSON: Buffer.from(JSON.stringify({
type: 'webauthn.create',
challenge: param.creationOptions.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}), 'utf-8').toString('base64url'),
attestationObject: cbor.encode({
fmt: 'none',
attStmt: {},
authData,
}).toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
};
};
const signinParam = (): {
username: string,
password: string,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
} => {
return {
username,
password,
'g-recaptcha-response': null,
'hcaptcha-response': null,
};
};
const signinWithSecurityKeyParam = (param: {
keyName: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): {
username: string,
password: string,
credential: AuthenticationResponseJSON,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
} => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([
rpIdHash(),
Buffer.from([0x05]), // flags(1)
Buffer.from([0x00, 0x00, 0x00, 0x01]), // signCount(4)
]);
const clientDataJSONBuffer = Buffer.from(JSON.stringify({
type: 'webauthn.get',
challenge: param.requestOptions.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}), 'utf-8');
const hashedclientDataJSON = crypto.createHash('sha256')
.update(clientDataJSONBuffer)
.digest();
const privateKey = crypto.createPrivateKey(pemToSign);
const signature = crypto.createSign('SHA256')
.update(Buffer.concat([authenticatorData, hashedclientDataJSON]))
.sign(privateKey);
return {
username,
password,
credential: <AuthenticationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAssertionResponseJSON>{
clientDataJSON: clientDataJSONBuffer.toString('base64url'),
authenticatorData: authenticatorData.toString('base64url'),
signature: signature.toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
'g-recaptcha-response': null,
'hcaptcha-response': null,
};
};
beforeAll(async () => {
alice = await signup({ username, password });
}, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
assert.notEqual(registerResponse.body.qr, undefined);
assert.notEqual(registerResponse.body.url, undefined);
assert.notEqual(registerResponse.body.secret, undefined);
assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', {
username,
}, alice);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
assert.notEqual(registerKeyResponse.body.rp, undefined);
assert.notEqual(registerKeyResponse.body.challenge, undefined);
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
} as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
} as any));
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
} as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('i/2fa/password-less', {
value: true,
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
const signinResponse = await api('signin', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
} as any),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
} as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key';
const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey,
credentialId: credentialId.toString('base64url'),
}, alice);
assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
assert.ok(iResponse.body.securityKeysList);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
assert.strictEqual(securityKeys.length, 1);
assert.strictEqual(securityKeys[0].name, renamedKey);
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
} as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
assert.ok(iResponse.body.securityKeysList);
for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
credentialId: key.id,
}, alice);
assert.strictEqual(removeKeyResponse.status, 200);
}
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('', async () => {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
});