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 )