Authen::SASL::Perl::DIGEST_MD5 - Digest MD5 Authentication class


Authen-SASL documentation Contained in the Authen-SASL distribution.

Index


Code Index:

NAME

Top

Authen::SASL::Perl::DIGEST_MD5 - Digest MD5 Authentication class

SYNOPSIS

Top

  use Authen::SASL qw(Perl);

  $sasl = Authen::SASL->new(
    mechanism => 'DIGEST-MD5',
    callback  => {
      user => $user, 
      pass => $pass,
      serv => $serv
    },
  );

DESCRIPTION

Top

This method implements the client and server parts of the DIGEST-MD5 SASL algorithm, as described in RFC 2831.

CALLBACK

The callbacks used are:

client

authname

The authorization id to use after successful authentication

user

The username to be used in the response

pass

The password to be used to compute the response.

serv

The service name when authenticating to a replicated service

realm

The authentication realm when overriding the server-provided default. If not given the server-provided value is used.

The callback will be passed the list of realms that the server provided in the initial response.

server

The default realm to provide to the client

returns the password associated with username and realm

PROPERTIES

The properties used are:

The maximum buffer size for receiving cipher text

The minimum SSF value that should be provided by the SASL security layer. The default is 0

The maximum SSF value that should be provided by the SASL security layer. The default is 2**31

The SSF value provided by an underlying external security layer. The default is 0

The actual SSF value provided by the SASL security layer after the SASL authentication phase has been completed. This value is read-only and set by the implementation after the SASL authentication phase has been completed.

The maximum plaintext buffer size for sending data to the peer. This value is set by the implementation after the SASL authentication phase has been completed and a SASL security layer is in effect.

SEE ALSO

Top

Authen::SASL, Authen::SASL::Perl

AUTHORS

Top

Graham Barr, Djamel Boudjerda (NEXOR), Paul Connolly, Julian Onions (NEXOR), Yann Kerherve.

Please report any bugs, or post any suggestions, to the perl-ldap mailing list <perl-ldap@perl.org>

COPYRIGHT

Top


Authen-SASL documentation Contained in the Authen-SASL distribution.

# Copyright (c) 2003-2009 Graham Barr, Djamel Boudjerda, Paul Connolly, Julian
# Onions, Nexor and Yann Kerherve.
# All rights reserved. This program is free software; you can redistribute
# it and/or modify it under the same terms as Perl itself.

# See http://www.ietf.org/rfc/rfc2831.txt for details

package Authen::SASL::Perl::DIGEST_MD5;

use strict;
use vars qw($VERSION @ISA $CNONCE $NONCE);
use Digest::MD5 qw(md5_hex md5);
use Digest::HMAC_MD5 qw(hmac_md5);

# TODO: complete qop support in server, should be configurable

$VERSION = "2.14";
@ISA = qw(Authen::SASL::Perl);

my %secflags = (
  noplaintext => 1,
  noanonymous => 1,
);

# some have to be quoted - some don't - sigh!
my (%cqdval, %sqdval);
@cqdval{qw(
  username authzid realm nonce cnonce digest-uri
)} = ();

## ...and server behaves different than client - double sigh!
@sqdval{keys %cqdval, qw(qop cipher)} = ();
#  username authzid realm nonce cnonce digest-uri qop cipher
#)} = ();

my %multi;
@{$multi{server}}{qw(realm auth-param)} = ();
@{$multi{client}}{qw()} = ();

my @server_required = qw(algorithm nonce);
my @client_required = qw(username nonce cnonce nc qop response);

