Golang : Lock executable to a specific machine with unique hash of the machine



Tags : golang unique-machine-hash lock-executable software-license md5

Problem:

Got a friend that was looking for ways to lock her Golang executable to a specific machine at her client's office. Her idea is not to allow her client to transfer the executable to another machine and let a potential rival copying her executable to study back in their office.

The business model her company employs is to charge by the number of software licenses purchased by the client. -relic from old days ;-)

She plans to use a file instead of a dongle to implement this locking mechanism.

How to do that?

Solution:

She can create a separate program to generate a file that contains a hash specific to the machine only and if the executable is transferred to another machine, the executable won't run. It will prompt the person to contact her for support and possibly sales of additional new license. $$$$

generatelicense.go - this file should not be shared with the client!


 package main

 import (
  "bufio"
  "crypto/md5"
  "fmt"
  "github.com/shirou/gopsutil/host"
  "io"
  "os"
  "os/exec"
  "runtime"
  "strings"
 )

 func dealwithErr(err error) {
  if err != nil {
 fmt.Println(err)
 os.Exit(-1)
  }
 }

 func main() {
  runtimeOS := runtime.GOOS

  // host or machine kernel, uptime, platform Info
  hostStat, err := host.Info()
  dealwithErr(err)

  fmt.Println("OS : ", runtimeOS)
  fmt.Println("Host ID(uuid) : ", hostStat.HostID)

  cmd := exec.Command("cat", "/proc/cpuinfo")
  stdout, err := cmd.StdoutPipe()
  dealwithErr(err)

  err = cmd.Start()
  dealwithErr(err)

  defer cmd.Wait()
  buff := bufio.NewScanner(stdout)

  var result []string

  for buff.Scan() {
 result = append(result, buff.Text()+"\n")
  }

  //fmt.Println("cat /proc/cpuinfo : ")
  //fmt.Println(result)

  // generate a unique hash for this machine
  // base on a combination of /proc/cpuinfo data, OS name and UUID

  hash := md5.New()

  // flatten the slice back into string
  resultString := strings.Join(result, "\n")

  io.WriteString(hash, resultString+runtimeOS+hostStat.HostID)

  fmt.Printf("Unique hash for this machine : %x\n", hash.Sum(nil))

  // create the license file
  licenseFile, err := os.Create("./license.txt")
  dealwithErr(err)

 data := fmt.Sprintf("%x", hash.Sum(nil))

  _, err = io.WriteString(licenseFile, data)
  dealwithErr(err)

  licenseFile.Close()
  fmt.Println("Saved to license.txt")

 }

and she generates a license file for the client during installation. Once the license file has been generated, secure wipe the generator program. Just keep the license file for the executable to load and check.

mainexecutable.go


 package main

 import (
  "bufio"
  "crypto/md5"
  "fmt"
  "github.com/shirou/gopsutil/host"
  "io"
  "io/ioutil"
  "os"
  "os/exec"
  "runtime"
  "strings"
 )

 func dealwithErr(err error) {
  if err != nil {
 fmt.Println(err)
 os.Exit(-1)
  }
 }

 func main() {
  runtimeOS := runtime.GOOS

  // host or machine kernel, uptime, platform Info
  hostStat, err := host.Info()
  dealwithErr(err)

  cmd := exec.Command("cat", "/proc/cpuinfo")
  stdout, err := cmd.StdoutPipe()
  dealwithErr(err)

  err = cmd.Start()
  dealwithErr(err)

  defer cmd.Wait()
  buff := bufio.NewScanner(stdout)

  var result []string

  for buff.Scan() {
 result = append(result, buff.Text()+"\n")
  }

  // generate a unique hash for this machine
  // base on a combination of /proc/cpuinfo data, OS name and UUID

  hash := md5.New()

  // flatten the slice back into string
  resultString := strings.Join(result, "\n")

  io.WriteString(hash, resultString+runtimeOS+hostStat.HostID)

  thisMachineData := fmt.Sprintf("%x", hash.Sum(nil))

  // read from license.gob file and get the approved hash
  // if the hash from license file doesn't match the hash we compute here
  // prompt the user to contact sales for new license. $$$$

  //open the license file
  licenseFile, err := ioutil.ReadFile("./license.txt")
  dealwithErr(err)

  licenseData := string(licenseFile)

  if licenseData != thisMachineData {
 fmt.Println("This executable cannot run on this machine, please call 11223344 or email support@company.com to purchase new license")
 os.Exit(0)
  }

  // ok, license from file matches this machine's hash so proceed
  fmt.Println("License file is valid. Proceesing ..... ")
 }

For each execution of the main executable program, it will generate a md5 hash from the supplied parameters - OS name, CPU info and UUID. If the hash generated is the same as the hash found in the license file, then proceed.

NOTE: This implementation has been kept simple for this tutorial sake. You can further enhance it for your own use. Such as adding an expiration date for the software licensing agreement or limit to a number of users per session, etc.

Happy coding!

References:

https://www.socketloop.com/tutorials/golang-secure-file-deletion-with-wipe-example

https://www.socketloop.com/tutorials/golang-read-file-and-convert-content-to-string

https://socketloop.com/tutorials/golang-capture-text-return-from-exec-function-example

https://www.socketloop.com/tutorials/golang-get-hardware-information-such-as-disk-memory-and-cpu-usage

  See also : Golang : Get hardware information such as disk, memory and CPU usage



Tags : golang unique-machine-hash lock-executable software-license md5

By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.


Helping out