UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

112 lines 3.94 kB
"use strict"; /* * Copyright 2025 Matti Airas * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.generateState = generateState; exports.generateNonce = generateNonce; exports.createAuthState = createAuthState; exports.validateState = validateState; exports.encryptState = encryptState; exports.decryptState = decryptState; const crypto_1 = require("crypto"); const types_1 = require("./types"); const pkce_1 = require("./pkce"); const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; /** * Generate a cryptographically random state parameter * Used for CSRF protection in OAuth2 flows */ function generateState() { return (0, crypto_1.randomBytes)(32).toString('base64url'); } /** * Generate a cryptographically random nonce * Used for ID token replay protection */ function generateNonce() { return (0, crypto_1.randomBytes)(32).toString('base64url'); } /** * Create a complete auth state object for the OIDC flow */ function createAuthState(redirectUri, originalUrl) { return { state: generateState(), codeVerifier: (0, pkce_1.generateCodeVerifier)(), nonce: generateNonce(), redirectUri, originalUrl, createdAt: Date.now() }; } /** * Validate that the returned state matches the stored state * and that the state hasn't expired */ function validateState(returnedState, storedState) { if (returnedState !== storedState.state) { throw new types_1.OIDCError('State mismatch - possible CSRF attack', 'INVALID_STATE'); } const age = Date.now() - storedState.createdAt; if (age > types_1.STATE_MAX_AGE_MS) { throw new types_1.OIDCError(`State expired (${Math.round(age / 1000)}s old, max ${types_1.STATE_MAX_AGE_MS / 1000}s)`, 'STATE_EXPIRED'); } } /** * Derive a 32-byte encryption key from the secret key */ function deriveKey(secretKey) { return (0, crypto_1.createHash)('sha256').update(secretKey).digest(); } /** * Encrypt the auth state for storage in a cookie * Uses AES-256-GCM for authenticated encryption */ function encryptState(state, secretKey) { const key = deriveKey(secretKey); const iv = (0, crypto_1.randomBytes)(IV_LENGTH); const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, key, iv); const plaintext = JSON.stringify(state); const encrypted = Buffer.concat([ cipher.update(plaintext, 'utf8'), cipher.final() ]); const authTag = cipher.getAuthTag(); // Combine IV + authTag + ciphertext const combined = Buffer.concat([iv, authTag, encrypted]); return combined.toString('base64url'); } /** * Decrypt the auth state from a cookie */ function decryptState(encryptedState, secretKey) { const key = deriveKey(secretKey); const combined = Buffer.from(encryptedState, 'base64url'); // Extract IV, authTag, and ciphertext const iv = combined.subarray(0, IV_LENGTH); const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH); const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH); const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, key, iv); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([ decipher.update(ciphertext), decipher.final() ]); return JSON.parse(decrypted.toString('utf8')); } //# sourceMappingURL=state.js.map