Net-Nostr

 view release on metacpan or  search on metacpan

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN

use Carp qw(croak);
use Crypt::AuthEnc::ChaCha20Poly1305;
use Crypt::PRNG qw(random_bytes);
use Crypt::ScryptKDF qw(scrypt_raw);
use Encode qw(encode);
use Unicode::Normalize qw(NFKC);
use Bitcoin::Crypto::Bech32 qw(encode_bech32 translate_5to8 translate_8to5);
use Exporter 'import';

our @EXPORT_OK = qw(
    encrypt_private_key
    decrypt_private_key
);

my $HEX64 = qr/\A[0-9a-f]{64}\z/;
my $VERSION_BYTE = 0x02;
my $MAX_BECH32_LENGTH = 5000;

# Bech32 decode without BIP-173's 90-char limit, same as in Bech32.pm
{
    my @ALPHABET = qw(
        q p z r y 9 x 8  g f 2 t v d w 0

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN


        my @data_values = map { $ALPHABET_MAP{$_} } split //, $data_part;
        my $check_values = [@{_hrp_expand($hrp)}, @data_values];
        croak "invalid bech32 checksum" unless _polymod($check_values) == 1;

        my @payload = @data_values[0 .. $#data_values - 6];
        return ($hrp, \@payload);
    }
}

sub encrypt_private_key {
    my (%args) = @_;
    my $privkey_hex  = $args{privkey_hex}  // croak "privkey_hex is required";
    my $password     = $args{password}     // croak "password is required";
    my $log_n        = $args{log_n}        // croak "log_n is required";
    my $key_security = $args{key_security} // 0x02;

    croak "privkey_hex must be 64-char lowercase hex" unless $privkey_hex =~ $HEX64;
    croak "password is required" unless length $password;
    croak "log_n must be between 1 and 22" unless $log_n >= 1 && $log_n <= 22;
    croak "key_security must be 0x00, 0x01, or 0x02"

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN


    my $aad = chr($key_security);
    my $ciphertext_and_tag = _xchacha20poly1305_encrypt($sym_key, $nonce, $aad, $privkey_raw);

    my $raw = chr($VERSION_BYTE) . chr($log_n) . $salt . $nonce . $aad . $ciphertext_and_tag;

    my $data5 = translate_8to5($raw);
    return encode_bech32('ncryptsec', $data5, 'bech32');
}

sub decrypt_private_key {
    my ($ncryptsec, $password, %opts) = @_;

    croak "ncryptsec string is required" unless defined $ncryptsec && length $ncryptsec;
    croak "password is required" unless defined $password && length $password;

    my ($hrp, $data5) = _nostr_decode_bech32($ncryptsec);
    croak "expected ncryptsec prefix, got $hrp" unless $hrp eq 'ncryptsec';

    my $raw = translate_5to8($data5);
    croak "invalid payload size" unless length($raw) == 91;

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN

1;

__END__

=head1 NAME

Net::Nostr::KeyEncrypt - NIP-49 private key encryption

=head1 SYNOPSIS

    use Net::Nostr::KeyEncrypt qw(encrypt_private_key decrypt_private_key);

    # Encrypt a private key with a password
    my $ncryptsec = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'my-strong-password',
        log_n       => 16,
    );
    # ncryptsec1...

    # Decrypt an encrypted private key
    my $privkey_hex = decrypt_private_key($ncryptsec, 'my-strong-password');
    # 'aa' x 32

    # Specify key security level
    my $ncryptsec = encrypt_private_key(
        privkey_hex  => $privkey_hex,
        password     => $password,
        log_n        => 20,
        key_security => 0x01,  # not known to have been handled insecurely
    );

=head1 DESCRIPTION

Implements NIP-49 private key encryption. Encrypts a user's private key
with a password using scrypt key derivation and XChaCha20-Poly1305 AEAD

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN

be stored or transferred safely.

The password is Unicode-normalized to NFKC form before use, ensuring
that the same password entered on different systems produces the same
encryption key.

=head1 FUNCTIONS

All functions are exportable. None are exported by default.

=head2 encrypt_private_key

    my $ncryptsec = encrypt_private_key(
        privkey_hex  => $hex_privkey,
        password     => $password,
        log_n        => $log_n,
        key_security => $byte,   # optional, defaults to 0x02
    );

Encrypts a private key with a password. Returns a bech32-encoded
C<ncryptsec> string. Croaks if any argument is missing, out of range,
or malformed.

lib/Net/Nostr/KeyEncrypt.pm  view on Meta::CPAN

force. Recommended: 16 (64 MiB, ~100ms) for interactive use, 20+ for
long-term storage.

=item C<key_security> - one of C<0x00> (key known to have been handled
insecurely), C<0x01> (key not known insecure), or C<0x02> (unknown).
Defaults to C<0x02>. This byte is included as associated data in the
AEAD encryption and stored in the payload.

=back

    my $ncryptsec = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'my-strong-password',
        log_n       => 16,
    );

