diff --git a/tests/auth.test.ts b/tests/auth.test.ts deleted file mode 100644 index 66b30ec..0000000 --- a/tests/auth.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { - buildKeyboardInteractiveAuthRequest, - buildKeyboardInteractiveResponse, - isSupportedPasswordPrompt, - parseKeyboardInteractiveChallenge, - passwordResponsesForChallenge, -} from '../src/ssh/auth-password.ts'; - -const IDENTITY = { username: 'alice', host: 'example.com' }; - -interface ChallengeFields { - name?: string; - instruction?: string; - language?: string; -} - -function encodeString(value: string): Uint8Array { - const bytes = new TextEncoder().encode(value); - const result = new Uint8Array(4 + bytes.length); - new DataView(result.buffer).setUint32(0, bytes.length, false); - result.set(bytes, 4); - return result; -} - -function challenge(prompts: Array<{ text: string; echo: boolean }>, fields: ChallengeFields = {}): Uint8Array { - const count = new Uint8Array(4); - new DataView(count.buffer).setUint32(0, prompts.length, false); - const parts = [ - new Uint8Array([60]), - encodeString(fields.name ?? ''), - encodeString(fields.instruction ?? ''), - encodeString(fields.language ?? ''), - count, - ...prompts.flatMap((prompt) => [encodeString(prompt.text), new Uint8Array([prompt.echo ? 1 : 0])]), - ]; - const result = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); - let offset = 0; - for (const part of parts) { result.set(part, offset); offset += part.length; } - return result; -} - -function readString(data: Uint8Array, offset: number): { value: string; next: number } { - const length = readUint32(data, offset); - const start = offset + 4; - return { value: new TextDecoder().decode(data.subarray(start, start + length)), next: start + length }; -} - -function readUint32(data: Uint8Array, offset: number): number { - return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(offset, false); -} - -test('builds an RFC 4256 keyboard-interactive request without the password', () => { - const request = buildKeyboardInteractiveAuthRequest('coss'); - assert.equal(request[0], 50); - let field = readString(request, 1); - assert.equal(field.value, 'coss'); - field = readString(request, field.next); - assert.equal(field.value, 'ssh-connection'); - field = readString(request, field.next); - assert.equal(field.value, 'keyboard-interactive'); - field = readString(request, field.next); - assert.equal(field.value, ''); - field = readString(request, field.next); - assert.equal(field.value, ''); - assert.equal(field.next, request.length); - assert.equal(new TextDecoder().decode(request).includes('secret'), false); -}); - -test('answers a password prompt', () => { - const parsed = parseKeyboardInteractiveChallenge(challenge([{ - text: 'Password for alice@example.com:', - echo: false, - }])); - const response = buildKeyboardInteractiveResponse(passwordResponsesForChallenge(parsed, 'test-secret', IDENTITY)); - assert.equal(response[0], 61); - assert.equal(readUint32(response, 1), 1); - const answer = readString(response, 5); - assert.equal(answer.value, 'test-secret'); - assert.equal(answer.next, response.length); -}); - -test('acknowledges zero-prompt keyboard-interactive notices', () => { - const parsed = parseKeyboardInteractiveChallenge(challenge([], { instruction: 'Authentication successful' })); - assert.deepEqual(parsed.prompts, []); - const response = buildKeyboardInteractiveResponse(passwordResponsesForChallenge(parsed, 'test-secret', IDENTITY)); - assert.equal(response[0], 61); - assert.equal(readUint32(response, 1), 0); - assert.equal(response.length, 5); -}); - -test('rejects multiple or non-password prompts', () => { - const multiple = parseKeyboardInteractiveChallenge(challenge([ - { text: 'Password:', echo: false }, - { text: 'Verification code:', echo: false }, - ])); - assert.throws(() => passwordResponsesForChallenge(multiple, 'test-secret', IDENTITY), /2 prompts/); - const otp = parseKeyboardInteractiveChallenge(challenge([{ text: 'Verification code:', echo: false }])); - assert.throws(() => passwordResponsesForChallenge(otp, 'test-secret', IDENTITY), /not a supported password prompt/); -}); - -test('only auto-answers hidden password prompts', () => { - const supported = (text: string, echo = false, fields: ChallengeFields = {}) => isSupportedPasswordPrompt( - parseKeyboardInteractiveChallenge(challenge([{ text, echo }], fields)), - IDENTITY, - ); - assert.equal(supported('Password:'), true); - assert.equal(supported('密码:'), true); - assert.equal(supported('Password for alice@example.com:'), true); - assert.equal(supported('password for alice@EXAMPLE.COM:'), true); - assert.equal(supported('Password for mallory@example.com:'), false); - assert.equal(supported('Password:', true), false); - assert.equal(supported('Verification code:'), false); - assert.equal(supported('Do not enter your password; enter OTP:'), false); - assert.equal(supported('Enter new password:'), false); - assert.equal(supported('Password + verification code:'), false); - assert.equal(supported('Passphrase for key:'), false); - assert.equal(supported('Password:', false, { instruction: 'Enter your one-time code' }), false); - assert.equal(supported('Password:', false, { name: 'Second factor' }), false); -}); - -test('strictly validates keyboard-interactive challenge framing', () => { - assert.throws(() => parseKeyboardInteractiveChallenge(challenge([{ text: '', echo: false }])), /Malformed/); - - const badBoolean = challenge([{ text: 'Password:', echo: false }]); - badBoolean[badBoolean.length - 1] = 2; - assert.throws(() => parseKeyboardInteractiveChallenge(badBoolean), /Malformed/); - - const invalidUtf8 = challenge([], { name: 'x' }); - invalidUtf8[5] = 0xff; - assert.throws(() => parseKeyboardInteractiveChallenge(invalidUtf8), /text/); - - const valid = challenge([{ text: 'Password:', echo: false }]); - const trailing = new Uint8Array(valid.length + 1); - trailing.set(valid); - assert.throws(() => parseKeyboardInteractiveChallenge(trailing), /trailing data/); - assert.throws(() => parseKeyboardInteractiveChallenge(valid.subarray(0, valid.length - 1)), /Malformed/); - - const tooMany = Array.from({ length: 17 }, () => ({ text: 'Password:', echo: false })); - assert.throws(() => parseKeyboardInteractiveChallenge(challenge(tooMany)), /too many prompts/); -}); diff --git a/tests/host-key.test.ts b/tests/host-key.test.ts deleted file mode 100644 index 133977e..0000000 --- a/tests/host-key.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { - classifyHostKey as classifyBrowserHostKey, - SSH_FINGERPRINT_RE, -} from '../frontend/src/host-key.ts'; -import { classifyHostKey as classifyWorkerHostKey } from '../src/ssh/host-key.ts'; - -const OLD = 'SHA256:McWyGS5cvgt9EvFLQxxLOTPDqAY1MmKx3iAhgXUpEiY'; -const CURRENT = 'SHA256:xs49vJHNJGmereADhVfIZMt1xdeY04wYkj5FpDpLR9c'; - -test('validates OpenSSH SHA-256 fingerprints', () => { - assert.equal(SSH_FINGERPRINT_RE.test(OLD), true); - assert.equal(SSH_FINGERPRINT_RE.test(CURRENT), true); - assert.equal(SSH_FINGERPRINT_RE.test(`${CURRENT}=`), false); - assert.equal(SSH_FINGERPRINT_RE.test('SHA256:not-a-fingerprint'), false); -}); - -test('browser prompts only for first-seen or changed host keys', () => { - assert.equal(classifyBrowserHostKey('', CURRENT), 'first-seen'); - assert.equal(classifyBrowserHostKey(CURRENT, CURRENT), 'matched'); - assert.equal(classifyBrowserHostKey(OLD, CURRENT), 'changed'); -}); - -test('worker never treats a changed host key as already trusted', () => { - assert.equal(classifyWorkerHostKey(undefined, CURRENT), 'first-seen'); - assert.equal(classifyWorkerHostKey(CURRENT, CURRENT), 'trusted'); - assert.equal(classifyWorkerHostKey(OLD, CURRENT), 'changed'); -}); -