The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use utf8;
use open qw/:std :utf8/;
use charnames ':full';

use Authen::SCRAM::Client;
use Authen::SCRAM::Server;
use Authen::SASL::SASLprep 1.100 qw/saslprep/;
use JSON::MaybeXS;
use MIME::Base64 qw/encode_base64/;
use Tie::IxHash;
use Path::Tiny;

my $JSON = JSON::MaybeXS->new( ascii => 1, canonical => 1, pretty => 1 );

my @common = (
    iters       => 4096,
    salt        => "saltSALTsalt",
    clientNonce => "clientNONCE",
    serverNonce => "serverNONCE",
);

my @CRED_INPUTS = (
    {
        label => "ASCII",
        user  => "user",
        pass  => "pencil",
        @common,
    },
    {
        label => "ASCII user",
        user  => "user",
        pass  => "p\N{U+00E8}ncil",
        @common,
    },
    {
        label => "ASCII pass",
        user  => "ram\N{U+00F5}n",
        pass  => "pencil",
        @common,
    },
    {
        label => "SASLprep normal",
        user  => "ram\N{U+00F5}n",
        pass  => "p\N{U+00C5}assword",
        @common,
    },
    {
        label => "SASLprep non-normal",
        user  => "ramo\N{U+0301}n",
        pass  => "p\N{U+212B}ssword",
        @common,
    },
    {
        label        => "no-SASLprep",
        user         => "ramo\N{U+0301}n",
        pass         => "p\N{U+212B}ssword",
        skipSASLprep => 1,
        @common,
    },
);

sub nice_string {
    join(
        "",
        map {
            $_ > 127 # if above ASCII
              ? sprintf( "\\u%04x", $_ ) # JSON-style escapes
              : chr($_)                  # else as themselves
        } unpack( "W*", $_[0] )
    );                                   # unpack Unicode characters
}

for my $digest_name (qw/SHA-1 SHA-256/) {
    for my $c (@CRED_INPUTS) {
        my $cred     = {%$c};
        my $label    = $cred->{label} = "$digest_name $cred->{label}";
        my $niceuser = nice_string( $cred->{user} );
        my $nicepass = nice_string( $cred->{pass} );
        say "Test Case: $label";
        say "User: '$niceuser'";
        say "Pass: '$nicepass'";

        my $client = Authen::SCRAM::Client->new(
            digest           => $digest_name,
            username         => $cred->{user},
            password         => $cred->{pass},
            skip_saslprep    => $cred->{skipSASLprep},
            _nonce_generator => sub { $cred->{clientNonce} },
        );

        my ( $stored_key, $client_key, $server_key ) =
          $client->computed_keys( $cred->{salt}, $cred->{iters} );

        my $prepped_user = saslprep( $cred->{user}, 1 );
        my $prepped_pass = saslprep( $cred->{pass}, 1 );
        if ( !$cred->{skipSASLprep} ) {
            say "Prepped User: '" . nice_string($prepped_user) . "'";
            say "Prepped Pass '" . nice_string($prepped_pass) . "'";
        }

        my $server = Authen::SCRAM::Server->new(
            digest        => $digest_name,
            credential_cb => sub {
                my $user = shift;
                if ( $user eq $prepped_user ) {
                    return ( $cred->{salt}, $stored_key, $server_key, $cred->{iters} );
                }
                else {
                    warn "BAD USERNAME MATCH FOR '$user'\n";
                    return;
                }
            },
            _nonce_generator => sub { $cred->{serverNonce} },
        );

        my ( $c1, $c2, $s1, $s2 );

        say "C1: " . nice_string( $c1 = $client->first_msg() );
        say "S1: " . nice_string( $s1 = $server->first_msg($c1) );
        say "C2: " . nice_string( $c2 = $client->final_msg($s1) );
        say "S2: " . nice_string( $s2 = $server->final_msg($c2) );

        $cred->{steps} = [ $c1, $s1, $c2, $s2, "" ];

        say eval { $cred->{valid} = $client->validate($s2) ? \1 : \0; 1 }
          ? "Server: valid"
          : "Server: invalid";
        say "";

        $cred->{salt64}       = encode_base64( delete $cred->{salt} );
        $cred->{authID}       = "";
        $cred->{skipSASLprep} = $cred->{skipSASLprep} ? \1 : \0;
        $cred->{digest}       = $digest_name;

        tie my %thash, "Tie::IxHash";
        for my $k (
            qw/label digest user pass authID skipSASLprep salt64 iters clientNonce serverNonce valid steps/
          )
        {
            $thash{$k} = $cred->{$k};
        }
        my $fname = lc "$label.json";
        $fname =~ tr[ ][-];
        path($fname)->spew( $JSON->encode( \%thash ) );
    }

}