rippex/ripple wallet lost passphrase
#1
Wink 
hey there ,  i've received a request from a friend , the guy 3 year ago  have purchased xrp (ripple) and put it  inside rippex wallet (which is not available anymore)  and ofc now he does not remember his password , ive tried to search online for tools however there is nothing like hashcat,  i've seen this https://github.com/7763sea/hashcat-ripple-cversion and this https://github.com/edward852/xrp-wallet-recovery  , in the first github link in his bat he puts some hash (which i guess suppose to be the wallet) with -m 10900  , when seeing the wallet that he shared there ,after decoding to base64  i saw that the salt is indeed the salt however the CT (cypher text?) is not in the decode base64 wallet .

in the second link there is a wallet + password  , i tried to copy the "template"  of the first github link but that did not work .


example of the command that i have used can be found here.

https://pastebin.com/nds2SWuf

basically what i tried was  "sha256:1000:SALT:CT"


testwallet.txt from the second github link (xrp-wallet-recovery) , the password of this hash/wallet is qwer123

Code:
eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9


decodes to that

Code:
{"iv":"Fq2aUC4ez/gmj0Sc1G5ngw==","v":1,"iter":1000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"IMlHbVi9yTM=","ct":"8CqnBuZarZk8K6wyzqsBX6bzU/D6NL5HllJuMRrNdMdHEq+65RxhqUulYnl6oNCY0RXPkrjD1mZIwBKnw2Ls/9uz0ExuukT0oHZzDt3OAxO308mlFGhPvhJRkgXa/03mD82Hhy9/dcTngm/LvxpWUEE1gVuSAQNvmsU5SHbCARjc/j5FgSo/b8Gb98qU24ljSgNhpTMKS84xkA=="}
Reply
#2
Well, the full algorithm (I just researched this a little bit) is PBKDF2-HMAC-SHA256 + AES256-CCM .

Your example with an intermediary key (the key generated by the PBKDF2-HMAC-SHA256 key derivation) isn't very usual (I'm talking about https://github.com/7763sea/hashcat-ripple-cversion). This user seems to somehow got hold of the key that is normally generated on-the-fly and is not the actual "result" (e.g. the encrypted or decrypted data), but as I said just a key generated from the password that still needs to be used to decrypt the data. (the user probably got hold of this key somehow and now tries to recover the password, but even this doesn't make sense most of the time: whenever you have the PBKDF2 key you can just decrypt the data and you are done... it only makes sense to recover the password if you really need to KNOW the password for instance for forensic reason i.e. "we now have this users password and they might have reused this elsewhere").

The algorithm itself is similar to what I've research already here: https://github.com/hashcat/hashcat/issue...-712962171

It also uses this SJCL way (JSON and base64 encoded etc) of describing the algorithm details (key size, tag length, AES mode, etc etc etc).

Here again the code, similar to the github issue above, but modified to work with your examples.

Perl implementation (ripple_rippex.pl):
Code:
#!/usr/bin/env perl

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

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# Instead of our own implementation, we could use:
# Crypt::AuthEnc::CCM (ccm_encrypt_authenticate)

# ATTENTION: the length of the password is prepended (and "|") for the key generation

use strict;
use warnings;

use Crypt::PBKDF2;
use Crypt::Mode::ECB;
use JSON         qw (decode_json);
use MIME::Base64 qw (encode_base64 decode_base64);


#
# Example:
#

# Example 1:

# my $pass = "qwer123"; # => 7|qwer123 (length + | + pass)
# my $data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9";

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

my $pass = "qwe949461747"; # => 12|qwe949461747 (length + | + pass)
my $data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9";

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Helper function:
#

sub aes_ccm_decrypt_tag
{
  my $key  = shift;
  my $iv   = shift;
  my $data = shift;

  my $tag = "\x00" x 8; # output

  my $len = length ($data);

  my $aes = Crypt::Mode::ECB->new ('AES', 0);

  my $iv_mod = substr ($iv, 0, 15 - 2); # WTF ? always max 13 IV ? just truncate it ?

  my $q = 2; # 16 - 1 - length ($iv_mod) = 15 - $iv_len = 15 - 13


  # init CTR:

  my $flags = $q - 1;

  my $ctr = "\x00" x 16;

  substr ($ctr,  0,  1) = pack ("C", $flags);
  substr ($ctr,  1, 13) = $iv_mod;
  substr ($ctr, 14,  2) = pack ("S>", 1);


  # set first block:

  $flags = 0;

  $flags |=             0 << 6; # no adata
  $flags |= ((8 - 2) / 2) << 3; # tag_len = 8
  $flags |= $q - 1;

  my $b = "\x00" x 16;

  substr ($b,  0,  1) = pack ("C", $flags);
  substr ($b,  1, 13) = $iv_mod;
  substr ($b, 14,  2) = pack ("S>", $len);


  # init y:

  my $y = $aes->encrypt ($b, $key); # => 16 bytes


  # main data loop:

  for (my $i = 0; $i < $len; $i += 16)
  {
    my $cur_len = 16;

    my $left = $len - $i;

    if ($left < 16)
    {
      $cur_len = $left;
    }


    # this is the main decryption code (tmp already contains the output):

    $b = $aes->encrypt ($ctr, $key);

    my $tmp = "\x00" x 16;

    for (my $j = 0; $j < $cur_len; $j++)
    {
      substr ($tmp, $j, 1) = substr ($data, $i + $j, 1) ^ substr ($b, $j, 1);
    }


    # set y:

    $b = "\x00" x 16;

    substr ($b, 0, $cur_len) = substr ($tmp, 0, $cur_len);

    for (my $j = 0; $j < 16; $j++)
    {
      substr ($y, $j, 1) ^= substr ($b, $j, 1);
    }

    $y = $aes->encrypt ($y, $key);


    # update CTR (increment the counter, the end/right of $ctr):
    # (could also use pack ("S>", $ctr_value + 1))

    for (my $j = 0; $j < $q; $j++)
    {
      my $val = ord (substr ($ctr, 15 - $j, 1)) + 1;

      if ($val >= 256)
      {
        $val = 0;
      }

      substr ($ctr, 15 - $j, 1) = chr ($val);

      if ($val != 0)
      {
        last;
      }
    }
  }

  for (my $i = 0; $i < $q; $i++)
  {
    substr ($ctr, 15 - $i, 1) = "\x00";
  }

  # Set the tag:

  $b = $aes->encrypt ($ctr, $key);

  for (my $i = 0; $i < 8; $i++) # or 16 for tag_len = 16
  {
    substr ($tag, $i, 1) = substr ($y, $i, 1) ^ substr ($b, $i, 1);
  }

  return $tag;
}


