From 15b07ea85d2759f0711171c4a893dbafe6ae945f Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Mon, 12 May 2025 22:43:42 +0200 Subject: [PATCH] feat(encryption): refactor encryption provider scripts, update KMS integration, and enhance key management --- .gitignore | 3 +- ansible/host_vars/prod-01.alexpires.me.yml | 4 +- ansible/roles/encryption/defaults/main.yml | 5 - ansible/roles/encryption/files/open_volume | 2 + .../files/start_encryption_provider | 78 +++++++++++ ansible/roles/encryption/tasks/kms.yml | 23 +--- ansible/roles/encryption/tasks/main.yml | 6 +- .../aws-encryption-provider.service.j2 | 2 +- .../templates/start-encryption-provider.sh.j2 | 6 - terraform/scaleway-iac/encryption.tf | 12 ++ terraform/scaleway-iac/sectool.env | 3 + terraform/scaleway-iac/variables.tf | 18 +++ tools/open_volume/Makefile | 11 ++ tools/open_volume/main.go | 126 +++++++++++++++++ tools/read_system_id/Makefile | 11 ++ tools/read_system_id/main.go | 79 +++++++++++ tools/read_system_id/original/read_system_id | 6 + tools/start_encryption_provider/Makefile | 11 ++ tools/start_encryption_provider/main.go | 130 ++++++++++++++++++ 19 files changed, 497 insertions(+), 39 deletions(-) create mode 100644 ansible/roles/encryption/files/start_encryption_provider delete mode 100644 ansible/roles/encryption/templates/start-encryption-provider.sh.j2 create mode 100644 tools/open_volume/Makefile create mode 100644 tools/open_volume/main.go create mode 100644 tools/read_system_id/Makefile create mode 100644 tools/read_system_id/main.go create mode 100755 tools/read_system_id/original/read_system_id create mode 100644 tools/start_encryption_provider/Makefile create mode 100644 tools/start_encryption_provider/main.go diff --git a/.gitignore b/.gitignore index 390e8f9..768210d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ drafts/* .molecule *.auto.tfvars dev/* -.ansible \ No newline at end of file +.ansible +*.x.c diff --git a/ansible/host_vars/prod-01.alexpires.me.yml b/ansible/host_vars/prod-01.alexpires.me.yml index 40cc881..4eb6346 100644 --- a/ansible/host_vars/prod-01.alexpires.me.yml +++ b/ansible/host_vars/prod-01.alexpires.me.yml @@ -138,9 +138,7 @@ encryption_volumes: mount_service_before: snap.microk8s.daemon-kubelite.service mount_service_wantedby: snap.microk8s.daemon-kubelite.service mount_permission: "0755" -encryption_aws_access_key_id: "{{ lookup('env', 'KMS_ACCESS_KEY') }}" -encryption_aws_secret_access_key: "{{ lookup('env', 'KMS_SECRET_KEY') }}" -encryption_kms_arn: "arn:aws:kms:eu-west-1:001604727293:key/977d08fd-8230-40bf-8a4f-d8a3264c5990" + encryption_kms_socket: /var/run/kmsplugin/socket.sock # Kubernetes specific settings diff --git a/ansible/roles/encryption/defaults/main.yml b/ansible/roles/encryption/defaults/main.yml index 419cf7a..4ece3f4 100644 --- a/ansible/roles/encryption/defaults/main.yml +++ b/ansible/roles/encryption/defaults/main.yml @@ -2,9 +2,4 @@ encryption_volumes: [] encryption_volumes_path: /var/lib/encrypted_volumes encryption_volumes_default_size: 1G - -encryption_aws_access_key_id: -encryption_aws_secret_access_key: -encryption_kms_arn: -encryption_kms_region: eu-west-1 encryption_kms_socket: /var/run/kmsplugin/socket.sock diff --git a/ansible/roles/encryption/files/open_volume b/ansible/roles/encryption/files/open_volume index a026e45..d72ac22 100644 --- a/ansible/roles/encryption/files/open_volume +++ b/ansible/roles/encryption/files/open_volume @@ -1,6 +1,8 @@ #!/bin/bash if [ "$#" -ne 3 ]; then echo "Usage: $0 " "" "" + echo "Example: $0 https://keyserver.example.com /path/to/image.img " + echo "Example: $0 https://keyserver.example.com /path/to/image.img /dev/mapper/crypt1" exit 1 fi diff --git a/ansible/roles/encryption/files/start_encryption_provider b/ansible/roles/encryption/files/start_encryption_provider new file mode 100644 index 0000000..60ae6d6 --- /dev/null +++ b/ansible/roles/encryption/files/start_encryption_provider @@ -0,0 +1,78 @@ +#!/bin/bash + +#!/bin/bash +if [ "$#" -lt 1 ]; then + echo "Usage: $0 " "[socket]" + echo "Example: $0 https://keyserver.example.com" + echo "Example: $0 https://keyserver.example.com /var/run/kmsplugin/socket.sock" + exit 1 +fi + +KEYSERVER=$1 +SOCKET=${2:-/var/run/kmsplugin/socket.sock} + +# if keyserver is not reachable, exit +if ! curl -s -I $KEYSERVER > /dev/null; then + logger "start_encryption_provider: Key server not reachable, exiting." + exit 1 +fi + +uuid=$(cat /proc/cpuinfo | grep 'model name' | head -1) +uuid+=$(ip link show eth0 | grep ether | awk '{print $2}') +uuid+=$(sudo dmidecode -s system-uuid) +KEYID=$(echo -n "$uuid" | md5sum | awk '{print $1}') + +logger "start_encryption_provider: Downloading key from key server," +LOCALKEYFILE=$(mktemp) +curl -s -o $LOCALKEYFILE $KEYSERVER/$KEYID-encryption-service-credentials.json +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to download key from key server" + rm $LOCALKEYFILE + exit 1 +fi + +logger "start_encryption_provider: Key downloaded successfully, extracting credentials" +export AWS_ACCESS_KEY_ID=$(jq -r '.access_key' $LOCALKEYFILE) +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to extract AWS credentials from key file" + rm $LOCALKEYFILE + exit 1 +fi + +export AWS_SECRET_ACCESS_KEY=$(jq -r '.secret_key' $LOCALKEYFILE) +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to extract AWS credentials from key file" + rm $LOCALKEYFILE + exit 1 +fi +KEYARN=$(jq -r '.key_arn' $LOCALKEYFILE) +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to extract KMS key ARN from key file" + rm $LOCALKEYFILE + exit 1 +fi +REGION=$(jq -r '.region' $LOCALKEYFILE) +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to extract region from key file" + rm $LOCALKEYFILE + exit 1 +fi + +logger "start_encryption_provider: Removing key file" +rm $LOCALKEYFILE + +logger "start_encryption_provider: Starting encryption provider" +mkdir {{ encryption_kms_socket_dir | dirname }} }} 2>/dev/null || true +chmod 700 {{ encryption_kms_socket_dir | dirname }} 2>/dev/null + +if [ ! -f /usr/local/bin/aws-encryption-provider ]; then + logger "start_encryption_provider: Encryption provider not found, exiting." + exit 1 +fi + +/usr/local/bin/aws-encryption-provider --key=$KEYARN --region=$REGION --listen=$SOCKET +if [ $? -ne 0 ]; then + logger "start_encryption_provider: Failed to start encryption provider" + exit 1 +fi +exit 0 diff --git a/ansible/roles/encryption/tasks/kms.yml b/ansible/roles/encryption/tasks/kms.yml index 94c09fd..3dc5f27 100644 --- a/ansible/roles/encryption/tasks/kms.yml +++ b/ansible/roles/encryption/tasks/kms.yml @@ -1,33 +1,16 @@ -- name: Check if required facts are set - ansible.builtin.fail: - msg: "The following variables are required: encryption_aws_access_key_id, encryption_aws_secret_access_key, encryption_kms_arn" - when: - - encryption_aws_access_key_id is not defined - - encryption_aws_secret_access_key is not defined - - encryption_kms_arn is not defined - - name: Copy aws-encryption-provider binary to the host become: true ansible.builtin.copy: src: aws-encryption-provider dest: /usr/local/bin/aws-encryption-provider - mode: '0755' - -- name: Copy start script to the host - become: true - ansible.builtin.template: - src: start-encryption-provider.sh.j2 - dest: /usr/local/bin/start-encryption-provider.sh - mode: '0755' - owner: root - group: root + mode: "0755" - name: Create KMS socket directory become: true ansible.builtin.file: path: /var/run/kmsplugin state: directory - mode: '0755' + mode: "0755" owner: root group: root @@ -36,7 +19,7 @@ ansible.builtin.template: src: aws-encryption-provider.service.j2 dest: /etc/systemd/system/aws-encryption-provider.service - mode: '0644' + mode: "0644" - name: Reload systemd daemon become: true diff --git a/ansible/roles/encryption/tasks/main.yml b/ansible/roles/encryption/tasks/main.yml index e3fbcd1..fb3bc4d 100644 --- a/ansible/roles/encryption/tasks/main.yml +++ b/ansible/roles/encryption/tasks/main.yml @@ -28,8 +28,9 @@ dest: "/usr/local/bin/" mode: "0750" with_items: - - "open_volume" - - "read_system_id" + - open_volume + - read_system_id + - start_encryption_provider tags: - roles::encryption::volume @@ -43,6 +44,5 @@ - name: Create KMS service ansible.builtin.include_tasks: kms.yml - when: encryption_aws_access_key_id is defined and encryption_aws_secret_access_key is defined and encryption_kms_arn is defined tags: - roles::encryption::kms diff --git a/ansible/roles/encryption/templates/aws-encryption-provider.service.j2 b/ansible/roles/encryption/templates/aws-encryption-provider.service.j2 index 8cf0efa..bdda027 100644 --- a/ansible/roles/encryption/templates/aws-encryption-provider.service.j2 +++ b/ansible/roles/encryption/templates/aws-encryption-provider.service.j2 @@ -5,7 +5,7 @@ Before=snap.microk8s.daemon-kubelite.service [Service] Type=simple -ExecStart=/usr/local/bin/start-encryption-provider.sh +ExecStart=/usr/local/bin/start_encryption_provider "{{ key_server }}" "{{ encryption_kms_socket }}" Restart=on-failure User=root diff --git a/ansible/roles/encryption/templates/start-encryption-provider.sh.j2 b/ansible/roles/encryption/templates/start-encryption-provider.sh.j2 deleted file mode 100644 index 44e10a4..0000000 --- a/ansible/roles/encryption/templates/start-encryption-provider.sh.j2 +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# This script is used to start the encryption provider for the application. -# Managed by Ansible. -export AWS_ACCESS_KEY_ID={{ encryption_aws_access_key_id }} -export AWS_SECRET_ACCESS_KEY={{ encryption_aws_secret_access_key }} -/usr/local/bin/aws-encryption-provider --key={{ encryption_kms_arn }} --region={{ encryption_kms_region }} --listen={{ encryption_kms_socket }} diff --git a/terraform/scaleway-iac/encryption.tf b/terraform/scaleway-iac/encryption.tf index 3cb0342..e12614c 100644 --- a/terraform/scaleway-iac/encryption.tf +++ b/terraform/scaleway-iac/encryption.tf @@ -4,3 +4,15 @@ resource "scaleway_object" "system_id_encryption_key" { content_base64 = base64encode(var.encryption_key) } + +resource "scaleway_object" "system_id_encryption_aws_credentials" { + bucket = scaleway_object_bucket.infra_assets.id + key = "keys/${var.system_id}-encryption-service-credentials.json" + + content = jsonencode({ + access_key = var.kms_access_key + secret_key = var.kms_secret_key + region = "eu-west-1" + key_arn = var.kms_key_arn + }) +} diff --git a/terraform/scaleway-iac/sectool.env b/terraform/scaleway-iac/sectool.env index 0703d1d..981a983 100644 --- a/terraform/scaleway-iac/sectool.env +++ b/terraform/scaleway-iac/sectool.env @@ -7,3 +7,6 @@ TF_VAR_scaleway_organization_id=$SCALEWAY_ORGANIZATION_ID TF_VAR_scaleway_user_id=$SCALEWAY_USER_ID TF_VAR_bucket_suffix=$BUCKET_SUFFIX TF_VAR_encryption_key=$ENCRYPT_KEY +TF_VAR_kms_access_key=$KMS_ACCESS_KEY +TF_VAR_kms_secret_key=$KMS_SECRET_KEY +TF_VAR_kms_key_arn=$KMS_KEY_ARN \ No newline at end of file diff --git a/terraform/scaleway-iac/variables.tf b/terraform/scaleway-iac/variables.tf index bfc94c0..b964a61 100644 --- a/terraform/scaleway-iac/variables.tf +++ b/terraform/scaleway-iac/variables.tf @@ -45,3 +45,21 @@ variable "system_id" { type = string sensitive = true } + +variable "kms_access_key" { + description = "KMS User Access key" + type = string + sensitive = true +} + +variable "kms_secret_key" { + description = "KMS User Secret key" + type = string + sensitive = true +} + +variable "kms_key_arn" { + description = "KMS Access key ARN" + type = string + sensitive = true +} diff --git a/tools/open_volume/Makefile b/tools/open_volume/Makefile new file mode 100644 index 0000000..222b8cb --- /dev/null +++ b/tools/open_volume/Makefile @@ -0,0 +1,11 @@ +BINARY=open_volume + +.PHONY: all build clean + +all: build + +build: + go build -o $(BINARY) main.go + +clean: + rm -f $(BINARY) \ No newline at end of file diff --git a/tools/open_volume/main.go b/tools/open_volume/main.go new file mode 100644 index 0000000..02be1ce --- /dev/null +++ b/tools/open_volume/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "os/exec" + "strings" +) + +func usage() { + fmt.Printf("Usage: %s \n", os.Args[0]) + fmt.Printf("Example: %s https://keyserver.example.com /path/to/image.img \n", os.Args[0]) + fmt.Printf("Example: %s https://keyserver.example.com /path/to/image.img /dev/mapper/crypt1\n", os.Args[0]) + os.Exit(1) +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func deviceExists(device string) bool { + _, err := os.Stat("/dev/mapper/" + device) + return err == nil +} + +func keyserverReachable(url string) bool { + resp, err := http.Head(url) + if err != nil { + return false + } + resp.Body.Close() + return resp.StatusCode < 400 +} + +func getUUID() string { + // Get CPU model name + cpuinfo, _ := ioutil.ReadFile("/proc/cpuinfo") + var model string + for _, line := range strings.Split(string(cpuinfo), "\n") { + if strings.HasPrefix(line, "model name") { + model = line + break + } + } + // Get eth0 MAC address + iface, err := net.InterfaceByName("eth0") + mac := "" + if err == nil { + mac = iface.HardwareAddr.String() + } + // Get system UUID + out, _ := exec.Command("sudo", "dmidecode", "-s", "system-uuid").Output() + sysuuid := strings.TrimSpace(string(out)) + uuid := model + mac + sysuuid + return uuid +} + +func main() { + log.SetFlags(0) + if len(os.Args) != 4 { + usage() + } + keyserver := os.Args[1] + image := os.Args[2] + device := os.Args[3] + + if !fileExists(image) { + log.Printf("open_volume: (%s) Image file not found", device) + os.Exit(1) + } + + if !keyserverReachable(keyserver) { + log.Printf("open_volume: (%s) Key server not reachable, exiting.", device) + os.Exit(1) + } + + if deviceExists(device) { + fmt.Printf("open_volume: (%s) Device already open, exiting.\n", device) + os.Exit(1) + } + + uuid := getUUID() + h := md5.Sum([]byte(uuid)) + keyid := hex.EncodeToString(h[:]) + + log.Printf("open_volume: (%s) Downloading key from key server,", device) + keyurl := fmt.Sprintf("%s/%s", keyserver, keyid) + resp, err := http.Get(keyurl) + if err != nil || resp.StatusCode != 200 { + log.Printf("open_volume: (%s) Failed to download key from key server", device) + os.Exit(1) + } + defer resp.Body.Close() + + tmpfile, err := ioutil.TempFile("", "keyfile") + if err != nil { + log.Printf("open_volume: (%s) Failed to create temp file", device) + os.Exit(1) + } + defer os.Remove(tmpfile.Name()) + _, err = io.Copy(tmpfile, resp.Body) + tmpfile.Close() + if err != nil { + log.Printf("open_volume: (%s) Failed to save key file", device) + os.Exit(1) + } + + log.Printf("open_volume:'%s' Key downloaded successfully, opening volume", device) + cmd := exec.Command("/usr/sbin/cryptsetup", "luksOpen", image, device, "-d", tmpfile.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("open_volume: (%s) Failed to open volume: %s", device, string(out)) + os.Exit(1) + } + + log.Printf("open_volume: (%s) Volume opened successfully, removing key file", device) + os.Exit(0) +} diff --git a/tools/read_system_id/Makefile b/tools/read_system_id/Makefile new file mode 100644 index 0000000..81d3cea --- /dev/null +++ b/tools/read_system_id/Makefile @@ -0,0 +1,11 @@ +BINARY=read_system_id + +.PHONY: all build clean + +all: build + +build: + go build -o $(BINARY) main.go + +clean: + rm -f $(BINARY) \ No newline at end of file diff --git a/tools/read_system_id/main.go b/tools/read_system_id/main.go new file mode 100644 index 0000000..c85ead5 --- /dev/null +++ b/tools/read_system_id/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "bufio" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "regexp" + "strings" +) + +func getModelName() string { + file, err := os.Open("/proc/cpuinfo") + if err != nil { + return "" + } + defer file.Close() + scanner := bufio.NewScanner(file) + re := regexp.MustCompile(`^model name\s*:\s*(.*)$`) + for scanner.Scan() { + line := scanner.Text() + if matches := re.FindStringSubmatch(line); matches != nil { + return matches[1] + } + } + return "" +} + +func getEth0Mac() string { + out, err := exec.Command("ip", "link").Output() + if err != nil { + return "" + } + re := regexp.MustCompile(`^(\d+):\s+([a-zA-Z0-9]+):.*<.*>\s+link/ether\s+([0-9a-f:]{17})`) + matches := re.FindAllStringSubmatch(string(out), -1) + fmt.Println("Matches:", matches) + if len(matches) == 0 { + return "" + } + first_device := matches[0][2] + fmt.Println("First device:", first_device) + // Get the MAC address of the first device + out, err = exec.Command("ip", "link", "show", first_device).Output() + if err != nil { + return "" + } + re = regexp.MustCompile(`ether\s+([0-9a-f:]{17})`) + match := re.FindStringSubmatch(string(out)) + if match != nil { + return match[1] + } + return "" +} + +func getSystemUUID() string { + out, err := exec.Command("sudo", "dmidecode", "-s", "system-uuid").Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func md5sum(s string) string { + h := md5.New() + io.WriteString(h, s) + return hex.EncodeToString(h.Sum(nil)) +} + +func main() { + uuid := getModelName() + uuid += getEth0Mac() + uuid += getSystemUUID() + fmt.Println(uuid) + hash := md5sum(uuid) + fmt.Println(hash) +} diff --git a/tools/read_system_id/original/read_system_id b/tools/read_system_id/original/read_system_id new file mode 100755 index 0000000..357e634 --- /dev/null +++ b/tools/read_system_id/original/read_system_id @@ -0,0 +1,6 @@ +#!/bin/bash +uuid=$(cat /proc/cpuinfo | grep 'model name' | head -1) +uuid+=$(ip link show wlo1 | grep ether | awk '{print $2}') +uuid+=$(sudo dmidecode -s system-uuid) +echo "UUID: $uuid" +echo -n "$uuid" | md5sum | awk '{print $1}' \ No newline at end of file diff --git a/tools/start_encryption_provider/Makefile b/tools/start_encryption_provider/Makefile new file mode 100644 index 0000000..f6a460e --- /dev/null +++ b/tools/start_encryption_provider/Makefile @@ -0,0 +1,11 @@ +BINARY=start_encryption_provider + +.PHONY: all build clean + +all: build + +build: + go build -o $(BINARY) main.go + +clean: + rm -f $(BINARY) \ No newline at end of file diff --git a/tools/start_encryption_provider/main.go b/tools/start_encryption_provider/main.go new file mode 100644 index 0000000..993952a --- /dev/null +++ b/tools/start_encryption_provider/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type Credentials struct { + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + KeyArn string `json:"key_arn"` + Region string `json:"region"` +} + +func main() { + if len(os.Args) < 2 { + fmt.Printf("Usage: %s [socket]\n", os.Args[0]) + fmt.Printf("Example: %s https://keyserver.example.com\n", os.Args[0]) + fmt.Printf("Example: %s https://keyserver.example.com /var/run/kmsplugin/socket.sock\n", os.Args[0]) + os.Exit(1) + } + + keyserver := os.Args[1] + socket := "/var/run/kmsplugin/socket.sock" + if len(os.Args) > 2 { + socket = os.Args[2] + } + + // Check if keyserver is reachable + resp, err := http.Head(keyserver) + if err != nil || resp.StatusCode >= 400 { + log.Println("Key server not reachable, exiting.") + os.Exit(1) + } + + // Generate UUID + uuid := getCPUModel() + getEth0MAC() + getSystemUUID() + keyID := md5sum(uuid) + + log.Println("Downloading key from key server") + keyURL := fmt.Sprintf("%s/%s-encryption-service-credentials.json", keyserver, keyID) + resp, err = http.Get(keyURL) + if err != nil || resp.StatusCode != 200 { + log.Println("Failed to download key from key server") + os.Exit(1) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) // Changed from ioutil.ReadAll + if err != nil { + log.Println("Failed to read key file") + os.Exit(1) + } + + var creds Credentials + if err := json.Unmarshal(body, &creds); err != nil { + log.Println("Failed to extract credentials from key file") + os.Exit(1) + } + + os.Setenv("AWS_ACCESS_KEY_ID", creds.AccessKey) + os.Setenv("AWS_SECRET_ACCESS_KEY", creds.SecretKey) + + // Prepare socket dir + socketDir := filepath.Dir(socket) + os.MkdirAll(socketDir, 0700) + os.Chmod(socketDir, 0700) + + if _, err := os.Stat("/usr/local/bin/aws-encryption-provider"); os.IsNotExist(err) { + log.Println("Encryption provider not found, exiting.") + os.Exit(1) + } + + cmd := exec.Command("/usr/local/bin/aws-encryption-provider", + "--key="+creds.KeyArn, + "--region="+creds.Region, + "--listen="+socket, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + log.Println("Failed to start encryption provider") + os.Exit(1) + } + os.Exit(0) +} + +func getCPUModel() string { + data, err := os.ReadFile("/proc/cpuinfo") // Changed from ioutil.ReadFile + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "model name") { + return line + } + } + return "" +} + +func getEth0MAC() string { + iface, err := net.InterfaceByName("eth0") + if err != nil { + return "" + } + return iface.HardwareAddr.String() +} + +func getSystemUUID() string { + out, err := exec.Command("sudo", "dmidecode", "-s", "system-uuid").Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func md5sum(s string) string { + h := md5.New() + io.WriteString(h, s) + return hex.EncodeToString(h.Sum(nil)) +}