feat: passkey support (#11804)
https://github.com/MisskeyIO/misskey/pull/149
This commit is contained in:
		@@ -9,8 +9,16 @@ import * as assert from 'assert';
 | 
			
		||||
import * as crypto from 'node:crypto';
 | 
			
		||||
import cbor from 'cbor';
 | 
			
		||||
import * as OTPAuth from 'otpauth';
 | 
			
		||||
import { loadConfig } from '../../src/config.js';
 | 
			
		||||
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
 | 
			
		||||
import { loadConfig } from '@/config.js';
 | 
			
		||||
import { api, signup, startServer } from '../utils.js';
 | 
			
		||||
import type {
 | 
			
		||||
	AuthenticationResponseJSON,
 | 
			
		||||
	AuthenticatorAssertionResponseJSON,
 | 
			
		||||
	AuthenticatorAttestationResponseJSON,
 | 
			
		||||
	PublicKeyCredentialCreationOptionsJSON,
 | 
			
		||||
	PublicKeyCredentialRequestOptionsJSON,
 | 
			
		||||
	RegistrationResponseJSON,
 | 
			
		||||
} from '@simplewebauthn/typescript-types';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
@@ -47,21 +55,18 @@ describe('2要素認証', () => {
 | 
			
		||||
 | 
			
		||||
	const rpIdHash = (): Buffer => {
 | 
			
		||||
		return crypto.createHash('sha256')
 | 
			
		||||
			.update(Buffer.from(config.hostname, 'utf-8'))
 | 
			
		||||
			.update(Buffer.from(config.host, 'utf-8'))
 | 
			
		||||
			.digest();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const keyDoneParam = (param: {
 | 
			
		||||
		keyName: string,
 | 
			
		||||
		challengeId: string,
 | 
			
		||||
		challenge: string,
 | 
			
		||||
		credentialId: Buffer,
 | 
			
		||||
		creationOptions: PublicKeyCredentialCreationOptionsJSON,
 | 
			
		||||
	}): {
 | 
			
		||||
		attestationObject: string,
 | 
			
		||||
		challengeId: string,
 | 
			
		||||
		clientDataJSON: string,
 | 
			
		||||
		password: string,
 | 
			
		||||
		name: string,
 | 
			
		||||
		credential: RegistrationResponseJSON,
 | 
			
		||||
	} => {
 | 
			
		||||
		// A COSE encoded public key
 | 
			
		||||
		const credentialPublicKey = cbor.encode(new Map<number, unknown>([
 | 
			
		||||
@@ -76,7 +81,7 @@ describe('2要素認証', () => {
 | 
			
		||||
		// AuthenticatorAssertionResponse.authenticatorData
 | 
			
		||||
		// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
 | 
			
		||||
		const credentialIdLength = Buffer.allocUnsafe(2);
 | 
			
		||||
		credentialIdLength.writeUInt16BE(param.credentialId.length);
 | 
			
		||||
		credentialIdLength.writeUInt16BE(param.credentialId.length, 0);
 | 
			
		||||
		const authData = Buffer.concat([
 | 
			
		||||
			rpIdHash(), // rpIdHash(32)
 | 
			
		||||
			Buffer.from([0x45]), // flags(1)
 | 
			
		||||
@@ -88,20 +93,27 @@ describe('2要素認証', () => {
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			attestationObject: cbor.encode({
 | 
			
		||||
				fmt: 'none',
 | 
			
		||||
				attStmt: {},
 | 
			
		||||
				authData,
 | 
			
		||||
			}).toString('hex'),
 | 
			
		||||
			challengeId: param.challengeId,
 | 
			
		||||
			clientDataJSON: JSON.stringify({
 | 
			
		||||
				type: 'webauthn.create',
 | 
			
		||||
				challenge: param.challenge,
 | 
			
		||||
				origin: config.scheme + '://' + config.host,
 | 
			
		||||
				androidPackageName: 'org.mozilla.firefox',
 | 
			
		||||
			}),
 | 
			
		||||
			password,
 | 
			
		||||
			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',
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
@@ -121,17 +133,12 @@ describe('2要素認証', () => {
 | 
			
		||||
 | 
			
		||||
	const signinWithSecurityKeyParam = (param: {
 | 
			
		||||
		keyName: string,
 | 
			
		||||
		challengeId: string,
 | 
			
		||||
		challenge: string,
 | 
			
		||||
		credentialId: Buffer,
 | 
			
		||||
		requestOptions: PublicKeyCredentialRequestOptionsJSON,
 | 
			
		||||
	}): {
 | 
			
		||||
		authenticatorData: string,
 | 
			
		||||
		credentialId: string,
 | 
			
		||||
		challengeId: string,
 | 
			
		||||
		clientDataJSON: string,
 | 
			
		||||
		username: string,
 | 
			
		||||
		password: string,
 | 
			
		||||
		signature: string,
 | 
			
		||||
		credential: AuthenticationResponseJSON,
 | 
			
		||||
		'g-recaptcha-response'?: string | null,
 | 
			
		||||
		'hcaptcha-response'?: string | null,
 | 
			
		||||
	} => {
 | 
			
		||||
@@ -144,10 +151,10 @@ describe('2要素認証', () => {
 | 
			
		||||
		]);
 | 
			
		||||
		const clientDataJSONBuffer = Buffer.from(JSON.stringify({
 | 
			
		||||
			type: 'webauthn.get',
 | 
			
		||||
			challenge: param.challenge,
 | 
			
		||||
			challenge: param.requestOptions.challenge,
 | 
			
		||||
			origin: config.scheme + '://' + config.host,
 | 
			
		||||
			androidPackageName: 'org.mozilla.firefox',
 | 
			
		||||
		}));
 | 
			
		||||
		}), 'utf-8');
 | 
			
		||||
		const hashedclientDataJSON = crypto.createHash('sha256')
 | 
			
		||||
			.update(clientDataJSONBuffer)
 | 
			
		||||
			.digest();
 | 
			
		||||
@@ -156,13 +163,19 @@ describe('2要素認証', () => {
 | 
			
		||||
			.update(Buffer.concat([authenticatorData, hashedclientDataJSON]))
 | 
			
		||||
			.sign(privateKey);
 | 
			
		||||
		return {
 | 
			
		||||
			authenticatorData: authenticatorData.toString('hex'),
 | 
			
		||||
			credentialId: param.credentialId.toString('base64'),
 | 
			
		||||
			challengeId: param.challengeId,
 | 
			
		||||
			clientDataJSON: clientDataJSONBuffer.toString('hex'),
 | 
			
		||||
			username,
 | 
			
		||||
			password,
 | 
			
		||||
			signature: signature.toString('hex'),
 | 
			
		||||
			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,
 | 
			
		||||
		};
 | 
			
		||||
@@ -222,19 +235,18 @@ describe('2要素認証', () => {
 | 
			
		||||
			password,
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(registerKeyResponse.status, 200);
 | 
			
		||||
		assert.notEqual(registerKeyResponse.body.challengeId, undefined);
 | 
			
		||||
		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({
 | 
			
		||||
			keyName,
 | 
			
		||||
			challengeId: registerKeyResponse.body.challengeId,
 | 
			
		||||
			challenge: registerKeyResponse.body.challenge,
 | 
			
		||||
			credentialId,
 | 
			
		||||
			creationOptions: registerKeyResponse.body,
 | 
			
		||||
		}), alice);
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.status, 200);
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex'));
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.body.name, keyName);
 | 
			
		||||
 | 
			
		||||
		const usersShowResponse = await api('/users/show', {
 | 
			
		||||
@@ -248,16 +260,14 @@ describe('2要素認証', () => {
 | 
			
		||||
		});
 | 
			
		||||
		assert.strictEqual(signinResponse.status, 200);
 | 
			
		||||
		assert.strictEqual(signinResponse.body.i, undefined);
 | 
			
		||||
		assert.notEqual(signinResponse.body.challengeId, undefined);
 | 
			
		||||
		assert.notEqual(signinResponse.body.challenge, undefined);
 | 
			
		||||
		assert.notEqual(signinResponse.body.securityKeys, undefined);
 | 
			
		||||
		assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex'));
 | 
			
		||||
		assert.notEqual(signinResponse.body.allowCredentials, undefined);
 | 
			
		||||
		assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
 | 
			
		||||
 | 
			
		||||
		const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
 | 
			
		||||
			keyName,
 | 
			
		||||
			challengeId: signinResponse.body.challengeId,
 | 
			
		||||
			challenge: signinResponse.body.challenge,
 | 
			
		||||
			credentialId,
 | 
			
		||||
			requestOptions: signinResponse.body,
 | 
			
		||||
		}));
 | 
			
		||||
		assert.strictEqual(signinResponse2.status, 200);
 | 
			
		||||
		assert.notEqual(signinResponse2.body.i, undefined);
 | 
			
		||||
@@ -283,9 +293,8 @@ describe('2要素認証', () => {
 | 
			
		||||
		const credentialId = crypto.randomBytes(0x41);
 | 
			
		||||
		const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
 | 
			
		||||
			keyName,
 | 
			
		||||
			challengeId: registerKeyResponse.body.challengeId,
 | 
			
		||||
			challenge: registerKeyResponse.body.challenge,
 | 
			
		||||
			credentialId,
 | 
			
		||||
			creationOptions: registerKeyResponse.body,
 | 
			
		||||
		}), alice);
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.status, 200);
 | 
			
		||||
 | 
			
		||||
@@ -310,9 +319,8 @@ describe('2要素認証', () => {
 | 
			
		||||
		const signinResponse2 = await api('/signin', {
 | 
			
		||||
			...signinWithSecurityKeyParam({
 | 
			
		||||
				keyName,
 | 
			
		||||
				challengeId: signinResponse.body.challengeId,
 | 
			
		||||
				challenge: signinResponse.body.challenge,
 | 
			
		||||
				credentialId,
 | 
			
		||||
				requestOptions: signinResponse.body,
 | 
			
		||||
			}),
 | 
			
		||||
			password: '',
 | 
			
		||||
		});
 | 
			
		||||
@@ -340,23 +348,22 @@ describe('2要素認証', () => {
 | 
			
		||||
		const credentialId = crypto.randomBytes(0x41);
 | 
			
		||||
		const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
 | 
			
		||||
			keyName,
 | 
			
		||||
			challengeId: registerKeyResponse.body.challengeId,
 | 
			
		||||
			challenge: registerKeyResponse.body.challenge,
 | 
			
		||||
			credentialId,
 | 
			
		||||
			creationOptions: registerKeyResponse.body,
 | 
			
		||||
		}), alice);
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.status, 200);
 | 
			
		||||
 | 
			
		||||
		const renamedKey = 'other-key';
 | 
			
		||||
		const updateKeyResponse = await api('/i/2fa/update-key', {
 | 
			
		||||
			name: renamedKey,
 | 
			
		||||
			credentialId: credentialId.toString('hex'),
 | 
			
		||||
			credentialId: credentialId.toString('base64url'),
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(updateKeyResponse.status, 200);
 | 
			
		||||
 | 
			
		||||
		const iResponse = await api('/i', {
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(iResponse.status, 200);
 | 
			
		||||
		const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex'));
 | 
			
		||||
		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);
 | 
			
		||||
@@ -382,9 +389,8 @@ describe('2要素認証', () => {
 | 
			
		||||
		const credentialId = crypto.randomBytes(0x41);
 | 
			
		||||
		const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
 | 
			
		||||
			keyName,
 | 
			
		||||
			challengeId: registerKeyResponse.body.challengeId,
 | 
			
		||||
			challenge: registerKeyResponse.body.challenge,
 | 
			
		||||
			credentialId,
 | 
			
		||||
			creationOptions: registerKeyResponse.body,
 | 
			
		||||
		}), alice);
 | 
			
		||||
		assert.strictEqual(keyDoneResponse.status, 200);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user