#
# Start:
#

my $data_raw = decode_base64 ($data);

my $json = decode_json ($data_raw);

if (! defined ($json->{'ct'})    ||
    ! defined ($json->{'iv'})    ||
    ! defined ($json->{'salt'})  ||
    ! defined ($json->{'iter'})  ||
    ! defined ($json->{'adata'}) ||
    ! defined ($json->{'ks'})    ||
    ! defined ($json->{'ts'})    ||
    ! defined ($json->{'mode'})  ||
    ! defined ($json->{'cipher'}))
{
  print "ERROR: invalid data provided\n";

  exit (1);
}

my $ct     = $json->{'ct'};
my $iv     = $json->{'iv'};
my $salt   = $json->{'salt'};
my $iter   = $json->{'iter'};
my $adata  = $json->{'adata'};
my $ks     = $json->{'ks'};
my $ts     = $json->{'ts'};
my $mode   = $json->{'mode'};
my $cipher = $json->{'cipher'};

my $ct_raw   = decode_base64 ($ct);
my $iv_raw   = decode_base64 ($iv);
my $salt_raw = decode_base64 ($salt);


#
# Sanity checks:
#

# mode:ccm, cipher:aes, ks:256, ts:64
# (ks = key size, ts = tag size)

if ($mode ne "ccm")
{
  print "ERROR: the mode is not 'ccm'\n";

  exit (1);
}

if ($cipher ne "aes")
{
  print "ERROR: the cipher is not 'aes'\n";

  exit (1);
}

if ($ks != 256)
{
  print "ERROR: the key size is not 256\n";

  exit (1);
}

if ($ts != 64)
{
  print "ERROR: the tag size is not 64\n";

  exit (1);
}

if ($adata ne '')
{
  print "ERROR: the adata should be empty in our case\n";

  exit (1);
}


# IMPORTANT: prepend length to password:

my $len_and_pass = length ($pass) . '|' . $pass;


#
# KDF (key derivation functions, generate key from password+salt):
#

# PBKDF2-HMAC-SHA256

my $pbkdf2 = Crypt::PBKDF2->new
(
  hasher     => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256),
  iterations => $iter,
  output_len => $ks / 8
);

# most heavy part of the algorithm:

my $key = $pbkdf2->PBKDF2 ($salt_raw, $len_and_pass);

# DEBUG:
# print "Key (-m 10900) is: " . encode_base64 ($key, '') . "\n";


#
# Decrypt with AES-256 CCM:
#

my $tag_len = $ts / 8;
my $ct_len  = length ($ct_raw);

my $ccm_data = substr ($ct_raw, 0, $ct_len - $tag_len);
my $ccm_tag  = substr ($ct_raw, $ct_len - $tag_len);

my $result_tag = aes_ccm_decrypt_tag ($key, $iv_raw, $ccm_data);


#
# Verify:
#

if ($result_tag eq $ccm_tag)
{
  print "Password found '$pass'\n";

  exit (0);
}

exit (1);

Similar code using SJCL (the library) in python (ripple_rippex.py):

Code:
#!/usr/bin/env python

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

# Credits go to https://gist.github.com/dalofeco/171398e1d4183b5452074ece7f4a5321
# for the algo/idea of this script

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# ATTENTION: the length of the pass is prepended (+ "|")

import sys
import base64
import json

from sjcl import SJCL

#
# Examples:
#

# Example 1:

# password = "qwer123" # => 7|qwer123 (length + | + pass)
# data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9"

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

password = "qwe949461747" # => 12|qwe949461747 (length + | + pass)
data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9"

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Start
#

passLength = str (len (password))

LengthAndPass = passLength + "|" + password

jsonData = base64.b64decode (data)
encryptedData = json.loads (jsonData)

try:
  decryptedData = SJCL ().decrypt (encryptedData, LengthAndPass)

  # DEBUG:
  # print ("%s" % decryptedData.decode ('utf-8'))

  wallet = json.loads (decryptedData)

  # DEBUG (e.g. masterkey):
  #for key, val in wallet.items ():
  #  print (" %s - %s" % (key, val))

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

except:
  # pass # skip "Wrong pass" message if you want to check more than one password (loop)

  # DEBUG:
  # print ("Wrong password")

  sys.exit (1)

sys.exit (0)


As you can see it's very similar to the SJCL hashcat github issue, the only major difference is that the password consist of the length of the password + "|" + the password (length ($pass) . "|" . $pass). This is a crucial difference (because it kind of changes the final password length), but it's a trivial change to make it work in perl/python (probably not so easy if you need to do this in OpenCL/CUDA, but still doable, because of the slow PBKDF2 algorithm which makes any such password length changes or similar a NOP, no-op, i.e. negligible).

It seems your example all have 1000 iterations, which isn't too much, but of course still makes it much slower than other (fast) hashcat modes.

This mode wouldn't be impossible to implement, but it would be interesting to discuss if we should rather implement the more generalized SJCL support... the problem with that approach is that (for your particular/special case !) you would need to create and provide to hashcat a new dict that already has the password length prepended to the password (not easy to do with rules ! except if you just prepend every length, but that would be quite a waste for a slow algorithm using PBKDF2).