=head2 decrypt_private_key

    my $hex = decrypt_private_key($ncryptsec, $password);
    my $hex = decrypt_private_key($ncryptsec, $password, log_n => $n);

Decrypts an C<ncryptsec> string with a password. Returns the private key
as a 64-char lowercase hex string. Validates the bech32 encoding,
C<ncryptsec> prefix, version byte (must be C<0x02>), and payload size
(must be 91 bytes). Croaks on wrong password, corrupted data, or
invalid format.

The C<log_n> parameter is optional. If omitted, the value embedded in
the C<ncryptsec> payload is used. Providing C<log_n> explicitly overrides
the embedded value, which is useful when you know the cost parameter
in advance.

    my $hex = decrypt_private_key(
        'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
        'nostr',
    );
    # '3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683'

=head1 SEE ALSO

L<NIP-49|https://github.com/nostr-protocol/nips/blob/master/49.md>,
L<Net::Nostr>, L<Net::Nostr::Key>

t/40-KeyEncrypt.t  view on Meta::CPAN

use strictures 2;
use Test2::V0 -no_srand => 1;

use Net::Nostr::KeyEncrypt qw(
    encrypt_private_key
    decrypt_private_key
);

###############################################################################
# POD example: encrypt then decrypt
###############################################################################

subtest 'POD: encrypt and decrypt round-trip' => sub {
    my $ncryptsec = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'my-strong-password',
        log_n       => 16,
    );
    like($ncryptsec, qr/\Ancryptsec1/, 'encrypt returns ncryptsec1...');

    my $privkey_hex = decrypt_private_key($ncryptsec, 'my-strong-password');
    is($privkey_hex, 'aa' x 32, 'decrypt recovers original key');
};

###############################################################################
# POD example: key_security level
###############################################################################

subtest 'POD: key_security 0x01' => sub {
    my $privkey_hex = 'bb' x 32;
    my $password    = 'test-password';
    my $ncryptsec = encrypt_private_key(
        privkey_hex  => $privkey_hex,
        password     => $password,
        log_n        => 20,
        key_security => 0x01,
    );
    like($ncryptsec, qr/\Ancryptsec1/, 'encrypted with key_security 0x01');
    my $decrypted = decrypt_private_key($ncryptsec, $password);
    is($decrypted, $privkey_hex, 'round-trips with key_security 0x01');
};

###############################################################################
# POD example: spec test vector decryption
###############################################################################

subtest 'POD: spec decryption example' => sub {
    my $hex = decrypt_private_key(
        'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
        'nostr',
    );
    is($hex, '3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683',
        'spec test vector decrypts correctly');
};

###############################################################################
# POD example: decrypt with explicit log_n
###############################################################################

subtest 'POD: decrypt with explicit log_n' => sub {
    my $hex = decrypt_private_key(
        'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p',
        'nostr',
        log_n => 16,
    );
    is($hex, '3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683',
        'spec test vector with explicit log_n');
};

###############################################################################
# exports
###############################################################################

subtest 'exports: functions available' => sub {
    ok(defined &encrypt_private_key, 'encrypt_private_key exported');
    ok(defined &decrypt_private_key, 'decrypt_private_key exported');
};

###############################################################################
# return type
###############################################################################

subtest 'encrypt: returns string' => sub {
    my $result = encrypt_private_key(
        privkey_hex => 'cc' x 32,
        password    => 'test',
        log_n       => 16,
    );
    ok(!ref $result, 'encrypt returns a plain scalar');
    like($result, qr/\Ancryptsec1[a-z0-9]+\z/, 'valid bech32 format');
};

subtest 'decrypt: returns lowercase hex' => sub {
    my $encrypted = encrypt_private_key(
        privkey_hex => 'dd' x 32,
        password    => 'test',
        log_n       => 16,
    );
    my $result = decrypt_private_key($encrypted, 'test');
    like($result, qr/\A[0-9a-f]{64}\z/, 'decrypt returns 64-char lowercase hex');
    is($result, 'dd' x 32, 'correct key recovered');
};

done_testing;

t/nip/49.t  view on Meta::CPAN

