From 8d8098029217008b4791d544a81b7c7f52f5cd3d Mon Sep 17 00:00:00 2001 From: Kern Sibbald Date: Thu, 17 Aug 2006 20:32:19 +0000 Subject: [PATCH] Add data encryption chapter --- docs/developers/version.tex | 2 +- docs/manual-de/version.tex | 2 +- docs/manual/autochangers.tex | 21 +++ docs/manual/dataencryption.tex | 119 ++++++++++++++ docs/manual/install.tex | 16 +- docs/manual/migration.tex | 283 ++++++++++++++++++++++++++++----- docs/manual/progs.tex | 9 +- docs/manual/state.tex | 6 +- docs/manual/thanks.tex | 18 +-- docs/manual/tls.tex | 9 +- docs/manual/version.tex | 2 +- 11 files changed, 412 insertions(+), 75 deletions(-) create mode 100644 docs/manual/dataencryption.tex diff --git a/docs/developers/version.tex b/docs/developers/version.tex index facc6b24..b757df6d 100644 --- a/docs/developers/version.tex +++ b/docs/developers/version.tex @@ -1 +1 @@ -1.39.18 (30 July 2006) +1.39.20 (16 August 2006) diff --git a/docs/manual-de/version.tex b/docs/manual-de/version.tex index facc6b24..b757df6d 100644 --- a/docs/manual-de/version.tex +++ b/docs/manual-de/version.tex @@ -1 +1 @@ -1.39.18 (30 July 2006) +1.39.20 (16 August 2006) diff --git a/docs/manual/autochangers.tex b/docs/manual/autochangers.tex index fd239f17..23cfae49 100644 --- a/docs/manual/autochangers.tex +++ b/docs/manual/autochangers.tex @@ -460,6 +460,27 @@ Pool { Any slot containing a barcode of CLNxxxx will be treated as a cleaning tape and will not be mounted. +\subsection*{Changing Cartridges} +\index[general]{Changing Cartridges } +\addcontentsline{toc}{subsection}{Changing Cartridges} +If you wish to insert or remove cartridges in your autochanger or +you manually run the {\bf mtx} program, you must first tell Bacula +to release the autochanger by doing: + +\footnotesize +\begin{verbatim} +unmount +(change cartridges and/or run mtx) +mount +\end{verbatim} +\normalsize + +If you do not do the unmount before making such a change, Bacula +will become completely confused about what is in the autochanger +and may stop function because it expects to have exclusive use +of the autochanger while it has the drive mounted. + + \label{Magazines} \subsection*{Dealing with Multiple Magazines} \index[general]{Dealing with Multiple Magazines } diff --git a/docs/manual/dataencryption.tex b/docs/manual/dataencryption.tex new file mode 100644 index 00000000..75ad43b2 --- /dev/null +++ b/docs/manual/dataencryption.tex @@ -0,0 +1,119 @@ + +\section*{Bacula -- Data Encryption} +\label{DataEncryption} +\index[general]{Data Encryption} +\index[general]{Encryption!Data} +\index[general]{Data Encryption} +\addcontentsline{toc}{section}{Data Encryption} + +Bacula permits file data encryption and signing within the File Daemon (or +Client) prior to sending data to the Storage Daemon. Upon restoration, +file signatures are validated and any mismatches are reported. At no time +does the Director or the Storage Daemon have access to unencrypted file +contents. + + +It is very important to specify what this implementation does NOT +do: +\begin{itemize} +\item There is still one major gotcha, namely, it's possible for the + director to restore new keys or a Bacula configuration file to the + client, and thus force later backups to be made with a compromised + key and/or with no encryption at all. You can avoid this by not backing + up your encryption keys using Bacula, and not changing the location + of the keys in your Bacula File daemon configuration file. However, + please be sure your File daemon keys securely backed up preferably + off-site. + +\item The implementation does not encrypt file metadata such as file path + names, permissions, and ownership. Extended attributes are also currently + not encrypted. However, Mac OS X resource forks are encrypted. +\end{itemize} + +Encryption and signing are implemented using RSA private keys coupled with +self-signed x509 public certificates. This is also sometimes known as PKI +or Public Key Infrastructure. + +Each File Daemon should be given its own unique private/public key pair. +In addition to this key pair, any number of "Master Keys" may be specified +-- these are key pairs that may be used to decrypt any backups should the +File Daemon key be lost. Only the Master Key's public certificate should +be made available to the File Daemon. Under no circumstances should the +Master Private Key be shared or stored on the Client machine. + +The Master Keys should be backed up to a secure location, such as a CD +placed in a in a fire-proof safe or bank safety deposit box. The Master +Keys should never be kept on the same machine as the Storage Daemon or +Director if you are worried about an unauthorized party compromising either +machine and accessing your encrypted backups. + +While less critical than the Master Keys, File Daemon Keys are also a prime +candidate for off-site backups; burn the key pair to a CD and send the CD +home with the owner of the machine. + +NOTE!!! If you lose your encryption keys, backups will be unrecoverable. +{\bf ALWAYS} store a copy of your master keys in a secure, off-site location. + +\subsection*{Building Bacula with Encryption Support} +\index[general]{Building Bacula with Encryption Support} +\addcontentsline{toc}{subsection}{Building Bacula with Encryption Support} + +The configuration option for enabling OpenSSL encryption support has not changed +since Bacula 1.38. To build Bacula with encryption support, you will need +the OpenSSL libraries and headers installed. When configuring Bacula, use: + +\begin{verbatim} + ./configure --with-openssl ... +\end{verbatim} + + + + +\subsection*{Generating Private/Public Encryption Keypairs} +\index[general]{Generating Private/Public Encryption Keypairs} +\addcontentsline{toc}{subsection}{Generating Private/Public Encryption Keypairs} + +Generate a Master Key Pair with: + +\footnotesize +\begin{verbatim} + openssl genrsa -out master.key 2048 + openssl req -new -key master.key -x509 -out master.cert +\end{verbatim} +\normalsize + +Generate a File Daemon Key Pair for each FD: + +\footnotesize +\begin{verbatim} + openssl genrsa -out fd-example.key 2048 + openssl req -new -key fd-example.key -x509 -out fd-example.cert + cat fd-example.key fd-example.cert >fd-example.pem +\end{verbatim} +\normalsize + + +\subsection*{Example Data Encryption Configuration} +\index[general]{Example!File Daemon Configuration File} +\index[general]{Example!Data Encryption Configuration File} +\index[general]{Example Data Encryption Configuration} +\addcontentsline{toc}{subsection}{Example Data Encryption Configuration} + + +{\bf bacula-fd.conf} +\footnotesize +\begin{verbatim} +FileDaemon { + Name = example-fd + FDport = 9102 # where we listen for the director + WorkingDirectory = /var/bacula/working + Pid Directory = /var/run + Maximum Concurrent Jobs = 20 + + PKI Signatures = Yes # Enable Data Signing + PKI Encryption = Yes # Enable Data Encryption + PKI Keypair = "/etc/bacula/fd-example.pem" # Public and Private Keys + PKI Master Key = "/etc/bacula/master.cert" # ONLY the Public Key +} +\end{verbatim} +\normalsize diff --git a/docs/manual/install.tex b/docs/manual/install.tex index af004557..fe8fe241 100644 --- a/docs/manual/install.tex +++ b/docs/manual/install.tex @@ -771,12 +771,20 @@ customize your installation. {-}{-}with-postgresql, otherwise the ./configure will fail. \item [ {-}{-}with-openssl=\lt{}path\gt{}] - This configuration option is necessary if you want to enable TLS (ssl) - in Bacula. Normally, the {\bf path} specification is not necessary since + This configuration option is necessary if you want to enable TLS (ssl), + which encrypts the communications within + Bacula or if you want to use File Daemon PKI data encryption. + Normally, the {\bf path} specification is not necessary since the configuration searches for the OpenSSL libraries in standard system locations. Enabling OpenSSL in Bacula permits secure communications - between the daemons. For more information on using TLS, please see the - \ilink{Bacula TLS}{_ChapterStart61} chapter of this manual. + between the daemons and/or data encryption in the File daemon. + For more information on using TLS, please see the + \ilink{Bacula TLS -- Communications Encryption}{CommEncryption} chapter + of this manual. + For more information on using PKI data encryption, please see the + \ilink{Bacula PKI -- Data Encryption}{DataEncryption} + chapter of this manual. + \item [ {-}{-}with-python=\lt{}path\gt{}] diff --git a/docs/manual/migration.tex b/docs/manual/migration.tex index 30dee543..91e4ee77 100644 --- a/docs/manual/migration.tex +++ b/docs/manual/migration.tex @@ -4,14 +4,13 @@ \index[general]{Migration} \addcontentsline{toc}{section}{Migration} -The term Migration as used in the context of Bacula means moving data from one -Volume to another, and in particular it is -a Job (similar to a backup job) that reads data that was previously -backed up to a Volume and writes it to another Volume purging the -catalog records associated with the first backup job. In other words, -Migration moves Bacula Job data from one Volume to another. Although we -mention Volumes, in reality, Migration is based on moving individual Jobs from -one Pool to another. +The term Migration as used in the context of Bacula means moving data from +one Volume to another, and in particular it is a Job (similar to a backup +job) that reads data that was previously backed up to a Volume and writes +it to another Volume purging the catalog records associated with the first +backup job. In other words, Migration moves Bacula Job data from one +Volume to another. Although we mention Volumes to simplify the subject, +in reality, Migration is based on moving individual Jobs from one Pool to another. Migrations can be based on quite a number of different criteria such as: a single previous Job; a Volume; a Client; a regular expression matching @@ -19,30 +18,34 @@ a Job, Volume, or Client name; the time a Job is on a Volume; high and low water marks (usage or occupation) of a Pool; Volume size, ... The details of these selection criteria will be defined below. - To run a Migration job, you must have defined a Job resource very similar to a Backup Job but with {\bf Type = Migrate} instead of {\bf Type = -Backup}. The migration job then starts much like a backup job starts and -searches for a previous backup Job or Jobs that matches the parameters you have -specified in the migration Job resource (detailed a bit later). Then -for each previous backup Job found, the Migration Job will run a new -Job which copies the old Job data from the previous Volume to a new -Volume defined in the Migration Pool. It is possible that no prior -Jobs are found for migration, in which case, the Migration job will -simply terminate having done nothing, but normally at a minimum, three jobs -are involved during a migration: +Backup}. One of the key points to remember is that the Pool that is +specified for the migration job is the only pool from which jobs will +be migrated, with one exception noted below. + +The migration job normally is either manually started or starts +from a Schedule much like a backup job. It searches +for a previous backup Job or Jobs that match the parameters you have +specified in the migration Job resource, primiarily a {\bf Selection Type} +(detailed a bit later). Then for +each previous backup JobId found, the Migration Job will run a new Job which +copies the old Job data from the previous Volume to a new Volume in +the Migration Pool. It is possible that no prior Jobs are found for +migration, in which case, the Migration job will simply terminate having +done nothing, but normally at a minimum, three jobs are involved during a +migration: \begin{itemize} -\item The currently running Migration Job -\item The previous Backup Job -\item A new Backup Job that writes the previous data to - the new Volume. +\item The currently running Migration control Job +\item The previous Backup Job (already run) +\item A new Migration Backup Job that moves the data from the + previous Backup job to the new Volume. \end{itemize} -If the Migration is for a particular Volume, multiple new Backup Jobs -may be started, one for each Job on the Volume. - - +If the Migration control job finds a number of JobIds to migrate (e.g. +it is asked to migrate one or more Volumes), it will start one new +migration backup job for each JobId found. \subsection*{Migration Job Resource Directives} \addcontentsline{toc}{section}{Migration Job Resource Directives} @@ -51,33 +54,88 @@ The following directives can appear in a Director's Job resource, and they are used to define a Migration job. \begin{description} +\item [Pool = \lt{}Pool-name\gt{}] The Pool specified in the Migration +control Job is not a new directive for the Job resource, but it is +particularly important because it determines what Pool will be examined for +finding JobIds to migrate. The exception to this is when {\bf Selection +Type = SQLQuery}, in which case no Pool is used, unless you +specifically include it in the SQL query. + \item [Type = Migrate] -This defines the job that is run as being a Migration Job. -Migration Jobs do not have any Files associated with them, and in -that sense they are more or less like an Admin job. They just -check to see if there is anything to Migrate then possibly start -and control new Backup jobs to move the data. +{\bf Migrate} is a new type that defines the job that is run as being a +Migration Job. A Migration Job is a sort of control job and does not have +any Files associated with it, and in that sense they are more or less like +an Admin job. Migration jobs simply check to see if there is anything to Migrate then +possibly start and control new Backup jobs to migrate the data from the +specified Pool to another Pool. \item [Selection Type = \lt{}Selection-type-keyword\gt{}] - Where \lt{}Selection-type-keyword\gt{} can be: + The \lt{}Selection-type-keyword\gt{} determines how the migration job + will go about selecting what JobIds to migrate. In most cases, it is + used in conjunction with a {\bf Selection Pattern} to give you fine + control over exactly what JobIds are selected. The possible values + for \lt{}Selection-type-keyword\gt{} are: \begin{description} - \item [SmallestVolume] This selection keyword selects the smallest volume in - the Pool to be migrated. - \item [OldestVolume] This selection keyword selects the oldest volume in - the Pool to be migrated. - \item [Client] - \item [Volume] - \item [Job] - \item [SQLQuery] + \item [SmallestVolume] This selection keyword selects the volume with the + fewest bytes from the Pool to be migrated. The Pool to be migrated + is the Pool defined in the Migration Job resource. The migration + control job will then start and run one migration backup job for + each of the Jobs found on this Volume. The Selection Pattern, if + specified, is not used. + \item [OldestVolume] This selection keyword selects the volume with the + oldest last write time in the Pool to be migrated. The Pool to be + migrated is the Pool defined in the Migration Job resource. The + migration control job will then start and run one migration backup + job for each of the Jobs found on this Volume. The Selection + Pattern, if specified, is not used. + \item [Client] The Client selection type, first selects all the Clients + that have been backed up in the Pool specified by the Migration + Job resource, then it applies the {\bf Selection Pattern} (defined + below) as a regular expression to the list of Client names, giving + a filtered Client name list. All jobs that were backed up for those + filtered (regexed) Clients will be migrated. + The migration control job will then start and run one migration + backup job for each of the JobIds found for those filtered Clients. + \item [Volume] The Volume selection type, first selects all the Volumes + that have been backed up in the Pool specified by the Migration + Job resource, then it applies the {\bf Selection Pattern} (defined + below) as a regular expression to the list of Volume names, giving + a filtered Volume list. All JobIds that were backed up for those + filtered (regexed) Volumes will be migrated. + The migration control job will then start and run one migration + backup job for each of the JobIds found on those filtered Volumes. + \item [Job] The Job selection type, first selects all the Jobs (as + defined on the {\bf Name} directive in a Job resource) + that have been backed up in the Pool specified by the Migration + Job resource, then it applies the {\bf Selection Pattern} (defined + below) as a regular expression to the list of Job names, giving + a filtered Job name list. All JobIds that were run for those + filtered (regexed) Job names will be migrated. Note, for a given + Job named, they can be many jobs (JobIds) that ran. + The migration control job will then start and run one migration + backup job for each of the Jobs found. + \item [SQLQuery] The SQLQuery selection type, used the {\bf Selection + Pattern} as an SQL query to obtain the JobIds to be migrated. + The Selection Pattern must be a valid SELECT SQL statement for your + SQL engine, and it must return the JobId as the first field + of the SELECT. \item [PoolOccupancy] Not yet implemented. \item [PoolTime] Not yet implemented. \end{description} -\end{description} - \item [Selection Pattern = \lt{}Quoted-string\gt{}] -The Selection Patterns permitted for each Selection-type-keyword -are described above. + The Selection Patterns permitted for each Selection-type-keyword are + described above. + + For the OldestVolume and SmallestVolume, this + Selection pattern is not used (ignored). + + For the Client, Volume, and Job + keywords, this pattern must be a valid regular expression that will filter + the appropriate item names found in the Pool. + + For the SQLQuery keyword, this pattern must be a valid SELECT SQL statement + that returns JobIds. \end{description} @@ -95,11 +153,148 @@ previous Backup Job or Jobs selected have been in the Pool longer than the specified PoolTime, then they will be migrated. \item [Migration High Bytes = \lt{}byte-specification\gt{}] + This directive specifies the number of bytes in the Pool which will + trigger a migration if a {\bf PoolOccupancy} migration selection + type has been specified. Please note, that the fact that the Pool + usage goes above this level does not automatically trigger a migration + job. \item [Migration Low Bytes = \lt{}byte-specification\gt{}] + This directive specifies the number of bytes in the Pool which will + stop a migration if a {\bf PoolOccupancy} migration selection + type has been specified and triggered by more than Migration High + Bytes being in the pool. In other words, once a migration job + is started with {\bf PoolOccupancy} migration selection and it + determines that there are more than Migration High Bytes, the + migration job will continue to run jobs until the number of + bytes in the Pool drop to or below Migration Low Bytes. \item [Next Pool = \lt{}pool-specification\gt{}] + The Next Pool directive specifies the pool to which Jobs will be + migrated. \item [Storage = \lt{}storage-specification\gt{}] + The Storage directive specifies what Storage resource will be used + for all Jobs that use this Pool. It takes precedence over any other + Storage specifications that may have been given such as in the + Schedule Run directive, or in the Job resource. \end{description} + +\subsection*{Example Migration Jobs} +\index[general]{Example Migration Jobs} +\addcontentsline{toc}{subsection}{Example Migration Jobs} + +As an example, suppose you have the following Job that +you run every night: + +\footnotesize +\begin{verbatim} +# Define the backup Job +Job { + Name = "NightlySave" + Type = Backup + Level = Incremental # default + Client=rufus-fd + FileSet="Full Set" + Schedule = "WeeklyCycle" + Messages = Standard + Pool = Default +} + +# Default pool definition +Pool { + Name = Default + Pool Type = Backup + AutoPrune = yes + Recycle = yes + Next Pool = Tape + Storage = File + LabelFormat = "File" +} + +# Tape pool definition +Pool { + Name = Tape + Pool Type = Backup + AutoPrune = yes + Recycle = yes + Storage = DLTDrive +} + +# Definition of File storage device +Storage { + Name = File + Address = rufus + Password = "ccV3lVTsQRsdIUGyab0N4sMDavui2hOBkmpBU0aQKOr9" + Device = "File" # same as Device in Storage daemon + Media Type = File # same as MediaType in Storage daemon +} + +# Definition of DLT tape storage device +Storage { + Name = DLTDrive + Address = rufus + Password = "ccV3lVTsQRsdIUGyab0N4sMDavui2hOBkmpBU0aQKOr9" + Device = "HP DLT 80" # same as Device in Storage daemon + Media Type = DLT8000 # same as MediaType in Storage daemon +} + +\end{verbatim} +\normalsize + +Where we have included only the essential information -- i.e. the +Director, FileSet, Catalog, Client, Schedule, and Messages resources are omitted. + +As you can see, by running the NightlySave Job, the data will be backed up +to File storage using the Default pool to specify the Storage as File. + +Now, if we add the following Job resource to this conf file. + +\footnotesize +\begin{verbatim} +Job { + Name = "migrate-volume" + Type = Migrate + Level = Full + Client = rufus-fd + FileSet = "Full Set" + Messages = Standard + Storage = DLTDrive + Pool = Default + Maximum Concurrent Jobs = 4 + Selection Type = Volume + Selection Pattern = "File" +} +\end{verbatim} +\normalsize + +and then run the job named {\bf migrate-volume}, all volumes in the Pool +named Default (as specified in the migrate-volume Job that match the +regular expression pattern {\bf File} will be migrated to tape storage +DLTDrive because the {\bf Next Pool} in the Default Pool specifies that +Migrations should go to the pool named {\bf Tape}, which uses +Storage {\bf DLTDrive}. + +If instead, we use a Job resource as follows: + +\footnotesize +\begin{verbatim} +Job { + Name = "migrate" + Type = Migrate + Level = Full + Client = rufus-fd + FileSet="Full Set" + Messages = Standard + Storage = DLTDrive + Pool = Default + Maximum Concurrent Jobs = 4 + Selection Type = Job + Selection Pattern = ".*Save" +} +\end{verbatim} +\normalsize + +All jobs ending with the name Save will be migrated from the File Default to +the Tape Pool, or from File storage to Tape storage. diff --git a/docs/manual/progs.tex b/docs/manual/progs.tex index 1b9fdc98..652d7c86 100644 --- a/docs/manual/progs.tex +++ b/docs/manual/progs.tex @@ -776,10 +776,11 @@ this program with two tape drives. \addcontentsline{toc}{subsection}{btape} This program permits a number of elementary tape operations via a tty command -interface. The {\bf test} command, described below, can be very useful for -testing older tape drive compatibility problems. Aside from initial testing of -tape drive compatibility with {\bf Bacula}, {\bf btape} will be mostly used by -developers writing new tape drivers. +interface. It works only with tapes and not with other kinds of Bacula +storage media (DVD, File, ...). The {\bf test} command, described below, +can be very useful for testing older tape drive compatibility problems. +Aside from initial testing of tape drive compatibility with {\bf Bacula}, +{\bf btape} will be mostly used by developers writing new tape drivers. {\bf btape} can be dangerous to use with existing {\bf Bacula} tapes because it will relabel a tape or write on the tape if so requested regardless that diff --git a/docs/manual/state.tex b/docs/manual/state.tex index fbe1ab74..9130464d 100644 --- a/docs/manual/state.tex +++ b/docs/manual/state.tex @@ -34,8 +34,10 @@ In other words, what is and what is not currently implemented and functional. capability (system break-in detection). \item CRAM-MD5 password authentication between each component (daemon). \item Configurable - \ilink{TLS (ssl) communications encryption}{_ChapterStart61} between each component. - \item Data (on Volume) encryption on a Client by Client basis. + \ilink{TLS (SSL) communications encryption}{CommEncryption} between each component. + \item Configurable + \ilink{Data (on Volume) encryption}{DataEncryption} + on a Client by Client basis. \item Computation of MD5 or SHA1 signatures of the file data if requested. \end{itemize} diff --git a/docs/manual/thanks.tex b/docs/manual/thanks.tex index d752b18d..432506ac 100644 --- a/docs/manual/thanks.tex +++ b/docs/manual/thanks.tex @@ -38,39 +38,29 @@ improved start/stop script and addition of Bacula userid and group, stunnel, ...), his continuing support of Bacula users. He also wrote the PostgreSQL driver for Bacula and has been a big help in correcting the SQL. -Thanks to Phil Stracchino for writing the gnome-console {\bf ConsoleFont} -configuration command, all the suggestions he has made, and his continuing -suppport of Bacula users. - Thanks to multiple other Bacula Packagers who make and release packages for different platforms for Bacula. Thanks to Christopher Hull for developing the native Win32 Bacula emulation code and for contributing it to the Bacula project. -Thanks to Nicolas Boichat for writing wx-console and the bacula-tray-monitor. -These are very nice GUI additions to Bacula. - Thanks to Thorsten Engel for his excellent knowledge of Win32 systems, and for making the Win32 File daemon Unicode compatible, as well as making the Win32 File daemon interface to Microsoft's Volume Shadow Copy (VSS). These two are big pluses for Bacula! -Thanks to Nic Bellamy for providing the bacula-dir.conf file that he uses to -implement daily tape rotation using multiple Pools. - -Thanks also to Jo Simoens for finding and correcting so many typos and -other problems with the manual. - Thanks to Arno Lehmann for his excellent and infatigable help and advice to users. Thanks to all the Bacula users, especially those of you who have contributed ideas, bug reports, patches, and new features. +Thanks to Nicolas Boichat for writing wx-console and the bacula-tray-monitor. +These are very nice GUI additions to Bacula. + The original variable expansion code used in the LabelFormat comes from the Open Source Software Project (www.ossp.org). It has been adapted and extended -for use in Bacula. +for use in Bacula. This code is now deprecated. For all those who I have left out, please send me a reminder, and in any case, thanks for your contribution. diff --git a/docs/manual/tls.tex b/docs/manual/tls.tex index ddb11afc..8234c7a3 100644 --- a/docs/manual/tls.tex +++ b/docs/manual/tls.tex @@ -1,6 +1,6 @@ -\section*{Bacula TLS} -\label{_ChapterStart61} +\section*{Bacula TLS -- Communications Encryption} +\label{CommEncryption} \index[general]{TLS -- Communications Encryption} \index[general]{Communications Encryption} \index[general]{Encryption!Communications} @@ -11,8 +11,9 @@ Bacula TLS (Transport Layer Security) is built-in network encryption code to provide secure network transport similar to -that offered by {\bf stunnel} or {\bf ssh}. The Bacula code was -written by Landon Fuller. +that offered by {\bf stunnel} or {\bf ssh}. The data written to +Volumes by the Storage daemon is not encrypted by this code. +The Bacula encryption code was written by Landon Fuller. Supported features of this code include: \begin{itemize} diff --git a/docs/manual/version.tex b/docs/manual/version.tex index facc6b24..b757df6d 100644 --- a/docs/manual/version.tex +++ b/docs/manual/version.tex @@ -1 +1 @@ -1.39.18 (30 July 2006) +1.39.20 (16 August 2006) -- 2.39.5