#!/usr/bin/perl
#===============================================================================
#
# Copyright (C) GemTalk Systems 1986-2024.  All Rights Reserved
#
# Name - secure_backup_extract.pl
# Written By: Norm Green, GemTalk Systems LLC
# Date: February 18, 2022
# Version: 1.0.1
#
# Utility to extract the signature or signing certificate from a GemStone secure
# backup to a file without GemStone installed. Only works on little endian systems
# and backups.
#
# Requirements:
#  bash
#  perl
#  openssl (certificate extraction only)
#
#===============================================================================

use Fcntl;
use strict;
use warnings;
use File::stat;
use File::Basename;
use File::Temp qw/ tempfile /;

sub usage {
  my $bn = basename($0);
  print "Usage: $bn <cert|sig> <backupFile> <outputFile>\n";
  print "where:\n";
  print "   <backupFile> - a valid GemStone/64 secure backup file.\n";
  print "   <outputFile> - the destination to write the extracted information.\n"; 
  print "      This file  will be created and must not exist.\n";
  print "   cert - extract public signing cert.\n"; 
  print "   sig - extract the digital signature.\n";
  exit 1;
}

sub writeBytesToFileHandle {
  my ($fh, $buffer, $numBytes) = @_;
  my $rc = syswrite($fh, $buffer, $numBytes);
  die "Error writing $numBytes to file" unless $rc == $numBytes;
}

sub createAndWriteToFileName {
  my ($fn, $buffer, $numBytes) = @_;
  sysopen(my $fh, $fn, O_CREAT|O_WRONLY) or die "Cannot open file $fn";
  writeBytesToFileHandle($fh, $buffer, $numBytes);
  close $fh;
}

sub readRawBytesFromFile {
  my ($fh, $offset, $numBytes) = @_;
  my $rv = sysseek($fh, $offset, 0);
  die "Error seeking in file" unless defined($rv);
  my $rBytes = sysread $fh, (my $buffer), $numBytes;
  die "Error reading $numBytes" unless $rBytes == $numBytes;
  return $buffer;
}

sub readInt8FromFile {
  my ($fh, $offset) = @_;
  my $buffer = readRawBytesFromFile($fh, $offset, 1);
  my $result = unpack 'C', $buffer;
  return $result;
}

sub readInt16FromFile {
  my ($fh, $offset) = @_;
  my $buffer = readRawBytesFromFile($fh, $offset, 2);
  my $result = unpack 'v', $buffer;
  return $result;
}

sub readInt32FromFile {
  my ($fh, $offset) = @_;
  my $buffer = readRawBytesFromFile($fh, $offset, 4);
  my $result = unpack 'V', $buffer;
  return $result;
}

if (!defined $ARGV[0] || !defined $ARGV[1] || !defined $ARGV[2]) {
  &usage;
}

my $mode = $ARGV[0];
my $backupFn = $ARGV[1];
my $outFn = $ARGV[2];

die "[Error]: Input backup file $backupFn does not exist"
  unless -e $backupFn;
die "[Error]: Output file $outFn already exists"
  unless ! -e $outFn;

sysopen(my $backupFh, $backupFn, O_RDONLY) or
  die "[Error]: Cannot open file $backupFn";

die "Invalid mode arugment" unless $mode eq "cert" || $mode eq "sig";

# Get the backup file size
my $sb = stat($backupFn);
my $backupSize = $sb->size;

# do checks for valid secure backup file.
die "[Error]: $backupFn is not a valid secure backup file"
  unless $backupSize > 131072;

sub checkRecordAtOffset {
  my ($fh, $offset, $expectedLogRecKind) =  @_;
  
  # check physical record kind
  my $physRecKind = readInt8FromFile($backupFh, $offset);
  die "[Error]: Invalid physical record kind: expected 19, found $physRecKind"
    unless $physRecKind == 19;

  # check physical record size in kb
  my $physRecSize = readInt16FromFile($backupFh, $offset + 2);
  die "[Error]: Invalid physical record size: expected 128, found $physRecSize"
    unless $physRecSize == 128;

  # check logical record kind
  my $logRecKindOffset = $offset + 34;
  my $logRecKind = readInt16FromFile($backupFh, $logRecKindOffset);
  die "[Error]: Invalid logical record kind: expected $expectedLogRecKind, found $logRecKind"
    unless $logRecKind == $expectedLogRecKind;
}

# check backup root record
checkRecordAtOffset($backupFh, 0, 201);
# check signature record
checkRecordAtOffset($backupFh, $backupSize - 131072, 215);
# check pre-signature record
checkRecordAtOffset($backupFh, $backupSize - 262144, 214);

# Check backup level in the root record
my $validBackupLevel = 850;
my $bkupLevel = readInt32FromFile($backupFh, 48);
die "[Error]: Invalid backup level found. Expected at least $validBackupLevel, found $bkupLevel"
  unless $bkupLevel >= $validBackupLevel;

if ($mode eq "sig") {
  # compute offsets
  my $sigSizeOffset = $backupSize - 131024;
  my $sigOffset = $backupSize - 131016;

  # Get size of signature
  my $sigSize = readInt16FromFile($backupFh, $sigSizeOffset);
  die "Invalid signature size $sigSize" unless $sigSize > 0;

  # read in the signature bytes
  my $buffer = readRawBytesFromFile($backupFh, $sigOffset, $sigSize);

  # write out to the destination
  createAndWriteToFileName($outFn, $buffer, $sigSize);

  close $backupFh;
  print "Success! $sigSize bytes written to signature file '$outFn'\n";
} elsif ($mode eq "cert") {
  system("bash -c \"type openssl\" >/dev/null 2>&1") &&
    die "Cannot find openssl. openssl is required to extract signing certificates";
  my $certSizeOffset = $backupSize - 262066;
  my $certOffset = $backupSize - 262056;
  my ($tmpFh, $tmpFn) = tempfile();
  my $certSize = readInt16FromFile($backupFh, $certSizeOffset);
  # read the cert from the file. It is in DER format.
  my $buffer = readRawBytesFromFile($backupFh, $certOffset, $certSize);
  # write DER to temp file.
  writeBytesToFileHandle($tmpFh, $buffer, $certSize);
  # flush but do not close temp file. openssl needs to read it.
  $tmpFh->flush;
  # use openssl to convert from DER to PEM form
  my $cmd = "openssl x509 -inform DER -outform PEM -in $tmpFn -out $outFn >/dev/null 2>&1";
  if (system($cmd)) {
    # retry conversion as public key instead of x509
    print "[Warning]: Conversion from DER to PEM as x509 failed. Retrying conversion as RSA public key\n";
    my $cmd2 = "openssl rsa -pubin -inform DER -outform PEM -in $tmpFn -out $outFn >/dev/null 2>&1";
    system($cmd2) && die "RSA public key conversion failed. openssl command: $cmd2";
  }
  # try to clean up
  close($tmpFh);
  unlink($tmpFn);
  print "Success! $certSize bytes written to certificate file '$outFn'\n";
} else {
  die "Invalid mode $mode";
}

exit 0;