use strictures 2;
use Test2::V0 -no_srand => 1;

use Net::Nostr::KeyEncrypt qw(
    encrypt_private_key
    decrypt_private_key
);
use Bitcoin::Crypto::Bech32 qw(encode_bech32 translate_5to8 translate_8to5);

my $HEX64 = qr/\A[0-9a-f]{64}\z/;

# Helper: decode ncryptsec bech32 to raw bytes (calls private decoder)
sub _decode_ncryptsec_raw {
    my ($ncryptsec) = @_;
    my ($hrp, $data5) = Net::Nostr::KeyEncrypt::_nostr_decode_bech32($ncryptsec);
    return ($hrp, translate_5to8($data5));
}

###############################################################################
# NIP-49 test vector: password unicode normalization
###############################################################################

subtest 'NIP-49: password NFKC normalization' => sub {
    # Spec test data: input "ÅΩẛ̣" (U+212B U+2126 U+1E9B U+0323)
    # NFKC normalized: "ÅΩẛ̣" (U+00C5 U+03A9 U+1E69)
    my $privkey = 'aa' x 32;
    my $encrypted = encrypt_private_key(
        privkey_hex => $privkey,
        password    => "\x{212B}\x{2126}\x{1E9B}\x{0323}",
        log_n       => 16,
    );
    like($encrypted, qr/\Ancryptsec1/, 'encrypted with unicode password');

    # Decrypt with NFKC-normalized form of the same password
    my $decrypted = decrypt_private_key($encrypted, "\x{00C5}\x{03A9}\x{1E69}");
    is($decrypted, $privkey, 'NFKC normalization makes both passwords equivalent');
};

###############################################################################
# NIP-49 test vector: decryption
###############################################################################

subtest 'NIP-49: spec decryption test vector' => sub {
    my $ncryptsec = 'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p';
    my $expected = '3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683';

    my $privkey = decrypt_private_key($ncryptsec, 'nostr', log_n => 16);
    is($privkey, $expected, 'decrypts to expected private key');
};

###############################################################################
# encrypt/decrypt round-trip
###############################################################################

subtest 'round-trip: encrypt then decrypt' => sub {
    my $privkey = 'bb' x 32;
    my $password = 'test-password-123';

    my $encrypted = encrypt_private_key(
        privkey_hex => $privkey,
        password    => $password,
        log_n       => 16,
    );
    like($encrypted, qr/\Ancryptsec1/, 'starts with ncryptsec1');

    my $decrypted = decrypt_private_key($encrypted, $password, log_n => 16);
    is($decrypted, $privkey, 'round-trips correctly');
};

subtest 'round-trip: different passwords produce different ciphertext' => sub {
    my $privkey = 'cc' x 32;
    my $e1 = encrypt_private_key(privkey_hex => $privkey, password => 'pass1', log_n => 16);
    my $e2 = encrypt_private_key(privkey_hex => $privkey, password => 'pass2', log_n => 16);
    isnt($e1, $e2, 'different passwords produce different output');
};

subtest 'round-trip: non-deterministic (random nonce)' => sub {
    my $privkey = 'dd' x 32;
    my $password = 'same-password';
    my $e1 = encrypt_private_key(privkey_hex => $privkey, password => $password, log_n => 16);
    my $e2 = encrypt_private_key(privkey_hex => $privkey, password => $password, log_n => 16);
    isnt($e1, $e2, 'same password + same key produces different output (random nonce)');

    # Both should decrypt to the same key
    is(decrypt_private_key($e1, $password, log_n => 16), $privkey, 'first decrypts');
    is(decrypt_private_key($e2, $password, log_n => 16), $privkey, 'second decrypts');
};

###############################################################################
# key_security_byte
###############################################################################

subtest 'key_security_byte: default is 0x02 (unknown)' => sub {
    my $privkey = 'ee' x 32;
    my $encrypted = encrypt_private_key(
        privkey_hex => $privkey,
        password    => 'test',
        log_n       => 16,
    );
    # Decrypt and verify it works (security byte is part of AAD)
    my $decrypted = decrypt_private_key($encrypted, 'test', log_n => 16);
    is($decrypted, $privkey, 'default security byte round-trips');
};

subtest 'key_security_byte: 0x00 (known insecure)' => sub {
    my $privkey = 'ee' x 32;
    my $encrypted = encrypt_private_key(
        privkey_hex      => $privkey,
        password         => 'test',
        log_n            => 16,
        key_security     => 0x00,
    );
    my $decrypted = decrypt_private_key($encrypted, 'test', log_n => 16);
    is($decrypted, $privkey, 'security byte 0x00 round-trips');
};

