Python programs can inspire KRL rulesets

When learning a programming language, it can be useful to compare something in the new language with the same functionality expressed in a familiar language. In that spirit, I present here the same idea in both Python and KRL.

The Python program

Recently, I came across a blog post about passwords. It included, in the comments section, this Python program, which uses a public API to provide information about how common a particular password is:

import hashlib
import requests

def test_pw(byte_string):
    hasher = hashlib.sha1()
    hasher.update(byte_string)
    digest = hasher.hexdigest().upper()
    print(f'Hash: {digest[:5]}, {digest[5:]}')
    print(f'GET https://api.pwnedpasswords.com/range/{digest[:5]}')
    pw_list = requests.get(f'https://api.pwnedpasswords.com/range/{digest[:5]}')
    for line in pw_list.text.split('\n'):
        info = line.split(':')
        if info[0] == digest[5:]:
            print(f'Pwned! Seen {int(info[1])} times.')
            break
    else:
        print('Not found')
    
test_pw(b'P@ssword')

In lines 1 and 2, needed libraries are imported. The corresponding libraries in KRL are math: and http:. Lines 4-17 define a Python function named test_pw which takes in a string and produces either a warning message (line 14) saying how many times that password appears in a list of hundreds of millions of passwords, or a message saying the provided password does not appear in the list (line 17). Lines 5-7 produce the SHA1 hash of the provided password. Lines 8-9 log information based on the provided password. Line 10 requests (from the public API) a list of password appearance counts based on the first 5 characters of the hash. Lines 11-17 peruse that list to extract the number of appearances and line 14 displays the number. Failing to find it, the program produces a message to that effect in line 17. In line 19 the function is invoked with the sample password "P@ssword".

The KRL program

In KRL, the unit of compilation is called a ruleset. I wrote this ruleset to behave as similarly as possible to the Python program:

ruleset com.pwnedpasswords.api {
  meta {
    shares __testing, range
  }
  global {
    __testing = { "queries":
      [ { "name": "__testing" } , { "name": "range", "args": [ "key" ] } ]
    }
    range = function(key){
      sha1 = math:hash("sha1",key);
      parts = sha1.extract(re#^(.....)(.*)$#);
      klog(<<Hash: #{parts.join()}>>);
      klog(<<GET https://api.pwnedpasswords.com/range/#{parts[0]}>>);
      list = http:get("https://api.pwnedpasswords.com/range/"+parts[0]){"content"};
      newline = (13.chr() + "?" + 10.chr()).as("RegExp");
      parts1 = parts[1].uc().as("RegExp");
      ours = list
        .split(newline)
        .filter(function(v){v.match(parts1)});
      ours.length()
        => <<Pwned! Seen #{ours.head().split(":")[1]} times.>> |
           "Not found"
    }
  }
}

The ruleset identifier (RID) that I selected is com.pwnedpasswords.api which normally "belongs to" whoever controls the pwnedpasswords.com domain. I wrote this code as if I worked for them. Line 3 shares the map named __testing and a function named range, both of which are defined in the global block (lines 5-24). Lines 6-8 define entries for the Testing tab of the UI. This will allow us to easily invoke the function, providing passwords to check, as many times as we need. Lines 9-23 define the range function, whose argument is named key (corresponding to byte_string in the Python function).

The SHA1 hash is computed in line 10. It is split into two parts, the first 5 characters and the remainder, in line 11. Since KRL does not have a string indexing notation like sha1[:5] and sha1[5:] we provide these two parts as an array. The array is produced by the String operator extract and the regular expression with two capture groups.

Lines 12 and 13 log the same information provided by the Python function (in its lines 8-9). Line 14 calls the same API and retrieves the response content as a string. Our list corresponds to the Python pw_list. Where Python uses the newline character to delimit lines, we use the regular expression defined in line 15 (and named newline). Instead of using equality to find the line of interest, we use the String operator match, for which we prepare (outside of any looping) the regular expression named parts1 (in line 16). Where Python uses a for loop, we use the KRL Array operator filter. Lines 17-19 here correspond to the Python for loop. We operate on the list by splitting it into an array of lines (line 18) using the String operator split (which corresponds to something of the same name in Python (a coincidence)). Then we filter (see Array operator filter) that array to produce a new array consisting only of those entries which match the parts1 regular expression (line 19).

Finally (lines 20-22) produce (as the result of our range function) one of two messages, corresponding to the two messages that can be printed in the Python function. The filtered array named ours will contain either the one line which matches, or the empty array. These three lines consist of a ternary expression and line 21 will be the result when line 20 produces a truthy value (1). It uses the Array operator head to unwrap the single line and the String operator split to produce an array of what comes before the colon in that line and the number that come after. The number is at index [1] and is injected into the chevron-quoted string using a beesting.

Running the KRL function

Once the ruleset has been registered with the node pico engine, we can install it into a pico. The screenshot below shows the Testing tab for that pico, along with the result of invoking the range function with the provided password shown:

Notice that the pico is named pwnedpasswords and that it has installed a ruleset named com.pwnedpasswords.api but the similarity was unintentional. The ruleset can be installed in any pico, regardless of the pico's name.

The range function invoked with a less-common (and thus probably more-secure) password:

Note that KRL is intended for use with picos, which are first-class Internet citizens, and thus does not have a command line interface. One can, however, use `curl` for that purpose, as shown here:

$ curl http://localhost:8080/sky/cloud/PdjS2LshhZkeeEJazmBFXr/com.pwnedpasswords.api/range.txt?key=P@ssword
Pwned! Seen 6605 times.

The random-looking string (PdjS2LshhZkeeEJazmBFXr) is the identifier of one of the channels belonging to the pico, commonly called an Event Channel Identifier (ECI). See the Sky Cloud API page.

For an explanation of the .txt which follows the name of the function (range) see Content negotiation with HTTP.

For other ways to invoke a KRL function, see Creating an Echo Server.