A Dead man's Switch in Go

A Dead man's Switch in Go

Dead Men Tell No Tales

repo

Why

I found this video, and found the concept a rather intriguing. Though, instead of the whole switching it on before the encryption starts, is definitely not how I would do it.

For me, a dead man's switch should be switched on by default, and only decrypt upon asked along with the proper password of course.

The Idea

The whole plan is simple enough, let's make a small go-program to encrypt a file on start, then, when we receive the password it shall decrypt out file again. Such, as before you go out to do the all the nastinesses, you start the program.

Now, for funzies and making this a tad more secure and obscure we will be using an e-mail to unlock our file again.

The Research

The E-mail

First of all, we need a way to receive e-mails, this is definitely the hardest part.

For this post, though, we will go with emersion's go-imap.

The Encryption

To be completely honest, I do not know much about encryption, hashes and decryption. So I will just go with what what seems like a great encryption algorithm, namely the AES 256.

AES 256 is virtually impenetrable using brute-force methods. While a 56-bit DES key can be cracked in less than a day, AES would take billions of years to break using current computing technology. Hackers would be foolish to even attempt this type of attack.

The Code

32 characters

First we need to create a secure 32 character password for our AES:

echo 'madco.me' | sha256sum | base64 | head -c 32 ; echo

Please, do not use madco.me as your password.

Code

First, let's set some rights for our files that will be created. These are stolen from stackoverflow.com/a/42718395/754471.

const (
    // https://stackoverflow.com/a/42718395/754471
    OS_READ       = 04
    OS_WRITE      = 02
    OS_USER_SHIFT = 6

    OS_USER_R = OS_READ << OS_USER_SHIFT
    OS_USER_W = OS_WRITE << OS_USER_SHIFT
)

I use this helper function a lot, to check for errors.

func e(err error) {
    if err != nil {
        log.Fatalln(err)
    }
}

Now we need an encrypt-func

func encrypt(key string, bytes []byte) []byte {
    c, err := aes.NewCipher([]byte(key))
    e(err)

    gcm, err := cipher.NewGCM(c)
    e(err)

    nonce := make([]byte, gcm.NonceSize())

    _, err = io.ReadFull(rand.Reader, nonce)
    e(err)

    return gcm.Seal(nonce, nonce, bytes, nil)
}

And of course a way to decrypt our file again

func decrypt(key, bytes []byte) ([]byte, error) {
    c, err := aes.NewCipher([]byte(key))
    if err != nil {
        return []byte(""), err
    }

    gcm, err := cipher.NewGCM(c)
    if err != nil {
        return []byte(""), err
    }

    nonceSize := gcm.NonceSize()
    if len(bytes) < nonceSize {
        return []byte(""), err
    }

    nonce, cipherText := bytes[:nonceSize], bytes[nonceSize:]
    plain, err := gcm.Open(nil, nonce, cipherText, nil)
    if err != nil {
        return []byte(""), err
    }

    return plain, nil
}

Great, now that these are done, let's build the e-mail checker

We use these two modules to access the gmail imap:

In here we read five messages, and return a true if we receive the Unlock subject along with the proper passphrase, we generated this here.

We then split our body of the e-mail into smaller strings, and use the third one as our code. I had to fiddle a bit to figure this. So try out yourselves if you want to checkout what values the body provides.

If we receive the proper code, we decrypt and write the back a new and decrypted version of the encrypted file.

func checker(user, pass, file string, bytes []byte) bool {
    c, err := client.DialTLS("imap.gmail.com:993", nil)
    e(err)

    defer func() {
        err := c.Logout()
        e(err)
    }()

    err = c.Login(user, pass)
    e(err)

    mbox, err := c.Select("INBOX", true)
    e(err)

    seqset := new(imap.SeqSet)
    seqset.AddRange(mbox.Messages-5, mbox.Messages)

    section := &imap.BodySectionName{}
    items := []imap.FetchItem{section.FetchItem()}

    msgs := make(chan *imap.Message, 5)
    done := make(chan error, 1)

    go func() {
        done <- c.Fetch(seqset, items, msgs)
    }()

    for msg := range msgs {
        m, err := mail.ReadMessage(msg.GetBody(section))
        if err != nil {
            log.Println(err)
            break
        }

        subject := m.Header.Get("Subject")
        if subject == "Unlock" {
            body, err := ioutil.ReadAll(m.Body)
            if err != nil {
                log.Println(err)
                break
            }

            bodies := strings.Split(string(body), "\r\n")

            dat, err := decrypt([]byte(bodies[3]), bytes)
            e(err)

            orig := strings.Split(file, ".enc")[0]
            return ioutil.WriteFile(orig, dat, OS_USER_R|OS_USER_W) == nil
        }
    }

    return false
}

Finally we have the main func, in here we set two flags:

  • e: To choose if the file has already been encrypted - which is great for testing, as the original file will be deleted upon running this program.
  • f: The file to encrypt and remove.

Next-up we read in the lines from a pipe, I'll explain how to do this in after the code. Basically we read all the piped in lines and use those - There should always be - at least - three(!).

Finally we check the mail server every 2 seconds, just to set a limit.

func main() {
    encryptFile := flag.Bool("e", false, "To encrypt before running?")
    file := flag.String("f", "./data.txt", "File to encrypt")

    flag.Parse()

    reader := bufio.NewReader(os.Stdin)

    var inputs []string
    for {
        input, _, err := reader.ReadLine()
        if err != nil {
            break
        }

        inputs = append(inputs, string(input))
    }

    base := filepath.Base(*file)
    dir := filepath.Dir(*file)

    var err error
    var encrypted []byte

    if *encryptFile {
        // If not encrypted, let's encrypt out file first
        fileBytes, err := ioutil.ReadFile(*file)
        e(err)

        key := inputs[0]
        encrypted = encrypt(key, fileBytes)

        err = ioutil.WriteFile(dir+"/"+base+".enc", encrypted, OS_USER_R|OS_USER_W)
        e(err)

        os.Remove(*file)
    } else {
        // Read the contents of the already encrypted file
        encrypted, err = ioutil.ReadFile(*file + ".enc")
        e(err)
    }

    mail, pass := inputs[1], inputs[2]
    for range time.Tick(2 * time.Second) {
        if succ := checker(mail, pass, *file+".enc", encrypted); succ {
            break
        }
    }
}

Running

To run this program is quite easy.

We simply create a credentials file first - make sure this stays secret for absolutely everybody. This file should contain three lines:

  • The 32 char secret
  • Your e-mail
  • An app password for your e-mail, as we always want to call back the password, if needed.

To make an app password you simply navigate to this page: myaccount.google.com/apppasswords and sign-in. Then you choose mail and choose Other, give it a name and you'll see a password. Save this, as you won't be able to see it again.

Prod?

You should not take this code as no way near production ready. But hack on it, and I believe in you! You'll make something out off this.


{ Best, Mads Bram Cordes }

Did you find this article valuable?

Support Mads B. Cordes by becoming a sponsor. Any amount is appreciated!