first commit

This commit is contained in:
Shotaro Nakamura
2026-06-18 22:59:53 +09:00
commit 20434dcd11
88 changed files with 13950 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
const requiredForSetup = ['CSBIE_SETUP_PASSWORD', 'CSBIE_SETUP_PASSWORD_HASH'] as const
const runtimeDefaults = {
PORT: '8787',
CSBIE_DATABASE_PATH: './data/csbie.sqlite',
CSBIE_ORIGIN: 'http://127.0.0.1:5173',
CSBIE_RP_ID: '127.0.0.1',
}
const optionalUrls = ['SBI_AUTH_BASE_URL', 'SBI_MTS_BASE_URL', 'CSBIE_ORIGIN', 'CSBIE_CORS_ORIGIN']
const present = (key: string) => Boolean(process.env[key]?.trim())
const validateUrl = (key: string) => {
const value = process.env[key]
if (!value) return undefined
try {
new URL(value)
return undefined
} catch {
return `${key} must be a valid URL`
}
}
const messages: string[] = []
for (const [key, fallback] of Object.entries(runtimeDefaults)) {
if (!present(key)) messages.push(`${key} is not set; runtime default is ${fallback}`)
}
for (const key of optionalUrls) {
const message = validateUrl(key)
if (message) messages.push(message)
}
if (!requiredForSetup.some(present)) {
messages.push(
'Set CSBIE_SETUP_PASSWORD or CSBIE_SETUP_PASSWORD_HASH before first owner passkey setup',
)
}
if (!present('SBI_AUTH_BASE_URL') || !present('SBI_MTS_BASE_URL')) {
messages.push('Set SBI_AUTH_BASE_URL and SBI_MTS_BASE_URL before connecting an SBI session')
}
if (messages.length === 0) {
console.log('env:check ok')
} else {
console.log('env:check notices:')
for (const message of messages) console.log(`- ${message}`)
}
+298
View File
@@ -0,0 +1,298 @@
;(() => {
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 = 'csbi'
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')
})()