Protection from Account Thieves

Words
335
Reading
2 min
Listen
Play
8y

Repository

https://github.com/r351574nc3/we-resist-bot

Motivation

Recently, a number of accounts have had funds stolen immediately by using an active/master key in a transfer memo. It's immediate because there is a bot that waits for keys to appear in a memo or comment, and then transfers funds from the account immediately. See for example

New Features

Below is a sample post that is made to notify the account owner of the compromise.

I wanted to provide a way to protect people from this. Many of us in spite of being careful not to post keys, do it anyway. A bot will automatically detect this mistake and prey upon people before they can correct it. I wanted to provide a way to lock down a user's account before the bot can get to it. It does this by

  • Adding the-resistance@the-resistance as an account manager
  • Remove existing keys from the account
  • Change the password

Details

Added check for transfer that have keys in them

case "transfer":
    try {
        const private_key = operation.memo
        let public_key = steem.auth.wifToPublic(private_key)
        wif_is_valid = steem.auth.wifIsValid(private_key, public_key)
        if (wif_is_valid && operation.from == 'perpetuator') {
            return processTransfer(operation, private_key, public_key)
        }
    }
    catch (error) {
            if (error.message.indexOf("Non-base58 character") < 0
                && error.message.indexOf("Expected version") < 0
                && error.message.indexOf("Index out of range") < 0) {
            console.log("Rethrowing ", error)
            throw error // rethrow
        }
    }
    break;

Prove it's a private key by generating a public key and verifying it. If this is the case, we then process the transfer.

Add a function for processing transfers

Assuming the transfer has a key in the memo

function processTransfer(transfer, private_key, public_key) {
    const password = steem.formatter.createSuggestedPassword();
    const account_name = transfer.from
    const new_active_keypair = generate_keys(account_name, password, "active");

A new password and set of keys are created

Make sure a record of the transfer and temporary keys is persisted in case anything goes wrong

Keys are temporary, so it's ok to store them

    // Save keys to datastore
    models.Recovery.create({ 
        username: transfer.from, 
        password: password, 
        memo: transfer.memo,
        privateKey: new_active_keypair.private_key,
        publicKey: new_active_keypair.public_key })
        .then((recovery) => {
            console.log("Recovery saved for ", transfer.from)
        })

Add authorities to the compromised account and remove the old keys

    // Add the-resistance as manager
    return addAccountAuth(private_key, transfer.from, "the-resistance", "active", 10000)
        .then((results) => {
            // Extra key for management
            return addKeyAuth(private_key, transfer.from, new_active_keypair.public_key, "active", 10000)
        })
        .then((results) => {
            // Remove the old key so things can't be stolen
            return removeKeyAuth(private_key, transfer.from, public_key, "active")
        })
        .then((results) => {

This makes it so that the-resistance@the-resistance now controls the account on behalf of the user and has removed the key that was compromised.

Notification of compromised account

            // Post something to let the account holder know what to do.
            const context = {
                owner: transfer.from
            }
            return loadTemplate(path.join(__dirname, '..', 'templates', 'hijack.hb'))
                .then((template) => {
                    var templateSpec = Handlebars.compile(template)
                    return templateSpec(context)
                })
                .then((message) => {
                    var new_permlink = 'this-account-is-protected' 
                        + '-' + new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '').toLowerCase();
                    console.log("Commenting on ", transfer.from, new_permlink)

                    return steem.broadcast.commentAsync(
                        wif,
                        "", // Leave parent author empty
                        "abuse", // Main tag
                        transfer.from, // Author
                        new_permlink, // Permlink
                        "This Account is Protected by @the-resistance",
                        message, // Body
                        { tags: ['the-resistance'], app: 'we-resist-bot/0.1.0' }
                    ).then((results) => {
                        console.log(results)
                        return results
                    })
                    .catch((err) => {
                        console.log("Error ", err.message)
                    })
                })

The above notifies via steemit post of the compromise. The intent is that the user notices the post was made in his/her own account and attempts to contact the-resistance@the-resistance for recovery

New methods for handling adding/removing authorities

+                active,
+                posting,
+                userAccount.memo_key,
+                userAccount.json_metadata
+            )
+        });
+}
+
+/**
+ * Removes an authority using a public key
+ * @param {*} signingKey 
+ * @param {*} username 
+ * @param {*} authorizedKey 
+ * @param {*} role 
+ */
+function removeKeyAuth(signingKey, username, authorizedKey, role) {
+    return steem.api.getAccountsAsync([username])
+        .map((userAccount) => {
+            const updatedAuthority = userAccount[role];
+            const totalAuthorizedKey = updatedAuthority.key_auths.length;
+            for (let i = 0; i < totalAuthorizedKey; i++) {
+                const user = updatedAuthority.key_auths[i];
+                if (user[0] === authorizedKey) {
+                    updatedAuthority.key_auths.splice(i, 1);
+                    break;
+                }
+            }
+
+            /** Release callback if the key does not exist in the key_auths array */
+            if (totalAuthorizedKey === updatedAuthority.key_auths.length) {
+                return null;
+            }
+
+            const owner = role === 'owner' ? updatedAuthority : undefined;
+            const active = role === 'active' ? updatedAuthority : undefined;
+            const posting = role === 'posting' ? updatedAuthority : undefined;
+
+            return steem.broadcast.accountUpdateAsync(
+                signingKey,
+                userAccount.name,
+                owner,
+                active,
+                posting,
+                userAccount.memo_key,
+                userAccount.json_metadata
+            );
+        });
+}

Added template loading to post a notification that the account was compromised

+
+function loadTemplate(template) {
+    return fs.readFileAsync(template, 'utf8')
+}
 
 function current_voting_power(vp_last, last_vote) {
     var seconds_since_vote = moment().add(7, 'hours').diff(moment(last_vote), 'seconds')
@@ -297,12 +303,204 @@ function processComment(comment) {
         })
 }

Added a function for generating keys from a new password

+function generate_keys(account, password, role) {
+    const private_key = steem.auth.toWif(account, password, role);
+    const public_key = steem.auth.wifToPublic(private_key);
+    return { private_key: private_key, public_key: public_key };
 }

GitHub Account

https://github.com/r351574nc3

Protection from Account Thieves | Ecency