rippex/ripple wallet lost passphrase
#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


Messages In This Thread
rippex/ripple wallet lost passphrase - by kiara - 04-18-2021, 08:13 PM
RE: rippex/ripple wallet lost passphrase - by philsmd - 04-20-2021, 10:47 AM