As always, I would recommend to think a little bit more about if such an implementation would make sense in terms of feasibility: how much does the user know about the password, charset, length etc ? how many password candidates do they need/want to test ? How sure are they about the password pattern etc... because there could be 2 cases in which an implementation wouldn't make much sense: 1. the password candidates the user wants to try are just a few thousands which can easily be run with perl/python/nodejs etc, 2. the user doesn't have much clue about the password and the password is quite long and random (probably infeasible to crack).

So my recommendation is to read the above paragraphs and algo details carefully and try to understand if an implementation really would make sense for your specific case (it's needless to say, that this algo is not yet implemented, but it's not completely impossible to implement it with the help and similar to the aes_ccm_decrypt_tag () decryption function from above).

Good luck. thx
Reply
#3
(04-20-2021, 10:47 AM)philsmd Wrote: Well, the full algorithm (I just researched this a little bit) is PBKDF2-HMAC-SHA256 + AES256-CCM .

Your example with an intermediary key (the key generated by the PBKDF2-HMAC-SHA256 key derivation) isn't very usual (I'm talking about https://github.com/7763sea/hashcat-ripple-cversion). This user seems to somehow got hold of the key that is normally generated on-the-fly and is not the actual "result" (e.g. the encrypted or decrypted data), but as I said just a key generated from the password that still needs to be used to decrypt the data. (the user probably got hold of this key somehow and now tries to recover the password, but even this doesn't make sense most of the time: whenever you have the PBKDF2 key you can just decrypt the data and you are done... it only makes sense to recover the password if you really need to KNOW the password for instance for forensic reason i.e. "we now have this users password and they might have reused this elsewhere").

The algorithm itself is similar to what I've research already here: https://github.com/hashcat/hashcat/issue...-712962171

It also uses this SJCL way (JSON and base64 encoded etc) of describing the algorithm details (key size, tag length, AES mode, etc etc etc).

Here again the code, similar to the github issue above, but modified to work with your examples.


[/code]


As you can see it's very similar to the SJCL hashcat github issue, the only major difference is that the password consist of the length of the password + "|" + the password (length ($pass) . "|" . $pass). This is a crucial difference (because it kind of changes the final password length), but it's a trivial change to make it work in perl/python (probably not so easy if you need to do this in OpenCL/CUDA, but still doable, because of the slow PBKDF2 algorithm which makes any such  password length changes or similar a NOP, no-op, i.e. negligible).

It seems your example all have 1000 iterations, which isn't too much, but of course still makes it much slower than other (fast) hashcat modes.

This mode wouldn't be impossible to implement, but it would be interesting to discuss if we should rather implement the more generalized SJCL support... the problem with that approach is that (for your particular/special case !) you would need to create and provide to hashcat a new dict that already has the password length prepended to the password (not easy to do with rules ! except if you just prepend every length, but that would be quite a waste for a slow algorithm using PBKDF2).

As always, I would recommend to think a little bit more about if such an implementation would make sense in terms of feasibility: how much does the user know about the password, charset, length etc ? how many password candidates do they need/want to test ? How sure are they about the password pattern etc...  because there could be 2 cases in which an implementation wouldn't make much sense: 1. the password candidates the user wants to try are just a few thousands which can easily be run with perl/python/nodejs etc, 2. the user doesn't have much clue about the password and the password is quite long and random (probably infeasible to crack).

So my recommendation is to read the above paragraphs and algo details carefully and try to understand if an implementation really would make sense for your specific case (it's needless to say, that this algo is not yet implemented, but it's not completely impossible to implement it with the help and similar to the aes_ccm_decrypt_tag () decryption function from above).

Good luck. thx

first , thank you for taking a look at it .

i have read your github comment and i tend to some what agree Smile
most of the crypto holders are normal people that have no clue about hash cracking or password recovering or even crypto itself ^,^   .
also they are mostly not young folks , i've heard countless time of people that forgot their passwords for their wallet , this happen mostly because some one told them to put a really strong password , since those password are not their typical password they are tend to be forgot fun fact is if your not using the password daily or some what regularly  the human brain will make it disappear unless you saved/wrote the password somewhere which tend to be lost aswell Smile 

now to the problem ,
if you noticed , in this github  https://github.com/edward852/xrp-wallet-recovery the password is unknow and the length of the password  is unknow .

i have sat with my friend and tried to go deep with what he remember and what type of password he used what it could be , he had a certain patern of a password.
i have tried to play with it  with passowrds that he might remember so ive run around 500k password on a laptop cpu Smile , but that was tedious to change the config file  each time to make all the small changes run it over and over to mimic a mask attack .
it was not near as fast , or easy or intuitive as hashcat .

in my case (the case of the friend wallet) , the user know some what , what the password could be and more or less the patern however as explained before i did try to run multiple hundred of thousand passwords over all without any success .
i think that would've been a different story to run it in hashcat with a gpu or even cpu .

if you ask me if hashcat dev team should implement it my answer would be yes , abit hypocrite of me  i know ^,^ because i try to help my friend
but if you look at it , i am sure they are lots of more or less cases with the same problem ,  hashcat do support some couple of wallet types  such as electrum , bitcoin, blockchain.. 

if you ask me if this should be as top priority , the answer is no , i am sure you guys have alot on your plate and have far more usefull algorithm to implement , yet  i think implementing soon this type of wallet would be a good idea .

also funny story and fun fact is , even if you have the right password for your wallet the desktop software will deny it Big Grin and tells you its a wrong password when its not ! ,  i know that because when i tried to check if the nodejs code on github works i created a wallet with a simple password , closed and reopen the ripple desktop wallet , put the right password and it didn't work  i did that 3 times just to make sure i wasn't dreaming or anything .
the nodejs code did work Smile , it recovery the password , and the private key/master key of the wallet .

anywho thats my take on it.
i do wish to see it in hashcat soon tho .
i am sure alot of people needs it (also because their freaking software does not work or available anymore , so even if its the right password the software will say its wrong .)