# available ciphers
my @ourciphers = (
  {
    name  => 'rc4',
    ssf   => 128,
    bs    => 1,
    ks    => 16,
    pkg   => 'Crypt::RC4',
    key   => sub { $_[0] },
    iv    => sub {},
    fixup => sub {
      # retrofit the Crypt::RC4 module with standard subs
      *Crypt::RC4::encrypt   = *Crypt::RC4::decrypt =
        sub { goto &Crypt::RC4::RC4; };
      *Crypt::RC4::keysize   =  sub {128};
      *Crypt::RC4::blocksize =  sub {1};
    }
  },
  {
    name  => '3des',
    ssf   => 112,
    bs    => 8,
    ks    => 16,
    pkg   => 'Crypt::DES3',
    key   => sub {
      pack('B8' x 16,
        map { $_ . '0' }
        map { unpack('a7' x 16, $_); }
        unpack('B*', substr($_[0], 0, 14)) );
    },
    iv => sub { substr($_[0], -8, 8) },
  },
  {
    name  => 'des',
    ssf   => 56,
    bs    => 8,
    ks    => 16,
    pkg   => 'Crypt::DES',
    key   => sub {
      pack('B8' x 8,
        map { $_ . '0' }
        map { unpack('a7' x 8, $_); }
        unpack('B*',substr($_[0], 0, 7)) );
    },
    iv => sub { substr($_[0], -8, 8) },
  },
  {
    name  => 'rc4-56',
    ssf   => 56,
    bs    => 1,
    ks    => 7,
    pkg   => 'Crypt::RC4',
    key   => sub { $_[0] },
    iv    => sub {},
    fixup => sub {
      *Crypt::RC4::encrypt   = *Crypt::RC4::decrypt =
        sub { goto &Crypt::RC4::RC4; };
      *Crypt::RC4::keysize   =  sub {56};
      *Crypt::RC4::blocksize =  sub {1};
    }
  },
  {
    name  => 'rc4-40',
    ssf   => 40,
    bs    => 1,
    ks    => 5,
    pkg   => 'Crypt::RC4',
    key   => sub { $_[0] },
    iv    => sub {},
    fixup => sub {
      *Crypt::RC4::encrypt   = *Crypt::RC4::decrypt =
        sub { goto &Crypt::RC4::RC4; };
      *Crypt::RC4::keysize   =  sub {40};
      *Crypt::RC4::blocksize =  sub {1};
    }
  },
);

## The system we are on, might not be able to crypt the stream
our $NO_CRYPT_AVAILABLE = 1;
for (@ourciphers) {
    eval "require $_->{pkg}";
    unless ($@) {
        $NO_CRYPT_AVAILABLE = 0;
        last;
    }
}

sub _order { 3 }
sub _secflags {
  shift;
  scalar grep { $secflags{$_} } @_;
}

sub mechanism { 'DIGEST-MD5' }

sub _init {
  my ($pkg, $self) = @_;
  bless $self, $pkg;

  # set default security properties
  $self->property('minssf',      0);
  $self->property('maxssf',      int 2**31 - 1);    # XXX - arbitrary "high" value
  $self->property('maxbuf',      0xFFFFFF);         # maximum supported by GSSAPI mech
  $self->property('externalssf', 0);

  $self;
}

sub _init_server {
  my $server  = shift;
  my $options = shift || {};
  if (!ref $options or ref $options ne 'HASH') {
    warn "options for DIGEST_MD5 should be a hashref";
    $options = {};
  }

  ## new server, means new nonce_counts
  $server->{nonce_counts} = {};

  ## determine supported qop
  my   @qop = ('auth');
  push @qop, 'auth-int'  unless $options->{no_integrity};
  push @qop, 'auth-conf' unless $options->{no_integrity}
                             or $options->{no_confidentiality}
                             or $NO_CRYPT_AVAILABLE;

  $server->{supported_qop} = { map { $_ => 1 } @qop };
}

sub init_sec_layer {
  my $self           = shift;
  $self->{cipher}    = undef;
  $self->{khc}       = undef;
  $self->{khs}       = undef;
  $self->{sndseqnum} = 0;
  $self->{rcvseqnum} = 0;

  # reset properties for new session
  $self->property(maxout => undef);
  $self->property(ssf    => undef);
}

# no initial value passed to the server
sub client_start {
  my $self = shift;

  $self->{need_step} = 1;
  $self->{error}     = undef;
  $self->{state}     = 0;
  $self->init_sec_layer;
  '';
}

sub server_start {
  my $self       = shift;
  my $challenge  = shift;
  my $cb         = shift || sub {};

  $self->{need_step} = 1;
  $self->{error}     = undef;
  $self->{nonce}     = md5_hex($NONCE || join (":", $$, time, rand));

  $self->init_sec_layer;

  my $qop = [ sort keys %{$self->{supported_qop}} ];

  ## get the realm using callbacks but default to the host specified
  ## during the instanciation of the SASL object
  my $realm = $self->_call('realm');
  $realm  ||= $self->host;

  my %response = (
    nonce         => $self->{nonce},
    charset       => 'utf-8',
    algorithm     => 'md5-sess',
    realm         => $realm,
    maxbuf        => $self->property('maxbuf'),

## IN DRAFT ONLY:
# If this directive is present multiple times the client MUST treat
# it as if it received a single qop directive containing a comma
# separated value from all instances. I.e.,
# 'qop="auth",qop="auth-int"' is the same as 'qop="auth,auth-int"

    'qop'         => $qop,
    'cipher'      => [ map { $_->{name} } @ourciphers ],
  );
  my $final_response = _response(\%response);
  $cb->($final_response);
  return;
}

