Crypt-JWT
view release on metacpan or search on metacpan
lib/Crypt/JWT.pm view on Meta::CPAN
sub _payload_unzip {
my ($payload, $z) = @_;
if ($z eq "DEF") {
my $d = Compress::Raw::Zlib::Inflate->new(
-Bufsize => $MAX_INFLATED_SIZE,
-WindowBits => -&MAX_WBITS(),
-LimitOutput => 1,
);
my $output = '';
my $status = $d->inflate($payload, $output);
croak "JWT: inflated payload exceeds limit ($MAX_INFLATED_SIZE bytes)" if $status == Z_BUF_ERROR;
croak "JWT: inflate failed (status=$status)" if $status != Z_STREAM_END;
$payload = $output;
}
else {
croak "JWT: unknown zip method '$z'";
}
return $payload;
}
sub _payload_enc {
my ($payload) = @_;
if (ref($payload) =~ /^(HASH|ARRAY)$/) {
$payload = JSON->new->utf8->canonical->encode($payload);
}
else {
utf8::downgrade($payload, 1) or croak "JWT: payload cannot contain wide character";
}
return $payload;
}
sub _payload_dec {
my ($payload, $decode_payload) = @_;
return $payload if defined $decode_payload && $decode_payload == 0;
my $de = $payload;
$de = eval { decode_json($de) };
if ($decode_payload) {
croak "JWT: payload not a valid JSON" unless $de;
return $de;
}
else {
return defined $de ? $de : $payload;
}
}
sub _encrypt_jwe_cek {
my ($key, $hdr) = @_;
my $alg = $hdr->{alg};
my $enc = $hdr->{enc};
if ($alg eq 'dir') {
return (_prepare_oct_key($key), '');
}
my $cek;
my $ecek;
if ($enc =~ /^A(128|192|256)GCM/) {
$cek = random_bytes($1/8);
}
elsif ($enc =~ /^A(128|192|256)CBC/) {
$cek = random_bytes(2*$1/8);
}
if ($alg =~ /^A(128|192|256)KW$/) {
# RFC 7518 sec 4.4 wraps via "AES Key Wrap algorithm specified in RFC 3394"
$ecek = aes_key_wrap(_prepare_oct_key($key), $cek, 'AES', 0);
return ($cek, $ecek);
}
elsif ($alg =~ /^A(128|192|256)GCMKW$/) {
my ($t, $i);
($ecek, $t, $i) = gcm_key_wrap(_prepare_oct_key($key), $cek);
$hdr->{tag} = encode_b64u($t);
$hdr->{iv} = encode_b64u($i);
return ($cek, $ecek);
}
elsif ($alg =~ /^PBES2-HS(512|384|256)\+A(128|192|256)KW$/) {
my $len = looks_like_number($hdr->{p2s}) && $hdr->{p2s} >= 8 && $hdr->{p2s} <= 9999 ? $hdr->{p2s} : 16;
my $salt = random_bytes($len);
my $iter = looks_like_number($hdr->{p2c}) ? $hdr->{p2c} : 5000;
$ecek = pbes2_key_wrap(_prepare_oct_key($key), $cek, $alg, $salt, $iter);
$hdr->{p2s} = encode_b64u($salt);
$hdr->{p2c} = $iter;
return ($cek, $ecek);
}
elsif ($alg =~ /^RSA(-OAEP|-OAEP-256|1_5)$/) {
$key = _prepare_rsa_key($key);
$ecek = rsa_key_wrap($key, $cek, $alg);
return ($cek, $ecek);
}
elsif ($alg =~ /^ECDH-ES\+A(128|192|256)KW$/) {
$key = _prepare_ecdh_key($key);
($ecek, $hdr->{epk}) = ecdhaes_key_wrap($key, $cek, $alg, $hdr->{apu}, $hdr->{apv});
return ($cek, $ecek);
}
elsif ($alg eq 'ECDH-ES') {
$key = _prepare_ecdh_key($key);
($cek, $hdr->{epk}) = ecdh_key_wrap($key, $enc, $hdr->{apu}, $hdr->{apv});
return ($cek, '');
}
croak "JWE: unknown alg '$alg'";
}
sub _decrypt_jwe_cek {
my ($ecek, $key, $hdr) = @_;
my $alg = $hdr->{alg};
my $enc = $hdr->{enc};
if ($alg eq 'dir') {
return _prepare_oct_key($key);
}
elsif ($alg =~ /^A(128|192|256)KW$/) {
return aes_key_unwrap(_prepare_oct_key($key), $ecek, 'AES', 0);
}
elsif ($alg =~ /^A(128|192|256)GCMKW$/) {
return gcm_key_unwrap(_prepare_oct_key($key), $ecek, decode_b64u($hdr->{tag}), decode_b64u($hdr->{iv}));
}
elsif ($alg =~ /^PBES2-HS(512|384|256)\+A(128|192|256)KW$/) {
my $p2c = $hdr->{p2c};
croak "JWE: invalid p2c" unless looks_like_number($p2c) && $p2c >= 1 && $p2c <= $MAX_PBES2_ITER;
return pbes2_key_unwrap(_prepare_oct_key($key), $ecek, $alg, decode_b64u($hdr->{p2s}), $p2c);
}
elsif ($alg =~ /^RSA(-OAEP|-OAEP-256|1_5)$/) {
$key = _prepare_rsa_key($key);
return rsa_key_unwrap($key, $ecek, $alg);
}
elsif ($alg =~ /^ECDH-ES\+A(128|192|256)KW$/) {
$key = _prepare_ecdh_key($key);
return ecdhaes_key_unwrap($key, $ecek, $alg, $hdr->{epk}, $hdr->{apu}, $hdr->{apv});
}
elsif ($alg eq 'ECDH-ES') {
$key = _prepare_ecdh_key($key);
return ecdh_key_unwrap($key, $enc, $hdr->{epk}, $hdr->{apu}, $hdr->{apv});
}
croak "JWE: unknown alg '$alg'";
}
sub _encrypt_jwe_payload {
my ($cek, $enc, $b64u_header, $b64u_aad, $payload) = @_;
my $aad = defined $b64u_aad ? "$b64u_header.$b64u_aad" : $b64u_header;
if ($enc =~ /^A(128|192|256)GCM$/) {
# https://tools.ietf.org/html/rfc7518#section-5.3
my $len1 = $1/8;
my $len2 = length($cek);
croak "JWE: wrong AES key length ($len1 vs. $len2) for $enc" unless $len1 == $len2;
my $iv = random_bytes(12); # for AESGCM always 12 (96 bits)
my ($ct, $tag) = gcm_encrypt_authenticate('AES', $cek, $iv, $aad, $payload);
return ($ct, $iv, $tag);
}
elsif ($enc =~ /^A(128|192|256)CBC-HS(256|384|512)$/) {
# https://tools.ietf.org/html/rfc7518#section-5.2
my ($size, $hash) = ($1/8, "SHA$2");
my $key_len = length($cek) / 2;
my $mac_key = substr($cek, 0, $key_len);
my $aes_key = substr($cek, $key_len, $key_len);
croak "JWE: wrong AES key length ($key_len vs. $size)" unless $key_len == $size;
my $iv = random_bytes(16); # for AES always 16
my $m = Crypt::Mode::CBC->new('AES');
my $ct = $m->encrypt($payload, $aes_key, $iv);
my $aad_len = length($aad);
# RFC 7518 5.2.2.1: AL = AAD length in bits as 64-bit big-endian.
# Split aad_len*8 into two 32-bit halves; both intermediate values
# stay within 32-bit range, so this is safe on 32-bit Perl too.
my $al_hi = $aad_len >> 29;
my $al_lo = ($aad_len & 0x1FFFFFFF) << 3;
my $mac_input = $aad . $iv . $ct . pack('N2', $al_hi, $al_lo);
my $mac = hmac($hash, $mac_key, $mac_input);
my $sig_len = length($mac) / 2;
my $sig = substr($mac, 0, $sig_len);
return ($ct, $iv, $sig);
}
croak "JWE: unsupported enc '$enc'";
}
sub _decrypt_jwe_payload {
my ($cek, $enc, $aad, $ct, $iv, $tag) = @_;
if ($enc =~ /^A(128|192|256)GCM$/) {
# https://tools.ietf.org/html/rfc7518#section-5.3
my $len1 = $1/8;
my $len2 = length($cek);
croak "JWE: wrong AES key length ($len1 vs. $len2) for $enc" unless $len1 == $len2;
return gcm_decrypt_verify('AES', $cek, $iv, $aad, $ct, $tag);
}
elsif ($enc =~ /^A(128|192|256)CBC-HS(256|384|512)$/) {
# https://tools.ietf.org/html/rfc7518#section-5.2
my ($size, $hash) = ($1/8, "SHA$2");
my $key_len = length($cek) / 2;
my $mac_key = substr($cek, 0, $key_len);
my $aes_key = substr($cek, $key_len, $key_len);
croak "JWE: wrong AES key length ($key_len vs. $size)" unless $key_len == $size;
my $aad_len = length($aad); # AAD == original encoded header
# RFC 7518 5.2.2.1: AL = AAD length in bits as 64-bit big-endian.
# Split aad_len*8 into two 32-bit halves; both intermediate values
# stay within 32-bit range, so this is safe on 32-bit Perl too.
my $al_hi = $aad_len >> 29;
my $al_lo = ($aad_len & 0x1FFFFFFF) << 3;
my $mac_input = $aad . $iv . $ct . pack('N2', $al_hi, $al_lo);
my $mac = hmac($hash, $mac_key, $mac_input);
my $sig_len = length($mac) / 2;
my $sig = substr($mac, 0, $sig_len);
croak "JWE: tag mismatch" unless slow_eq($sig, $tag);
my $m = Crypt::Mode::CBC->new('AES');
my $pt = $m->decrypt($ct, $aes_key, $iv);
return $pt;
}
croak "JWE: unsupported enc '$enc'";
}
sub _encode_jwe {
my %args = @_;
my $payload = $args{payload};
my $alg = $args{alg};
my $enc = $args{enc};
my $header = $args{extra_headers} ? \%{$args{extra_headers}} : {};
croak "JWE: missing 'enc'" if !defined $enc;
croak "JWE: missing 'payload'" if !defined $payload;
# add claims to payload
_add_claims($payload, %args) if ref $payload eq 'HASH';
# serialize payload
$payload = _payload_enc($payload);
# compress payload
$payload = _payload_zip($payload, $header, $args{zip}) if $args{zip}; # may set some header items
# prepare header
$header->{alg} = $alg;
$header->{enc} = $enc;
# key
croak "JWE: missing 'key'" if !$args{key};
my $key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
# prepare cek
my ($cek, $ecek) = _encrypt_jwe_cek($key, $header); # adds some header items
# encode header
my $json_header = encode_json($header);
my $b64u_header = encode_b64u($json_header);
my $b64u_aad = defined $args{aad} ? encode_b64u($args{aad}) : undef;
# encrypt payload
my ($ct, $iv, $tag) = _encrypt_jwe_payload($cek, $enc, $b64u_header, $b64u_aad, $payload);
# return token parts
return ( $b64u_header,
encode_b64u($ecek),
encode_b64u($iv),
encode_b64u($ct),
encode_b64u($tag),
$b64u_aad);
}
sub _decode_jwe {
my ($b64u_header, $b64u_ecek, $b64u_iv, $b64u_ct, $b64u_tag, $b64u_aad, $unprotected, $shared_unprotected, %args) = @_;
my $header = _b64u_to_hash($b64u_header);
my $ecek = decode_b64u($b64u_ecek);
my $ct = decode_b64u($b64u_ct);
my $iv = decode_b64u($b64u_iv);
my $tag = decode_b64u($b64u_tag);
croak "JWE: invalid header part" if $b64u_header && !$header;
croak "JWE: invalid ecek part" if $b64u_ecek && !$ecek;
croak "JWE: invalid ct part" if $b64u_ct && !$ct;
croak "JWE: invalid iv part" if $b64u_iv && !$iv;
croak "JWE: invalid tag part" if $b64u_tag && !$tag;
my $key;
if (exists $args{key}) {
$key = defined $args{keypass} ? [$args{key}, $args{keypass}] : $args{key};
}
lib/Crypt/JWT.pm view on Meta::CPAN
Mandatory. Accepts a string (raw bytes), a HASH ref, or an ARRAY ref.
HASH ref and ARRAY ref payloads are serialized as JSON strings; string
payloads are passed through verbatim.
my $token = encode_jwt(payload=>"any raw data", key=>$k, alg=>'HS256');
my $token = encode_jwt(payload=>{a=>1, b=>2}, key=>$k, alg=>'HS256');
my $token = encode_jwt(payload=>[11,22,33,44], key=>$k, alg=>'HS256');
=item alg
The 'alg' header value is mandatory for both JWE and JWS tokens.
Supported JWE 'alg' algorithms:
dir
A128KW
A192KW
A256KW
A128GCMKW
A192GCMKW
A256GCMKW
PBES2-HS256+A128KW
PBES2-HS384+A192KW
PBES2-HS512+A256KW
RSA-OAEP
RSA-OAEP-256
RSA1_5
ECDH-ES+A128KW
ECDH-ES+A192KW
ECDH-ES+A256KW
ECDH-ES
Supported JWS algorithms:
none ... no integrity (NOTE: disabled by default)
HS256 ... HMAC+SHA256 integrity
HS384 ... HMAC+SHA384 integrity
HS512 ... HMAC+SHA512 integrity
RS256 ... RSA+PKCS1-V1_5 + SHA256 signature
RS384 ... RSA+PKCS1-V1_5 + SHA384 signature
RS512 ... RSA+PKCS1-V1_5 + SHA512 signature
PS256 ... RSA+PSS + SHA256 signature
PS384 ... RSA+PSS + SHA384 signature
PS512 ... RSA+PSS + SHA512 signature
ES256 ... ECDSA + SHA256 signature
ES256K ... ECDSA + SHA256 signature
ES384 ... ECDSA + SHA384 signature
ES512 ... ECDSA + SHA512 signature
EdDSA ... Ed25519 signature
=item enc
The 'enc' header is mandatory for JWE tokens.
Supported 'enc' algorithms:
A128GCM
A192GCM
A256GCM
A128CBC-HS256
A192CBC-HS384
A256CBC-HS512
=item key
A key used for token encryption (JWE) or token signing (JWS). The value depends on C<alg> token header value.
JWS alg header key value
------------------ ----------------------------------
none no key required
HS256 string (raw octets) of any length (or perl HASH ref with JWK, kty=>'oct')
HS384 same as HS256
HS512 same as HS256
RS256 private RSA key, perl HASH ref with JWK key structure,
a reference to SCALAR string with PEM or DER or JSON/JWK data,
object: Crypt::PK::RSA, Crypt::OpenSSL::RSA, Crypt::X509 or Crypt::OpenSSL::X509
RS384 private RSA key, see RS256
RS512 private RSA key, see RS256
PS256 private RSA key, see RS256
PS384 private RSA key, see RS256
PS512 private RSA key, see RS256
ES256 private ECC key, perl HASH ref with JWK key structure,
a reference to SCALAR string with PEM or DER or JSON/JWK data,
an instance of Crypt::PK::ECC
ES256K private ECC key, see ES256
ES384 private ECC key, see ES256
ES512 private ECC key, see ES256
EdDSA private Ed25519 key
JWE alg header key value
------------------ ----------------------------------
dir string (raw octets) or perl HASH ref with JWK, kty=>'oct', length depends on 'enc' algorithm
A128KW string (raw octets) 16 bytes (or perl HASH ref with JWK, kty=>'oct')
A192KW string (raw octets) 24 bytes (or perl HASH ref with JWK, kty=>'oct')
A256KW string (raw octets) 32 bytes (or perl HASH ref with JWK, kty=>'oct')
A128GCMKW string (raw octets) 16 bytes (or perl HASH ref with JWK, kty=>'oct')
A192GCMKW string (raw octets) 24 bytes (or perl HASH ref with JWK, kty=>'oct')
A256GCMKW string (raw octets) 32 bytes (or perl HASH ref with JWK, kty=>'oct')
PBES2-HS256+A128KW string (raw octets) of any length (or perl HASH ref with JWK, kty=>'oct')
PBES2-HS384+A192KW string (raw octets) of any length (or perl HASH ref with JWK, kty=>'oct')
PBES2-HS512+A256KW string (raw octets) of any length (or perl HASH ref with JWK, kty=>'oct')
RSA-OAEP public RSA key, perl HASH ref with JWK key structure,
a reference to SCALAR string with PEM or DER or JSON/JWK data,
an instance of Crypt::PK::RSA or Crypt::OpenSSL::RSA
RSA-OAEP-256 public RSA key, see RSA-OAEP
RSA1_5 public RSA key, see RSA-OAEP
ECDH-ES public ECC or X25519 key, perl HASH ref with JWK key structure,
a reference to SCALAR string with PEM or DER or JSON/JWK data,
an instance of Crypt::PK::ECC
ECDH-ES+A128KW public ECC or X25519 key, see ECDH-ES
ECDH-ES+A192KW public ECC or X25519 key, see ECDH-ES
ECDH-ES+A256KW public ECC or X25519 key, see ECDH-ES
=item keypass
Optional. When the C<key> parameter is an encrypted private RSA or ECC
key (PEM/DER), this parameter holds the password used to decrypt it.
=item allow_none
C<1> - allow JWS with C<none> 'alg' header value (which means that token has no signature), B<BEWARE: DANGEROUS, INSECURE.>
( run in 0.658 second using v1.01-cache-2.11-cpan-df04353d9ac )