Getting back a password from Polkawallet JSON File
#1
Hello guys,

I have quite an annoying problem.

I was using a Polkawallet (Polkawallet.js Chrome extension). 

I had stored my seed phrase and password on Keepass, but they were not saved yet, and a power outage disconnected everything. I lost both password and seed phrase in the process. 
I had exported the account before, so I still have my JSON file.

I don't know yet how to use Hashcat and other similar cracking softwares. I would like to know first if you think there is a way to get back my password from the JSON file? In such a case, I will do my best to learn how Hashcat works in order to get the password back.

Thanks in advance for your help. I am kinda stuck right now.
Reply
#2
wait ? you have send some money to that cryptocurrency address without saving the password first ?

I don't think that Polkawallet is officially supported by hashcat, somebody would need to look the exact algorithm up and see if it makes sense to add it to hashcat (or if some other mode already works).

but what type of password are we talking about here? is it even short and easy to guess ?

a "seed phrase" doesn't really sound like a feasible target to crack. Or is it a password from a dictioanry / word list file instead ? how feasible is it to guess the password itself ? do you know some possible password candidates ?
Reply
#3
Thanks for your reply Phil.

Actually, it was a wallet to receive a presale (Centrifuge) and an airdrop. I received it, the wallet is still open, but I can't send the tokens somewhere else, as my password is required.
I had stored the password in an entry in Keepass, but forgot to save the Keepass file, and a power outage erased everything shortly after that...

About the password, it should be composed of a word I think I remember, plus some numbers and maybe a few special characters like ** or //. I tried many possibilities yet, but without success.
Reply
#4
okay, so that's already good that you think a "easy-to-guess password" could unlock the account.

I think this might be related: https://github.com/polkawallet-io/app/issues/22

but I'm not sure about that. somebody would need to investigate if this is the correct algorithm.

The bad news (at least if that github issue and the claims within it are applicable here) is that it uses some heavy algorithms (scrypt is involved, GPU-resistent, hard to crack).

Do you think you can find some JSON files with known password online (maybe test accounts), or generate a completely new one (without any funds etc) with known password and maybe sent it in a PM to me and/or open a hashcat github issue with the full details.

It would also make sense if we would know which exact code is involved here, for instance is it true that https://github.com/polkawallet-io/app/ is the software and https://www.npmjs.com/package/@polkadot/keyring and https://github.com/polkadot-js/common/tr...es/keyring are related here ?

Unfortunately, until now, I myself didn't know anything about this specific wallet... maybe somebody else has already a clue what the exact algo is here , and can help us a little bit here !?




update: there might be some examples with password here: https://github.com/polkadot-js/common/bl...#L539-L540
but I'm not 100% sure if this is the correct wallet and version (there are different version 2 / 3 etc !!!)
Reply
#5
Thank you very much for your help Phil.

I will send you another JSON file from another wallet with known password via PM.

