#!/usr/bin/perl

# Copyright 2005-2007 Messiah College. All rights reserved.
# Jason Long <jlong@messiah.edu>

# Copyright (c) 2004 Anthony D. Urso. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.

use strict;
use warnings;

use Mail::DKIM::Canonicalization::nowsp;
use Mail::DKIM::Canonicalization::relaxed;
use Mail::DKIM::Canonicalization::simple;

package Mail::DKIM::Algorithm::Base;
use Carp;
use MIME::Base64;

sub new
{
	my $class = shift;
	my %args = @_;
	my $self = bless \%args, $class;
	$self->init;
	return $self;
}

sub init
{
	my $self = shift;

	croak "no signature" unless $self->{Signature};

	$self->{mode} = $self->{Signature}->signature ? "verify" : "sign";

	# allows subclasses to set the header_digest and body_digest
	# properties
	$self->init_digests;

	my ($header_method, $body_method)
		= $self->{Signature}->canonicalization;

	my $header_class = $self->get_canonicalization_class($header_method);
	my $body_class = $self->get_canonicalization_class($body_method);
	$self->{canon} = $header_class->new(
			output_digest => $self->{header_digest},
			Signature => $self->{Signature},
			Debug_Canonicalization => $self->{Debug_Canonicalization});
	$self->{body_canon} = $body_class->new(
			output_digest => $self->{body_digest},
			Signature => $self->{Signature},
			Debug_Canonicalization => $self->{Debug_Canonicalization});
}

# override this method, please...
# this method should set the "header_digest" and "body_digest" properties
sub init_digests
{
	die "not implemented";
}

# private method - DKIM-specific
sub get_canonicalization_class
{
	my $self = shift;
	croak "wrong number of arguments" unless (@_ == 1);
	my ($method) = @_;

	my $class = $method eq "nowsp" ? "Mail::DKIM::Canonicalization::nowsp" :
			$method eq "relaxed" ? "Mail::DKIM::Canonicalization::relaxed" :
			$method eq "simple" ? "Mail::DKIM::Canonicalization::simple" :
		die "unknown method $method\n";
	return $class;
}

=head1 NAME

Mail::DKIM::Algorithm::Base - base class for DKIM "algorithms"

=head1 SYNOPSIS

  my $algorithm = new Mail::DKIM::Algorithm::rsa_sha1(
                      Signature => $dkim_signature
                  );

  # add headers
  $algorithm->add_header("Subject: this is the subject\015\012");
  $algorithm->finish_header;

  # add body
  $algorithm->add_body("This is the body.\015\012");
  $algorithm->add_body("Another line of the body.\015\012");
  $algorithm->finish_body;

  # now sign or verify...
  # TODO...

=head1 CONSTRUCTOR

You should not create an object of this class directly. Instead, use one
of the DKIM algorithm implementation classes, such as rsa_sha1:

  my $algorithm = new Mail::DKIM::Algorithm::rsa_sha1(
                      Signature => $dkim_signature
                  );

=head1 METHODS

=head2 add_body() - feeds part of the body into the algorithm/canonicalization

  $algorithm->add_body("This is the body.\015\012");
  $algorithm->add_body("Another line of the body.\015\012");

The body should be fed one "line" at a time.

=cut

sub add_body
{
	my $self = shift;
	my $canon = $self->{body_canon} || $self->{canon};
	$canon->add_body(@_);
}

=head2 add_header() - feeds a header field into the algorithm/canonicalization

  $algorithm->add_header("Subject: this is the subject\015\012");

The header must start with the header field name and continue through any
folded lines (including the embedded <CRLF> sequences). It terminates with
the <CRLF> at the end of the header field.

=cut

sub add_header
{
	my $self = shift;
	$self->{canon}->add_header(@_);
}

=head2 finish_body() - signals the end of the message body

  $algorithm->finish_body

Call this method when all lines from the body have been submitted.
After calling this method, use sign() or verify() to get the results
from the algorithm.

=cut

sub finish_body
{
	my $self = shift;
	my $body_canon = $self->{body_canon} || $self->{canon};
	$body_canon->finish_body;
	$self->finish_message;
}

=head2 finish_header() - signals the end of the header field block

  $algorithm->finish_header;

Call this method when all the headers have been submitted.

=cut

sub finish_header
{
	my $self = shift;
	$self->{canon}->finish_header;
}

# checks the bh= tag of the signature to see if it has the same body
# hash as computed by canonicalizing/digesting the actual message body.
# If it doesn't match, a false value is returned, and the
# verification_details property is set to "body has been altered"
sub check_body_hash
{
	my $self = shift;

	# The body_hash value is set in finish_message(), if we're operating
	# from a version of the DKIM spec that uses the bh= tag. Otherwise,
	# the signature shouldn't have a bh= tag to check.

	if ($self->{body_hash})
	{
		my $body_hash = $self->{body_hash};
		my $expected = decode_base64($self->{Signature}->body_hash);
		if ($body_hash ne $expected)
		{
			$self->{verification_details} = "body has been altered";
	#		print STDERR "I calculated  "
	#			. encode_base64($body_hash, "") . "\n";
	#		print STDERR "signature has "
	#			. encode_base64($expected, "") . "\n";
			return;
		}
	}
	return 1;
}

sub finish_message
{
	my $self = shift;

	# DKIM requires the signature itself to be committed into the digest.
	# But first, we need to set the bh= tag on the signature, then
	# "prettify" it.

	$self->{body_hash} = $self->{body_digest}->digest;
	if ($self->{mode} eq "sign")
	{
		$self->{Signature}->body_hash(
				encode_base64($self->{body_hash}, ""));
	}

	if ($self->{mode} eq "sign")
	{
		$self->{Signature}->prettify;
	}

	my $sig_line = $self->{Signature}->as_string_without_data;
	my $canonicalized = $self->{canon}->canonicalize_header($sig_line);

	$self->{canon}->output($canonicalized);
}

=head2 sign() - generates a signature using a private key

  $base64 = $algorithm->sign($private_key);

=cut

# override this method, please...
sub sign
{
	die "Not implemented";
}

=head2 signature() - get/set the signature worked on by this algorithm

  my $old_signature = $algorithm->signature;
  $algorithm->signature($new_signature);

=cut

sub signature
{
	my $self = shift;
	@_ and
		$self->{Signature} = shift;
	return $self->{Signature};
}

=head2 verify() - verifies a signature

  $result = $algorithm->verify();

Must be called after finish_body().

The result is a true/false value: true indicates the signature data
is valid, false indicates it is invalid.

For an invalid signature, details may be obtained from
$algorithm->{verification_details} or $@.

=cut

# override this method, please...
sub verify
{
	die "Not implemented";
}

1;

__END__

=head1 SEE ALSO

L<Mail::DKIM>

=head1 AUTHOR

Jason Long, E<lt>jlong@messiah.eduE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2005-2007 by Messiah College

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.6 or,
at your option, any later version of Perl 5 you may have available.

=cut