subtest 'key_security_byte: 0x01 (not known insecure)' => sub {
    my $privkey = 'ee' x 32;
    my $encrypted = encrypt_private_key(
        privkey_hex      => $privkey,
        password         => 'test',
        log_n            => 16,
        key_security     => 0x01,
    );
    my $decrypted = decrypt_private_key($encrypted, 'test', log_n => 16);
    is($decrypted, $privkey, 'security byte 0x01 round-trips');
};

subtest 'key_security_byte: invalid value rejected' => sub {
    like(
        dies {
            encrypt_private_key(
                privkey_hex  => 'aa' x 32,
                password     => 'test',
                log_n        => 16,
                key_security => 0x03,
            )
        },
        qr/key_security must be 0x00, 0x01, or 0x02/,
        'invalid security byte rejected'
    );
};

###############################################################################
# version
###############################################################################

subtest 'version: only 0x02 accepted' => sub {
    # Modify a valid ncryptsec to have version 0x01 and verify rejection
    # We can't easily do this without raw manipulation, so test via decrypt
    # with a known good one first
    my $ncryptsec = 'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p';
    my $decrypted = decrypt_private_key($ncryptsec, 'nostr', log_n => 16);
    like($decrypted, $HEX64, 'version 0x02 accepted');
};

###############################################################################
# validation
###############################################################################

subtest 'encrypt: missing privkey_hex' => sub {
    like(
        dies { encrypt_private_key(password => 'test', log_n => 16) },
        qr/privkey_hex is required/,
        'missing privkey_hex croaks'
    );
};

subtest 'encrypt: invalid privkey_hex' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'not-hex', password => 'test', log_n => 16) },
        qr/privkey_hex must be 64-char lowercase hex/,
        'bad hex rejected'
    );
};

subtest 'encrypt: missing password' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'aa' x 32, log_n => 16) },
        qr/password is required/,
        'missing password croaks'
    );
};

subtest 'encrypt: empty password' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'aa' x 32, password => '', log_n => 16) },
        qr/password is required/,
        'empty password croaks'
    );
};

subtest 'encrypt: missing log_n' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'aa' x 32, password => 'test') },
        qr/log_n is required/,
        'missing log_n croaks'
    );
};

subtest 'encrypt: log_n out of range' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'aa' x 32, password => 'test', log_n => 0) },
        qr/log_n must be between 1 and 22/,
        'log_n=0 rejected'
    );
    like(
        dies { encrypt_private_key(privkey_hex => 'aa' x 32, password => 'test', log_n => 23) },
        qr/log_n must be between 1 and 22/,
        'log_n=23 rejected'
    );
};

subtest 'decrypt: wrong password fails' => sub {
    my $encrypted = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'correct',
        log_n       => 16,
    );
    like(
        dies { decrypt_private_key($encrypted, 'wrong', log_n => 16) },
        qr/decryption failed/i,
        'wrong password rejected'
    );
};

subtest 'decrypt: invalid ncryptsec prefix' => sub {
    # Use a real npub (valid bech32, wrong prefix for ncryptsec)
    use Net::Nostr::Bech32 qw(encode_npub);
    my $npub = encode_npub('aa' x 32);
    like(
        dies { decrypt_private_key($npub, 'test') },
        qr/expected ncryptsec prefix/,
        'wrong bech32 prefix rejected'
    );
};

subtest 'decrypt: garbled data' => sub {
    like(
        dies { decrypt_private_key('ncryptsec1invaliddata', 'test') },
        qr/./,
        'garbled data rejected'
    );
};

###############################################################################
# log_n values
###############################################################################

subtest 'log_n: boundary values work' => sub {
    my $privkey = 'ff' x 31 . 'fe';
    for my $log_n (16) {
        my $encrypted = encrypt_private_key(
            privkey_hex => $privkey,
            password    => 'test',
            log_n       => $log_n,
        );
        my $decrypted = decrypt_private_key($encrypted, 'test', log_n => $log_n);
        is($decrypted, $privkey, "log_n=$log_n round-trips");
    }
};

###############################################################################
# output format
###############################################################################

subtest 'output: 91 bytes raw before bech32 encoding' => sub {
    my $encrypted = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'test',
        log_n       => 16,
    );
    like($encrypted, qr/\Ancryptsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+\z/,
        'output is valid ncryptsec bech32');
};

###############################################################################
# NIP-49 spec: decrypted log_n embedded in payload
###############################################################################

