[Tools] ns8-candados: herramienta unificada de seguridad (reemplaza candado.rb + secret_box.rb)

This commit is contained in:
Ricardo Monla
2026-02-28 08:37:44 -03:00
parent 72f1944090
commit 3555b788e7
6 changed files with 277 additions and 193 deletions
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env ruby
# script: candado.rb
# Uso: ruby scripts/candado.rb [minutos]
# Descripción: Genera un token de autorización temporal para que el Agente pueda ejecutar tareas críticas.
# Requiere que el usuario tenga claves SSH cargadas (prueba de identidad).
require 'json'
require 'fileutils'
TOKEN_FILE = File.expand_path('../../.agent_token', __dir__)
DEFAULT_DURATION_MINS = 5
# 1. Validación de Identidad (Proof of User Presence)
puts "🔐 Requerimiento de Protocolo: Autorización FRESCA."
puts "🧹 Limpiando identidades previas en el agente SSH para exigir validación..."
system("ssh-add -D > /dev/null 2>&1") # Borra claves cacheadas para forzar el prompt
puts "🔑 Por favor, ingresa tu passphrase para autorizar al Agente:"
# Intentamos cargar las claves interactivamente (ahora sí pedirá pass porque borramos las anteriores)
system("ssh-add")
# Verificamos que la carga haya sido exitosa
system("ssh-add -l > /dev/null 2>&1")
unless $?.success?
puts "❌ ERROR: No se detectaron identidades SSH."
puts " Debes completar el desafío de identidad (passphrase) para continuar."
exit 1
end
# 2. Generación del Token
duration = (ARGV[0] || DEFAULT_DURATION_MINS).to_i
expires_at = Time.now + (duration * 60)
token_data = {
created_at: Time.now.to_s,
expires_at: expires_at.to_s,
duration_mins: duration,
authorized_by: ENV['USER'] || 'unknown',
signature: rand(36**8).to_s(36) # Firma aleatoria simple
}
File.write(TOKEN_FILE, JSON.pretty_generate(token_data))
FileUtils.chmod(0600, TOKEN_FILE) # Solo lectura para el usuario
puts "\n🔓 VACANCIA DE SEGURIDAD ACTIVADA"
puts "--------------------------------"
puts "✅ Agente AUTORIZADO para operaciones críticas."
puts "⏳ Ventana de tiempo: #{duration} minutos."
puts "💀 Expira: #{expires_at.strftime('%H:%M:%S')}"
puts "🔑 Token: #{TOKEN_FILE}"
puts "\n>> Ahora puedes pedirle al Agente que ejecute sus tareas."
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'openssl'
require 'base64'
require 'json'
class SecretBox
ALGORITHM = 'aes-256-gcm'.freeze
def initialize(master_key_content)
# Generar una clave de 32 bytes usando SHA256 sobre el contenido del master_key
@key = Digest::SHA256.digest(master_key_content.strip)
end
def encrypt(plaintext)
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.encrypt
cipher.key = @key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext.to_s) + cipher.final
auth_tag = cipher.auth_tag
# Base64 encodear las 3 partes para un JSON
payload = {
iv: Base64.strict_encode64(iv),
tag: Base64.strict_encode64(auth_tag),
data: Base64.strict_encode64(ciphertext)
}
Base64.strict_encode64(payload.to_json)
end
def decrypt(encrypted_payload)
parsed_payload = JSON.parse(Base64.strict_decode64(encrypted_payload), symbolize_names: true)
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.decrypt
cipher.key = @key
cipher.iv = Base64.strict_decode64(parsed_payload[:iv])
cipher.auth_tag = Base64.strict_decode64(parsed_payload[:tag])
cipher.update(Base64.strict_decode64(parsed_payload[:data])) + cipher.final
rescue => e
raise "Error al desencriptar. Clave incorrecta o payload dañado. Detalles: #{e.message}"
end
end
if __FILE__ == $0
action = ARGV[0]
unless ['encrypt', 'decrypt'].include?(action)
puts "Uso:"
puts " ruby #{$0} encrypt <texto_plano>"
puts " ruby #{$0} decrypt <texto_encriptado>"
exit 1
end
text = ARGV[1]
if text.nil? || text.empty?
puts "Por favor, proporciona el texto a #{action}."
puts "Ejemplo:"
puts " ruby #{$0} encrypt \"MiSuperPassword\""
exit 1
end
# Buscar el archivo .master.key en el root del proyecto
key_path = File.join(File.expand_path('../..', __dir__), '.master.key')
unless File.exist?(key_path)
require 'securerandom'
File.write(key_path, SecureRandom.hex(32))
puts "⚠️ ATENCIÓN: Se ha creado una nueva clave maestra en '#{key_path}'"
puts " Asegúrate de NO subir este archivo a ningún repositorio (ya está cubierto por **/*.key en .gitignore)."
puts " Cualquiera que tenga este archivo podrá desencriptar los textos.\n\n"
end
box = SecretBox.new(File.read(key_path))
begin
if action == 'encrypt'
puts box.encrypt(text)
else
puts box.decrypt(text)
end
rescue => e
puts "Error: #{e.message}"
exit 1
end
end