and no my friend is not a millioners from what he remembers he think he has now 3~k$ , he dont even know what his public address xD
Reply
#4
Quote:now to the problem ,
if you noticed , in this github https://github.com/edward852/xrp-wallet-recovery the password is unknow and the length of the password is unknow .

I'm not sure what you mean by this, but the code uses the password length + "|"+ password tripple for the key generation: https://github.com/edward852/xrp-wallet-...ery.js#L65

so yeah, the password is unknown of course but comes with the "dictionary" and the length can be determined by the password (byte length of the password itself).

Of course you can simple modify my perl script to make it a small cracker that accepts a dictionary like this (ripple_rippex_cracker.pl):
Code:
#!/usr/bin/env perl

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

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# Instead of our own implementation, we could use:
# Crypt::AuthEnc::CCM (ccm_encrypt_authenticate)

# ATTENTION: the length of the password is prepended (and "|") for the key generation

use strict;
use warnings;

use Crypt::PBKDF2;
use Crypt::Mode::ECB;
use JSON         qw (decode_json);
use MIME::Base64 qw (encode_base64 decode_base64);


#
# Example:
#

# Example 1:

my $data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9";
# my $pass = "qwer123"; # => 7|qwer123 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

#my $data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9";
#my $pass = "qwe949461747"; # => 12|qwe949461747 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Helper function:
#

sub aes_ccm_decrypt_tag
{
  my $key  = shift;
  my $iv   = shift;
  my $data = shift;

  my $tag = "\x00" x 8; # output

  my $len = length ($data);

  my $aes = Crypt::Mode::ECB->new ('AES', 0);

  my $iv_mod = substr ($iv, 0, 15 - 2); # WTF ? always max 13 IV ? just truncate it ?

  my $q = 2; # 16 - 1 - length ($iv_mod) = 15 - $iv_len = 15 - 13


  # init CTR:

  my $flags = $q - 1; # => 2 - 1 = 1

  my $ctr = "\x00" x 16;

  substr ($ctr,  0,  1) = pack ("C", $flags);
  substr ($ctr,  1, 13) = $iv_mod;
  substr ($ctr, 14,  2) = pack ("S>", 1);

  # 0x01[IV_MOD:13]0001 (16 bytes)


  # set first block:

  $flags = 0;

  $flags |=             0 << 6; # no adata
  $flags |= ((8 - 2) / 2) << 3; # tag_len = 8
  $flags |= $q - 1;

  # => 25 = 0x19

  my $b = "\x00" x 16;

  substr ($b,  0,  1) = pack ("C", $flags);
  substr ($b,  1, 13) = $iv_mod;
  substr ($b, 14,  2) = pack ("S>", $len);

  # 0x19[IV_MOD:13][DATA_WITHOUT_TAG_LEN:2] (16 bytes)


  # init y:

  my $y = $aes->encrypt ($b, $key); # => 16 bytes


  # main data loop:

  for (my $i = 0; $i < $len; $i += 16)
  {
    my $cur_len = 16;

    my $left = $len - $i;

    if ($left < 16)
    {
      $cur_len = $left;
    }


    # this is the main decryption code (tmp already contains the output):

    $b = $aes->encrypt ($ctr, $key);

    my $tmp = "\x00" x 16;

    for (my $j = 0; $j < $cur_len; $j++)
    {
      substr ($tmp, $j, 1) = substr ($data, $i + $j, 1) ^ substr ($b, $j, 1);
    }


    # set y:

    $b = "\x00" x 16;

    substr ($b, 0, $cur_len) = substr ($tmp, 0, $cur_len);

    for (my $j = 0; $j < 16; $j++)
    {
      substr ($y, $j, 1) ^= substr ($b, $j, 1);
    }

    $y = $aes->encrypt ($y, $key);


    # update CTR (increment the counter, the end/right of $ctr):
    # (could also use pack ("S>", $ctr_value + 1))
    #
    # my $ctr_value = unpack ("S>", substr ($ctr, 16 - $q)); # get last 2 bytes
    #
    # $ctr_value++;
    #
    # substr ($ctr, 16 - $q) = pack ("S>", $ctr_value); # set last 2 bytes

    for (my $j = 0; $j < $q; $j++) # q is always 2
    {
      my $val = ord (substr ($ctr, 15 - $j, 1)) + 1;

      if ($val >= 256)
      {
        $val = 0;
      }

      substr ($ctr, 15 - $j, 1) = chr ($val);

      if ($val != 0)
      {
        last;
      }
    }
  }

  # clear some $ctr bytes if needed

  for (my $i = 0; $i < $q; $i++) # q is always 2
  {
    substr ($ctr, 15 - $i, 1) = "\x00";
  }

  # Set the tag:

  $b = $aes->encrypt ($ctr, $key);

  for (my $i = 0; $i < 8; $i++) # or 16 for tag_len = 16
  {
    substr ($tag, $i, 1) = substr ($y, $i, 1) ^ substr ($b, $i, 1);
  }

  return $tag;
}


#
# Start:
#

my $data_raw = decode_base64 ($data);

my $json = decode_json ($data_raw);

if (! defined ($json->{'ct'})    ||
    ! defined ($json->{'iv'})    ||
    ! defined ($json->{'salt'})  ||
    ! defined ($json->{'iter'})  ||
    ! defined ($json->{'adata'}) ||
    ! defined ($json->{'ks'})    ||
    ! defined ($json->{'ts'})    ||
    ! defined ($json->{'mode'})  ||
    ! defined ($json->{'cipher'}))
{
  print "ERROR: invalid data provided\n";

  exit (1);
}

my $ct     = $json->{'ct'};
my $iv     = $json->{'iv'};
my $salt   = $json->{'salt'};
my $iter   = $json->{'iter'};
my $adata  = $json->{'adata'};
my $ks     = $json->{'ks'};
my $ts     = $json->{'ts'};
my $mode   = $json->{'mode'};
my $cipher = $json->{'cipher'};

my $ct_raw   = decode_base64 ($ct);
my $iv_raw   = decode_base64 ($iv);
my $salt_raw = decode_base64 ($salt);


