signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
112 lines • 3.94 kB
JavaScript
;
/*
* 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