first commit
This commit is contained in:
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Live Steganography Experiments
|
||||
|
||||
This project explores the concept of serving live image requests while embedding encrypted metadata into images using real-time steganography.
|
||||
|
||||
## Core Concept
|
||||
|
||||
The server begins by caching all images into memory (if configured to do so). When a client requests an image:
|
||||
|
||||
1. The server intercepts the request.
|
||||
2. It embeds encrypted metadata — including the client’s IP address, timestamp, and headers — directly into the image pixels using LSB (Least Significant Bit) encoding.
|
||||
3. The modified image is returned to the client in real time.
|
||||
4. If the image is later disseminated (and remains uncompressed), it can be traced back to the original request.
|
||||
|
||||
Key properties of the system:
|
||||
|
||||
- **Invisible**: Embedding is imperceptible to the human eye.
|
||||
- **Encrypted**: Metadata is secured with AES-GCM; only those with the key can decrypt it.
|
||||
- **Stateless**: No logs are stored on the server; the information is embedded within the image itself.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Watermarking downloaded content per user
|
||||
- Forensic tracking and auditing of media distribution
|
||||
|
||||
## Limitations
|
||||
|
||||
- For large images, encoding can introduce noticeable latency.
|
||||
- Embedded images are generated per request, which limits caching efficiency.
|
||||
|
||||
## Components
|
||||
|
||||
- `stego-server/`: HTTP server that encodes client metadata into images on-the-fly.
|
||||
- `stego-client/`: CLI tool to decode and decrypt embedded metadata from images.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Do not use this technology in ways that violate laws, privacy regulations, or platform terms of service.
|
3
stego-client/config.yaml
Normal file
3
stego-client/config.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
security:
|
||||
# Generate with: openssl rand -base64 32
|
||||
aes_key: "mBH9cMfhD939xPiQz1mnDvECeUu8ydrVEg1YptjTzH4="
|
129
stego-client/decode.go
Normal file
129
stego-client/decode.go
Normal file
@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/auyer/steganography"
|
||||
"github.com/chai2010/webp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type DecodeConfig struct {
|
||||
Security struct {
|
||||
AESKey string `yaml:"aes_key"` // Base64 encoded 32-byte key
|
||||
} `yaml:"security"`
|
||||
}
|
||||
|
||||
var (
|
||||
aesKey []byte
|
||||
cfg DecodeConfig
|
||||
)
|
||||
|
||||
func loadDecodeConfig(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config file read error: %w", err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("config parse error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptData(ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
func decodeImage(file *os.File) (image.Image, error) {
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
// Try to decode with image.Decode first
|
||||
img, _, err := image.Decode(reader)
|
||||
if err == nil {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Try fallback: WebP
|
||||
// Reset file offset to 0
|
||||
_, _ = file.Seek(0, 0)
|
||||
img, err = webp.Decode(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unsupported image format or decode failure: %w", err)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run decode.go <encoded_image>")
|
||||
return
|
||||
}
|
||||
|
||||
if err := loadDecodeConfig("config.yaml"); err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
filePath := os.Args[1]
|
||||
|
||||
inFile, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open file: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer inFile.Close()
|
||||
|
||||
// Initialize AES key
|
||||
if cfg.Security.AESKey != "" {
|
||||
aesKey, err = base64.StdEncoding.DecodeString(cfg.Security.AESKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid AES key in config: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Fatal("AES key not provided in config.")
|
||||
}
|
||||
|
||||
img, err := decodeImage(inFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to decode image: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
size := steganography.GetMessageSizeFromImage(img)
|
||||
message := steganography.Decode(size, img)
|
||||
if message == nil {
|
||||
fmt.Println("No message found in the image.")
|
||||
return
|
||||
}
|
||||
|
||||
decryptedMessage, err := decryptData(message)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to decrypt message: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Decrypted message: %s\n", decryptedMessage)
|
||||
}
|
9
stego-client/go.mod
Normal file
9
stego-client/go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module stego-client
|
||||
|
||||
go 1.21.9
|
||||
|
||||
require (
|
||||
github.com/auyer/steganography v1.0.3
|
||||
github.com/chai2010/webp v1.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
8
stego-client/go.sum
Normal file
8
stego-client/go.sum
Normal file
@ -0,0 +1,8 @@
|
||||
github.com/auyer/steganography v1.0.3 h1:NAFie4xVYY/LbYLkYCr0CJyyG/7yIXwC+yEC6H2jiSA=
|
||||
github.com/auyer/steganography v1.0.3/go.mod h1:Q2qN+f1ixaXnKTCT4xkSDCZ/5NiOpUeTgOCLwQdJD+A=
|
||||
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
||||
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
BIN
stego-client/stego-client
Executable file
BIN
stego-client/stego-client
Executable file
Binary file not shown.
10
stego-server/config.yaml
Normal file
10
stego-server/config.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
server:
|
||||
port: "8219"
|
||||
image_dir: "../images/dist"
|
||||
base_path: "/images/dist"
|
||||
cache_images: true
|
||||
|
||||
|
||||
security:
|
||||
# Generate with: openssl rand -base64 32
|
||||
aes_key: "mBH9cMfhD939xPiQz1mnDvECeUu8ydrVEg1YptjTzH4="
|
11
stego-server/go.mod
Normal file
11
stego-server/go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module stego-server
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.9
|
||||
|
||||
require (
|
||||
github.com/auyer/steganography v1.0.3 // indirect
|
||||
golang.org/x/image v0.27.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
7
stego-server/go.sum
Normal file
7
stego-server/go.sum
Normal file
@ -0,0 +1,7 @@
|
||||
github.com/auyer/steganography v1.0.3 h1:NAFie4xVYY/LbYLkYCr0CJyyG/7yIXwC+yEC6H2jiSA=
|
||||
github.com/auyer/steganography v1.0.3/go.mod h1:Q2qN+f1ixaXnKTCT4xkSDCZ/5NiOpUeTgOCLwQdJD+A=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
349
stego-server/main.go
Normal file
349
stego-server/main.go
Normal file
@ -0,0 +1,349 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/auyer/steganography"
|
||||
"golang.org/x/image/webp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port string `yaml:"port"`
|
||||
ImageDir string `yaml:"image_dir"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
CacheImages bool `yaml:"cache_images"`
|
||||
} `yaml:"server"`
|
||||
Security struct {
|
||||
AESKey string `yaml:"aes_key"` // Base64 encoded 32-byte key
|
||||
} `yaml:"security"`
|
||||
}
|
||||
|
||||
func loadConfig(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config file read error: %w", err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("config parse error: %w", err)
|
||||
}
|
||||
|
||||
// Set defaults if not specified
|
||||
if cfg.Server.Port == "" {
|
||||
cfg.Server.Port = "8219"
|
||||
}
|
||||
if cfg.Server.ImageDir == "" {
|
||||
cfg.Server.ImageDir = "./images"
|
||||
}
|
||||
|
||||
if cfg.Server.BasePath == "" {
|
||||
cfg.Server.BasePath = "/"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
decodedImageCache sync.Map // path -> *cachedImage
|
||||
aesKey []byte
|
||||
cfg Config
|
||||
)
|
||||
|
||||
type cachedImage struct {
|
||||
img *image.NRGBA
|
||||
mimeType string
|
||||
maxPayloadSize uint32
|
||||
size uint32
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := loadConfig("config.yaml"); err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Security.AESKey != "" {
|
||||
var err error
|
||||
aesKey, err = base64.StdEncoding.DecodeString(cfg.Security.AESKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid AES key in config: %v", err)
|
||||
}
|
||||
} else {
|
||||
aesKey = make([]byte, 32)
|
||||
if _, err := rand.Read(aesKey); err != nil {
|
||||
log.Fatalf("Failed to generate AES key: %v", err)
|
||||
}
|
||||
log.Printf("Generated new AES key (base64): %s", base64.StdEncoding.EncodeToString(aesKey))
|
||||
}
|
||||
|
||||
if cfg.Server.CacheImages {
|
||||
if err := loadImages(); err != nil {
|
||||
log.Printf("Warning: could not preload images: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadImages() error {
|
||||
return filepath.WalkDir(cfg.Server.ImageDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
data, err := os.ReadFile(path)
|
||||
size := uint32(len(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Preload decoded image
|
||||
img, mimeType, err := decodeImageData(data, ext)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding %s: %v", path, err)
|
||||
}
|
||||
|
||||
nrgba := image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
|
||||
maxSize := steganography.MaxEncodeSize(nrgba)
|
||||
maxPlaintext, err := maxPlaintextSize(maxSize)
|
||||
if err != nil {
|
||||
log.Printf("Skipping %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
decodedImageCache.Store(path, &cachedImage{
|
||||
img: nrgba,
|
||||
mimeType: mimeType,
|
||||
maxPayloadSize: maxPlaintext,
|
||||
size: size,
|
||||
})
|
||||
log.Printf("Cached decoded image: %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func decodeImageData(data []byte, ext string) (image.Image, string, error) {
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
img, err := jpeg.Decode(bytes.NewReader(data))
|
||||
return img, "image/jpeg", err
|
||||
case ".png":
|
||||
img, err := png.Decode(bytes.NewReader(data))
|
||||
return img, "image/png", err
|
||||
case ".webp":
|
||||
img, err := webp.Decode(bytes.NewReader(data))
|
||||
return img, "image/webp", err
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unsupported format: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func encryptData(plaintext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func maxPlaintextSize(totalOutputSize uint32) (uint32, error) {
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
overhead := uint32(gcm.NonceSize() + gcm.Overhead())
|
||||
if totalOutputSize < overhead {
|
||||
return 0, fmt.Errorf("output size too small")
|
||||
}
|
||||
|
||||
return totalOutputSize - overhead, nil
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func embedData(img *image.NRGBA, data []byte) ([]byte, error) {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
if buf.Cap() <= 2<<20 { // 2MB cap
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
}()
|
||||
|
||||
err := steganography.Encode(buf, img, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode data into image: %w", err)
|
||||
}
|
||||
|
||||
result := make([]byte, buf.Len())
|
||||
copy(result, buf.Bytes())
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getClientInfo(r *http.Request) string {
|
||||
ip := getClientIP(r)
|
||||
var headers strings.Builder
|
||||
for name, values := range r.Header {
|
||||
for _, value := range values {
|
||||
headers.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s\n%s\n%s", time.Now().UTC().Format(time.RFC3339), ip, headers.String())
|
||||
}
|
||||
|
||||
func getClientIP(r *http.Request) string {
|
||||
headers := []string{"X-Forwarded-For"}
|
||||
for _, h := range headers {
|
||||
if ip := r.Header.Get(h); ip != "" {
|
||||
parts := strings.Split(ip, ",")
|
||||
clientIP := strings.TrimSpace(parts[0])
|
||||
if len(parts) > 1 && isTrustedProxy(clientIP) {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
return clientIP
|
||||
}
|
||||
}
|
||||
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
return ip
|
||||
}
|
||||
|
||||
func isTrustedProxy(ip string) bool {
|
||||
return true // Implement actual IP validation if needed
|
||||
}
|
||||
|
||||
func imageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
relativePath := strings.TrimPrefix(r.URL.Path, cfg.Server.BasePath)
|
||||
requestedPath := filepath.Clean(strings.TrimPrefix(relativePath, "/"))
|
||||
imagePath := filepath.Join(cfg.Server.ImageDir, requestedPath)
|
||||
|
||||
log.Printf("Requested image path: %s", imagePath)
|
||||
|
||||
if !strings.HasPrefix(imagePath, filepath.Clean(cfg.Server.ImageDir)+string(os.PathSeparator)) {
|
||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(imagePath); errors.Is(err, os.ErrNotExist) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var ci *cachedImage
|
||||
if cached, ok := decodedImageCache.Load(imagePath); ok {
|
||||
ci = cached.(*cachedImage)
|
||||
} else {
|
||||
data, err := os.ReadFile(imagePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File read error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(imagePath))
|
||||
img, mimeType, err := decodeImageData(data, ext)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, imagePath)
|
||||
return
|
||||
}
|
||||
|
||||
nrgba := image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
|
||||
maxSize := steganography.MaxEncodeSize(nrgba)
|
||||
maxPlaintext, err := maxPlaintextSize(maxSize)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, imagePath)
|
||||
return
|
||||
}
|
||||
|
||||
ci = &cachedImage{
|
||||
img: nrgba,
|
||||
mimeType: mimeType,
|
||||
maxPayloadSize: maxPlaintext,
|
||||
size: uint32(len(data)),
|
||||
}
|
||||
decodedImageCache.Store(imagePath, ci)
|
||||
}
|
||||
|
||||
metadata := getClientInfo(r)
|
||||
if len(metadata) > int(ci.maxPayloadSize) {
|
||||
metadata = metadata[:ci.maxPayloadSize]
|
||||
}
|
||||
|
||||
encryptedData, err := encryptData([]byte(metadata))
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, imagePath)
|
||||
return
|
||||
}
|
||||
|
||||
modifiedImage, err := embedData(ci.img, encryptedData)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, imagePath)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(modifiedImage)))
|
||||
w.Write(modifiedImage)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := os.Stat(cfg.Server.ImageDir); os.IsNotExist(err) {
|
||||
log.Fatalf("Image directory does not exist: %s", cfg.Server.ImageDir)
|
||||
}
|
||||
|
||||
basePath := strings.TrimSuffix(cfg.Server.BasePath, "/")
|
||||
http.HandleFunc(basePath+"/", imageHandler)
|
||||
|
||||
log.Printf("Starting server on :%s", cfg.Server.Port)
|
||||
log.Fatal(http.ListenAndServe(":"+cfg.Server.Port, nil))
|
||||
}
|
BIN
stego-server/stego-server
Executable file
BIN
stego-server/stego-server
Executable file
Binary file not shown.
Reference in New Issue
Block a user