sub client_step {   # $self, $server_sasl_credentials
  my ($self, $challenge) = @_;
  $self->{server_params} = \my %sparams;

  # Parse response parameters
  $self->_parse_challenge(\$challenge, server => $self->{server_params})
    or return $self->set_error("Bad challenge: '$challenge'");

  if ($self->{state} == 1) {
    # check server's `rspauth' response
    return $self->set_error("Server did not send rspauth in step 2")
      unless ($sparams{rspauth});
    return $self->set_error("Invalid rspauth in step 2")
      unless ($self->{rspauth} eq $sparams{rspauth});

    # all is well
    $self->set_success;
    return '';
  }

  # check required fields in server challenge
  if (my @missing = grep { !exists $sparams{$_} } @server_required) {
    return $self->set_error("Server did not provide required field(s): @missing")
  }

  my %response = (
    nonce        => $sparams{'nonce'},
    cnonce       => md5_hex($CNONCE || join (":", $$, time, rand)),
    'digest-uri' => $self->service . '/' . $self->host,
    # calc how often the server nonce has been seen; server expects "00000001"
    nc           => sprintf("%08d",     ++$self->{nonce_counts}{$sparams{'nonce'}}),
    charset      => $sparams{'charset'},
  );

  return $self->set_error("Server qop too weak (qop = $sparams{'qop'})")
    unless ($self->_client_layer(\%sparams,\%response));

  # let caller-provided fields override defaults: authorization ID, service name, realm

  my $s_realm = $sparams{realm} || [];
  my $realm = $self->_call('realm', @$s_realm);
  unless (defined $realm) {
    # If the user does not pick a realm, use the first from the server
    $realm = $s_realm->[0];
  }
  if (defined $realm) {
    $response{realm} = $realm;
  }

  my $authzid = $self->_call('authname');
  if (defined $authzid) {
    $response{authzid} = $authzid;
  }

  my $serv_name = $self->_call('serv');
  if (defined $serv_name) {
    $response{'digest-uri'} .= '/' . $serv_name;
  }

  my $user = $self->_call('user');
  return $self->set_error("Username is required")
    unless defined $user;
  $response{username} = $user;

  my $password = $self->_call('pass');
  return $self->set_error("Password is required")
    unless defined $password;

  $self->property('maxout', $sparams{maxbuf} || 65536);

  # Generate the response value
  $self->{state} = 1;

  my ($response, $rspauth)
    = $self->_compute_digests_and_set_keys($password, \%response);

  $response{response} = $response;
  $self->{rspauth}    = $rspauth;

  # finally, return our response token
  return _response(\%response, "is_client");
}