#
# Sanity checks:
#

# mode:ccm, cipher:aes, ks:256, ts:64
# (ks = key size, ts = tag size)

if ($mode ne "ccm")
{
  print "ERROR: the mode is not 'ccm'\n";

  exit (1);
}

if ($cipher ne "aes")
{
  print "ERROR: the cipher is not 'aes'\n";

  exit (1);
}

if ($ks != 256)
{
  print "ERROR: the key size is not 256\n";

  exit (1);
}

if ($ts != 64)
{
  print "ERROR: the tag size is not 64\n";

  exit (1);
}

if ($adata ne '')
{
  print "ERROR: the adata should be empty in our case\n";

  exit (1);
}


my $tag_len = $ts / 8; # => 8
my $ct_len  = length ($ct_raw); # => 154 ?

my $ccm_data = substr ($ct_raw, 0, $ct_len - $tag_len); # length: 154 - 8 = 146
my $ccm_tag  = substr ($ct_raw, $ct_len - $tag_len); # last 8 bytes

# init PBKDF2-HMAC-SHA256

my $pbkdf2 = Crypt::PBKDF2->new
(
  hasher     => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256),
  iterations => $iter,
  output_len => $ks / 8
);

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

  # IMPORTANT: prepend length to password:

  my $len_and_pass = length ($pass) . '|' . $pass;


  #
  # KDF (key derivation functions, generate key from password+salt):
  #

  # most heavy part of the algorithm:

  my $key = $pbkdf2->PBKDF2 ($salt_raw, $len_and_pass);

  # DEBUG:
  # print "Key (-m 10900) is: " . encode_base64 ($key, '') . "\n";


  #
  # Decrypt with AES-256 CCM:
  #

  my $result_tag = aes_ccm_decrypt_tag ($key, $iv_raw, $ccm_data);


  #
  # Verify:
  #

  if ($result_tag eq $ccm_tag)
  {
    print "Password found '$pass'\n";

    exit (0);
  }
}

exit (1);

(note: you would only need to change the $data with your own wallet data, of course, if you are testing a different wallet than the "qwer123" test wallet)

it just loops over all password from STDIN/pipe or a dictionary:
Code:
perl ripple_rippex_cracker.pl dict.txt


yeah, I agree many algorithms make somehow sense to be implemented. It's for sure an interesting algorithm and actually NOT too bad to crack (with "only" 1000 iterations)... but again it's very important that the user has quite some idea what the password could be... otherwise it still could become infeasible to crack.

You can run the above script also within several shells and split the dictionary in chunks and therefore use all your CPUs power ( 1 script execution for each CPU thread ).

Maybe you can test it a little bit also with a smaller dict and a new test wallet (generated with the same software) with a known password)... e.g. a dictionary with only a few thousands of words in the dict etc.

That's already an important step to see if the algorithm is correct and that the POC is working, maybe later on we could think about creating a new github issue or even comment on the old SJCL issue (but the algorithm and especially the input is not exactly the same, the other algo doesn't need the password length to be prepended).

Maybe this cracker script already helps, let me know. thx
Reply
#5
as a POC it works, i tried with a wallet that i created , stdin and dict are working .

took my some time to figure out how to install all the dependencies since im not using perl.
if some one wants to run it aswell , couple of things need to be install Smile


Code:
sudo apt install perl  cmake -y
sudo perl -MCPAN -e shell

install Crypt::PBKDF2
install Crypt::ECB
install CryptX
install JSON


and yea its very very very slow Big Grin 

i took rockyou dictionary , put my password at the 500k mark (line) .
i use my laptop cpu (i7 8665U) ,  it TOOK 22 min and 28 sec ! to run through 500k lines , yikes xD 
gpu are a must ^,^ 

[Image: 5Cscses.png]
Reply
#6
very good, thanks for testing.

Well, for better speed you could also use the modified version (variant) that uses the perl module (Crypt::AuthEnc::CCM), which itself is optimized (but the speed isn't that much better, because even the version above has only some additional "slow perl code" around the AES-ECB - a module again - decryption calls, which again is optimized in ANSI C/assembly).

So the variant would be (ripple_rippex_cracker_with_module.pl) :
Code:
#!/usr/bin/env perl

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

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# ATTENTION: the length of the password is prepended (and "|") for the key generation

use strict;
use warnings;

use JSON         qw (decode_json);
use MIME::Base64 qw (decode_base64);

use Crypt::PBKDF2;
use Crypt::AuthEnc::CCM;


#
# Example:
#

# Example 1:

#my $data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9";
# my $pass = "qwer123"; # => 7|qwer123 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

my $data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9";
#my $pass = "qwe949461747"; # => 12|qwe949461747 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Start:
#

my $data_raw = decode_base64 ($data);

my $json = decode_json ($data_raw);

if (! defined ($json->{'ct'})    ||
    ! defined ($json->{'iv'})    ||
    ! defined ($json->{'salt'})  ||
    ! defined ($json->{'iter'})  ||
    ! defined ($json->{'adata'}) ||
    ! defined ($json->{'ks'})    ||
    ! defined ($json->{'ts'})    ||
    ! defined ($json->{'mode'})  ||
    ! defined ($json->{'cipher'}))
{
  print "ERROR: invalid data provided\n";

  exit (1);
}

my $ct     = $json->{'ct'};
my $iv     = $json->{'iv'};
my $salt   = $json->{'salt'};
my $iter   = $json->{'iter'};
my $adata  = $json->{'adata'};
my $ks     = $json->{'ks'};
my $ts     = $json->{'ts'};
my $mode   = $json->{'mode'};
my $cipher = $json->{'cipher'};

my $ct_raw   = decode_base64 ($ct);
my $iv_raw   = decode_base64 ($iv);
my $salt_raw = decode_base64 ($salt);


#
# Sanity checks:
#

# mode:ccm, cipher:aes, ks:256, ts:64
# (ks = key size, ts = tag size)

if ($mode ne "ccm")
{
  print "ERROR: the mode is not 'ccm'\n";

  exit (1);
}

if ($cipher ne "aes")
{
  print "ERROR: the cipher is not 'aes'\n";

  exit (1);
}

if ($ks != 256)
{
  print "ERROR: the key size is not 256\n";

  exit (1);
}

if ($ts != 64)
{
  print "ERROR: the tag size is not 64\n";

  exit (1);
}

if ($adata ne '')
{
  print "ERROR: the adata should be empty in our case\n";

  exit (1);
}


my $tag_len = $ts / 8;                      # => 8
my $ct_len  = length ($ct_raw);             # => 154
my $pt_len  = length ($ct_raw) - $tag_len;  # => 146

my $ccm_data = substr ($ct_raw, 0, $ct_len - $tag_len); # length: 154 - 8 = 146
my $ccm_tag  = substr ($ct_raw, $ct_len - $tag_len); # last 8 bytes


# init PBKDF2-HMAC-SHA256

my $pbkdf2 = Crypt::PBKDF2->new
(
  hasher     => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256),
  iterations => $iter,
  output_len => $ks / 8
);

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

  # IMPORTANT: prepend length to password:

  my $len_and_pass = length ($pass) . '|' . $pass;


  #
  # KDF (key derivation functions, generate key from password+salt):
  # (most heavy part of the algorithm)
  #

  my $key = $pbkdf2->PBKDF2 ($salt_raw, $len_and_pass);


  #
  # Decrypt with AES-256 CCM:
  #

  my $aes = Crypt::AuthEnc::CCM->new ("AES", $key, $iv_raw, $adata, $tag_len, $pt_len);

  $aes->decrypt_add ($ccm_data); # we don't need the result (plaintext) for the verification

  my $result_tag = $aes->decrypt_done ();

  $result_tag = substr ($result_tag, 0, $tag_len);


  #
  # Verify:
  #

  if ($result_tag eq $ccm_tag)
  {
    print "Password found '$pass'\n";

    exit (0);
  }
}