###############################################################################
# payload structure (spec lines 62-73)
###############################################################################

subtest 'payload: 91 bytes with correct layout' => sub {
    my $encrypted = encrypt_private_key(
        privkey_hex  => 'aa' x 32,
        password     => 'test',
        log_n        => 16,
        key_security => 0x01,
    );
    my ($hrp, $raw) = _decode_ncryptsec_raw($encrypted);
    is($hrp, 'ncryptsec', 'bech32 hrp is ncryptsec');
    is(length($raw), 91, 'raw payload is 91 bytes');

    # Verify layout: version(1) + log_n(1) + salt(16) + nonce(24) + aad(1) + ciphertext+tag(48)

t/nip/49.t  view on Meta::CPAN

    is(ord(substr($raw, 42, 1)), 0x01, 'key_security byte is 0x01');
    is(length(substr($raw, 43)), 48, 'ciphertext + tag is 48 bytes (32 + 16)');
};

###############################################################################
# version rejection
###############################################################################

subtest 'version: non-0x02 version rejected' => sub {
    # Encrypt normally, then tamper with the version byte
    my $encrypted = encrypt_private_key(
        privkey_hex => 'aa' x 32,
        password    => 'test',
        log_n       => 16,
    );
    my ($hrp, $raw) = _decode_ncryptsec_raw($encrypted);

    # Change version from 0x02 to 0x01
    substr($raw, 0, 1, chr(0x01));
    my $tampered_data5 = translate_8to5($raw);
    my $tampered = encode_bech32('ncryptsec', $tampered_data5, 'bech32');

    like(
        dies { decrypt_private_key($tampered, 'test', log_n => 16) },
        qr/unknown version/,
        'version 0x01 rejected'
    );
};

###############################################################################
# AAD tamper detection (key_security_byte is associated data)
###############################################################################

subtest 'AAD tamper: modified key_security_byte causes decryption failure' => sub {
    my $encrypted = encrypt_private_key(
        privkey_hex  => 'aa' x 32,
        password     => 'test',
        log_n        => 16,
        key_security => 0x00,
    );
    my ($hrp, $raw) = _decode_ncryptsec_raw($encrypted);

    # Change key_security from 0x00 to 0x02
    substr($raw, 42, 1, chr(0x02));
    my $tampered_data5 = translate_8to5($raw);
    my $tampered = encode_bech32('ncryptsec', $tampered_data5, 'bech32');

    like(
        dies { decrypt_private_key($tampered, 'test', log_n => 16) },
        qr/decryption failed/i,
        'tampered AAD causes MAC failure'
    );
};

###############################################################################
# additional validation edge cases
###############################################################################

subtest 'encrypt: uppercase hex rejected' => sub {
    like(
        dies { encrypt_private_key(privkey_hex => 'AA' x 32, password => 'test', log_n => 16) },
        qr/privkey_hex must be 64-char lowercase hex/,
        'uppercase hex rejected'
    );
};

subtest 'decrypt: missing ncryptsec' => sub {
    like(
        dies { decrypt_private_key(undef, 'test') },
        qr/ncryptsec string is required/,
        'undef ncryptsec rejected'
    );
    like(
        dies { decrypt_private_key('', 'test') },
        qr/ncryptsec string is required/,
        'empty ncryptsec rejected'
    );
};

subtest 'decrypt: missing password' => sub {
    like(
        dies { decrypt_private_key('ncryptsec1abc', undef) },
        qr/password is required/,
        'undef password rejected'
    );
    like(
        dies { decrypt_private_key('ncryptsec1abc', '') },
        qr/password is required/,
        'empty password rejected'
    );
};

subtest 'decrypt: invalid payload size' => sub {
    # Create a valid bech32 string with wrong payload length (too short)
    my $short_raw = chr(0x02) . chr(16) . ("\x00" x 10);
    my $data5 = translate_8to5($short_raw);
    my $bad = encode_bech32('ncryptsec', $data5, 'bech32');
    like(
        dies { decrypt_private_key($bad, 'test') },
        qr/invalid payload size/,
        'short payload rejected'
    );
};

###############################################################################
# NIP-49 spec: decrypted log_n embedded in payload
###############################################################################

subtest 'decrypt: log_n from payload used when not specified' => sub {
    # The test vector has log_n=16 embedded
    my $ncryptsec = 'ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p';
    my $decrypted = decrypt_private_key($ncryptsec, 'nostr');
    is($decrypted, '3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683',
        'log_n read from payload when not given');
};

done_testing;



( run in 2.422 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )