How to implement Python's os.urandom method in Perl 6?

You could always just use Python's urandom

sub py-urandom( UInt $size ){
  use Inline::Python;

  state $py = Inline::Python.new; # reuse the same instance
  once $py.import('os');          # load the Python os library only once

  $py.call('os','urandom', $size);
}

say py-urandom(10)».fmt('%02X').join;  # 1473A7D5455F15D3726B

To get the above to work required installing the python-dev operating system package. Then installing Inline::Python with zef.


You could use binascii.hexlify as well

sub create-key ( UInt $size ) {
  use Inline::Python;

  state $py = Inline::Python.new;
  once $py.import('os');
  once $py.import('binascii');

  $py.call('binascii','hexlify', $py.call('os','urandom',$size)).decode('ascii');
}

I'm sure there is a better way to do the above, but this is the first time I have used Inline::Python. (Which should be obvious because I had to install python-dev to answer this question)


Another way, which may be better in the long-run is to just call getrandom, getentropy, or CryptGenRandom depending on if it is running on Linux, OpenBSD, or Windows. Basically copy the implementation of os.urandom.

Below is a quickly written example.

sub urandom ( UInt $size ){
  use NativeCall;

  my constant is-win = $*DISTRO.is-win;
  my constant is-openbsd = $*DISTRO.name eq 'openbsd';

  if is-win {
    fail "urandom doesn't handle Windows yet";
    # It is more involved on Windows, and I don't use Windows

  } elsif is-openbsd {
    # note that this is untested as I don't use OpenBSD
    if $size > 256 {
      fail "urandom doesn't handle more than 256 on OpenBSD"
      # note that this could be changed to load it in 256 byte chunks
    }

    sub getentropy( Buf \buf, size_t \buflen --> int32 ) is native {}

    my Buf $buf .= allocate($size);
    my $result = getentropy( $buf, $size );

    fail if $result !== 0;
    $buf

  } else { # presumably Linux or other UNIX-like

    sub getrandom (Buf \buf, size_t \buflen, uint32 \flags --> ssize_t) is native {}

    my Buf $buf .= allocate($size);
    my $total = getrandom( $buf, $size, 0 );

    fail unless $total == $size; # could be changed to call it for the rest
    $buf;
  }
}
say urandom(10)».fmt('%02X').join; # 0EF9EDB3EBC724C0E9CE

If you are on a system with /dev/urandom, you could just read from that instead.

sub urandom ( UInt $size ){
  my $urandom will leave {.close}
    = '/dev/urandom'.IO.open(:bin,:ro);
  $urandom.read( $size )
}

say urandom(10)».fmt('%02X').join; # 01B6C41AD0A77732C328

The best route would be to use a module that already does the above like Crypt::Random.
It implements the code required for Windows that I didn't, but it uses the /dev/urandom file on *NIX systems.

# alias &Crypt::Random::crypt_random_buf as &urandom
my &urandom = do {
  use Crypt::Random;
  &crypt_random_buf
}

say urandom(10)».fmt('%02X').join; # 841720513678B1811E2D

Using the method or sub roll will not give you random bytes suitable for cryptographic use. They just use the built-in pseudorandom number generator that perl6 offers, which is a mersenne twister in the case of rakudo on moarvm.

What you will want instead is a library like for example Crypt::Random, which mimics arc4random and is described to use /dev/urandom on unices and CryptGenRandom on windows. You can find it on github or install it directly with zef install Crypt::Random.

Using the crypt_random_buf sub from that module will give you a buf of the size you want, which you can then turn into a string of hex digits just like in the other answer, with .list.fmt("%x","").


sub urandom(Int:D \size) { Buf.new: (^256).roll(size) }
say urandom(16);  # Buf:0x<98 43 10 A7 5A FD 62 4B AB 1E 42 6D 24 70 E6 89>

alternately, as string:

say urandom(16).list.fmt("%x","");  # bfa1c6fef9784ba31b17cdb135ce6622

or put that inside of the urandom sub:

sub urandom(Int:D \size) { Buf.new((^256).roll(size)).list.fmt("%x","") }
say urandom(16);  # bfa1c6fef9784ba31b17cdb135ce6622

Tags:

Raku