Cuenta de externas, arreglos de seguridad, títulos arriba y despliegue

- Rol «externas» para la asamblea de externas: pide sin cupo, siempre con el
  colectivo de fuera y su contacto, y sus solicitudes salen en naranja hasta
  que la asamblea de la Enre las confirma.
- /pedir/ explica en una línea que los colectivos externos piden fecha en la
  asamblea de externas, los lunes de 19:00 a 20:00.
- Seguridad: argon2 limitado a dos cálculos a la vez y el intento se cuenta
  antes de calcular (60 accesos simultáneos llevaban el proceso a 3,8 GB);
  bloqueo por cuenta a 20 fallos para que no se pueda dejar fuera a la
  asamblea; avisos por sesión en vez de por URL; sig sin «..»; eventos de
  31 días como mucho; parámetros de argon2 acotados.
- Títulos pegados arriba en todas las páginas; la franja de la enredadera
  recupera ese alto para seguir llenando la pantalla.
- despliegue/: instalar.sh, cuenta.sh, servicio de systemd con el proceso
  encerrado, fragmento de nginx que además deja de servir .git y las notas
  internas, y configuración de ejemplo. Ninguna contraseña en el repositorio.
This commit is contained in:
SITO 2026-09-23 12:15:33 +02:00
parent ce3127baff
commit 7d5dbeec60
24 changed files with 606 additions and 96 deletions

View file

@ -26,6 +26,19 @@ const (
argonSal = 16
)
// argonHuecos limita cuántos cálculos de argon2 corren a la vez. Cada uno
// reserva argonMemoria (64 MiB): sin este tope, sesenta intentos de acceso
// simultáneos llevaban el proceso a casi 4 GB y podían tumbar la máquina, que
// comparte con gitea y el resto de servicios de la Enre. Los que llegan de más
// esperan su turno en vez de reservar memoria.
var argonHuecos = make(chan struct{}, 2)
func argonID(clave, sal []byte, t, m uint32, p uint8, largo uint32) []byte {
argonHuecos <- struct{}{}
defer func() { <-argonHuecos }()
return argon2.IDKey(clave, sal, t, m, p, largo)
}
func aleatorio(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
@ -40,7 +53,7 @@ func nuevoID() string { return hex.EncodeToString(aleatorio(16)) }
// lleva dentro los parametros con los que se genero.
func HashClave(clave string) string {
sal := aleatorio(argonSal)
h := argon2.IDKey([]byte(clave), sal, argonIteraciones, argonMemoria, argonHilos, argonLargo)
h := argonID([]byte(clave), sal, argonIteraciones, argonMemoria, argonHilos, argonLargo)
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, argonMemoria, argonIteraciones, argonHilos,
base64.RawStdEncoding.EncodeToString(sal),
@ -72,7 +85,13 @@ func VerificarClave(codificado, clave string) bool {
if err != nil {
return false
}
calculado := argon2.IDKey([]byte(clave), sal, uint32(iteraciones), uint32(memoria),
// Los parámetros salen del propio hash, que escribimos nosotros; aun así se
// acotan, para que un registro manipulado no pida gigas de memoria.
if memoria <= 0 || memoria > argonMemoria || iteraciones <= 0 || iteraciones > 10 ||
hilos <= 0 || hilos > 16 || len(esperado) == 0 || len(esperado) > 64 {
return false
}
calculado := argonID([]byte(clave), sal, uint32(iteraciones), uint32(memoria),
uint8(hilos), uint32(len(esperado)))
return subtle.ConstantTimeCompare(calculado, esperado) == 1
}
@ -187,6 +206,27 @@ func (s *Sesiones) TomarClave(id string) (string, string) {
return usuario, clave
}
func (s *Sesiones) PonerAviso(id, aviso, fallo string) {
s.mu.Lock()
defer s.mu.Unlock()
if ses, ok := s.m[id]; ok {
ses.Aviso, ses.Error = aviso, fallo
}
}
// TomarAviso devuelve el aviso pendiente y lo borra: se enseña una vez.
func (s *Sesiones) TomarAviso(id string) (string, string) {
s.mu.Lock()
defer s.mu.Unlock()
ses, ok := s.m[id]
if !ok {
return "", ""
}
aviso, fallo := ses.Aviso, ses.Error
ses.Aviso, ses.Error = "", ""
return aviso, fallo
}
func (s *Sesiones) Borrar(id string) {
s.mu.Lock()
defer s.mu.Unlock()
@ -234,29 +274,22 @@ func NuevoCerrojo(maxIntentos int, ventana, espera time.Duration) *Cerrojo {
}
}
func (c *Cerrojo) Bloqueado(claves ...string) (bool, time.Duration) {
// Intentar comprueba y apunta el intento en la misma operación, antes de
// calcular la clave. Si se apuntara después, como hacía Fallo, cien intentos
// simultáneos pasaban todos la comprobación antes de que se apuntara el
// primero.
func (c *Cerrojo) Intentar(clave string) (bool, time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
ahora := time.Now()
for _, k := range claves {
recientes := c.podar(k, ahora)
if len(recientes) >= c.maxIntentos {
ultimo := recientes[len(recientes)-1]
if falta := c.espera - ahora.Sub(ultimo); falta > 0 {
return true, falta
}
recientes := c.podar(clave, ahora)
if len(recientes) >= c.maxIntentos {
if falta := c.espera - ahora.Sub(recientes[len(recientes)-1]); falta > 0 {
return false, falta
}
}
return false, 0
}
func (c *Cerrojo) Fallo(claves ...string) {
c.mu.Lock()
defer c.mu.Unlock()
ahora := time.Now()
for _, k := range claves {
c.fallos[k] = append(c.podar(k, ahora), ahora)
}
c.fallos[clave] = append(recientes, ahora)
return true, 0
}
func (c *Cerrojo) Limpiar(claves ...string) {