sub _compute_digests_and_set_keys {
  my $self     = shift;
  my $password = shift;
  my $params   = shift;

  if (defined $params->{realm} and ref $params->{realm} eq 'ARRAY') {
    $params->{realm} = $params->{realm}[0];
  }

  my $realm = $params->{realm};
  $realm = "" unless defined $realm;

  my $A1 = join (":",
    md5(join (":", $params->{username}, $realm, $password)),
    @$params{defined($params->{authzid})
      ? qw(nonce cnonce authzid)
      : qw(nonce cnonce)
    }
  );

  # pre-compute MD5(A1) and HEX(MD5(A1)); these are used multiple times below
  my $hdA1 = unpack("H*", (my $dA1 = md5($A1)) );

  # derive keys for layer encryption / integrity
  $self->{kic} = md5($dA1,
    'Digest session key to client-to-server signing key magic constant');

  $self->{kis} = md5($dA1,
    'Digest session key to server-to-client signing key magic constant');

  if (my $cipher = $self->{cipher}) {
    &{ $cipher->{fixup} || sub{} };

    # compute keys for encryption
    my $ks = $cipher->{ks};
    $self->{kcc} = md5(substr($dA1,0,$ks),
      'Digest H(A1) to client-to-server sealing key magic constant');
    $self->{kcs} = md5(substr($dA1,0,$ks),
      'Digest H(A1) to server-to-client sealing key magic constant');

    # get an encryption and decryption handle for the chosen cipher
    $self->{khc} = $cipher->{pkg}->new($cipher->{key}->($self->{kcc}));
    $self->{khs} = $cipher->{pkg}->new($cipher->{key}->($self->{kcs}));

    # initialize IVs
    $self->{ivc} = $cipher->{iv}->($self->{kcc});
    $self->{ivs} = $cipher->{iv}->($self->{kcs});
  }

  my $A2 = "AUTHENTICATE:" . $params->{'digest-uri'};
  $A2 .= ":00000000000000000000000000000000" if ($params->{qop} ne 'auth');

  my $response = md5_hex(
    join (":", $hdA1, @$params{qw(nonce nc cnonce qop)}, md5_hex($A2))
  );

  # calculate server `rspauth' response, so we can check in step 2
  # the only difference here is in the A2 string which from which
  # `AUTHENTICATE' is omitted in the calculation of `rspauth'
  $A2 = ":" . $params->{'digest-uri'};
  $A2 .= ":00000000000000000000000000000000" if ($params->{qop} ne 'auth');

  my $rspauth = md5_hex(
    join (":", $hdA1, @$params{qw(nonce nc cnonce qop)}, md5_hex($A2))
  );

  return ($response, $rspauth);
}

sub server_step {
  my $self      = shift;
  my $challenge = shift;
  my $cb        = shift || sub {};

  $self->{client_params} = \my %cparams;
  unless ( $self->_parse_challenge(\$challenge, client => $self->{client_params}) ) {
   $self->set_error("Bad challenge: '$challenge'");
   return $cb->();
  }

  # check required fields in server challenge
  if (my @missing = grep { !exists $cparams{$_} } @client_required) {
    $self->set_error("Client did not provide required field(s): @missing");
    return $cb->();
  }

  my $count = hex ($cparams{'nc'} || 0);
  unless ($count == ++$self->{nonce_counts}{$cparams{nonce}}) {
    $self->set_error("nonce-count doesn't match: $count");
    return $cb->();
  }

  my $qop = $cparams{'qop'} || "auth";
  unless ($self->is_qop_supported($qop)) {
    $self->set_error("Client qop not supported (qop = '$qop')");
    return $cb->();
  }

  my $username = $cparams{'username'};
  unless ($username) {
    $self->set_error("Client didn't provide a username");
    return $cb->();
  }

  # "The authzid MUST NOT be an empty string."
  if (exists $cparams{authzid} && $cparams{authzid} eq '') {
      $self->set_error("authzid cannot be empty");
      return $cb->();
  }
  my $authzid = $cparams{authzid};

  # digest-uri: "Servers SHOULD check that the supplied value is correct.
  # This will detect accidental connection to the incorrect server, as well as
  # some redirection attacks"
  my $digest_uri = $cparams{'digest-uri'};
  my ($cservice, $chost, $cservname) = split '/', $digest_uri, 3;
  if ($cservice ne $self->service or $chost ne $self->host) {
      # XXX deal with serv_name
      $self->set_error("Incorrect digest-uri");
      return $cb->(); 
  }

  unless (defined $self->callback('getsecret')) {
    $self->set_error("a getsecret callback MUST be defined");
    $cb->();
    return;
  }

  my $realm = $self->{client_params}->{'realm'};
  my $response_check = sub {
    my $password = shift;
    return $self->set_error("Cannot get the passord for $username") 
        unless defined $password;
 
    ## configure the security layer
    $self->_server_layer($qop)
        or return $self->set_error("Cannot negociate the security layer");
 
    my ($expected, $rspauth)
        = $self->_compute_digests_and_set_keys($password, $self->{client_params});
 
    return $self->set_error("Incorrect response $self->{client_params}->{response} <> $expected")
        unless $expected eq $self->{client_params}->{response};
 
    my %response = (
        rspauth => $rspauth,
    );
 
    # I'm not entirely sure of what I am doing
    $self->{answer}{$_} = $self->{client_params}->{$_} for qw/username authzid realm serv/;
 
    $self->set_success;
    return _response(\%response);
  };

  $self->callback('getsecret')->(
    $self,
    { user => $username, realm => $realm, authzid => $authzid },
    sub { $cb->( $response_check->( shift ) ) },
  );
}