exit (1);


It would also be interesting if we can make any assumptions about the "data length". Is it always the same ?
We can see that the decrypted (plain) text has some arrays in it like: "contacts":[]
This probably isn't always empty (the contacts list)... so I'm not sure if the data length is always the same.

In theory you could make some further optimizations if you know that the data length is always the same/fixed (and therefore how often we need to iterate/loop within the main decryption loop)... I show the little bit more optimized version with fixed data length below:
Code:
#!/usr/bin/env perl

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

# This script is a POC for the AES-CCM Ripple Rippex wallet data decryption

# Instead of our own implementation, we could use:
# Crypt::AuthEnc::CCM (ccm_encrypt_authenticate)

# ATTENTION: the length of the password is prepended (and "|") for the key generation

use strict;
use warnings;

use Crypt::PBKDF2;
use Crypt::Mode::ECB;
use JSON         qw (decode_json);
use MIME::Base64 qw (decode_base64);


#
# Example:
#

# Example 1:

# my $data = "eyJpdiI6IkZxMmFVQzRlei9nbWowU2MxRzVuZ3c9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IklNbEhiVmk5eVRNPSIsImN0IjoiOENxbkJ1WmFyWms4SzZ3eXpxc0JYNmJ6VS9ENk5MNUhsbEp1TVJyTmRNZEhFcSs2NVJ4aHFVdWxZbmw2b05DWTBSWFBrcmpEMW1aSXdCS253MkxzLzl1ejBFeHV1a1Qwb0haekR0M09BeE8zMDhtbEZHaFB2aEpSa2dYYS8wM21EODJIaHk5L2RjVG5nbS9MdnhwV1VFRTFnVnVTQVFOdm1zVTVTSGJDQVJqYy9qNUZnU28vYjhHYjk4cVUyNGxqU2dOaHBUTUtTODR4a0E9PSJ9";
# my $pass = "qwer123"; # => 7|qwer123 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is eC60JsTVBir/DcYcLKXUA71a8kwFFRsA/+JbX2PKBdQ=
# (search for DEBUG below)


# Example 2:

my $data = "eyJpdiI6InlteHBZMFpWSzJiRXNIYldreWFrSlE9PSIsInYiOjEsIml0ZXIiOjEwMDAsImtzIjoyNTYsInRzIjo2NCwibW9kZSI6ImNjbSIsImFkYXRhIjoiIiwiY2lwaGVyIjoiYWVzIiwic2FsdCI6IjVCbnp4VXZvbEYwPSIsImN0IjoiaHBNNWI1THNUQks3Wjk3WXdyc3h5NkxadG9tMGdtL0pjakxMNnVXL1FBNmh1d1ZZZ3Fab0pYVmwxTkZ6RlJhMy9WUzRoSlJsZ0RFdzcrRlQxeGEyWUxtNkFyMXJFTHA2TlhtUGk4Q2lQanVyRjBtNnFiMU4yU0ROMmhuTU54bWVOWnpqYzZDZVRUeXFGZWt4ZEFTSHVsZ0JKWU5jeGczRlV6VDg4d0FTb0NGRzNlbXZSbkpDRnFsQzFXN3dvSUdtck4yS25xV1dOSVhobEE9PSJ9";
#my $pass = "qwe949461747"; # => 12|qwe949461747 (length + | + pass)

# note: the PBKDF2 key (NOT known without the correct pass) is Q6T/AlK6i+K+xFzIakMkXvU0fi0qabz+OJe2HzyVyrI=


#
# Helper function:
#

