aboutsummaryrefslogtreecommitdiff
path: root/lib/vnlib.browser/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vnlib.browser/src')
-rw-r--r--lib/vnlib.browser/src/index.ts1
-rw-r--r--lib/vnlib.browser/src/mfa/config.ts7
-rw-r--r--lib/vnlib.browser/src/mfa/fido.ts184
-rw-r--r--lib/vnlib.browser/src/mfa/login.ts10
-rw-r--r--lib/vnlib.browser/src/session/internal.ts16
-rw-r--r--lib/vnlib.browser/src/webcrypto.ts41
6 files changed, 237 insertions, 22 deletions
diff --git a/lib/vnlib.browser/src/index.ts b/lib/vnlib.browser/src/index.ts
index de0f651..d450dba 100644
--- a/lib/vnlib.browser/src/index.ts
+++ b/lib/vnlib.browser/src/index.ts
@@ -30,6 +30,7 @@ export type { WebMessage, ServerValidationError } from './types'
export * from './mfa/login'
export * from './mfa/pki'
export * from './mfa/config'
+export * from './mfa/fido'
//Social exports
export * from './social'
diff --git a/lib/vnlib.browser/src/mfa/config.ts b/lib/vnlib.browser/src/mfa/config.ts
index d4bce35..0343ccb 100644
--- a/lib/vnlib.browser/src/mfa/config.ts
+++ b/lib/vnlib.browser/src/mfa/config.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Vaughn Nugent
+// Copyright (c) 2024 Vaughn Nugent
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
@@ -22,6 +22,7 @@ import { type MaybeRef } from "vue";
import { useAxiosInternal } from "../axios"
import type { MfaMethod } from "./login"
import type { WebMessage } from '../types'
+import type { AxiosRequestConfig } from "axios";
export type UserArg = object;
@@ -56,9 +57,9 @@ export interface MfaApi{
* @param mfaEndpoint The server mfa endpoint relative to the base url
* @returns An object containing the mfa api
*/
-export const useMfaConfig = (mfaEndpoint: MaybeRef<string>): MfaApi =>{
+export const useMfaConfig = (mfaEndpoint: MaybeRef<string>, axiosConfig?: MaybeRef<AxiosRequestConfig | undefined | null>): MfaApi =>{
- const axios = useAxiosInternal(null)
+ const axios = useAxiosInternal(axiosConfig)
const getMethods = async () => {
//Get the mfa methods
diff --git a/lib/vnlib.browser/src/mfa/fido.ts b/lib/vnlib.browser/src/mfa/fido.ts
new file mode 100644
index 0000000..5eaa166
--- /dev/null
+++ b/lib/vnlib.browser/src/mfa/fido.ts
@@ -0,0 +1,184 @@
+// Copyright (c) 2024 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { get } from "@vueuse/core";
+import { type MaybeRef } from "vue";
+import { useAxiosInternal } from "../axios";
+import type { WebMessage } from "../types";
+import type { AxiosRequestConfig } from "axios";
+import type {
+ IMfaFlowContinuiation,
+ IMfaMessage,
+ IMfaTypeProcessor,
+ MfaSumissionHandler
+} from "./login";
+import { startRegistration } from "@simplewebauthn/browser";
+import type { RegistrationResponseJSON, PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/types";
+
+export type IFidoServerOptions = PublicKeyCredentialCreationOptionsJSON
+
+export interface IFidoRequestOptions{
+ readonly password: string;
+}
+
+export interface IFidoDevice{
+ readonly id: string;
+ readonly name: string;
+ readonly registered_at: number;
+}
+
+
+interface FidoRegistration{
+ readonly id: string;
+ readonly publicKey?: string;
+ readonly publicKeyAlgorithm: number;
+ readonly clientDataJSON: string;
+ readonly authenticatorData?: string;
+ readonly attestationObject?: string;
+}
+
+export interface IFidoApi {
+ /**
+ * Gets fido credential options from the server for a currently logged-in user
+ * @returns A promise that resolves to the server options for the FIDO API
+ */
+ beginRegistration: (options?: Partial<IFidoRequestOptions>) => Promise<PublicKeyCredentialCreationOptionsJSON>;
+
+ /**
+ * Creates a new credential for the currently logged-in user
+ * @param credential The credential to create
+ * @returns A promise that resolves to a web message
+ */
+ registerCredential: (credential: RegistrationResponseJSON, commonName: string) => Promise<WebMessage>;
+
+ /**
+ * Registers the default device for the currently logged-in user
+ * @returns A promise that resolves to a web message status of the operation
+ */
+ registerDefaultDevice: (commonName: string, options?: Partial<IFidoRequestOptions>) => Promise<WebMessage>;
+
+ /**
+ * Lists all devices for the currently logged-in user
+ * @returns A promise that resolves to a list of devices
+ */
+ listDevices: () => Promise<IFidoDevice[]>;
+
+ /**
+ * Disables a device for the currently logged-in user.
+ * May require a password to be passed in the options
+ * @param device The device descriptor to disable
+ * @param options The options to pass to the server
+ * @returns A promise that resolves to a web message status of the operation
+ */
+ disableDevice: (device: IFidoDevice, options?: Partial<IFidoRequestOptions>) => Promise<WebMessage>;
+
+ /**
+ * Disables all devices for the currently logged-in user.
+ * May require a password to be passed in the options
+ * @param options The options to pass to the server
+ * @returns A promise that resolves to a web message status of the operation
+ */
+ disableAllDevices: (options?: Partial<IFidoRequestOptions>) => Promise<WebMessage>;
+}
+
+/**
+ * Creates a fido api for configuration and management of fido client devices
+ * @param endpoint The fido server endpoint
+ * @param axiosConfig The optional axios configuration to use
+ * @returns An object containing the fido api
+ */
+export const useFidoApi = (endpoint: MaybeRef<string>, axiosConfig?: MaybeRef<AxiosRequestConfig | undefined | null>)
+ : IFidoApi =>{
+ const ep = () => get(endpoint);
+
+ const axios = useAxiosInternal(axiosConfig)
+
+ const beginRegistration = async (options?: Partial<IFidoRequestOptions>) : Promise<IFidoServerOptions> => {
+ const { data } = await axios.value.put<WebMessage<IFidoServerOptions>>(ep(), options);
+ return data.getResultOrThrow();
+ }
+
+ const registerCredential = async (registration: RegistrationResponseJSON, commonName: string): Promise<WebMessage> => {
+
+ const response: FidoRegistration = {
+ id: registration.id,
+ publicKey: registration.response.publicKey,
+ publicKeyAlgorithm: registration.response.publicKeyAlgorithm!,
+ clientDataJSON: registration.response.clientDataJSON,
+ authenticatorData: registration.response.authenticatorData,
+ attestationObject: registration.response.attestationObject
+ }
+
+ const { data } = await axios.value.post<WebMessage>(ep(), { response, commonName });
+ return data;
+ }
+
+ const registerDefaultDevice = async (commonName: string, options?: Partial<IFidoRequestOptions>): Promise<WebMessage> => {
+ //begin registration
+ const serverOptions = await beginRegistration(options);
+
+ const reg = await startRegistration(serverOptions);
+
+ return await registerCredential(reg, commonName);
+ }
+
+ const listDevices = async (): Promise<IFidoDevice[]> => {
+ const { data } = await axios.value.get<WebMessage<IFidoDevice[]>>(ep());
+ return data.getResultOrThrow();
+ }
+
+ const disableDevice = async (device: IFidoDevice, options?: Partial<IFidoRequestOptions>): Promise<WebMessage> => {
+ const { data } = await axios.value.post<WebMessage>(ep(), { delete: device, ...options });
+ return data;
+ }
+
+ const disableAllDevices = async (options?: Partial<IFidoRequestOptions>): Promise<WebMessage> => {
+ const { data } = await axios.value.post<WebMessage>(ep(), options);
+ return data;
+ }
+
+ return {
+ beginRegistration,
+ registerCredential,
+ registerDefaultDevice,
+ listDevices,
+ disableDevice,
+ disableAllDevices
+ }
+}
+
+/**
+ * Enables fido as a supported multi-factor authentication method
+ * @returns A mfa login processor for fido multi-factor
+ */
+export const fidoMfaProcessor = () : IMfaTypeProcessor => {
+
+ const processMfa = (payload: IMfaMessage, onSubmit: MfaSumissionHandler) : Promise<IMfaFlowContinuiation> => {
+
+ return Promise.resolve({
+ ...payload,
+ submit: onSubmit.submit,
+ })
+ }
+
+ return{
+ type: "fido",
+ processMfa
+ }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/mfa/login.ts b/lib/vnlib.browser/src/mfa/login.ts
index a2bf120..57465ef 100644
--- a/lib/vnlib.browser/src/mfa/login.ts
+++ b/lib/vnlib.browser/src/mfa/login.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Vaughn Nugent
+// Copyright (c) 2024 Vaughn Nugent
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
@@ -29,9 +29,7 @@ import type { Axios } from "axios";
import type { ITokenResponse } from "../session";
import type { WebMessage } from "../types";
-export enum MfaMethod {
- TOTP = 'totp'
-}
+export type MfaMethod = 'totp' | 'fido' | 'pkotp';
export interface IMfaSubmission {
/**
@@ -100,7 +98,7 @@ export interface IMfaLoginManager {
const getMfaProcessor = (user: IUserInternal, axios:Ref<Axios>) =>{
//Store handlers by their mfa type
- const handlerMap = new Map<string, IMfaTypeProcessor>();
+ const handlerMap = new Map<MfaMethod, IMfaTypeProcessor>();
//Creates a submission handler for an mfa upgrade
const createSubHandler = (upgrade : string, finalize: (res: ITokenResponse) => Promise<void>) :MfaSumissionHandler => {
@@ -177,7 +175,7 @@ export const totpMfaProcessor = (): IMfaTypeProcessor => {
}
return {
- type: MfaMethod.TOTP,
+ type: 'totp',
processMfa
}
}
diff --git a/lib/vnlib.browser/src/session/internal.ts b/lib/vnlib.browser/src/session/internal.ts
index 4fba638..0d60a38 100644
--- a/lib/vnlib.browser/src/session/internal.ts
+++ b/lib/vnlib.browser/src/session/internal.ts
@@ -21,7 +21,7 @@ import { defaults, isEmpty, isNil, noop } from 'lodash-es';
import { computed, watch, type Ref } from "vue";
import { get, set, toRefs } from '@vueuse/core';
import { SignJWT } from 'jose'
-import crypto, { decryptAsync, getRandomHex } from "../webcrypto";
+import { getCryptoOrThrow, decryptAsync, getRandomHex } from "../webcrypto";
import { ArrayBuffToBase64, Base64ToUint8Array } from '../binhelpers'
import { debugLog } from "../util";
import type { CookieMonitor } from './cookies'
@@ -63,6 +63,8 @@ const createKeyStore = (storage: Ref<IKeyStorage>, keyAlg: Ref<AlgorithmIdentifi
}
const setCredentialAsync = async (keypair: CryptoKeyPair): Promise<void> => {
+ const crypto = getCryptoOrThrow();
+
// Store the private key
const newPrivRaw = await crypto.exportKey('pkcs8', keypair.privateKey);
const newPubRaw = await crypto.exportKey('spki', keypair.publicKey);
@@ -83,10 +85,11 @@ const createKeyStore = (storage: Ref<IKeyStorage>, keyAlg: Ref<AlgorithmIdentifi
return;
}
+ const crypto = getCryptoOrThrow();
+
// If not, generate a new key pair
const keypair = await crypto.generateKey(keyAlg.value, true, ['encrypt', 'decrypt']) as CryptoKeyPair;
-
- //Set credential
+
await setCredentialAsync(keypair);
debugLog("Generated new client keypair, none were found")
@@ -102,10 +105,11 @@ const createKeyStore = (storage: Ref<IKeyStorage>, keyAlg: Ref<AlgorithmIdentifi
// Convert the private key to a Uint8Array from its base64 string
const keyData = Base64ToUint8Array(priv.value || "")
+ const crypto = getCryptoOrThrow();
+
//import private key as pkcs8
const privKey = await crypto.importKey('pkcs8', keyData, keyAlg.value, false, ['decrypt'])
-
- // Decrypt the data and return it
+
return await decryptAsync(keyAlg.value, privKey, data, false) as ArrayBuffer
}
@@ -113,6 +117,8 @@ const createKeyStore = (storage: Ref<IKeyStorage>, keyAlg: Ref<AlgorithmIdentifi
// Decrypt the data
const decrypted = await decryptDataAsync(data)
+ const crypto = getCryptoOrThrow();
+
// Hash the decrypted data
const hashed = await crypto.digest({ name: 'SHA-256' }, decrypted)
diff --git a/lib/vnlib.browser/src/webcrypto.ts b/lib/vnlib.browser/src/webcrypto.ts
index d2c7640..1f796ea 100644
--- a/lib/vnlib.browser/src/webcrypto.ts
+++ b/lib/vnlib.browser/src/webcrypto.ts
@@ -20,7 +20,16 @@
import { isArrayBuffer, isPlainObject, isString } from 'lodash-es';
import { ArrayBuffToBase64, Base64ToUint8Array, ArrayToHexString } from './binhelpers';
-const crypto = window?.crypto?.subtle || {};
+export const isCryptoSupported = () : boolean => {
+ return !!(window.isSecureContext && window.crypto && window.crypto.subtle);
+}
+
+export const getCryptoOrThrow = () => {
+ if (!isCryptoSupported()) {
+ throw new Error('Your browser does not support the Web Cryptography API');
+ }
+ return window.crypto.subtle;
+}
/**
* Signs the dataBuffer using the specified key and hmac algorithm by its name eg. 'SHA-256'
@@ -29,9 +38,13 @@ const crypto = window?.crypto?.subtle || {};
* @param {String} alg The name of the hmac algorithm to use eg. 'SHA-256'
* @param {String} [toBase64 = false] The output format, the array buffer data, or true for base64 string
* @returns {Promise<ArrayBuffer | String>} The signature as an ArrayBuffer or a base64 string
+ * @throws An error if the browser does not support the Web Cryptography API
*/
export const hmacSignAsync = async (keyBuffer: ArrayBuffer | string, dataBuffer: ArrayBuffer | string, alg : string, toBase64 = false)
: Promise<ArrayBuffer | string> => {
+
+ const crypto = getCryptoOrThrow()
+
// Check key argument type
const rawKeyBuffer = isString(keyBuffer) ? Base64ToUint8Array(keyBuffer as string) : keyBuffer as ArrayBuffer;
@@ -47,6 +60,7 @@ export const hmacSignAsync = async (keyBuffer: ArrayBuffer | string, dataBuffer:
// Encode to base64 if needed
return toBase64 ? ArrayBuffToBase64(digest) : digest;
}
+
/**
* @function decryptAsync Decrypts syncrhonous or asyncrhonsous en encypted data
* asynchronously.
@@ -55,13 +69,17 @@ export const hmacSignAsync = async (keyBuffer: ArrayBuffer | string, dataBuffer:
* @param {Object} algorithm The algorithm object to use for decryption.
* @param {Boolean} toBase64 If true, the decrypted data will be returned as a base64 string.
* @returns {Promise} The decrypted data.
+ * @throws An error if the browser does not support the Web Cryptography API
*/
export const decryptAsync = async (
algorithm: AlgorithmIdentifier,
privKey: BufferSource | CryptoKey | JsonWebKey,
data: string | ArrayBuffer,
- toBase64 = false): Promise<string | ArrayBuffer> =>
+ toBase64 = false
+): Promise<string | ArrayBuffer> =>
{
+ const crypto = getCryptoOrThrow()
+
// Check data argument type and decode if needed
const dataBuffer = isString(data) ? Base64ToUint8Array(data as string) : data as ArrayBuffer;
@@ -84,14 +102,21 @@ export const decryptAsync = async (
return toBase64 ? ArrayBuffToBase64(decrypted) : decrypted
}
+/**
+ * Gets a random hex string of the specified size
+ * @param size The number of bytes to generate
+ * @returns A random hex string of the specified size
+ * @throws An error if the browser does not support the Web Cryptography API
+ */
export const getRandomHex = (size: number) : string => {
- // generate a new random secret and store it
+ if (!isCryptoSupported()) {
+ throw new Error('Your browser does not support the Web Cryptography API');
+ }
+
const randBuffer = new Uint8Array(size)
- // generate random id directly on the window.crypto object
+
window.crypto.getRandomValues(randBuffer)
- // Store the id in the session as hex
+
+ //Convert the random buffer to a hex string
return ArrayToHexString(randBuffer)
}
-
-//default export subtle crypto
-export default crypto; \ No newline at end of file