sub is_qop_supported {
    my $self = shift;
    my $qop  = shift;
    return $self->{supported_qop}{$qop};
}

sub _response {
  my $response  = shift;
  my $is_client = shift;

  my @out;
  for my $k (sort keys %$response) {
    my $is_array = ref $response->{$k} && ref $response->{$k} eq 'ARRAY';
    my @values = $is_array ? @{$response->{$k}} : ($response->{$k});
    # Per spec, one way of doing it: multiple k=v
    #push @out, [$k, $_] for @values;
    # other way: comma separated list
    push @out, [$k, join (',', @values)];
  }
  return join (",", map { _qdval($_->[0], $_->[1], $is_client) } @out);
}

sub _parse_challenge {
  my $self          = shift;
  my $challenge_ref = shift;
  my $type          = shift;
  my $params        = shift;

  while($$challenge_ref =~
           s/^(?:\s*,)*\s*            # remaining or crap
                          ([\w-]+)                 # key, eg: qop
                          =
                          ("([^\\"]+|\\.)*"|[^,]+) # value, eg: auth-conf or "NoNcE"
                          \s*(?:,\s*)*             # remaining
                      //x) {

    my ($k, $v) = ($1,$2);
    if ($v =~ /^"(.*)"$/s) {
      ($v = $1) =~ s/\\(.)/$1/g;
    }
    if (exists $multi{$type}{$k}) {
      my $aref = $params->{$k} ||= [];
      push @$aref, $v;
    }
    elsif (defined $params->{$k}) {
      return $self->set_error("Bad challenge: '$$challenge_ref'");
    }
    else {
      $params->{$k} = $v;
    }
  }
  return length $$challenge_ref ? 0 : 1;
}