sub aes_ccm_decrypt_tag
{
  my $key  = shift;
  my $iv   = shift;
  my $data = shift;

  my $LEN = 146; # length ($data);
  my $DIV = 9; # int ($LEN / 16);
  my $MOD = 2; # int ($LEN % 16);

  my $ctr = "\x01" . $iv . "\x00\x01"; # CTR
  my $b   = "\x19" . $iv . "\x00\x92"; # block , pack ("S>", $LEN);

  my $aes = Crypt::Mode::ECB->new ('AES', 0);


  # init y:

  my $y = $aes->encrypt ($b, $key);


  # main data loop:

  for (my ($i, $j) = (2, 0); $i < $DIV + 2; $i += 1, $j += 16)
  {
    $b = $aes->encrypt ($ctr, $key);

    my $d = substr ($data, $j, 16);

    $b ^= $d; # already contains the output

    # set y:

    $y ^= $b;

    $y = $aes->encrypt ($y, $key);

    # set CTR (increment the counter, the end/right of $ctr):

    substr ($ctr, 14) = pack ("S>", $i);
  }

  # remainder:

  #if ($MOD != 0) # for (my $i = 0; $i <= $LEN % 16; $i += 16)
  #if (1)
  {
    $b = $aes->encrypt ($ctr, $key);

    my $p = $DIV * 16;

    my $d = substr ($data, $p, $MOD);

    $b ^= $d; # already contains the output

    $b = substr ($b, 0, $MOD);

    # set y:

    $y ^= $b;

    $y = $aes->encrypt ($y, $key);
  }

  # clear counter:

  substr ($ctr, 14) = pack ("S>", 0); # "\x00\x00"

  # set tag:

  $b = $aes->encrypt ($ctr, $key);

  return substr ($y ^ $b, 0, 8);
}

#
# Start:
#

my $data_raw = decode_base64 ($data);

my $json = decode_json ($data_raw);

if (! defined ($json->{'ct'})    ||
    ! defined ($json->{'iv'})    ||
    ! defined ($json->{'salt'})  ||
    ! defined ($json->{'iter'})  ||
    ! defined ($json->{'adata'}) ||
    ! defined ($json->{'ks'})    ||
    ! defined ($json->{'ts'})    ||
    ! defined ($json->{'mode'})  ||
    ! defined ($json->{'cipher'}))
{
  print "ERROR: invalid data provided\n";

  exit (1);
}

my $ct     = $json->{'ct'};
my $iv     = $json->{'iv'};
my $salt   = $json->{'salt'};
my $iter   = $json->{'iter'};
my $adata  = $json->{'adata'};
my $ks     = $json->{'ks'};
my $ts     = $json->{'ts'};
my $mode   = $json->{'mode'};
my $cipher = $json->{'cipher'};

my $ct_raw   = decode_base64 ($ct);
my $iv_raw   = decode_base64 ($iv);
my $salt_raw = decode_base64 ($salt);


#
# Sanity checks:
#

if ($mode ne "ccm")
{
  print "ERROR: the mode is not 'ccm'\n";

  exit (1);
}

if ($cipher ne "aes")
{
  print "ERROR: the cipher is not 'aes'\n";

  exit (1);
}

if ($ks != 256)
{
  print "ERROR: the key size is not 256\n";

  exit (1);
}

if ($ts != 64)
{
  print "ERROR: the tag size is not 64\n";

  exit (1);
}

if ($adata ne '')
{
  print "ERROR: the adata should be empty in our case\n";

  exit (1);
}


my $tag_len =   8; # $ts / 8
my $ct_len  = 154; # length ($ct_raw)

my $ccm_data = substr ($ct_raw, 0, $ct_len - $tag_len); # length: 154 - 8 = 146
my $ccm_tag  = substr ($ct_raw, $ct_len - $tag_len);    # last 8 bytes

my $iv_mod = substr ($iv_raw, 0, 13);

# init PBKDF2-HMAC-SHA256

my $pbkdf2 = Crypt::PBKDF2->new
(
  hasher     => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256),
  iterations => $iter,
  output_len => $ks / 8
);

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

  # IMPORTANT: prepend length to password:

  my $len_and_pass = length ($pass) . '|' . $pass;


  #
  # KDF (key derivation functions, generate key from password+salt):
  #

  # most heavy part of the algorithm:

  my $key = $pbkdf2->PBKDF2 ($salt_raw, $len_and_pass);


  #
  # Decrypt with AES-256 CCM:
  #

  my $result_tag = aes_ccm_decrypt_tag ($key, $iv_mod, $ccm_data);


  #
  # Verify:
  #

  if ($result_tag eq $ccm_tag)
  {
    print "Password found '$pass'\n";

    exit (0);
  }
}

exit (1);

This of course assumes that the data length is always the same (154 total, 154 - 8 = 146 without the tag). Not sure if this is correct for real world wallets that have been used a lot (with contacts etc). So yeah, there are still several open questions, but at least the algorithm is quite clear.

