Files
realy/encryption/nip4.go

136 lines
4.2 KiB
Go

package encryption
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"lukechampine.com/frand"
"realy.lol/chk"
"realy.lol/errorf"
"realy.lol/p256k"
)
// ComputeSharedSecret returns a shared secret key used to encrypt messages. The private and
// public keys should be hex encoded. Uses the Diffie-Hellman key exchange (ECDH) (RFC 4753).
func ComputeSharedSecret(pk, sk []byte) (sharedSecret []byte, err error) {
signer := new(p256k.Signer)
if err = signer.InitSec(sk); chk.E(err) {
return
}
if sharedSecret, err = signer.ECDH(pk); chk.E(err) {
return
}
return
}
// EncryptNip4 encrypts message with key using aes-256-cbc. key should be the shared secret
// generated by ComputeSharedSecret.
//
// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func EncryptNip4(msg []byte, key []byte) (ct []byte, err error) {
// validate key length for AES-256
if len(key) != 32 {
err = errorf.E("key must be 32 bytes for AES-256, got %d", len(key))
return
}
// block size is 16 bytes
iv := make([]byte, 16)
if _, err = frand.Read(iv); chk.E(err) {
err = errorf.E("error creating initialization vector: %w", err)
return
}
// automatically picks aes-256 based on key length (32 bytes)
var block cipher.Block
if block, err = aes.NewCipher(key); chk.E(err) {
err = errorf.E("error creating block cipher: %w", err)
return
}
mode := cipher.NewCBCEncrypter(block, iv)
plaintext := msg
// add padding
base := len(plaintext)
// this will be a number between 1 and 16 (inclusive), never 0
bs := block.BlockSize()
padding := bs - base%bs
// encode the padding in all the padding bytes themselves
padText := bytes.Repeat([]byte{byte(padding)}, padding)
paddedMsgBytes := append(plaintext, padText...)
ciphertext := make([]byte, len(paddedMsgBytes))
mode.CryptBlocks(ciphertext, paddedMsgBytes)
return []byte(base64.StdEncoding.EncodeToString(ciphertext) + "?iv=" +
base64.StdEncoding.EncodeToString(iv)), nil
}
// DecryptNip4 decrypts a content string using the shared secret key. The inverse operation to message ->
// EncryptNip4(message, key).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func DecryptNip4(content, key []byte) (msg []byte, err error) {
// validate key length for AES-256
if len(key) != 32 {
err = errorf.E("key must be 32 bytes for AES-256, got %d", len(key))
return
}
parts := bytes.Split(content, []byte("?iv="))
if len(parts) < 2 {
return nil, errorf.E(
"error parsing encrypted message: no initialization vector")
}
ciphertext := make([]byte, base64.StdEncoding.DecodedLen(len(parts[0])))
iv := make([]byte, base64.StdEncoding.DecodedLen(len(parts[1])))
var ctLen, ivLen int
if ctLen, err = base64.StdEncoding.Decode(ciphertext, parts[0]); chk.E(err) {
err = errorf.E("error decoding ciphertext from base64: %w", err)
return
}
if ivLen, err = base64.StdEncoding.Decode(iv, parts[1]); chk.E(err) {
err = errorf.E("error decoding iv from base64: %w", err)
return
}
// trim buffers to actual decoded length
ciphertext = ciphertext[:ctLen]
iv = iv[:ivLen]
// validate IV length
if len(iv) != 16 {
err = errorf.E("initialization vector must be 16 bytes, got %d", len(iv))
return
}
var block cipher.Block
if block, err = aes.NewCipher(key); chk.E(err) {
err = errorf.E("error creating block cipher: %w", err)
return
}
mode := cipher.NewCBCDecrypter(block, iv)
msg = make([]byte, len(ciphertext))
mode.CryptBlocks(msg, ciphertext)
// remove padding using proper PKCS#7 validation
var (
plaintextLen = len(msg)
)
if plaintextLen == 0 {
err = errorf.E("empty ciphertext")
return
}
// the padding amount is encoded in the padding bytes themselves
padding := int(msg[plaintextLen-1])
if padding == 0 || padding > 16 || padding > plaintextLen {
err = errorf.E("invalid padding amount: %d", padding)
return
}
// validate that all padding bytes have the correct value (PKCS#7)
for i := plaintextLen - padding; i < plaintextLen; i++ {
if msg[i] != byte(padding) {
err = errorf.E("invalid padding: byte at position %d should be %d, got %d",
i, padding, msg[i])
return
}
}
msg = msg[0 : plaintextLen-padding]
return msg, nil
}