sub _qdval {
  my ($k, $v, $is_client) = @_;

  my $qdval = $is_client ? \%cqdval : \%sqdval;

  if (!defined $v) {
    return;
  }
  elsif (exists $qdval->{$k}) {
    $v =~ s/([\\"])/\\$1/g;
    return qq{$k="$v"};
  }

  return "$k=$v";
}

sub _server_layer {
  my ($self, $auth) = @_;

  # XXX dupe
  # construct our qop mask
  my $maxssf = $self->property('maxssf') - $self->property('externalssf');
  $maxssf = 0 if ($maxssf < 0);
  my $minssf = $self->property('minssf') - $self->property('externalssf');
  $minssf = 0 if ($minssf < 0);

  return undef if ($maxssf < $minssf); # sanity check

  my $ciphers = [ map { $_->{name} } @ourciphers ];
  if ((     $auth eq 'auth-conf')
        and $self->_select_cipher($minssf, $maxssf, $ciphers )) {
    $self->property('ssf', $self->{cipher}->{ssf});
    return 1;
  }
  if ($auth eq 'auth-int') {
    $self->property('ssf', 1);
    return 1;
  }
  if ($auth eq 'auth') {
    $self->property('ssf', 0);
    return 1;
  }

  return undef;
}

sub _client_layer {
  my ($self, $sparams, $response) = @_;

  # construct server qop mask
  # qop in server challenge is optional: if not there "auth" is assumed
  my $smask = 0;
  map {
    m/^auth$/      and $smask |= 1;
    m/^auth-int$/  and $smask |= 2;
    m/^auth-conf$/ and $smask |= 4;
  } split(/,/, $sparams->{qop}||'auth'); # XXX I think we might have a bug here bc. of LWS

  # construct our qop mask
  my $cmask = 0;
  my $maxssf = $self->property('maxssf') - $self->property('externalssf');
  $maxssf = 0 if ($maxssf < 0);
  my $minssf = $self->property('minssf') - $self->property('externalssf');
  $minssf = 0 if ($minssf < 0);

  return undef if ($maxssf < $minssf); # sanity check

  # ssf values > 1 mean integrity and confidentiality 
  # ssf == 1 means integrity but no confidentiality
  # ssf < 1 means neither integrity nor confidentiality
  # no security layer can be had if buffer size is 0
  $cmask |= 1 if ($minssf < 1);
  $cmask |= 2 if ($minssf <= 1 and $maxssf >= 1);
  $cmask |= 4 if ($maxssf > 1);

  # find common bits
  $cmask &= $smask;

  # parse server cipher options
  my @sciphers = split(/,/, $sparams->{'cipher-opts'}||$sparams->{cipher}||'');

  if (($cmask & 4) and $self->_select_cipher($minssf,$maxssf,\@sciphers)) {
    $response->{qop} = 'auth-conf';
    $response->{cipher} = $self->{cipher}->{name};
    $self->property('ssf', $self->{cipher}->{ssf});
    return 1;
  }
  if ($cmask & 2) {
    $response->{qop} = 'auth-int';
    $self->property('ssf', 1);
    return 1;
  }
  if ($cmask & 1) {
    $response->{qop} = 'auth';
    $self->property('ssf', 0);
    return 1;
  }

  return undef;
}

sub _select_cipher {
  my ($self, $minssf, $maxssf, $ciphers) = @_;

  # compose a subset of candidate ciphers based on ssf and peer list
  my @a = map {
    my $c = $_;
    (grep { $c->{name} eq $_ } @$ciphers and
      $c->{ssf} >= $minssf and $c->{ssf} <= $maxssf) ? $_ : ()
  } @ourciphers;

  # from these, select the first one we can create an instance of
  for (@a) {
    next unless eval "require $_->{pkg}";
    $self->{cipher} = $_;
    return 1;
  }

  return 0;
}

use Digest::HMAC_MD5 qw(hmac_md5);

sub encode {  # input: self, plaintext buffer,length (length not used here)
  my $self   = shift;
  my $seqnum = pack('N', $self->{sndseqnum}++);
  my $mac    = substr(hmac_md5($seqnum . $_[0], $self->{kic}), 0, 10);

  # if integrity only, return concatenation of buffer, MAC, TYPE and SEQNUM
  return $_[0] . $mac.pack('n',1) . $seqnum unless ($self->{khc});

  # must encrypt, block ciphers need padding bytes
  my $pad = '';
  my $bs = $self->{cipher}->{bs};
  if ($bs > 1) {
    # padding is added in between BUF and MAC
    my $n = $bs - ((length($_[0]) + 10) & ($bs - 1));
    $pad = chr($n) x $n;
  }

  # XXX - for future AES cipher support, the currently used common _crypt()
  # function probably wont do; we might to switch to per-cipher routines
  # like so:
  #  return $self->{khc}->encrypt($_[0] . $pad . $mac) . pack('n', 1) . $seqnum;
  return $self->_crypt(0, $_[0] . $pad . $mac) . pack('n', 1) . $seqnum;
}

sub decode {  # input: self, cipher buffer,length
  my ($self, $buf, $len) = @_;

  return if ($len <= 16);

  # extract TYPE/SEQNUM from end of buffer
  my ($type,$seqnum) = unpack('na[4]', substr($buf, -6, 6, ''));

  # decrypt remaining buffer, if necessary
  if ($self->{khs}) {
    # XXX - see remark above in encode() #$buf = $self->{khs}->decrypt($buf);
    $buf = $self->_crypt(1, $buf);
  }
  return unless ($buf);

  # extract 10-byte MAC from the end of (decrypted) buffer
  my ($mac) = unpack('a[10]', substr($buf, -10, 10, ''));

  if ($self->{khs} and $self->{cipher}->{bs} > 1) {
    # remove padding
    my $n = ord(substr($buf, -1, 1));
    substr($buf, -$n, $n, '');
  }

  # check the MAC
  my $check = substr(hmac_md5($seqnum . $buf, $self->{kis}), 0, 10);
  return if ($mac ne $check);
  return if (unpack('N', $seqnum) != $self->{rcvseqnum});
  $self->{rcvseqnum}++;

  return $buf;
}

sub _crypt {  # input: op(decrypting=1/encrypting=0)), buffer
  my ($self,$d) = (shift,shift);
  my $bs = $self->{cipher}->{bs};

  if ($bs <= 1) {
    # stream cipher
    return $d ? $self->{khs}->decrypt($_[0]) : $self->{khc}->encrypt($_[0])
  }

  # the remainder of this sub is for block ciphers

  # get current IV
  my $piv = \$self->{$d ? 'ivs' : 'ivc'};
  my $iv = $$piv;

  my $result = join '', map {
    my $x = $d
      ? $iv ^ $self->{khs}->decrypt($_)
      : $self->{khc}->encrypt($iv ^ $_);
    $iv = $d ? $_ : $x;
    $x;
  } unpack("a$bs "x(int(length($_[0])/$bs)), $_[0]);

  # store current IV
  $$piv = $iv;
  return $result;
}

1;

__END__