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
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 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
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 };
}