Skip to content

Commit

Permalink
chore: refactor webauthn code after bumping web-auth/webauthn-lib
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <[email protected]>
  • Loading branch information
st3iny committed Apr 19, 2024
1 parent a713141 commit 4f1cca9
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 55 deletions.
4 changes: 2 additions & 2 deletions lib/Db/PublicKeyCredentialEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
namespace OCA\TwoFactorWebauthn\Db;

use OCP\AppFramework\Db\Entity;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Uid\Uuid;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\TrustPath\TrustPathLoader;

Expand Down Expand Up @@ -134,7 +134,7 @@ public static function fromPublicKeyCrendentialSource(string $name,
$publicKeyCredentialEntity->setTransports(json_encode($publicKeyCredentialSource->getTransports()));
$publicKeyCredentialEntity->setAttestationType($publicKeyCredentialSource->getAttestationType());
$publicKeyCredentialEntity->setTrustPath(json_encode($publicKeyCredentialSource->getTrustPath()->jsonSerialize()));
$publicKeyCredentialEntity->setAaguid($publicKeyCredentialSource->getAaguid()->toString());
$publicKeyCredentialEntity->setAaguid($publicKeyCredentialSource->getAaguid()->toRfc4122());
$publicKeyCredentialEntity->setCredentialPublicKey(base64_encode($publicKeyCredentialSource->getCredentialPublicKey()));
$publicKeyCredentialEntity->setUserHandle($publicKeyCredentialSource->getUserHandle());
$publicKeyCredentialEntity->setCounter($publicKeyCredentialSource->getCounter());
Expand Down
15 changes: 6 additions & 9 deletions lib/Service/WebAuthnManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

namespace OCA\TwoFactorWebauthn\Service;

use Assert\Assertion;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA;
use Cose\Algorithm\Signature\EdDSA;
Expand Down Expand Up @@ -132,20 +131,20 @@ public function startRegistration(IUser $user, string $serverHost): PublicKeyCre

$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
false,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
);

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
$rpEntity,
$userEntity,
$challenge,
$publicKeyCredentialParametersList,
$timeout,
$excludedPublicKeyDescriptors,
$authenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
null
$excludedPublicKeyDescriptors,
$timeout,
);

$this->session->set(self::TWOFACTORAUTH_WEBAUTHN_REGISTRATION, $publicKeyCredentialCreationOptions->jsonSerialize());
Expand Down Expand Up @@ -280,15 +279,15 @@ function ($device) {

$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
random_bytes(32), // Challenge
60000, // Timeout
null, // Relying Party ID
[], // Registered PublicKeyCredentialDescriptor classes
null, // User verification requirement
60000, // Timeout
$extensions
);
$publicKeyCredentialRequestOptions
->setRpId($this->stripPort($serverHost))
->allowCredentials($registeredPublicKeyCredentialDescriptors)
->allowCredentials(...$registeredPublicKeyCredentialDescriptors)
->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DISCOURAGED);

