= ({
)
}
- if (isAuthenticated) {
+ if (hasKeys && keys) {
return (
Autenticado
- {session?.isAdmin && (
-
- Admin
-
- )}
+
+ {keys.publicKey.substring(0, 16)}...
+
-
- {showPermissions && session?.permissions && (
-
- {session.permissions.map((permission: string) => (
-
- {permission}
-
- ))}
-
- )}
-
+
= ({
)
}
-export default LoginButton
\ No newline at end of file
+export default LoginButton
diff --git a/plugins/crypto-auth/client/components/ProtectedRoute.tsx b/plugins/crypto-auth/client/components/ProtectedRoute.tsx
index b7128e15..85add709 100644
--- a/plugins/crypto-auth/client/components/ProtectedRoute.tsx
+++ b/plugins/crypto-auth/client/components/ProtectedRoute.tsx
@@ -1,15 +1,16 @@
/**
* Componente de Rota Protegida
- * Protege componentes que requerem autenticação
+ * Protege componentes que requerem chaves criptográficas no client-side
+ *
+ * NOTA: Este componente apenas verifica se há chaves locais.
+ * A autenticação real acontece no backend via validação de assinatura.
*/
-import React, { ReactNode } from 'react'
+import React, { type ReactNode } from 'react'
import { useAuth } from './AuthProvider'
export interface ProtectedRouteProps {
children: ReactNode
- requireAdmin?: boolean
- requiredPermissions?: string[]
fallback?: ReactNode
loadingComponent?: ReactNode
unauthorizedComponent?: ReactNode
@@ -17,19 +18,17 @@ export interface ProtectedRouteProps {
export const ProtectedRoute: React.FC = ({
children,
- requireAdmin = false,
- requiredPermissions = [],
fallback,
loadingComponent,
unauthorizedComponent
}) => {
- const { isAuthenticated, isAdmin, permissions, isLoading, error } = useAuth()
+ const { hasKeys, isLoading, error } = useAuth()
// Componente de loading padrão
const defaultLoadingComponent = (
-
Verificando autenticação...
+
Verificando chaves...
)
@@ -41,14 +40,9 @@ export const ProtectedRoute: React.FC = ({
- Acesso Negado
+ Chaves Não Encontradas
- {!isAuthenticated
- ? 'Você precisa estar autenticado para acessar esta página.'
- : requireAdmin && !isAdmin
- ? 'Você precisa de privilégios de administrador para acessar esta página.'
- : 'Você não tem as permissões necessárias para acessar esta página.'
- }
+ Você precisa gerar chaves criptográficas para acessar esta página.
{error && (
@@ -58,33 +52,19 @@ export const ProtectedRoute: React.FC = ({
)
- // Mostrar loading enquanto verifica autenticação
+ // Mostrar loading enquanto verifica
if (isLoading) {
return <>{loadingComponent || defaultLoadingComponent}>
}
- // Verificar se está autenticado
- if (!isAuthenticated) {
+ // Verificar se tem chaves
+ if (!hasKeys) {
return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}>
}
- // Verificar se requer admin
- if (requireAdmin && !isAdmin) {
- return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}>
- }
-
- // Verificar permissões específicas
- if (requiredPermissions.length > 0) {
- const hasRequiredPermissions = requiredPermissions.every(permission =>
- permissions.includes(permission) || permissions.includes('admin')
- )
-
- if (!hasRequiredPermissions) {
- return <>{unauthorizedComponent || fallback || defaultUnauthorizedComponent}>
- }
- }
-
- // Usuário autorizado, renderizar children
+ // Tem chaves, renderizar children
+ // NOTA: Isso não garante autenticação no backend!
+ // O backend ainda validará a assinatura em cada requisição.
return <>{children}>
}
@@ -102,8 +82,8 @@ export function withAuth(
)
WrappedComponent.displayName = `withAuth(${Component.displayName || Component.name})`
-
+
return WrappedComponent
}
-export default ProtectedRoute
\ No newline at end of file
+export default ProtectedRoute
diff --git a/plugins/crypto-auth/client/components/SessionInfo.tsx b/plugins/crypto-auth/client/components/SessionInfo.tsx
deleted file mode 100644
index d669c5cc..00000000
--- a/plugins/crypto-auth/client/components/SessionInfo.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * Componente de Informações da Sessão
- * Exibe informações detalhadas sobre a sessão atual
- */
-
-import React, { useState } from 'react'
-import { useAuth } from './AuthProvider'
-
-export interface SessionInfoProps {
- className?: string
- showPrivateKey?: boolean
- showFullSessionId?: boolean
- compact?: boolean
-}
-
-export const SessionInfo: React.FC = ({
- className = '',
- showPrivateKey = false,
- showFullSessionId = false,
- compact = false
-}) => {
- const { session, isAuthenticated, isAdmin, permissions, isLoading } = useAuth()
- const [showDetails, setShowDetails] = useState(!compact)
-
- if (isLoading) {
- return (
-
- )
- }
-
- if (!isAuthenticated || !session) {
- return (
-
- )
- }
-
- const formatDate = (date: Date) => {
- return new Intl.DateTimeFormat('pt-BR', {
- dateStyle: 'short',
- timeStyle: 'medium'
- }).format(date)
- }
-
- const truncateId = (id: string, length: number = 8) => {
- return showFullSessionId ? id : `${id.substring(0, length)}...`
- }
-
- const copyToClipboard = async (text: string) => {
- try {
- await navigator.clipboard.writeText(text)
- // Você pode adicionar um toast/notification aqui
- } catch (err) {
- console.error('Erro ao copiar para clipboard:', err)
- }
- }
-
- if (compact) {
- return (
-
-
-
-
- {truncateId(session.sessionId)}
-
- {isAdmin && (
-
- Admin
-
- )}
-
-
setShowDetails(!showDetails)}
- className="text-gray-400 hover:text-gray-600 transition-colors"
- >
-
-
-
-
-
- {showDetails && (
-
-
-
- )}
-
- )
- }
-
- return (
-
-
-
- )
-}
-
-interface SessionDetailsProps {
- session: any
- isAdmin: boolean
- permissions: string[]
- showPrivateKey: boolean
- showFullSessionId: boolean
- onCopy: (text: string) => void
-}
-
-const SessionDetails: React.FC = ({
- session,
- isAdmin,
- permissions,
- showPrivateKey,
- showFullSessionId,
- onCopy
-}) => {
- const formatDate = (date: Date) => {
- return new Intl.DateTimeFormat('pt-BR', {
- dateStyle: 'short',
- timeStyle: 'medium'
- }).format(date)
- }
-
- const CopyButton: React.FC<{ text: string }> = ({ text }) => (
- onCopy(text)}
- className="ml-2 text-gray-400 hover:text-gray-600 transition-colors"
- title="Copiar"
- >
-
-
-
-
- )
-
- return (
-
-
-
Informações da Sessão
-
-
-
-
-
-
Session ID
-
-
- {showFullSessionId ? session.sessionId : `${session.sessionId.substring(0, 16)}...`}
-
-
-
-
-
-
-
Chave Pública
-
-
- {showFullSessionId ? session.publicKey : `${session.publicKey.substring(0, 16)}...`}
-
-
-
-
-
- {showPrivateKey && (
-
-
- Chave Privada
- (Confidencial)
-
-
-
- {session.privateKey.substring(0, 16)}...
-
-
-
-
- )}
-
-
-
-
Status
-
-
- {isAdmin ? 'Administrador' : 'Usuário'}
-
-
-
-
-
-
Permissões
-
- {permissions.map((permission) => (
-
- {permission}
-
- ))}
-
-
-
-
-
-
- Criado em
- {formatDate(session.createdAt)}
-
-
-
- Último uso
- {formatDate(session.lastUsed)}
-
-
-
-
- )
-}
-
-export default SessionInfo
\ No newline at end of file
diff --git a/plugins/crypto-auth/client/components/index.ts b/plugins/crypto-auth/client/components/index.ts
index 18369a9f..1092e65b 100644
--- a/plugins/crypto-auth/client/components/index.ts
+++ b/plugins/crypto-auth/client/components/index.ts
@@ -9,7 +9,4 @@ export { AuthProvider, useAuth } from './AuthProvider'
export type { AuthProviderProps, AuthContextValue } from './AuthProvider'
export { ProtectedRoute, withAuth } from './ProtectedRoute'
-export type { ProtectedRouteProps } from './ProtectedRoute'
-
-export { SessionInfo } from './SessionInfo'
-export type { SessionInfoProps } from './SessionInfo'
\ No newline at end of file
+export type { ProtectedRouteProps } from './ProtectedRoute'
\ No newline at end of file
diff --git a/plugins/crypto-auth/client/index.ts b/plugins/crypto-auth/client/index.ts
index a53facd3..3d9b7ad8 100644
--- a/plugins/crypto-auth/client/index.ts
+++ b/plugins/crypto-auth/client/index.ts
@@ -3,7 +3,7 @@
*/
export { CryptoAuthClient } from './CryptoAuthClient'
-export type { SessionInfo, AuthConfig, SignedRequestOptions } from './CryptoAuthClient'
+export type { KeyPair, AuthConfig, SignedRequestOptions } from './CryptoAuthClient'
// Componentes React
export * from './components'
diff --git a/plugins/crypto-auth/config/index.ts b/plugins/crypto-auth/config/index.ts
new file mode 100644
index 00000000..385eb91d
--- /dev/null
+++ b/plugins/crypto-auth/config/index.ts
@@ -0,0 +1,34 @@
+/**
+ * Crypto Auth Plugin Configuration
+ * Declarative config using FluxStack config system
+ */
+
+import { defineConfig, config } from '@/core/utils/config-schema'
+
+const cryptoAuthConfigSchema = {
+ // Enable/disable plugin
+ enabled: config.boolean('CRYPTO_AUTH_ENABLED', true),
+
+ // Security settings
+ maxTimeDrift: config.number('CRYPTO_AUTH_MAX_TIME_DRIFT', 300000, false), // 5 minutes in ms
+
+ // Admin keys (array of public keys in hex format)
+ adminKeys: config.array('CRYPTO_AUTH_ADMIN_KEYS', []),
+
+ // Metrics and monitoring
+ enableMetrics: config.boolean('CRYPTO_AUTH_ENABLE_METRICS', true),
+
+ // Session configuration (for future features)
+ sessionTimeout: config.number('CRYPTO_AUTH_SESSION_TIMEOUT', 1800000, false), // 30 minutes
+
+ // Nonce configuration
+ nonceLength: config.number('CRYPTO_AUTH_NONCE_LENGTH', 16, false), // bytes
+
+ // Rate limiting (requests per minute per public key)
+ rateLimitPerMinute: config.number('CRYPTO_AUTH_RATE_LIMIT', 100, false),
+} as const
+
+export const cryptoAuthConfig = defineConfig(cryptoAuthConfigSchema)
+
+export type CryptoAuthConfig = typeof cryptoAuthConfig
+export default cryptoAuthConfig
diff --git a/plugins/crypto-auth/examples/basic-usage.tsx b/plugins/crypto-auth/examples/basic-usage.tsx
deleted file mode 100644
index ff02fd22..00000000
--- a/plugins/crypto-auth/examples/basic-usage.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-/**
- * Exemplo básico de uso do plugin Crypto Auth
- */
-
-import React from 'react'
-import {
- AuthProvider,
- useAuth,
- LoginButton,
- ProtectedRoute,
- SessionInfo
-} from '../client'
-
-// Componente principal da aplicação
-function App() {
- return (
- {
- console.log('Status de autenticação mudou:', { isAuthenticated, session })
- }}
- onError={(error) => {
- console.error('Erro de autenticação:', error)
- }}
- >
-
-
-
-
-
-
-
- )
-}
-
-// Header com informações de autenticação
-function Header() {
- return (
-
- )
-}
-
-// Dashboard principal
-function Dashboard() {
- const { isAuthenticated, isLoading } = useAuth()
-
- if (isLoading) {
- return (
-
- )
- }
-
- return (
-
- {/* Área pública */}
-
-
- {/* Área protegida */}
- {isAuthenticated && (
-
- )}
-
- {/* Área admin */}
-
-
-
-
- )
-}
-
-// Seção pública
-function PublicSection() {
- return (
-
- Área Pública
-
- Esta seção é visível para todos os usuários, autenticados ou não.
-
-
-
Como funciona:
-
- • Clique em "Entrar" para gerar automaticamente um par de chaves Ed25519
- • Sua chave privada fica apenas no seu navegador
- • Todas as requisições são assinadas criptograficamente
- • Sem senhas, sem cadastros, sem complicação!
-
-
-
- )
-}
-
-// Seção protegida
-function ProtectedSection() {
- const { client, session, permissions } = useAuth()
- const [apiData, setApiData] = React.useState(null)
- const [loading, setLoading] = React.useState(false)
-
- const callProtectedAPI = async () => {
- setLoading(true)
- try {
- const response = await client.fetch('/api/protected/data', {
- method: 'POST',
- body: JSON.stringify({ message: 'Hello from client!' })
- })
-
- if (response.ok) {
- const data = await response.json()
- setApiData(data)
- } else {
- const error = await response.json()
- alert(`Erro na API: ${error.error || 'Erro desconhecido'}`)
- }
- } catch (error) {
- console.error('Erro ao chamar API:', error)
- alert('Erro ao chamar API')
- } finally {
- setLoading(false)
- }
- }
-
- return (
-
- Área Protegida
-
- Esta seção é visível apenas para usuários autenticados.
-
-
-
-
-
Suas Informações:
-
-
Session ID: {session?.sessionId.substring(0, 16)}...
-
Permissões: {permissions.join(', ')}
-
Criado em: {session?.createdAt.toLocaleString()}
-
-
-
-
-
Testar API Protegida:
-
- {loading ? 'Carregando...' : 'Chamar API'}
-
-
- {apiData && (
-
-
- {JSON.stringify(apiData, null, 2)}
-
-
- )}
-
-
-
- )
-}
-
-// Seção admin
-function AdminSection() {
- const { client } = useAuth()
- const [stats, setStats] = React.useState(null)
-
- React.useEffect(() => {
- loadStats()
- }, [])
-
- const loadStats = async () => {
- try {
- const response = await client.fetch('/api/admin/stats')
- if (response.ok) {
- const data = await response.json()
- setStats(data)
- }
- } catch (error) {
- console.error('Erro ao carregar stats:', error)
- }
- }
-
- return (
-
-
- Área Administrativa
-
-
- Esta seção é visível apenas para administradores.
-
-
- {stats && (
-
-
Estatísticas do Sistema:
-
-
-
Sessões Ativas
-
{stats.activeSessions}
-
-
-
Admins Online
-
{stats.adminSessions}
-
-
-
Total Sessões
-
{stats.totalSessions}
-
-
-
Chaves Admin
-
{stats.adminKeys}
-
-
-
- )}
-
- )
-}
-
-export default App
\ No newline at end of file
diff --git a/plugins/crypto-auth/index.ts b/plugins/crypto-auth/index.ts
index a76de02c..9016d4d7 100644
--- a/plugins/crypto-auth/index.ts
+++ b/plugins/crypto-auth/index.ts
@@ -6,7 +6,15 @@
import type { FluxStack, PluginContext, RequestContext, ResponseContext } from "../../core/plugins/types"
type Plugin = FluxStack.Plugin
+import { Elysia, t } from "elysia"
import { CryptoAuthService, AuthMiddleware } from "./server"
+import { makeProtectedRouteCommand } from "./cli/make-protected-route.command"
+
+// ✅ Plugin carrega sua própria configuração (da pasta config/ do plugin)
+import { cryptoAuthConfig } from "./config"
+
+// Store config globally for hooks to access
+let pluginConfig: any = cryptoAuthConfig
export const cryptoAuthPlugin: Plugin = {
name: "crypto-auth",
@@ -25,30 +33,15 @@ export const cryptoAuthPlugin: Plugin = {
type: "boolean",
description: "Habilitar autenticação criptográfica"
},
- sessionTimeout: {
- type: "number",
- minimum: 300000, // 5 minutos mínimo
- description: "Timeout da sessão em millisegundos"
- },
maxTimeDrift: {
type: "number",
minimum: 30000,
- description: "Máximo drift de tempo permitido em millisegundos"
+ description: "Máximo drift de tempo permitido em millisegundos (previne replay attacks)"
},
adminKeys: {
type: "array",
items: { type: "string" },
- description: "Chaves públicas dos administradores"
- },
- protectedRoutes: {
- type: "array",
- items: { type: "string" },
- description: "Rotas que requerem autenticação"
- },
- publicRoutes: {
- type: "array",
- items: { type: "string" },
- description: "Rotas públicas (não requerem autenticação)"
+ description: "Chaves públicas dos administradores (hex 64 caracteres)"
},
enableMetrics: {
type: "boolean",
@@ -57,174 +50,113 @@ export const cryptoAuthPlugin: Plugin = {
},
additionalProperties: false
},
-
+
defaultConfig: {
enabled: true,
- sessionTimeout: 1800000, // 30 minutos
maxTimeDrift: 300000, // 5 minutos
adminKeys: [],
- protectedRoutes: ["/api/admin/*", "/api/protected/*"],
- publicRoutes: ["/api/auth/*", "/api/health", "/api/docs"],
enableMetrics: true
},
+ // CLI Commands
+ commands: [
+ makeProtectedRouteCommand
+ ],
+
setup: async (context: PluginContext) => {
- const config = getPluginConfig(context)
-
- if (!config.enabled) {
+ // ✅ Plugin usa sua própria configuração (já importada no topo)
+ if (!cryptoAuthConfig.enabled) {
context.logger.info('Crypto Auth plugin desabilitado por configuração')
return
}
- // Inicializar serviço de autenticação
+ // Inicializar serviço de autenticação (SEM SESSÕES)
const authService = new CryptoAuthService({
- sessionTimeout: config.sessionTimeout,
- maxTimeDrift: config.maxTimeDrift,
- adminKeys: config.adminKeys,
+ maxTimeDrift: cryptoAuthConfig.maxTimeDrift,
+ adminKeys: cryptoAuthConfig.adminKeys,
logger: context.logger
})
- // Inicializar middleware de autenticação
+ // Inicializar middleware de autenticação (sem path matching)
const authMiddleware = new AuthMiddleware(authService, {
- protectedRoutes: config.protectedRoutes,
- publicRoutes: config.publicRoutes,
logger: context.logger
})
- // Armazenar instâncias no contexto para uso posterior
- ;(context as any).authService = authService
- ;(context as any).authMiddleware = authMiddleware
-
- // Registrar rotas de autenticação
- context.app.group("/api/auth", (app: any) => {
- // Rota para inicializar sessão
- app.post("/session/init", async ({ body, set }: any) => {
- try {
- const result = await authService.initializeSession(body)
- return result
- } catch (error) {
- context.logger.error("Erro ao inicializar sessão", { error })
- set.status = 500
- return { success: false, error: "Erro interno do servidor" }
- }
- })
-
- // Rota para validar sessão
- app.post("/session/validate", async ({ body, set }: any) => {
- try {
- const result = await authService.validateSession(body)
- return result
- } catch (error) {
- context.logger.error("Erro ao validar sessão", { error })
- set.status = 500
- return { success: false, error: "Erro interno do servidor" }
- }
- })
+ // Armazenar instâncias no contexto global
+ ;(global as any).cryptoAuthService = authService
+ ;(global as any).cryptoAuthMiddleware = authMiddleware
- // Rota para obter informações da sessão
- app.get("/session/info", async ({ headers, set }: any) => {
- try {
- const sessionId = headers['x-session-id']
- if (!sessionId) {
- set.status = 401
- return { success: false, error: "Session ID não fornecido" }
- }
-
- const sessionInfo = await authService.getSessionInfo(sessionId)
- return { success: true, session: sessionInfo }
- } catch (error) {
- context.logger.error("Erro ao obter informações da sessão", { error })
- set.status = 500
- return { success: false, error: "Erro interno do servidor" }
- }
- })
-
- // Rota para logout
- app.post("/session/logout", async ({ headers, set }: any) => {
- try {
- const sessionId = headers['x-session-id']
- if (!sessionId) {
- set.status = 401
- return { success: false, error: "Session ID não fornecido" }
- }
-
- await authService.destroySession(sessionId)
- return { success: true, message: "Sessão encerrada com sucesso" }
- } catch (error) {
- context.logger.error("Erro ao encerrar sessão", { error })
- set.status = 500
- return { success: false, error: "Erro interno do servidor" }
- }
- })
- })
-
- context.logger.info("Crypto Auth plugin inicializado com sucesso", {
- sessionTimeout: config.sessionTimeout,
- adminKeys: config.adminKeys.length,
- protectedRoutes: config.protectedRoutes.length
+ context.logger.info("✅ Crypto Auth plugin inicializado", {
+ mode: 'middleware-based',
+ maxTimeDrift: cryptoAuthConfig.maxTimeDrift,
+ adminKeys: cryptoAuthConfig.adminKeys.length,
+ usage: 'Use cryptoAuthRequired(), cryptoAuthAdmin(), cryptoAuthOptional() nas rotas'
})
},
- onRequest: async (context: RequestContext) => {
- const pluginContext = (context as any).pluginContext
- if (!pluginContext?.authMiddleware) return
-
- // Aplicar middleware de autenticação
- const authResult = await pluginContext.authMiddleware.authenticate(context)
-
- if (!authResult.success && authResult.required) {
- // Marcar como handled para evitar processamento adicional
- ;(context as any).handled = true
- ;(context as any).authError = authResult.error
- } else if (authResult.success && authResult.user) {
- // Adicionar usuário ao contexto
- ;(context as any).user = authResult.user
- }
- },
+ // @ts-ignore - plugin property não está no tipo oficial mas é suportada
+ plugin: new Elysia({ prefix: "/api/auth" })
+ .get("/info", () => ({
+ name: "FluxStack Crypto Auth",
+ description: "Autenticação baseada em assinatura Ed25519",
+ version: "1.0.0",
+ mode: "middleware-based",
+ how_it_works: {
+ step1: "Cliente gera par de chaves Ed25519 (pública + privada) localmente",
+ step2: "Cliente armazena chave privada no navegador (NUNCA envia ao servidor)",
+ step3: "Para cada requisição, cliente assina com chave privada",
+ step4: "Cliente envia: chave pública + assinatura + dados",
+ step5: "Servidor valida assinatura usando chave pública recebida"
+ },
+ required_headers: {
+ "x-public-key": "Chave pública Ed25519 (hex 64 chars)",
+ "x-timestamp": "Timestamp da requisição (milliseconds)",
+ "x-nonce": "Nonce aleatório (previne replay)",
+ "x-signature": "Assinatura Ed25519 da mensagem (hex)"
+ },
+ admin_keys: (global as any).cryptoAuthService?.getStats().adminKeys || 0,
+ usage: {
+ required: "import { cryptoAuthRequired } from '@/plugins/crypto-auth/server'",
+ admin: "import { cryptoAuthAdmin } from '@/plugins/crypto-auth/server'",
+ optional: "import { cryptoAuthOptional } from '@/plugins/crypto-auth/server'",
+ permissions: "import { cryptoAuthPermissions } from '@/plugins/crypto-auth/server'"
+ }
+ })),
onResponse: async (context: ResponseContext) => {
- const config = getPluginConfig((context as any).pluginContext)
-
- if (config.enableMetrics) {
- // Log métricas de autenticação
- const user = (context as any).user
- const authError = (context as any).authError
-
- if (user) {
- ;((context as any).pluginContext?.logger || console).debug("Requisição autenticada", {
- sessionId: user.sessionId,
- isAdmin: user.isAdmin,
- path: context.path,
- method: context.method,
- duration: context.duration
- })
- } else if (authError) {
- ;((context as any).pluginContext?.logger || console).warn("Falha na autenticação", {
- error: authError,
- path: context.path,
- method: context.method
- })
- }
+ if (!cryptoAuthConfig.enableMetrics) return
+
+ // Log métricas de autenticação
+ const user = (context as any).user
+ const authError = (context as any).authError
+
+ if (user) {
+ console.debug("Requisição autenticada", {
+ publicKey: user.publicKey?.substring(0, 8) + "...",
+ isAdmin: user.isAdmin,
+ path: context.path,
+ method: context.method,
+ duration: context.duration
+ })
+ } else if (authError) {
+ console.warn("Falha na autenticação", {
+ error: authError,
+ path: context.path,
+ method: context.method
+ })
}
},
onServerStart: async (context: PluginContext) => {
- const config = getPluginConfig(context)
-
- if (config.enabled) {
- context.logger.info("Crypto Auth plugin ativo", {
- protectedRoutes: config.protectedRoutes.length,
- publicRoutes: config.publicRoutes.length,
- adminKeys: config.adminKeys.length
+ if (cryptoAuthConfig.enabled) {
+ context.logger.info("✅ Crypto Auth plugin ativo", {
+ mode: 'middleware-based',
+ adminKeys: cryptoAuthConfig.adminKeys.length,
+ maxTimeDrift: `${cryptoAuthConfig.maxTimeDrift}ms`,
+ usage: 'Use cryptoAuthRequired(), cryptoAuthAdmin() nas rotas'
})
}
}
}
-// Helper function para obter configuração do plugin
-function getPluginConfig(context: PluginContext) {
- const pluginConfig = context.config.plugins.config?.['crypto-auth'] || {}
- return { ...cryptoAuthPlugin.defaultConfig, ...pluginConfig }
-}
-
export default cryptoAuthPlugin
\ No newline at end of file
diff --git a/plugins/crypto-auth/package.json b/plugins/crypto-auth/package.json
index fb980c88..19366ad1 100644
--- a/plugins/crypto-auth/package.json
+++ b/plugins/crypto-auth/package.json
@@ -39,14 +39,15 @@
}
},
"dependencies": {
- "@noble/curves": "^1.2.0",
- "@noble/hashes": "^1.3.2"
+ "@noble/curves": "1.2.0",
+ "@noble/hashes": "1.3.2"
},
"devDependencies": {
"@types/react": "^18.0.0",
"typescript": "^5.0.0"
},
"fluxstack": {
+ "plugin": true,
"version": "^1.0.0",
"hooks": [
"setup",
diff --git a/plugins/crypto-auth/plugin.json b/plugins/crypto-auth/plugin.json
deleted file mode 100644
index 029de5c7..00000000
--- a/plugins/crypto-auth/plugin.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "crypto-auth",
- "version": "1.0.0",
- "description": "Sistema de autenticação baseado em criptografia Ed25519 para FluxStack",
- "author": "FluxStack Team",
- "license": "MIT",
- "main": "index.ts",
- "keywords": ["fluxstack", "plugin", "authentication", "ed25519", "cryptography"],
- "fluxstack": {
- "plugin": true,
- "version": "^1.0.0",
- "category": "auth",
- "tags": ["authentication", "ed25519", "cryptography", "security"]
- },
- "dependencies": {
- "@noble/curves": "^1.2.0",
- "@noble/hashes": "^1.3.2",
- "react": ">=16.8.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- },
- "files": [
- "index.ts",
- "server/",
- "client/",
- "README.md"
- ]
-}
\ No newline at end of file
diff --git a/plugins/crypto-auth/server/AuthMiddleware.ts b/plugins/crypto-auth/server/AuthMiddleware.ts
index cbccbda4..dd3dba15 100644
--- a/plugins/crypto-auth/server/AuthMiddleware.ts
+++ b/plugins/crypto-auth/server/AuthMiddleware.ts
@@ -1,6 +1,6 @@
/**
- * Middleware de Autenticação
- * Intercepta requisições e valida autenticação
+ * Middleware de Autenticação Simplificado
+ * Apenas valida autenticação - routing é feito pelos middlewares Elysia
*/
import type { RequestContext } from '../../../core/plugins/types'
@@ -14,57 +14,35 @@ export interface Logger {
}
export interface AuthMiddlewareConfig {
- protectedRoutes: string[]
- publicRoutes: string[]
logger?: Logger
}
export interface AuthMiddlewareResult {
success: boolean
- required: boolean
error?: string
user?: {
- sessionId: string
+ publicKey: string
isAdmin: boolean
- isSuperAdmin: boolean
permissions: string[]
}
}
export class AuthMiddleware {
private authService: CryptoAuthService
- private config: AuthMiddlewareConfig
private logger?: Logger
- constructor(authService: CryptoAuthService, config: AuthMiddlewareConfig) {
+ constructor(authService: CryptoAuthService, config: AuthMiddlewareConfig = {}) {
this.authService = authService
- this.config = config
this.logger = config.logger
}
/**
- * Autenticar requisição
+ * Autenticar requisição (sem path matching - é responsabilidade dos middlewares Elysia)
*/
async authenticate(context: RequestContext): Promise {
const path = context.path
const method = context.method
- // Verificar se a rota é pública
- if (this.isPublicRoute(path)) {
- return {
- success: true,
- required: false
- }
- }
-
- // Verificar se a rota requer autenticação
- if (!this.isProtectedRoute(path)) {
- return {
- success: true,
- required: false
- }
- }
-
// Extrair headers de autenticação
const authHeaders = this.extractAuthHeaders(context.headers)
if (!authHeaders) {
@@ -76,15 +54,14 @@ export class AuthMiddleware {
return {
success: false,
- required: true,
error: "Headers de autenticação obrigatórios"
}
}
- // Validar sessão
+ // Validar assinatura da requisição
try {
- const validationResult = await this.authService.validateSession({
- sessionId: authHeaders.sessionId,
+ const validationResult = await this.authService.validateRequest({
+ publicKey: authHeaders.publicKey,
timestamp: authHeaders.timestamp,
nonce: authHeaders.nonce,
signature: authHeaders.signature,
@@ -92,16 +69,15 @@ export class AuthMiddleware {
})
if (!validationResult.success) {
- this.logger?.warn("Falha na validação da sessão", {
+ this.logger?.warn("Falha na validação da assinatura", {
path,
method,
- sessionId: authHeaders.sessionId.substring(0, 8) + "...",
+ publicKey: authHeaders.publicKey.substring(0, 8) + "...",
error: validationResult.error
})
return {
success: false,
- required: true,
error: validationResult.error
}
}
@@ -109,13 +85,12 @@ export class AuthMiddleware {
this.logger?.debug("Requisição autenticada com sucesso", {
path,
method,
- sessionId: authHeaders.sessionId.substring(0, 8) + "...",
+ publicKey: authHeaders.publicKey.substring(0, 8) + "...",
isAdmin: validationResult.user?.isAdmin
})
return {
success: true,
- required: true,
user: validationResult.user
}
} catch (error) {
@@ -127,53 +102,26 @@ export class AuthMiddleware {
return {
success: false,
- required: true,
error: "Erro interno de autenticação"
}
}
}
- /**
- * Verificar se a rota é pública
- */
- private isPublicRoute(path: string): boolean {
- return this.config.publicRoutes.some(route => {
- if (route.endsWith('/*')) {
- const prefix = route.slice(0, -2)
- return path.startsWith(prefix)
- }
- return path === route
- })
- }
-
- /**
- * Verificar se a rota é protegida
- */
- private isProtectedRoute(path: string): boolean {
- return this.config.protectedRoutes.some(route => {
- if (route.endsWith('/*')) {
- const prefix = route.slice(0, -2)
- return path.startsWith(prefix)
- }
- return path === route
- })
- }
-
/**
* Extrair headers de autenticação
*/
private extractAuthHeaders(headers: Record): {
- sessionId: string
+ publicKey: string
timestamp: number
nonce: string
signature: string
} | null {
- const sessionId = headers['x-session-id']
+ const publicKey = headers['x-public-key']
const timestampStr = headers['x-timestamp']
const nonce = headers['x-nonce']
const signature = headers['x-signature']
- if (!sessionId || !timestampStr || !nonce || !signature) {
+ if (!publicKey || !timestampStr || !nonce || !signature) {
return null
}
@@ -183,7 +131,7 @@ export class AuthMiddleware {
}
return {
- sessionId,
+ publicKey,
timestamp,
nonce,
signature
@@ -207,7 +155,7 @@ export class AuthMiddleware {
}
/**
- * Verificar se usuário tem permissão para acessar rota
+ * Verificar se usuário tem permissão
*/
hasPermission(user: any, requiredPermission: string): boolean {
if (!user || !user.permissions) {
@@ -225,13 +173,9 @@ export class AuthMiddleware {
}
/**
- * Obter estatísticas do middleware
+ * Obter estatísticas do serviço de autenticação
*/
getStats() {
- return {
- protectedRoutes: this.config.protectedRoutes.length,
- publicRoutes: this.config.publicRoutes.length,
- authService: this.authService.getStats()
- }
+ return this.authService.getStats()
}
-}
\ No newline at end of file
+}
diff --git a/plugins/crypto-auth/server/CryptoAuthService.ts b/plugins/crypto-auth/server/CryptoAuthService.ts
index 7419bcc9..e4a1d765 100644
--- a/plugins/crypto-auth/server/CryptoAuthService.ts
+++ b/plugins/crypto-auth/server/CryptoAuthService.ts
@@ -1,11 +1,13 @@
/**
* Serviço de Autenticação Criptográfica
- * Implementa autenticação baseada em Ed25519
+ * Implementa autenticação baseada em Ed25519 - SEM SESSÕES
+ * Cada requisição é validada pela assinatura da chave pública
*/
import { ed25519 } from '@noble/curves/ed25519'
import { sha256 } from '@noble/hashes/sha256'
-import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
+import { hexToBytes } from '@noble/hashes/utils'
+
export interface Logger {
debug(message: string, meta?: any): void
info(message: string, meta?: any): void
@@ -13,221 +15,128 @@ export interface Logger {
error(message: string, meta?: any): void
}
-export interface SessionData {
- sessionId: string
- publicKey: string
- createdAt: Date
- lastUsed: Date
- isAdmin: boolean
- permissions: string[]
-}
-
export interface AuthResult {
success: boolean
- sessionId?: string
error?: string
user?: {
- sessionId: string
+ publicKey: string
isAdmin: boolean
- isSuperAdmin: boolean
permissions: string[]
}
}
export interface CryptoAuthConfig {
- sessionTimeout: number
maxTimeDrift: number
adminKeys: string[]
logger?: Logger
}
export class CryptoAuthService {
- private sessions: Map = new Map()
private config: CryptoAuthConfig
private logger?: Logger
+ private usedNonces: Map = new Map() // Para prevenir replay attacks
constructor(config: CryptoAuthConfig) {
this.config = config
this.logger = config.logger
- // Limpar sessões expiradas a cada 5 minutos
+ // Limpar nonces antigos a cada 5 minutos
setInterval(() => {
- this.cleanupExpiredSessions()
+ this.cleanupOldNonces()
}, 5 * 60 * 1000)
}
/**
- * Inicializar uma nova sessão
+ * Validar assinatura de requisição
+ * PRINCIPAL: Valida se assinatura é válida para a chave pública fornecida
*/
- async initializeSession(data: { publicKey?: string }): Promise {
- try {
- let publicKey: string
-
- if (data.publicKey) {
- // Validar chave pública fornecida
- if (!this.isValidPublicKey(data.publicKey)) {
- return {
- success: false,
- error: "Chave pública inválida"
- }
- }
- publicKey = data.publicKey
- } else {
- // Gerar novo par de chaves
- const privateKey = ed25519.utils.randomPrivateKey()
- publicKey = bytesToHex(ed25519.getPublicKey(privateKey))
- }
-
- const sessionId = publicKey
- const isAdmin = this.config.adminKeys.includes(publicKey)
-
- const sessionData: SessionData = {
- sessionId,
- publicKey,
- createdAt: new Date(),
- lastUsed: new Date(),
- isAdmin,
- permissions: isAdmin ? ['admin', 'read', 'write'] : ['read']
- }
-
- this.sessions.set(sessionId, sessionData)
-
- this.logger?.info("Nova sessão inicializada", {
- sessionId: sessionId.substring(0, 8) + "...",
- isAdmin,
- permissions: sessionData.permissions
- })
-
- return {
- success: true,
- sessionId,
- user: {
- sessionId,
- isAdmin,
- isSuperAdmin: isAdmin,
- permissions: sessionData.permissions
- }
- }
- } catch (error) {
- this.logger?.error("Erro ao inicializar sessão", { error })
- return {
- success: false,
- error: "Erro interno ao inicializar sessão"
- }
- }
- }
-
- /**
- * Validar uma sessão com assinatura
- */
- async validateSession(data: {
- sessionId: string
+ async validateRequest(data: {
+ publicKey: string
timestamp: number
nonce: string
signature: string
message?: string
}): Promise {
try {
- const { sessionId, timestamp, nonce, signature, message = "" } = data
+ const { publicKey, timestamp, nonce, signature, message = "" } = data
- // Verificar se a sessão existe
- const session = this.sessions.get(sessionId)
- if (!session) {
+ // Validar chave pública
+ if (!this.isValidPublicKey(publicKey)) {
return {
success: false,
- error: "Sessão não encontrada"
+ error: "Chave pública inválida"
}
}
- // Verificar se a sessão não expirou
+ // Verificar drift de tempo (previne replay de requisições antigas)
const now = Date.now()
- const sessionAge = now - session.lastUsed.getTime()
- if (sessionAge > this.config.sessionTimeout) {
- this.sessions.delete(sessionId)
+ const timeDrift = Math.abs(now - timestamp)
+ if (timeDrift > this.config.maxTimeDrift) {
return {
success: false,
- error: "Sessão expirada"
+ error: "Timestamp inválido ou expirado"
}
}
- // Verificar drift de tempo
- const timeDrift = Math.abs(now - timestamp)
- if (timeDrift > this.config.maxTimeDrift) {
+ // Verificar nonce (previne replay attacks)
+ const nonceKey = `${publicKey}:${nonce}`
+ if (this.usedNonces.has(nonceKey)) {
return {
success: false,
- error: "Timestamp inválido"
+ error: "Nonce já utilizado (possível replay attack)"
}
}
// Construir mensagem para verificação
- const messageToVerify = `${sessionId}:${timestamp}:${nonce}:${message}`
+ const messageToVerify = `${publicKey}:${timestamp}:${nonce}:${message}`
const messageHash = sha256(new TextEncoder().encode(messageToVerify))
- // Verificar assinatura
- const publicKeyBytes = hexToBytes(session.publicKey)
+ // Verificar assinatura usando chave pública
+ const publicKeyBytes = hexToBytes(publicKey)
const signatureBytes = hexToBytes(signature)
-
+
const isValidSignature = ed25519.verify(signatureBytes, messageHash, publicKeyBytes)
-
+
if (!isValidSignature) {
+ this.logger?.warn("Assinatura inválida", {
+ publicKey: publicKey.substring(0, 8) + "..."
+ })
return {
success: false,
error: "Assinatura inválida"
}
}
- // Atualizar último uso da sessão
- session.lastUsed = new Date()
+ // Marcar nonce como usado
+ this.usedNonces.set(nonceKey, timestamp)
+
+ // Verificar se é admin
+ const isAdmin = this.config.adminKeys.includes(publicKey)
+ const permissions = isAdmin ? ['admin', 'read', 'write', 'delete'] : ['read']
+
+ this.logger?.debug("Requisição autenticada", {
+ publicKey: publicKey.substring(0, 8) + "...",
+ isAdmin,
+ permissions
+ })
return {
success: true,
- sessionId,
user: {
- sessionId,
- isAdmin: session.isAdmin,
- isSuperAdmin: session.isAdmin,
- permissions: session.permissions
+ publicKey,
+ isAdmin,
+ permissions
}
}
} catch (error) {
- this.logger?.error("Erro ao validar sessão", { error })
+ this.logger?.error("Erro ao validar requisição", { error })
return {
success: false,
- error: "Erro interno ao validar sessão"
+ error: "Erro interno ao validar requisição"
}
}
}
- /**
- * Obter informações da sessão
- */
- async getSessionInfo(sessionId: string): Promise {
- const session = this.sessions.get(sessionId)
- if (!session) {
- return null
- }
-
- // Verificar se não expirou
- const now = Date.now()
- const sessionAge = now - session.lastUsed.getTime()
- if (sessionAge > this.config.sessionTimeout) {
- this.sessions.delete(sessionId)
- return null
- }
-
- return { ...session }
- }
-
- /**
- * Destruir uma sessão
- */
- async destroySession(sessionId: string): Promise {
- this.sessions.delete(sessionId)
- this.logger?.info("Sessão destruída", {
- sessionId: sessionId.substring(0, 8) + "..."
- })
- }
-
/**
* Verificar se uma chave pública é válida
*/
@@ -245,49 +154,33 @@ export class CryptoAuthService {
}
/**
- * Limpar sessões expiradas
+ * Limpar nonces antigos (previne crescimento infinito da memória)
*/
- private cleanupExpiredSessions(): void {
+ private cleanupOldNonces(): void {
const now = Date.now()
+ const maxAge = this.config.maxTimeDrift * 2 // Dobro do tempo máximo permitido
let cleanedCount = 0
- for (const [sessionId, session] of this.sessions.entries()) {
- const sessionAge = now - session.lastUsed.getTime()
- if (sessionAge > this.config.sessionTimeout) {
- this.sessions.delete(sessionId)
+ for (const [nonceKey, timestamp] of this.usedNonces.entries()) {
+ if (now - timestamp > maxAge) {
+ this.usedNonces.delete(nonceKey)
cleanedCount++
}
}
if (cleanedCount > 0) {
- this.logger?.debug(`Limpeza de sessões: ${cleanedCount} sessões expiradas removidas`)
+ this.logger?.debug(`Limpeza de nonces: ${cleanedCount} nonces antigos removidos`)
}
}
/**
- * Obter estatísticas das sessões
+ * Obter estatísticas do serviço
*/
getStats() {
- const now = Date.now()
- let activeSessions = 0
- let adminSessions = 0
-
- for (const session of this.sessions.values()) {
- const sessionAge = now - session.lastUsed.getTime()
- if (sessionAge <= this.config.sessionTimeout) {
- activeSessions++
- if (session.isAdmin) {
- adminSessions++
- }
- }
- }
-
return {
- totalSessions: this.sessions.size,
- activeSessions,
- adminSessions,
- sessionTimeout: this.config.sessionTimeout,
- adminKeys: this.config.adminKeys.length
+ usedNoncesCount: this.usedNonces.size,
+ adminKeys: this.config.adminKeys.length,
+ maxTimeDrift: this.config.maxTimeDrift
}
}
}
\ No newline at end of file
diff --git a/plugins/crypto-auth/server/index.ts b/plugins/crypto-auth/server/index.ts
index 6bacaa82..9935ec32 100644
--- a/plugins/crypto-auth/server/index.ts
+++ b/plugins/crypto-auth/server/index.ts
@@ -3,7 +3,20 @@
*/
export { CryptoAuthService } from './CryptoAuthService'
-export type { SessionData, AuthResult, CryptoAuthConfig } from './CryptoAuthService'
+export type { AuthResult, CryptoAuthConfig } from './CryptoAuthService'
export { AuthMiddleware } from './AuthMiddleware'
-export type { AuthMiddlewareConfig, AuthMiddlewareResult } from './AuthMiddleware'
\ No newline at end of file
+export type { AuthMiddlewareConfig, AuthMiddlewareResult } from './AuthMiddleware'
+
+// Middlewares Elysia
+export {
+ cryptoAuthRequired,
+ cryptoAuthAdmin,
+ cryptoAuthPermissions,
+ cryptoAuthOptional,
+ getCryptoAuthUser,
+ isCryptoAuthAuthenticated,
+ isCryptoAuthAdmin,
+ hasCryptoAuthPermission
+} from './middlewares'
+export type { CryptoAuthUser, CryptoAuthMiddlewareOptions } from './middlewares'
\ No newline at end of file
diff --git a/plugins/crypto-auth/server/middlewares.ts b/plugins/crypto-auth/server/middlewares.ts
new file mode 100644
index 00000000..3d38f7b7
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares.ts
@@ -0,0 +1,19 @@
+/**
+ * Crypto Auth Middlewares
+ * Middlewares Elysia para autenticação criptográfica
+ *
+ * Uso:
+ * ```typescript
+ * import { cryptoAuthRequired, cryptoAuthAdmin } from '@/plugins/crypto-auth/server'
+ *
+ * export const myRoutes = new Elysia()
+ * .use(cryptoAuthRequired())
+ * .get('/protected', ({ request }) => {
+ * const user = getCryptoAuthUser(request)
+ * return { user }
+ * })
+ * ```
+ */
+
+// Re-export tudo do módulo middlewares
+export * from './middlewares/index'
diff --git a/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts b/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts
new file mode 100644
index 00000000..9b84fada
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts
@@ -0,0 +1,65 @@
+/**
+ * Middleware que REQUER privilégios de administrador
+ * Bloqueia requisições não autenticadas (401) ou sem privilégios admin (403)
+ */
+
+import { Elysia } from 'elysia'
+import { createGuard } from '@/core/server/middleware/elysia-helpers'
+import type { Logger } from '@/core/utils/logger'
+import { validateAuthSync, type CryptoAuthUser } from './helpers'
+
+export interface CryptoAuthMiddlewareOptions {
+ logger?: Logger
+}
+
+export const cryptoAuthAdmin = (options: CryptoAuthMiddlewareOptions = {}) => {
+ return new Elysia({ name: 'crypto-auth-admin' })
+ .derive(async ({ request }) => {
+ const result = await validateAuthSync(request as Request, options.logger)
+
+ if (result.success && result.user) {
+ ;(request as any).user = result.user
+ }
+
+ return {}
+ })
+ .use(
+ createGuard({
+ name: 'crypto-auth-admin-check',
+ check: ({ request }) => {
+ const user = (request as any).user as CryptoAuthUser | undefined
+ return user && user.isAdmin
+ },
+ onFail: (set, { request }) => {
+ const user = (request as any).user as CryptoAuthUser | undefined
+
+ if (!user) {
+ set.status = 401
+ return {
+ error: {
+ message: 'Authentication required',
+ code: 'CRYPTO_AUTH_REQUIRED',
+ statusCode: 401
+ }
+ }
+ }
+
+ options.logger?.warn('Admin access denied', {
+ publicKey: user.publicKey.substring(0, 8) + '...',
+ permissions: user.permissions
+ })
+
+ set.status = 403
+ return {
+ error: {
+ message: 'Admin privileges required',
+ code: 'ADMIN_REQUIRED',
+ statusCode: 403,
+ yourPermissions: user.permissions
+ }
+ }
+ }
+ })
+ )
+ .as('plugin')
+}
diff --git a/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts b/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts
new file mode 100644
index 00000000..106d79c3
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts
@@ -0,0 +1,26 @@
+/**
+ * Middleware OPCIONAL - adiciona user se autenticado, mas não requer
+ * Não bloqueia requisições não autenticadas - permite acesso público
+ */
+
+import { Elysia } from 'elysia'
+import type { Logger } from '@/core/utils/logger'
+import { validateAuthSync } from './helpers'
+
+export interface CryptoAuthMiddlewareOptions {
+ logger?: Logger
+}
+
+export const cryptoAuthOptional = (options: CryptoAuthMiddlewareOptions = {}) => {
+ return new Elysia({ name: 'crypto-auth-optional' })
+ .derive(async ({ request }) => {
+ const result = await validateAuthSync(request as Request, options.logger)
+
+ if (result.success && result.user) {
+ ;(request as any).user = result.user
+ }
+
+ return {}
+ })
+ .as('plugin')
+}
diff --git a/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts b/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts
new file mode 100644
index 00000000..987557bf
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts
@@ -0,0 +1,76 @@
+/**
+ * Middleware que REQUER permissões específicas
+ * Bloqueia requisições sem as permissões necessárias (403)
+ */
+
+import { Elysia } from 'elysia'
+import { createGuard } from '@/core/server/middleware/elysia-helpers'
+import type { Logger } from '@/core/utils/logger'
+import { validateAuthSync, type CryptoAuthUser } from './helpers'
+
+export interface CryptoAuthMiddlewareOptions {
+ logger?: Logger
+}
+
+export const cryptoAuthPermissions = (
+ requiredPermissions: string[],
+ options: CryptoAuthMiddlewareOptions = {}
+) => {
+ return new Elysia({ name: 'crypto-auth-permissions' })
+ .derive(async ({ request }) => {
+ const result = await validateAuthSync(request as Request, options.logger)
+
+ if (result.success && result.user) {
+ ;(request as any).user = result.user
+ }
+
+ return {}
+ })
+ .use(
+ createGuard({
+ name: 'crypto-auth-permissions-check',
+ check: ({ request }) => {
+ const user = (request as any).user as CryptoAuthUser | undefined
+
+ if (!user) return false
+
+ const userPermissions = user.permissions
+ return requiredPermissions.every(
+ perm => userPermissions.includes(perm) || userPermissions.includes('admin')
+ )
+ },
+ onFail: (set, { request }) => {
+ const user = (request as any).user as CryptoAuthUser | undefined
+
+ if (!user) {
+ set.status = 401
+ return {
+ error: {
+ message: 'Authentication required',
+ code: 'CRYPTO_AUTH_REQUIRED',
+ statusCode: 401
+ }
+ }
+ }
+
+ options.logger?.warn('Permission denied', {
+ publicKey: user.publicKey.substring(0, 8) + '...',
+ required: requiredPermissions,
+ has: user.permissions
+ })
+
+ set.status = 403
+ return {
+ error: {
+ message: 'Insufficient permissions',
+ code: 'PERMISSION_DENIED',
+ statusCode: 403,
+ required: requiredPermissions,
+ yours: user.permissions
+ }
+ }
+ }
+ })
+ )
+ .as('plugin')
+}
diff --git a/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts b/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts
new file mode 100644
index 00000000..5450141d
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts
@@ -0,0 +1,45 @@
+/**
+ * Middleware que REQUER autenticação
+ * Bloqueia requisições não autenticadas com 401
+ */
+
+import { Elysia } from 'elysia'
+import { createGuard } from '@/core/server/middleware/elysia-helpers'
+import type { Logger } from '@/core/utils/logger'
+import { validateAuthSync } from './helpers'
+
+export interface CryptoAuthMiddlewareOptions {
+ logger?: Logger
+}
+
+export const cryptoAuthRequired = (options: CryptoAuthMiddlewareOptions = {}) => {
+ return new Elysia({ name: 'crypto-auth-required' })
+ .derive(async ({ request }) => {
+ const result = await validateAuthSync(request as Request, options.logger)
+
+ if (result.success && result.user) {
+ ;(request as any).user = result.user
+ }
+
+ return {}
+ })
+ .use(
+ createGuard({
+ name: 'crypto-auth-check',
+ check: ({ request }) => {
+ return !!(request as any).user
+ },
+ onFail: (set) => {
+ set.status = 401
+ return {
+ error: {
+ message: 'Authentication required',
+ code: 'CRYPTO_AUTH_REQUIRED',
+ statusCode: 401
+ }
+ }
+ }
+ })
+ )
+ .as('plugin')
+}
diff --git a/plugins/crypto-auth/server/middlewares/helpers.ts b/plugins/crypto-auth/server/middlewares/helpers.ts
new file mode 100644
index 00000000..3bed8a16
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/helpers.ts
@@ -0,0 +1,140 @@
+/**
+ * Crypto Auth Middleware Helpers
+ * Funções compartilhadas para validação de autenticação
+ */
+
+import type { Logger } from '@/core/utils/logger'
+
+export interface CryptoAuthUser {
+ publicKey: string
+ isAdmin: boolean
+ permissions: string[]
+}
+
+/**
+ * Get auth service from global
+ */
+export function getAuthService() {
+ const service = (global as any).cryptoAuthService
+ if (!service) {
+ throw new Error('CryptoAuthService not initialized. Make sure crypto-auth plugin is loaded.')
+ }
+ return service
+}
+
+/**
+ * Get auth middleware from global
+ */
+export function getAuthMiddleware() {
+ const middleware = (global as any).cryptoAuthMiddleware
+ if (!middleware) {
+ throw new Error('AuthMiddleware not initialized. Make sure crypto-auth plugin is loaded.')
+ }
+ return middleware
+}
+
+/**
+ * Extract and validate authentication from request
+ * Versão SÍNCRONA para evitar problemas com Elysia
+ */
+export function extractAuthHeaders(request: Request): {
+ publicKey: string
+ timestamp: number
+ nonce: string
+ signature: string
+} | null {
+ const headers = request.headers
+ const publicKey = headers.get('x-public-key')
+ const timestampStr = headers.get('x-timestamp')
+ const nonce = headers.get('x-nonce')
+ const signature = headers.get('x-signature')
+
+ if (!publicKey || !timestampStr || !nonce || !signature) {
+ return null
+ }
+
+ const timestamp = parseInt(timestampStr, 10)
+ if (isNaN(timestamp)) {
+ return null
+ }
+
+ return { publicKey, timestamp, nonce, signature }
+}
+
+/**
+ * Build message for signature verification
+ */
+export function buildMessage(request: Request): string {
+ const url = new URL(request.url)
+ return `${request.method}:${url.pathname}`
+}
+
+/**
+ * Validate authentication synchronously
+ */
+export async function validateAuthSync(request: Request, logger?: Logger): Promise<{
+ success: boolean
+ user?: CryptoAuthUser
+ error?: string
+}> {
+ try {
+ const authHeaders = extractAuthHeaders(request)
+
+ if (!authHeaders) {
+ return {
+ success: false,
+ error: 'Missing authentication headers'
+ }
+ }
+
+ const authService = getAuthService()
+ const message = buildMessage(request)
+
+ const result = await authService.validateRequest({
+ publicKey: authHeaders.publicKey,
+ timestamp: authHeaders.timestamp,
+ nonce: authHeaders.nonce,
+ signature: authHeaders.signature,
+ message
+ })
+
+ return result
+ } catch (error) {
+ logger?.error('Auth validation error', { error })
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ }
+ }
+}
+
+/**
+ * Helper: Obter usuário autenticado do request
+ */
+export function getCryptoAuthUser(request: Request): CryptoAuthUser | null {
+ return (request as any).user || null
+}
+
+/**
+ * Helper: Verificar se request está autenticado
+ */
+export function isCryptoAuthAuthenticated(request: Request): boolean {
+ return !!(request as any).user
+}
+
+/**
+ * Helper: Verificar se usuário é admin
+ */
+export function isCryptoAuthAdmin(request: Request): boolean {
+ const user = getCryptoAuthUser(request)
+ return user?.isAdmin || false
+}
+
+/**
+ * Helper: Verificar se usuário tem permissão específica
+ */
+export function hasCryptoAuthPermission(request: Request, permission: string): boolean {
+ const user = getCryptoAuthUser(request)
+ if (!user) return false
+ return user.permissions.includes(permission) || user.permissions.includes('admin')
+}
diff --git a/plugins/crypto-auth/server/middlewares/index.ts b/plugins/crypto-auth/server/middlewares/index.ts
new file mode 100644
index 00000000..f938c2e8
--- /dev/null
+++ b/plugins/crypto-auth/server/middlewares/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Crypto Auth Middlewares
+ * Exports centralizados de todos os middlewares
+ */
+
+// Middlewares
+export { cryptoAuthRequired } from './cryptoAuthRequired'
+export { cryptoAuthAdmin } from './cryptoAuthAdmin'
+export { cryptoAuthOptional } from './cryptoAuthOptional'
+export { cryptoAuthPermissions } from './cryptoAuthPermissions'
+
+// Helpers
+export {
+ getCryptoAuthUser,
+ isCryptoAuthAuthenticated,
+ isCryptoAuthAdmin,
+ hasCryptoAuthPermission,
+ type CryptoAuthUser
+} from './helpers'
+
+// Types
+export type { CryptoAuthMiddlewareOptions } from './cryptoAuthRequired'
diff --git a/test-crypto-auth.ts b/test-crypto-auth.ts
new file mode 100644
index 00000000..0a3c9b69
--- /dev/null
+++ b/test-crypto-auth.ts
@@ -0,0 +1,101 @@
+/**
+ * Script de teste para validar autenticação criptográfica
+ */
+
+import { ed25519 } from '@noble/curves/ed25519'
+import { sha256 } from '@noble/hashes/sha256'
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
+
+async function testCryptoAuth() {
+ console.log('🔐 Testando Autenticação Criptográfica Ed25519\n')
+
+ // 1. Gerar par de chaves
+ console.log('1️⃣ Gerando par de chaves Ed25519...')
+ const privateKey = ed25519.utils.randomPrivateKey()
+ const publicKeyBytes = ed25519.getPublicKey(privateKey)
+ const publicKey = bytesToHex(publicKeyBytes)
+ const privateKeyHex = bytesToHex(privateKey)
+
+ console.log(` ✅ Chave pública: ${publicKey.substring(0, 16)}...`)
+ console.log(` ✅ Chave privada: ${privateKeyHex.substring(0, 16)}... (NUNCA enviar ao servidor!)\n`)
+
+ // 2. Preparar requisição
+ console.log('2️⃣ Preparando requisição assinada...')
+ const url = '/api/crypto-auth/protected'
+ const method = 'GET'
+ const timestamp = Date.now()
+ const nonce = generateNonce()
+
+ // 3. Construir mensagem para assinar
+ const message = `${method}:${url}`
+ const fullMessage = `${publicKey}:${timestamp}:${nonce}:${message}`
+
+ console.log(` 📝 Mensagem: ${fullMessage.substring(0, 50)}...\n`)
+
+ // 4. Assinar mensagem
+ console.log('3️⃣ Assinando mensagem com chave privada...')
+ const messageHash = sha256(new TextEncoder().encode(fullMessage))
+ const signatureBytes = ed25519.sign(messageHash, privateKey)
+ const signature = bytesToHex(signatureBytes)
+
+ console.log(` ✅ Assinatura: ${signature.substring(0, 32)}...\n`)
+
+ // 5. Fazer requisição ao servidor
+ console.log('4️⃣ Enviando requisição ao servidor...')
+ const response = await fetch('http://localhost:3000/api/crypto-auth/protected', {
+ method: 'GET',
+ headers: {
+ 'x-public-key': publicKey,
+ 'x-timestamp': timestamp.toString(),
+ 'x-nonce': nonce,
+ 'x-signature': signature
+ }
+ })
+
+ const data = await response.json()
+
+ console.log(` 📡 Status: ${response.status}`)
+ console.log(` 📦 Resposta:`, JSON.stringify(data, null, 2))
+
+ if (data.success) {
+ console.log('\n✅ SUCESSO! Assinatura validada corretamente pelo servidor!')
+ console.log(` 👤 Dados protegidos recebidos: ${data.data?.secretInfo}`)
+ } else {
+ console.log('\n❌ ERRO! Assinatura rejeitada pelo servidor')
+ console.log(` ⚠️ Erro: ${data.error}`)
+ }
+
+ // 6. Testar replay attack (reutilizar mesma assinatura)
+ console.log('\n5️⃣ Testando proteção contra replay attack...')
+ const replayResponse = await fetch('http://localhost:3000/api/crypto-auth/protected', {
+ method: 'GET',
+ headers: {
+ 'x-public-key': publicKey,
+ 'x-timestamp': timestamp.toString(),
+ 'x-nonce': nonce, // Mesmo nonce
+ 'x-signature': signature // Mesma assinatura
+ }
+ })
+
+ const replayData = await replayResponse.json()
+
+ console.log(` 📡 Replay Status: ${replayResponse.status}`)
+ console.log(` 📦 Replay Response:`, JSON.stringify(replayData, null, 2))
+
+ if (!replayData.success && replayData.error?.includes('nonce')) {
+ console.log(' ✅ Proteção funcionando! Replay attack bloqueado.')
+ } else if (replayResponse.status === 401) {
+ console.log(' ✅ Proteção funcionando! Replay attack bloqueado (status 401).')
+ } else {
+ console.log(' ⚠️ ATENÇÃO: Replay attack NÃO foi bloqueado!')
+ }
+}
+
+function generateNonce(): string {
+ const bytes = new Uint8Array(16)
+ crypto.getRandomValues(bytes)
+ return bytesToHex(bytes)
+}
+
+// Executar teste
+testCryptoAuth().catch(console.error)