Files
csbi/scripts/mock-create.js
T
2026-06-21 21:37:07 +09:00

299 lines
10 KiB
JavaScript

;(() => {
if (!globalThis.navigator?.credentials?.create) {
throw new Error('navigator.credentials.create is not available on this page')
}
if (navigator.credentials.__sbiClientMockCreateInstalled) {
console.warn('[sbi-client] mock navigator.credentials.create is already installed')
return
}
const originalCreate = navigator.credentials.create.bind(navigator.credentials)
const textEncoder = new TextEncoder()
const credentialLabel = 'mnie'
const base64Url = (value) => {
const bytes =
value instanceof ArrayBuffer
? new Uint8Array(value)
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
let binary = ''
for (const byte of bytes) binary += String.fromCharCode(byte)
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
}
const fromBase64Url = (value) => {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='))
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
return bytes
}
const toBytes = (value) => {
if (value instanceof ArrayBuffer) return new Uint8Array(value)
if (ArrayBuffer.isView(value))
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
if (Array.isArray(value)) return new Uint8Array(value)
if (typeof value === 'string') return fromBase64Url(value)
throw new Error('unsupported binary value')
}
const concat = (...parts) => {
const length = parts.reduce((sum, part) => sum + part.length, 0)
const out = new Uint8Array(length)
let offset = 0
for (const part of parts) {
out.set(part, offset)
offset += part.length
}
return out
}
const sha256 = async (value) => new Uint8Array(await crypto.subtle.digest('SHA-256', value))
const randomBytes = (length) => {
const bytes = new Uint8Array(length)
crypto.getRandomValues(bytes)
return bytes
}
const cborUInt = (major, value) => {
if (value < 24) return Uint8Array.of((major << 5) | value)
if (value < 0x100) return Uint8Array.of((major << 5) | 24, value)
if (value < 0x10000) return Uint8Array.of((major << 5) | 25, value >> 8, value & 0xff)
return Uint8Array.of(
(major << 5) | 26,
(value >>> 24) & 0xff,
(value >>> 16) & 0xff,
(value >>> 8) & 0xff,
value & 0xff,
)
}
const cborEncode = (value) => {
if (value === null) return Uint8Array.of(0xf6)
if (value === false) return Uint8Array.of(0xf4)
if (value === true) return Uint8Array.of(0xf5)
if (typeof value === 'number') {
if (!Number.isInteger(value)) throw new Error('CBOR encoder only supports integers')
return value >= 0 ? cborUInt(0, value) : cborUInt(1, -1 - value)
}
if (typeof value === 'string') {
const bytes = textEncoder.encode(value)
return concat(cborUInt(3, bytes.length), bytes)
}
if (value instanceof Uint8Array) return concat(cborUInt(2, value.length), value)
if (Array.isArray(value)) return concat(cborUInt(4, value.length), ...value.map(cborEncode))
if (value && typeof value === 'object') {
const entries = value instanceof Map ? [...value.entries()] : Object.entries(value)
const encoded = entries.flatMap(([key, item]) => [cborEncode(key), cborEncode(item)])
return concat(cborUInt(5, entries.length), ...encoded)
}
throw new Error('unsupported CBOR value')
}
const derToRawP256 = (spki) => {
const bytes = new Uint8Array(spki)
const marker = Uint8Array.of(0x03, 0x42, 0x00, 0x04)
for (let i = 0; i <= bytes.length - marker.length - 64; i++) {
if (marker.every((byte, index) => bytes[i + index] === byte)) {
return bytes.slice(i + marker.length, i + marker.length + 64)
}
}
throw new Error('could not extract P-256 public key from SPKI')
}
const selectAlgorithm = (params = []) => {
if (params.some((param) => param.type === 'public-key' && param.alg === -7)) return -7
throw new Error('mock-create currently supports ES256 only')
}
const makeCredential = async (publicKey) => {
const alg = selectAlgorithm(publicKey.pubKeyCredParams)
const rpId = publicKey.rp?.id || location.hostname
const origin = location.origin
const challenge = toBytes(publicKey.challenge)
const credentialId = randomBytes(32)
const userHandle = publicKey.user?.id ? toBytes(publicKey.user.id) : undefined
const keyPair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
])
const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
const spki = await crypto.subtle.exportKey('spki', keyPair.publicKey)
const rawPublicKey = derToRawP256(spki)
const cosePublicKey = cborEncode(
new Map([
[1, 2],
[3, alg],
[-1, 1],
[-2, rawPublicKey.slice(0, 32)],
[-3, rawPublicKey.slice(32, 64)],
]),
)
const clientDataJSON = textEncoder.encode(
JSON.stringify({
type: 'webauthn.create',
challenge: base64Url(challenge),
origin,
crossOrigin: false,
}),
)
const rpIdHash = await sha256(textEncoder.encode(rpId))
const flags = Uint8Array.of(0x45)
const signCount = Uint8Array.of(0, 0, 0, 0)
const aaguid = new Uint8Array(16)
const credentialIdLength = Uint8Array.of(
(credentialId.length >> 8) & 0xff,
credentialId.length & 0xff,
)
const authenticatorData = concat(
rpIdHash,
flags,
signCount,
aaguid,
credentialIdLength,
credentialId,
cosePublicKey,
)
const attestationObject = cborEncode(
new Map([
['fmt', 'none'],
['attStmt', new Map()],
['authData', authenticatorData],
]),
)
const now = new Date().toISOString()
const vaultObject = {
version: 1,
kind: 'webauthn-credential',
provider: 'sbi-sec',
label: credentialLabel,
rpId,
origin,
credentialId: base64Url(credentialId),
userHandle: userHandle ? base64Url(userHandle) : undefined,
alg,
publicKey: {
format: 'jwk',
jwk: publicJwk,
},
authenticator: {
aaguid: '00000000-0000-0000-0000-000000000000',
signCount: 0,
discoverable:
publicKey.residentKey === 'required' ||
publicKey.authenticatorSelection?.residentKey === 'required',
userVerification:
publicKey.userVerification ||
publicKey.authenticatorSelection?.userVerification ||
'preferred',
transports: ['internal'],
backupEligible: false,
backupState: false,
},
secretPlaintext: {
privateKey: {
format: 'jwk',
jwk: privateJwk,
},
registration: {
attestationObject: base64Url(attestationObject),
clientDataJSON: base64Url(clientDataJSON),
},
},
createdAt: now,
updatedAt: now,
}
const credential = {
id: base64Url(credentialId),
rawId: credentialId.buffer.slice(
credentialId.byteOffset,
credentialId.byteOffset + credentialId.byteLength,
),
type: 'public-key',
authenticatorAttachment: 'platform',
response: {
attestationObject: attestationObject.buffer.slice(
attestationObject.byteOffset,
attestationObject.byteOffset + attestationObject.byteLength,
),
clientDataJSON: clientDataJSON.buffer.slice(
clientDataJSON.byteOffset,
clientDataJSON.byteOffset + clientDataJSON.byteLength,
),
getAuthenticatorData: () =>
authenticatorData.buffer.slice(
authenticatorData.byteOffset,
authenticatorData.byteOffset + authenticatorData.byteLength,
),
getPublicKey: () => spki,
getPublicKeyAlgorithm: () => alg,
getTransports: () => ['internal'],
},
getClientExtensionResults: () => ({}),
toJSON: () => ({
id: base64Url(credentialId),
rawId: base64Url(credentialId),
type: 'public-key',
authenticatorAttachment: 'platform',
response: {
attestationObject: base64Url(attestationObject),
clientDataJSON: base64Url(clientDataJSON),
authenticatorData: base64Url(authenticatorData),
publicKey: base64Url(new Uint8Array(spki)),
publicKeyAlgorithm: alg,
transports: ['internal'],
},
clientExtensionResults: {},
}),
}
return { credential, vaultObject }
}
const openJsonTab = (value) => {
const json = JSON.stringify(value, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const tab = window.open(url, '_blank', 'noopener,noreferrer')
if (!tab) {
console.warn('[sbi-client] could not open JSON tab; popup may have been blocked')
URL.revokeObjectURL(url)
return
}
window.setTimeout(() => URL.revokeObjectURL(url), 60_000)
}
navigator.credentials.create = async (options) => {
if (!options?.publicKey) return originalCreate(options)
const { credential, vaultObject } = await makeCredential(options.publicKey)
globalThis.__sbiClientLastCreatedCredential = vaultObject
console.log('[sbi-client] created mock WebAuthn credential')
console.log(JSON.stringify(vaultObject, null, 2))
openJsonTab(vaultObject)
return credential
}
Object.defineProperty(navigator.credentials, '__sbiClientMockCreateInstalled', {
value: true,
enumerable: false,
})
globalThis.__sbiClientRestoreCreate = () => {
navigator.credentials.create = originalCreate
delete globalThis.__sbiClientRestoreCreate
console.log('[sbi-client] restored native navigator.credentials.create')
}
console.log('[sbi-client] installed mock navigator.credentials.create')
})()