It is about this one:
https://github.com/polkadot-js/
Version is 0.38.3 it seems.
Reply
#6
I've received your PM , but I can't do much about it without knowing the exact password (I'm not going to waste my time to crack it). DId you generate this hash or is this a public available test (github link ?) ?

but there is also good news, I've investigated this now a little bit and found out the exact algorithm details.

As expected, it's using Scrypt as key derivation (derive a key from the password) with parameters N = 32768, r = 8 and p = 1 (so quite high N value, difficult to crack fast).

After that the "encoded" part needs to be split again (it also contains the salt for scrypt and the N,r,p values at the start, but the Nrp parameters are hard-coded in the source code at the time of this writing). The "encoded" part contains both the verifier and also the message that needs to be "decrypted".
Polkawallet uses xsalsa20 to generate a new subkey from the key derived from the scrypt hashing of the password and afterwards uses Poly1305 to verify/decrypt/test the message (authentication, verifier). So it basically just uses the NaCl standard.

I've coded this as a POC (proof of concept) cracker for you in both python and perl, just for demonstration.

python (polkawallet.py):
Code:
#!/usr/bin/env python3

# Author: philsmd
# Date: July 2021
# License: public domain, credits go to philsmd and hashcat

# Note: NaCl uses XSalsa20 and Poly1305 for decrypting the data.
# Key derivation is done by scrypt (32768, 8, 1)

# only tested with version 3 of a PolkaWallet test wallet

from base64 import b64decode

import sys
import struct
import scrypt # py-scrypt (or use hashlib.scrypt or passlib.hash.scrypt)

from nacl.secret import SecretBox # install PyNaCl

#
# Constants
#

SCRYPT_DEFAULT_N = 32768 # 1 << 15 (2^15)
SCRYPT_DEFAULT_P =     1
SCRYPT_DEFAULT_R =     8


#
# Examples
#

# const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
# PASS3 = "version3";

ENCODED = "ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4"


#
# Start
#

if len (sys.argv) < 2:
  print ("ERROR: Please specify the dict file within the command line", file=sys.stderr)

  sys.exit (1)

fp = None

try:
  fp = open (sys.argv[1])
except:
  print ("ERROR: Could not open dictionary file '%s'" % sys.argv[1], file=sys.stderr)

  sys.exit (1)

raw_data = b64decode (ENCODED)

salt = raw_data[0:32]

scrypt_n = struct.unpack ("<I", raw_data[32:36])[0]
scrypt_p = struct.unpack ("<I", raw_data[36:40])[0]
scrypt_r = struct.unpack ("<I", raw_data[40:44])[0]

if scrypt_n != SCRYPT_DEFAULT_N:
  print ("ERROR: Scrypt N value not valid", file=sys.stderr)

  sys.exit (1)

if scrypt_p != SCRYPT_DEFAULT_P:
  print ("ERROR: Scrypt P value not valid", file=sys.stderr)

  sys.exit (1)

if scrypt_r != SCRYPT_DEFAULT_R:
  print ("ERROR: Scrypt R value not valid", file=sys.stderr)

  sys.exit (1)

offset = 32 + (3 * 4) # 32 byte salt + 3 numbers (N, p, r)

nonce     = raw_data[offset +  0:offset + 24]
encrypted = raw_data[offset + 24:]

cracked = False

password = fp.readline ()

while password:
  key = scrypt.hash (password.strip (), salt, N = SCRYPT_DEFAULT_N, r = SCRYPT_DEFAULT_R, p = SCRYPT_DEFAULT_P, buflen = 32)

  box = SecretBox (key)

  try:
    box.decrypt (encrypted, nonce)

    print ("Password found: '%s'" % password.strip ())

    cracked = True

    break
  except:
    password = fp.readline ()


# Cleanup:

fp.close ()


# Exit codes:

if cracked:
  sys.exit (0)
else:
  sys.exit (1)

(note PyNaCl and scrypt must be installed with python pip or similar)

perl (polkawallet.pl):
Code:
#!/usr/bin/env perl

# Author: philsmd
# Date: July 2021
# License: public domain, credits go to philsmd and hashcat

# Note: NaCl uses XSalsa20 and Poly1305 for decrypting the data.
# Key derivation is done by scrypt (32768, 8, 1)

# only tested with version 3 of a PolkaWallet test wallet

use strict;
use warnings;

use MIME::Base64     qw (decode_base64);
use Crypt::ScryptKDF qw (scrypt_raw);
use Crypt::Sodium    qw (crypto_secretbox_open);
# Unfortunately, Crypt::NaCl::Sodium seems to be outdated (and not compiling)

#
# Constants
#

my $SCRYPT_DEFAULT_N = 32768; # 1 << 15 (2 ^ 15)
my $SCRYPT_DEFAULT_P =     1;
my $SCRYPT_DEFAULT_R =     8;


#
# Examples
#

# const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
#my $PASS    = "version3";

my $ENCODED = "ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4";


#
# Start
#

my $raw_data = decode_base64 ($ENCODED);

my $salt = substr ($raw_data, 0, 32);

my $scrypt_n = unpack ("I<", substr ($raw_data, 32, 4));
my $scrypt_p = unpack ("I<", substr ($raw_data, 36, 4));
my $scrypt_r = unpack ("I<", substr ($raw_data, 40, 4));

if ($scrypt_n != $SCRYPT_DEFAULT_N)
{
  print STDERR "ERROR: Scrypt N value not valid\n";

  exit (1);
}

if ($scrypt_p != $SCRYPT_DEFAULT_P)
{
  print STDERR "ERROR: Scrypt P value not valid\n";

  exit (1);
}

if ($scrypt_r != $SCRYPT_DEFAULT_R)
{
  print STDERR "ERROR: Scrypt R value not valid\n";

  exit (1);
}

my $nonce     = substr ($raw_data, 32 + (3 * 4) +  0, 24);
my $encrypted = substr ($raw_data, 32 + (3 * 4) + 24);

while (my $pass = <>)
{
  chomp ($pass);

  my $key = scrypt_raw ($pass, $salt, $SCRYPT_DEFAULT_N, $SCRYPT_DEFAULT_R, $SCRYPT_DEFAULT_P, 32);

  my $decrypted = crypto_secretbox_open ($encrypted, $nonce, $key);

  next if (! defined ($decrypted));

  print "Password found: '$pass'\n";

  exit (0);
}

exit (1);

(note Crypt::ScryptKDF and Crypt::Sodium must be installed with perl cpan or similar)

perl with my own xsalsa20 + poly1305 code/explanation of how that works (polkawallet_xsalsa20_poly1305.pl):
Code:
#!/usr/bin/env perl

# Author: philsmd
# Date: July 2021
# License: public domain, credits go to philsmd and hashcat

# Note: NaCl uses XSalsa20 and Poly1305 for decrypting the data.
# Key derivation is done by scrypt (32768, 8, 1)

use strict;
use warnings;

use MIME::Base64     qw (decode_base64);
use Crypt::ScryptKDF qw (scrypt_raw);


#
# Constants
#

my $SCRYPT_DEFAULT_N = 32768; # 1 << 15 (2 ^ 15)
my $SCRYPT_DEFAULT_P =     1;
my $SCRYPT_DEFAULT_R =     8;


#
# Examples
#

# const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
#my $PASS    = "version3";

my $ENCODED = "ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4";


#
# Helper functions
#

sub rotate
{
  my $x = shift;
  my $o = shift; # offset

  $x &= 0xffffffff;

  return (($x << $o) | ($x >> (32 - $o))) & 0xffffffff;
}

sub u32le_from_bytes
{
  my $x = shift;
  my $o = shift; # offset

  return (ord (substr ($x, $o + 0, 1)) <<  0) |
         (ord (substr ($x, $o + 1, 1)) <<  8) |
         (ord (substr ($x, $o + 2, 1)) << 16) |
         (ord (substr ($x, $o + 3, 1)) << 24);
}

sub u32le_to_bytes
{
  my $x = shift;

  return chr (($x >>  0) & 0xff) .
         chr (($x >>  8) & 0xff) .
         chr (($x >> 16) & 0xff) .
         chr (($x >> 24) & 0xff);
}

sub salsa20_core
{
  my $n = shift; # nonce
  my $k = shift; # key
  my $t = shift; # type

  my $S = "expand 32-byte k"; # SIGMA

  my @x = ();

  $x[ 0] = u32le_from_bytes ($S,  0);
  $x[ 1] = u32le_from_bytes ($k,  0);
  $x[ 2] = u32le_from_bytes ($k,  4);
  $x[ 3] = u32le_from_bytes ($k,  8);
  $x[ 4] = u32le_from_bytes ($k, 12);
  $x[ 5] = u32le_from_bytes ($S,  4);

  $x[ 6] = u32le_from_bytes ($n,  0);
  $x[ 7] = u32le_from_bytes ($n,  4);
  $x[ 8] = u32le_from_bytes ($n,  8);
  $x[ 9] = u32le_from_bytes ($n, 12);

  $x[10] = u32le_from_bytes ($S,  8);
  $x[11] = u32le_from_bytes ($k, 16);
  $x[12] = u32le_from_bytes ($k, 20);
  $x[13] = u32le_from_bytes ($k, 24);
  $x[14] = u32le_from_bytes ($k, 28);
  $x[15] = u32le_from_bytes ($S, 12);

  my @j = ();

  for (my $i = 0; $i < 16; $i++)
  {
    $j[$i] = $x[$i];
  }

  for (my $i = 20; $i > 0; $i -= 2) # 20 rounds
  {
    $x[ 4] ^= rotate ($x[ 0] + $x[12],  7); # don't forget the u32 logical ANDs (& 0xffffffff)
    $x[ 8] ^= rotate ($x[ 4] + $x[ 0],  9);
    $x[12] ^= rotate ($x[ 8] + $x[ 4], 13);
    $x[ 0] ^= rotate ($x[12] + $x[ 8], 18);

    $x[ 9] ^= rotate ($x[ 5] + $x[ 1],  7);
    $x[13] ^= rotate ($x[ 9] + $x[ 5],  9);
    $x[ 1] ^= rotate ($x[13] + $x[ 9], 13);
    $x[ 5] ^= rotate ($x[ 1] + $x[13], 18);

    $x[14] ^= rotate ($x[10] + $x[ 6],  7);
    $x[ 2] ^= rotate ($x[14] + $x[10],  9);
    $x[ 6] ^= rotate ($x[ 2] + $x[14], 13);
    $x[10] ^= rotate ($x[ 6] + $x[ 2], 18);

    $x[ 3] ^= rotate ($x[15] + $x[11],  7);
    $x[ 7] ^= rotate ($x[ 3] + $x[15],  9);
    $x[11] ^= rotate ($x[ 7] + $x[ 3], 13);
    $x[15] ^= rotate ($x[11] + $x[ 7], 18);

    $x[ 1] ^= rotate ($x[ 0] + $x[ 3],  7);
    $x[ 2] ^= rotate ($x[ 1] + $x[ 0],  9);
    $x[ 3] ^= rotate ($x[ 2] + $x[ 1], 13);
    $x[ 0] ^= rotate ($x[ 3] + $x[ 2], 18);

    $x[ 6] ^= rotate ($x[ 5] + $x[ 4],  7);
    $x[ 7] ^= rotate ($x[ 6] + $x[ 5],  9);
    $x[ 4] ^= rotate ($x[ 7] + $x[ 6], 13);
    $x[ 5] ^= rotate ($x[ 4] + $x[ 7], 18);

    $x[11] ^= rotate ($x[10] + $x[ 9],  7);
    $x[ 8] ^= rotate ($x[11] + $x[10],  9);
    $x[ 9] ^= rotate ($x[ 8] + $x[11], 13);
    $x[10] ^= rotate ($x[ 9] + $x[ 8], 18);

    $x[12] ^= rotate ($x[15] + $x[14],  7);
    $x[13] ^= rotate ($x[12] + $x[15],  9);
    $x[14] ^= rotate ($x[13] + $x[12], 13);
    $x[15] ^= rotate ($x[14] + $x[13], 18);
  }

  for (my $i = 0; $i < 16; $i++)
  {
    $x[$i] += $j[$i]; # & 0xffffffff
  }

  if ($t == 1)
  {
    $x[ 0] -= u32le_from_bytes ($S,  0);
    $x[ 5] -= u32le_from_bytes ($S,  4);
    $x[10] -= u32le_from_bytes ($S,  8);
    $x[15] -= u32le_from_bytes ($S, 12);

    $x[ 6] -= u32le_from_bytes ($n,  0);
    $x[ 7] -= u32le_from_bytes ($n,  4);
    $x[ 8] -= u32le_from_bytes ($n,  8);
    $x[ 9] -= u32le_from_bytes ($n, 12);
  }


  # Output:

  my $out = "";

  if ($t == 1)
  {
    $out .= u32le_to_bytes ($x[ 0]);
    $out .= u32le_to_bytes ($x[ 5]);
    $out .= u32le_to_bytes ($x[10]);
    $out .= u32le_to_bytes ($x[15]);

    $out .= u32le_to_bytes ($x[ 6]);
    $out .= u32le_to_bytes ($x[ 7]);
    $out .= u32le_to_bytes ($x[ 8]);
    $out .= u32le_to_bytes ($x[ 9]);
  }
  else
  {
    $out .= u32le_to_bytes ($x[ 0]);
    $out .= u32le_to_bytes ($x[ 1]);
    $out .= u32le_to_bytes ($x[ 2]);
    $out .= u32le_to_bytes ($x[ 3]);

    $out .= u32le_to_bytes ($x[ 4]);
    $out .= u32le_to_bytes ($x[ 5]);
    $out .= u32le_to_bytes ($x[ 6]);
    $out .= u32le_to_bytes ($x[ 7]);
  }

  return $out;
}

sub xsalsa20_subkey
{
  my $nonce = shift;
  my $key1  = shift;

  my $n1 = substr ($nonce,  0, 16);
  my $n2 = substr ($nonce, 16,  8) . "\x00" x 8; # also use last 8 bytes of the nonce

  my $key2 = salsa20_core ($n1, $key1, 1);
  my $out  = salsa20_core ($n2, $key2, 0);

  return $out;
}

sub poly1305_add
{
  my $h = shift; # array pointer
  my $c = shift; # array pointer

  my $u = 0;

  for (my $i = 0; $i < 17; $i++)
  {
    $u = ($u + $$h[$i] + $$c[$i]) & 0xffffffff;

    $$h[$i] = $u & 255;

    $u >>= 8;
  }
}

sub poly1305_calc_verifier
{
  my $m = shift; # message
  my $k = shift; # key

  my $l = length ($m);

  my @PN = (5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252); # -P

  my @x = (0) x 17;
  my @h = (0) x 17;
  my @c = (0) x 17;
  my @r = (0) x 17;

  for (my $i = 0; $i < 16; $i++)
  {
    $r[$i] = ord (substr ($k, $i, 1));
  }

  $r[ 3] &=  15;
  $r[ 4] &= 252;
  $r[ 7] &=  15;
  $r[ 8] &= 252;
  $r[11] &=  15;
  $r[12] &= 252;
  $r[15] &=  15;

  my $pos = 0;

  while ($l > 0)
  {
    for (my $i = 0; $i < 17; $i++)
    {
      $c[$i] = 0;
    }

    my $z = 0;

    for ($z = 0; ($z < 16) && ($z < $l); $z++)
    {
      $c[$z] = ord (substr ($m, $pos + $z, 1));
    }

    $c[$z] = 1;

    $pos += $z;
    $l   -= $z;

    poly1305_add (\@h, \@c);

    for (my $i = 0; $i < 17; $i++)
    {
      $x[$i] = 0;

      for (my $j = 0; $j < 17; $j++)
      {
        if ($j <= $i)
        {
          $x[$i] += $h[$j] * $r[$i - $j +  0] *   1;
        }
        else
        {
          $x[$i] += $h[$j] * $r[$i - $j + 17] * 320;
        }

        $x[$i] &= 0xffffffff; # needed ? just to be safe
      }
    }

    for (my $i = 0; $i < 17; $i++)
    {
      $h[$i] = $x[$i];
    }

    my $u = 0;

    for (my $i = 0; $i < 16; $i++)
    {
      $u = ($u + $h[$i]) & 0xffffffff;

      $h[$i] = $u & 255;

      $u >>= 8;
    }

    $u = ($u + $h[16]) & 0xffffffff;

    $h[16] = $u & 3;

    $u = (5 * ($u >> 2)) & 0xffffffff;

    for (my $i = 0; $i < 16; $i++)
    {
      $u = ($u + $h[$i]) & 0xffffffff;

      $h[$i] = $u & 255;

      $u >>= 8;
    }

    $u = ($u + $h[16]) & 0xffffffff;

    $h[16] = $u;
  }


  my @g = (0) x 17;

  for (my $i = 0; $i < 17; $i++)
  {
    $g[$i] = $h[$i];
  }

  poly1305_add (\@h, \@PN);


  my $s = -($h[16] >> 7);


  for (my $i = 0; $i < 17; $i++)
  {
    $h[$i] ^= $s & ($g[$i] ^ $h[$i]);
  }


  for (my $i = 0; $i < 16; $i++)
  {
    $c[$i] = ord (substr ($k, $i + 16, 1));
  }

  $c[16] = 0;

  poly1305_add (\@h, \@c);


  # Output:

  my $out = "";

  for (my $i = 0; $i < 16; $i++)
  {
    $out .= chr ($h[$i]);
  }

  return $out;
}

sub poly1305_auth_verify
{
  my $v = shift; # verifier
  my $m = shift; # message
  my $k = shift; # key

  my $x = poly1305_calc_verifier ($m, $k);

  if ($x eq $v)
  {
    return 1; # success
  }

  return 0; # fail
}


#
# Start
#

my $raw_data = decode_base64 ($ENCODED);

my $salt = substr ($raw_data, 0, 32);

my $scrypt_n = unpack ("I<", substr ($raw_data, 32, 4));
my $scrypt_p = unpack ("I<", substr ($raw_data, 36, 4));
my $scrypt_r = unpack ("I<", substr ($raw_data, 40, 4));

if ($scrypt_n != $SCRYPT_DEFAULT_N)
{
  print STDERR "ERROR: Scrypt N value not valid\n";

  exit (1);
}

if ($scrypt_p != $SCRYPT_DEFAULT_P)
{
  print STDERR "ERROR: Scrypt P value not valid\n";

  exit (1);
}

if ($scrypt_r != $SCRYPT_DEFAULT_R)
{
  print STDERR "ERROR: Scrypt R value not valid\n";

  exit (1);
}

my $nonce    = substr ($raw_data, 32 + (3 * 4) +  0, 24);
my $verifier = substr ($raw_data, 32 + (3 * 4) + 24, 16);
my $message  = substr ($raw_data, 32 + (3 * 4) + 40);

while (my $pass = <>)
{
  chomp ($pass);

  my $key = scrypt_raw ($pass, $salt, $SCRYPT_DEFAULT_N, $SCRYPT_DEFAULT_R, $SCRYPT_DEFAULT_P, 32);

  my $subkey = xsalsa20_subkey ($nonce, $key);

  next if (poly1305_auth_verify ($verifier, $message, $subkey) == 0); # 0 means incorrect

  print "Password found: '$pass'\n";

  exit (0);
}

exit (1);

you can run these "cracker" tools like this, for python
Code:
python3 polkawallet.py dict.txt

for perl:
Code:
perl polkawallet.pl dict.txt

the dictionary file (dict.txt) must contain the password (in this case "version3" without quotes works for that specific wallet).

If you want to test/crack another wallet, you need to change the "ENCODED" part with your specific data within the python/perl scripts.

I"m not sure if this specific algorithm should be implemented in hashcat, you can still request it on github, but it's very diffictult to crack and I didn't hear a lot (unitl now) about this wallet, so maybe the interest from other users is very tiny.

Nonetheless, you can still use the crackers/POC above to test thousands up to millions of password candidates that you want to test quite quickly.



update: want to include some sources for interested readers about the algorithm details:
- https://github.com/polkadot-js/common/bl...s.ts#L5-L7
- https://github.com/jedisct1/libsodium/bl...1305.c#L25
- https://github.com/dchest/tweetnacl-js/b...#L259-L260
- https://github.com/dchest/tweetnacl-js/b...cl.js#L186
- https://github.com/neilalexander/jnacl/b...va#L58-L60
- https://stackoverflow.com/questions/6347...-nodejs-12
- https://github.com/dchest/tweetnacl-util...cl-util.js
- https://github.com/neilalexander/jnacl/b...va#L58-L60
- https://github.com/neilalexander/jnacl/b...0.java#L44
- https://github.com/dchest/tweetnacl-js/b...cl.js#L259
- https://github.com/dchest/tweetnacl-js/b...cl.js#L163
Reply
#7
philsmd, thank you for your work on these scripts.

It looks like you wrote these scripts for a version 3 json wallet.  I'm trying to run it for a version 2 json wallet.

I first ran it on the version 3 wallet listed in your python script, and it found the password correctly.

Next, I ran it on the test version 2 wallet listed at lines 489 and 490 on this page (https://github.com/polkadot-js/common/bl...#L539-L540)

When I ran: python3 polkawallet dict.txt

I got the message: "ERROR: Scrypt N value not valid"

And, here I'm getting stuck.  The ENCODED string in a version 2 wallet appears to be longer than the ENCODED string in a version 3 wallet. 

And, in your script, here's how the value of script_n is getting populated:

scrypt_n = struct.unpack ("<I", raw_data[32:36])[0]

It appears that I need to unpack the script_n variable (and perhaps the other variables) from different positions in the ENCODED string, but I'm a bit lost.

Any chance you could update your script to work with version 2 wallets as well, or point me in the right direction?

Thanks!
Reply
#8
hey madbury,

the answer to what the differences are between version 2 and version 3 Polkawallet algorithms can be seen within the source code of for instance the github repo linked above (polkadot-js or probably others too).

The main differences are these: NO SCRYPT !!! which should give you a HUGE performance advantage because scrypt is very slow (intentionally slow of course ! this is to make it difficult to brute-force or mask attack etc)

The main differences are here and these are probably also the 2 parts that you didn't find that easily (without my help, hehe):
1. no scrypt: https://github.com/polkadot-js/common/bl...ts#L27-L33
2. password to key conversion (just NUL-byte padding, hehe): https://github.com/polkadot-js/common/bl...ts#L20-L32

here is the python3 version, you could easily also convert the perl version etc... now that you have all the algorithm details:
polkawallet2.py:
Code:
#!/usr/bin/env python3

# Author: philsmd
# Date: April 2022
# License: public domain, credits go to philsmd and hashcat

# Note: NaCl uses XSalsa20 and Poly1305 for decrypting the data.
# there is *NO* key derivation for version 2 wallets ! i.e. the password is used AS-IS,
# but NULL-byte (\x00) padded to 32 bytes

# only tested with version 2 of a PolkaWallet test wallet

import binascii
import sys
from nacl.secret import SecretBox # install PyNaCl


#
# Examples
#

# const PAIR = '{"address":"5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu","encoded":"0xee8f236e2ac3217ce689692a4afc612220dc77fddaed0482f8f95136a7c3e034cccfbc495410a6e9b2439904974ed1d207abeca536ff6985ceb78edeeb3dc343e561c184c488101af8811d1331430b4ccf0e96ef507132e5132964e8564232e7100d973c5bee7b231dd0c8ad5273f3501515a422c8d7ed9d20a73c0ed17c98ee4588e54844bb73052dcad81f7a1094613d63c162fec7446c88b1fae70e","encoding":{"content":["pkcs8","sr25519"],"type":"xsalsa20-poly1305","version":"2"},"meta":{"genesisHash":"0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e","name":"json v2","tags":[],"whenCreated":1595243159596}}';
# PASS2 = "versionTwo";

ENCODED = "0xee8f236e2ac3217ce689692a4afc612220dc77fddaed0482f8f95136a7c3e034cccfbc495410a6e9b2439904974ed1d207abeca536ff6985ceb78edeeb3dc343e561c184c488101af8811d1331430b4ccf0e96ef507132e5132964e8564232e7100d973c5bee7b231dd0c8ad5273f3501515a422c8d7ed9d20a73c0ed17c98ee4588e54844bb73052dcad81f7a1094613d63c162fec7446c88b1fae70e"


#
# Start
#

if len (sys.argv) < 2:
  print ("ERROR: Please specify the dict file within the command line", file=sys.stderr)

  sys.exit (1)

fp = None

try:
  fp = open (sys.argv[1])
except:
  print ("ERROR: Could not open dictionary file '%s'" % sys.argv[1], file=sys.stderr)

  sys.exit (1)

if ENCODED[0:2] != "0x":
  print ("ERROR: not a valid ENCODED string for version 2 wallets", file=sys.stderr)

  sys.exit (1)

if len (ENCODED) < (2 + 24 + 32):
  print ("ERROR: too short ENCODED string for version 2 wallets", file=sys.stderr)

  sys.exit (1)

raw_data = binascii.unhexlify (ENCODED[2:])

nonce     = raw_data[ 0:24]
encrypted = raw_data[24:  ]

cracked = False

password = fp.readline ()

while password:
  password = password.strip ()

  key = (password + ("\x00" * 32)) [0:32] # pad with 32 \x00 bytes, take the first 32 bytes as key
  key = key.encode () # python 3 shenanigans

  box = SecretBox (key)

  try:
    box.decrypt (encrypted, nonce)

    print ("Password found: '%s'" % password)

    cracked = True

    break
  except:
    password = fp.readline ()


# Cleanup:

fp.close ()


# Exit codes:

if cracked:
  sys.exit (0)
else:
  sys.exit (1)

of course you would need to adapt the "ENCODED" string (the most important data from the JSON file that is needed to verify a password). one could also think about adapting the script and taking 2 command line arguments, first could be the json wallet, second one could be the password... for convenience

hope this helps, thx
Reply