Cómo escribir código mas seguro en Go?
En esta ocasion, vamos a ver funciones de hash
y de cifrado/decifrado
con su principal diferencia. Como bonus, una repasada a las funciones pseudo aleatorias del lenguaje.
- Intro
- Hash con SHA512
- Usos comunes del hashing
- Cifrado y descifrado
- Usos comunes del cifrado
- Números aleatorios
- Conclusiones
Intro
Para comenzar, tenemos el paquete Crypto de la stdlib nos va a proveer el 100% (casi siempre) de las utilidades que necesitamos. Por eso es importante tener a mano su documentacion oficial para solventar dudas de implementación y lograr entender los pormenores.
Segundo, pero igual de importante, es que esta operación es "one way", es decir, con el resultado no puedo volver hacia atrás y obtener el valor ingresado.
Hash con SHA512
Seguramente habrán visto la sigla SHA en algún lugar de la web, curso, etc, ésta corresponde a Secure Hash Algorithm y viene de larga data acompañándonos. Fue desarrollada en los Estados Unidos en el 2001, por la NSA y aprobada ese mismo año para su uso, existen variantes del algoritmo, comenzando por SHA-0 (luego vinieron 1, 2 y 3 que es la actual) y el 512 refiere al tamaño en bits de la "salida" o el digest en inglés.
EL funcionamiento básico es muy simple de explicar sin entrar en tecnisismos: recibe una entrada (arreglo de bytes) de cualquier tamaño y devuelve una palabra de siempre la misma longitud (en este caso 512 bits). Cabe destacar que la entrada puede ser realmente muy grande, segun los datos oficiales es de (2^128-1).
Ya sabemos como funcionan, ahora un pequño problema que nosotros no nos vamos a encontrar, pero si los involucrados en el desarrollo de estas fantásticas soluciones, fue que al tener un tamaño de salida definido y tanta capacidad para recibir muchos valores, se encontron con la no-grata sorpresa de que destintas entradas tenian como respuesta el mismo resultado, comúnmente llamadas colisiones.
La buena noticia es que a partir de SHA-2
, no se volvieron a encontrar y es algo por lo cual no nos tenemos que preocupar.
Ahora pasamos un poco al código
// hashShortVersion simplemente usa la función para del package, devuelve slice de bytes, simple y efectivo.
func hashShortVersion(s string) string {
hasher := sha512.Sum512([]byte(s))
return fmt.Sprintf("%x", hasher)
}
// hasherVersion creamos una interface hash.Hasher, le asignamos una implemntación de sha512 y usamos el método sum de la interface. Nos puede servir para hacer composición o polimorfismo, el resultado es distinto al anterior.
func hasherVersion(s string) string {
var hasher hash.Hash
hasher = sha512.New()
b := hasher.Sum([]byte(s))
res := fmt.Sprintf("%x", b)
return res
}
// binaryVersion convierte a binario nuestros datos.
func binaryVersion(s string) string {
var hasher hash.Hash
hasher = sha512.New()
hasher.Write([]byte(s))
m, _ := hasher.(encoding.BinaryMarshaler)
_, _ = m.MarshalBinary()
return string(hasher.Sum(nil))
}
Como expresan tanto el código y los comentarios, los resultados no son iguales ya que usan métodos/funciones distintas. El MarshallBinary()
no es recomandable usar para guardar los datos en un storage "normal" como bases de datos relacionales, de documentos o archivos porque tiene un formato muy distinto, además de ser ilegible para los humanos y no podemos compararlos con el operador "=="
.
Usos mas comunes
Decíamos que las funciones hash, tienen como objetivo captar una entrada "digerirla" y devolver un resultado siempre del mismo tamaño. No importa si ingresamos un solo caracter, Don Quijote entero o todo el código fuente de el Kernel de Linux, la salida tiene siempre el mismo largo. Y como toda función pura, ante la misma entrada, vamos a tener la misma salida, es por eso que el uso mas común es el de garantizar la autenticidad de un software, cuando entregamos el SHA de un compilado o ejecutable, el usuario que lo descarga puede hacer el mismo proceso y validar que haya descargado el mismo que la documentación oficial dice.
Por otra parte, un uso muy común pero que no recomiendo, es para contraseñas, ya que existen tablas con muchisimos datos llamadas Rainbow tables que se usan para poder crackear contraseñas, por lo que no es un caso de uso muy acertado.
Bueno, hasta acá de hashing, ahora pasamos al cifrado
Cifrado y descifrado
LA principal diferencia con el hashing, es que aca si podemos volver a obtener lo que enviamos en un principio y la segunda, es que ante la misma entrada, podemos no obtener la misma salida (mientras menos colisiones haya, mejor) por sofisticadas funciones matématicas que se denomina sal (o salt en inglés) y es un conjunto de bits aleatorios que su objetivo es el de, justamente, cambiar el resultado ante los mismos valores de entrada.
ejemplos en código:
func Encrypt(s string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(s), bcrypt.MinCost) // puede ser también defaultCost
}
func Decrypt(encryptedPassword, plainPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(plainPassword))
}
Bueno con este si que no nos podemos quejar, lo fácil y claro que es no deja margen de dudas.
Si prueban imprimir solo el string resultante varias veces con la misma entrada, pueden ver como obtienen distintos resultados.
Usos más comunes
Claramente el uso mas común (hace muchísimos años) es el de ocultar un mensaje y que un receptor pueda interpretarlo. En informática lo vamos a usar extensamente para contraseñas, tokens y valores que el usuario envía y son sensibles.
Números aleatorios
Antes de empezar a abordar el tema, no son aleatorios, son pseudo aleatorios y es una definición teórica que pueden ver explicado acá.
En éste código, vemos dos casos de uso muy comunes, el primero es obtener un string aleatorio dado un alfabeto. El parámetro de entrada es uno solo y corresponde a que tan larga debería ser la cadena resultante. Así de simple y sencillo.
La segunda función, es un poco mas de lo mismo, solo que generamos únicamente números y el parametro de entrada corresponde al límete superior sin incluir, es decir, si ponemos 1000
, vamos a generar un número entre 0
y 999
.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, 0, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret = append(ret, letters[num.Int64()])
}
return string(ret), nil
}
func GenerateRandNum(n int) int64 {
b, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
if err != nil {
return 0
}
return b.Int64()
}
Con estos dos snippets
ya tienen para cubrir bastantes casos.
Conclusiones
Aprendimos (o repasamos) las principales diferencias entre el hashing y el cifrado, tanto conceptualmente como implementaciones en Golang y sus usos mas frecuentes en la industria, también como el desarrollos académicos.
Por último, cubrimos brevemente que es y como se usa el paquete crypto.Rand y claramente, agregamos un poco de código.
Ya saben, si quieren sponsorearme, pueden hacerlo acá!
Top comments (2)
Muy buen post, una explicación sencilla para un tema que tiene mucha utilidad.
Gracias Gabi. Se viene la lista de concurrencia.