Again, you are testing only with one thread.... the script itself is not multi-threaded, you would need to split rockyou.txt and run the script on each and every core of your CPU (CPU threads) and therefore get almost the full speed (again ripple_rippex_cracker_with_module.pl should be even faster, because the module is written in C/assembly, not slow perl code.... but again, this normally isn't the right place where optimizations occur, the most important thing is the slow PBKDF2 key derivation and this of course would be much faster with GPUs, the remaining part of the algorithm is almost negligible).

Please do some more tests and investigate on the data length etc. thx
Reply
#7
i will try it soon , like tomorrow ,
but to answer you from what i remember the data length is not the same .
the wallet i created as a longer length then the one in those github posts .

i will tomorrow post multiple wallets that i will create on a certain version , with different passwords , to see if length changes at all or it remain the same , perhaps the ripple desktop wallet version is the one that changes the length itself.
Reply
#8
the most important part of course is the "ct":"...." part... It would be intersting to know if this part is always of the same length.

there is another question that comes to my mind: we currently have no fully compatible JSON decoder within hashcat... but I think a new hash format wouldn't hurt in this case.... it might just be a little bit less convenient, but hashcat users are actually used to use the "xyz2hashcat.py" or similar tools.

Also in this case the JSON decoding wouldn't be much of a problem because we could just "search" for "ct", "iter" etc within the base64 decoded string. The only problem would be the reconstruction of the "cracked hash" (and in JSON the order doesn't really matter, but the output should be same as input, therefore this needs to be stored somehow.... except if we assume the order of the JSON attributes is always the same, which isn't normally the case).
Reply
#9
I have some good news for you kiara... I've just managed to have a further glance at this algorithm and tried to completely implement the whole module/kernel/tests/extractor with this hashcat testing branch:

repo: https://github.com/philsmd/hashcat/tree/..._hash_algo
commit: https://github.com/philsmd/hashcat/commit/576b277

of course this is still very early code and we could think about some optimizations or checks/restrictions of the input etc etc etc

I've decided to create tools/rippex2hashcat.py for the wallet to hash conversion (the format is $rippex$*iter*salt*iv*ciphertext)

While developing the kernel code I've also noticed that there could be a theoretical reason to worry about collisions (the final check, AES-CCM tag, only has 8 bytes, 64 bits), but with 1000 iterations of PBKDF2-HMAC-SHA256 it's not "too bad" (but remember even a "md5" hash consists of 16 bytes). we might need to think about adding a --keep-guessing warning, or also check the plaintext for a low enough entropy/randomness.

it's needless to say that the hash number/selection -m 27100 might change in the future, it ("27100") is just one of the currently free / next hash modes available.

Please test everything, including the conversion tool rippex2hashcat.py (I have no wallet software or wallet files, I just assumed that the files just contain the base64 string, as the eyJp...example above)

Hope this helps and if it's working perfectly fine of course we shouldn't hesitate to open a github issue/pull request on the main hashcat repo

Thx
Reply
#10
(04-27-2021, 06:47 PM)philsmd Wrote: I have some good news for you kiara... I've just managed to have a further glance at this algorithm and tried to completely implement the whole module/kernel/tests/extractor with this hashcat testing branch:

repo: https://github.com/philsmd/hashcat/tree/..._hash_algo
commit: https://github.com/philsmd/hashcat/commit/576b277

of course this is still very early code and we could think about some optimizations or checks/restrictions of the input etc etc etc

I've decided to create tools/rippex2hashcat.py for the wallet to hash conversion (the format is $rippex$*iter*salt*iv*ciphertext)

While developing the kernel code I've also noticed that there could be a theoretical reason to worry about collisions (the final check, AES-CCM tag, only has 8 bytes, 64 bits), but with 1000 iterations of PBKDF2-HMAC-SHA256 it's not "too bad" (but remember even a "md5" hash consists of 16 bytes). we might need to think about adding a --keep-guessing warning, or also check the plaintext for a low enough entropy/randomness.

it's needless to say that the hash number/selection -m 27100 might change in the future, it ("27100") is just one of the currently free / next hash modes available.

Please test everything, including the conversion tool rippex2hashcat.py (I have no wallet software or wallet files, I just assumed that the files just contain the base64 string, as the eyJp...example above)

Hope this helps and if it's working perfectly fine of course we shouldn't hesitate to open a github issue/pull request on the main hashcat repo

Thx

just compiled the tree 

seems to work Big Grin Big Grin Big Grin <3 

[Image: ppTHKEO.png]

command used that failed
Code:
./hashcat -m 27100 -a 3 "$rippex$*1000*64pZmnDPa4o=*k38ln/SfQ50cB9uTU0TgSw==*89rZdoDc8C5M9S093cEAargb96ZnVoxXFJ1IBX7bjnaFSeYjO3b5Ns9hN4esJkc+IatwrE3NeB7Jgit6vgFXdDYS6wpcVKEhUu+J2O9h1WDJdrllhjJblfWnGMX0WtEDJAyN79F/b5Q/C9YYSiivPqOnYvHFhKCdTdj/7/vI4IhKHYkGUCQn6/RY0aBGRbZ2VuVFYnPe9U9nQH7Z8NB+O31zPmSjHFU=" Aa123456Aa123456Aa123?a?a?a?a?a

command used that worked . (i just put the hash inside the txt file)

Code:
./hashcat -m 27100 -a 3 hash.txt  Aa123456Aa123456Aa123?a?a?a?a?a

test was done on a 2080 TI 

benchmark 
Code:
hashcat (v6.1.1) starting in benchmark mode...

Benchmarking uses hand-optimized kernel code by default.
You can use it in your cracking session by setting the -O option.
Note: Using optimized kernel code limits the maximum supported password length.
To disable the optimized kernel code in benchmark mode, use the -w option.

CUDA API (CUDA 10.2)
====================
* Device #1: GeForce RTX 2080 Ti, 10855/11019 MB, 68MCU

OpenCL API (OpenCL 1.2 CUDA 10.2.141) - Platform #1 [NVIDIA Corporation]
========================================================================
* Device #2: GeForce RTX 2080 Ti, skipped

Benchmark relevant options:
===========================
* --optimized-kernel-enable

Hashmode: 27100 - Ripple Rippex Wallet (Iterations: 999)

Speed.#1.........:  2726.3 kH/s (77.42ms) @ Accel:16 Loops:249 Thr:1024 Vec:1

Started: Tue Apr 27 21:23:08 2021
Stopped: Tue Apr 27 21:23:28 2021

and as promise here are some wallet that i've created .
all of the wallet that i've created are 548 char long. (rippex wallet desktop Version: 1.4.1)
however as mention yersteday the example wallet (from those repos) are longer.

and the real wallet from my friend is longer , 692 chars before decoding , start also with eyJp , and decoded it has 517 chars. vs usually 409 chars from the wallet that i've created .


Attached Files
.txt   evenlonger password longlonglongpasswordlonglonglongpasswordlonglonglongpasswordlonglonglongpassword1!!.txt (Size: 548 bytes / Downloads: 4)
.txt   longpasswordwallet Aa123456Aa123456Aa123456!@.txt (Size: 548 bytes / Downloads: 4)
.txt   shortpassword Aa11!!.txt (Size: 548 bytes / Downloads: 3)
.txt   wallet Aa123456!.txt (Size: 548 bytes / Downloads: 6)
Reply