$this->session->set(self::TWOFACTORAUTH_WEBAUTHN_REQUEST, $publicKeyCredentialRequestOptions->jsonSerialize());
Expand Down Expand Up @@ -364,7 +363,6 @@ public function finishAuthenticate(IUser $user, string $data) {

public function removeDevice(IUser $user, int $id) {
$credential = $this->mapper->findById($id);
Assertion::eq($credential->getUserHandle(), $user->getUID());

$this->mapper->delete($credential);

Expand All @@ -382,7 +380,6 @@ public function deactivateAllDevices(IUser $user) {

public function changeActivationState(IUser $user, int $id, bool $active) {
$credential = $this->mapper->findById($id);
Assertion::eq($credential->getUserHandle(), $user->getUID());

$credential->setActive($active);

Expand Down
32 changes: 6 additions & 26 deletions src/components/AddDeviceDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
} from '../services/RegistrationService.js'
import logger from '../logger.js'
import { arrayToBase64UnpaddedString, base64url2base64 } from '../utils/base64.js'
const RegistrationSteps = Object.freeze({
READY: 1,
Expand Down Expand Up @@ -106,27 +107,6 @@ export default {
},
methods: {
arrayToBase64String(a) {
return btoa(String.fromCharCode(...a))
},
base64url2base64(input) {
input = input
.replace(/=/g, '')
.replace(/-/g, '+')
.replace(/_/g, '/')
const pad = input.length % 4
if (pad) {
if (pad === 1) {
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
}
input += new Array(5 - pad).join('=')
}
return input
},
async start() {
this.errorMessage = null
logger.info('Starting to add a new twofactor webauthn device')
Expand Down Expand Up @@ -156,11 +136,11 @@ export default {
async getRegistrationData() {
try {
const publicKey = await startRegistration()
publicKey.challenge = Uint8Array.from(window.atob(this.base64url2base64(publicKey.challenge)), c => c.charCodeAt(0))
publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), c => c.charCodeAt(0))
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c => c.charCodeAt(0))
if (publicKey.excludeCredentials) {
publicKey.excludeCredentials = publicKey.excludeCredentials.map(data => {
data.id = Uint8Array.from(window.atob(this.base64url2base64(data.id)), c => c.charCodeAt(0))
data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), c => c.charCodeAt(0))
return data
})
}
Expand All @@ -180,10 +160,10 @@ export default {
this.credential = {
id: data.id,
type: data.type,
rawId: this.arrayToBase64String(new Uint8Array(data.rawId)),
rawId: arrayToBase64UnpaddedString(new Uint8Array(data.rawId)),
response: {
clientDataJSON: this.arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
attestationObject: this.arrayToBase64String(new Uint8Array(data.response.attestationObject)),
clientDataJSON: arrayToBase64UnpaddedString(new Uint8Array(data.response.clientDataJSON)),
attestationObject: arrayToBase64UnpaddedString(new Uint8Array(data.response.attestationObject)),
},
}
logger.debug('mapped credentials data')
Expand Down
12 changes: 6 additions & 6 deletions src/components/Challenge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
import { mapGetters } from 'vuex'
import { NcButton } from '@nextcloud/vue'
import logger from '../logger.js'
import { arrayToBase64String, base64StringToArray, base64url2base64 } from '../utils/base64.js'
import { arrayToBase64UnpaddedString, base64StringToArray, base64url2base64 } from '../utils/base64.js'
export default {
name: 'Challenge',
Expand Down Expand Up @@ -138,12 +138,12 @@ export default {
const challenge = {
id: data.id,
type: data.type,
rawId: arrayToBase64String(new Uint8Array(data.rawId)),
rawId: arrayToBase64UnpaddedString(new Uint8Array(data.rawId)),
response: {
authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
signature: arrayToBase64String(new Uint8Array(data.response.signature)),
userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null,
authenticatorData: arrayToBase64UnpaddedString(new Uint8Array(data.response.authenticatorData)),
clientDataJSON: arrayToBase64UnpaddedString(new Uint8Array(data.response.clientDataJSON)),
signature: arrayToBase64UnpaddedString(new Uint8Array(data.response.signature)),
userHandle: data.response.userHandle ? arrayToBase64UnpaddedString(new Uint8Array(data.response.userHandle)) : null,
},
}
logger.debug('mapped credentials', { challenge })
Expand Down
12 changes: 6 additions & 6 deletions src/tests/unit/utils/base64.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
*
*/

import { arrayToBase64String, base64StringToArray, base64url2base64 } from '../../../utils/base64.js'
import { arrayToBase64UnpaddedString, base64StringToArray, base64url2base64 } from '../../../utils/base64.js'

describe('utils/base64', () => {
it('should convert byte arrays to base64 strings', () => {
expect(arrayToBase64String(new Uint8Array([4, 2]))).to.equal('BAI=')
expect(arrayToBase64String(new Uint8Array([4]))).to.equal('BA==')
expect(arrayToBase64String(new Uint8Array([1, 2, 3, 4, 5, 6]))).to.equal('AQIDBAUG')
expect(arrayToBase64String(new Uint8Array([]))).to.equal('')
it('should convert byte arrays to unpadded base64 strings', () => {
expect(arrayToBase64UnpaddedString(new Uint8Array([4, 2]))).to.equal('BAI')
expect(arrayToBase64UnpaddedString(new Uint8Array([4]))).to.equal('BA')
expect(arrayToBase64UnpaddedString(new Uint8Array([1, 2, 3, 4, 5, 6]))).to.equal('AQIDBAUG')
expect(arrayToBase64UnpaddedString(new Uint8Array([]))).to.equal('')
})

it('should convert base64 strings to byte arrays', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/utils/base64.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
*/

/**
* Encode an array of bytes as a base64 string.
* Encode an array of bytes as an unpadded base64 string.
*
* @param {Uint8Array} a An array of bytes
* @return {string} A base64 string
* @return {string} An unpadded base64 string
*/
export function arrayToBase64String(a) {
return btoa(String.fromCharCode(...a))
export function arrayToBase64UnpaddedString(a) {
return btoa(String.fromCharCode(...a)).replace(/=+$/g, '')
}

/**
Expand Down
8 changes: 7 additions & 1 deletion tests/Unit/Controller/SettingsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;

class SettingsControllerTest extends TestCase {

Expand Down Expand Up @@ -64,7 +66,11 @@ public function testStartRegister(): void {
$this->userSession->expects(self::once())
->method('getUser')
->willReturn($user);
$publicKeyCredentialCreationOptions = $this->createMock(PublicKeyCredentialCreationOptions::class);
$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
new PublicKeyCredentialRpEntity('relying_party'),
new PublicKeyCredentialUserEntity('user', 'user_id', 'User'),
'challenge',
);
$this->webauthnManager->expects(self::once())
->method('startRegistration')
->with(self::equalTo($user))
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Provider/WebAuthnProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public function testGetDescription(): void {

public function testGetTemplate(): void {
$user = $this->createMock(IUser::class);
$key = $this->createMock(PublicKeyCredentialRequestOptions::class);
$key = new PublicKeyCredentialRequestOptions('challenge');
$serverHost = 'my.next.cloud';
$this->request->expects(self::once())
->method('getServerHost')
Expand Down

0 comments on commit 4f1cca9

Please sign in to comment.