001/*
002 * Copyright 2007-2021 The jdeb developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.vafer.jdeb;
018
019import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
020import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
021import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
022import org.apache.commons.io.FileUtils;
023import org.apache.commons.io.FilenameUtils;
024import org.apache.commons.io.IOUtils;
025import org.bouncycastle.bcpg.HashAlgorithmTags;
026import org.bouncycastle.crypto.digests.MD5Digest;
027import org.bouncycastle.jce.provider.BouncyCastleProvider;
028import org.bouncycastle.openpgp.PGPSignature;
029import org.bouncycastle.openpgp.PGPSignatureGenerator;
030import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
031import org.bouncycastle.util.encoders.Hex;
032import org.vafer.jdeb.changes.ChangeSet;
033import org.vafer.jdeb.changes.ChangesProvider;
034import org.vafer.jdeb.changes.TextfileChangesProvider;
035import org.vafer.jdeb.debian.BinaryPackageControlFile;
036import org.vafer.jdeb.debian.ChangesFile;
037import org.vafer.jdeb.signing.PGPSigner;
038import org.vafer.jdeb.utils.PGPSignatureOutputStream;
039import org.vafer.jdeb.utils.Utils;
040import org.vafer.jdeb.utils.VariableResolver;
041
042import java.io.ByteArrayOutputStream;
043import java.io.File;
044import java.io.FileInputStream;
045import java.io.FileOutputStream;
046import java.io.IOException;
047import java.io.InputStream;
048import java.math.BigInteger;
049import java.security.MessageDigest;
050import java.security.NoSuchAlgorithmException;
051import java.security.Security;
052import java.text.SimpleDateFormat;
053import java.util.ArrayList;
054import java.util.Collection;
055import java.util.Date;
056import java.util.List;
057import java.util.Locale;
058
059/**
060 * A generic class for creating Debian archives. Even supports signed changes
061 * files.
062 */
063public class DebMaker {
064
065    /** A console to output log message with */
066    private Console console;
067
068    /** The Debian package produced */
069    private File deb;
070
071    /** The directory containing the control files to build the package */
072    private File control;
073
074    /** The name of the package. Default value if not specified in the control file */
075    private String packageName;
076
077    /** The section of the package. Default value if not specified in the control file */
078    private String section = "java";
079
080    /** The dependencies of the package. */
081    private String depends;
082
083    /** The description of the package. Default value if not specified in the control file */
084    private String description;
085
086    /** The homepage of the application. Default value if not specified in the control file */
087    private String homepage;
088
089    /** The file containing the PGP keys */
090    private File keyring;
091
092    /** The key to use in the keyring */
093    private String key;
094
095    /** The passphrase for the key to sign the changes file */
096    private String passphrase;
097
098    /** The file to read the changes from */
099    private File changesIn;
100
101    /** The file where to write the changes to */
102    private File changesOut;
103
104    /** The file where to write the changes of the changes input to */
105    private File changesSave;
106
107    /** The compression method used for the data file (none, gzip, bzip2 or xz) */
108    private String compression = "gzip";
109
110    /** Whether to sign the package that is created */
111    private boolean signPackage;
112
113    /** Whether to sign the changes file that is created */
114    private boolean signChanges;
115
116    /** Defines which utility is used to verify the signed package */
117    private String signMethod;
118
119    /** Defines the role to sign with */
120    private String signRole;
121
122    /** Defines the longFileMode of the tar file that is built */
123    private String tarLongFileMode;
124
125    /** Defines the bigNumberMode of the tar file that is built */
126    private String tarBigNumberMode;
127
128    private VariableResolver variableResolver;
129    private String openReplaceToken;
130    private String closeReplaceToken;
131
132    private final Collection<DataProducer> dataProducers = new ArrayList<DataProducer>();
133
134    private final Collection<DataProducer> conffilesProducers = new ArrayList<DataProducer>();
135    private String digest = "SHA1";
136
137    public DebMaker(Console console, Collection<DataProducer> dataProducers, Collection<DataProducer> conffileProducers) {
138        this.console = console;
139        if (dataProducers != null) {
140            this.dataProducers.addAll(dataProducers);
141        }
142        if (conffileProducers != null) {
143            this.conffilesProducers.addAll(conffileProducers);
144        }
145
146        Security.addProvider(new BouncyCastleProvider());
147    }
148
149    public void setDeb(File deb) {
150        this.deb = deb;
151    }
152
153    public void setControl(File control) {
154        this.control = control;
155    }
156
157    public void setPackage(String packageName) {
158        this.packageName = packageName;
159    }
160
161    public void setSection(String section) {
162        this.section = section;
163    }
164
165    public void setDepends(String depends) {
166        this.depends = depends;
167    }
168
169    public void setDescription(String description) {
170        this.description = description;
171    }
172
173    public void setHomepage(String homepage) {
174        this.homepage = homepage;
175    }
176
177    public void setChangesIn(File changes) {
178        this.changesIn = changes;
179    }
180
181    public void setChangesOut(File changes) {
182        this.changesOut = changes;
183    }
184
185    public void setChangesSave(File changes) {
186        this.changesSave = changes;
187    }
188
189    public void setSignPackage(boolean signPackage) {
190        this.signPackage = signPackage;
191    }
192
193    public void setSignChanges(boolean signChanges) {
194        this.signChanges = signChanges;
195    }
196
197    public void setSignMethod(String signMethod) {
198        this.signMethod = signMethod;
199    }
200
201    public void setSignRole(String signRole) {
202        this.signRole = signRole;
203    }
204
205    public void setKeyring(File keyring) {
206        this.keyring = keyring;
207    }
208
209    public void setKey(String key) {
210        this.key = key;
211    }
212
213    public void setPassphrase(String passphrase) {
214        this.passphrase = passphrase;
215    }
216
217    public void setCompression(String compression) {
218        this.compression = compression;
219    }
220
221    public void setResolver(VariableResolver variableResolver) {
222        this.variableResolver = variableResolver;
223    }
224
225    private boolean isWritableFile(File file) {
226        return !file.exists() || file.isFile() && file.canWrite();
227    }
228
229    public String getDigest() {
230        return digest;
231    }
232
233    public void setDigest(String digest) {
234        this.digest = digest;
235    }
236
237    public void setTarLongFileMode(String tarLongFileMode) {
238        this.tarLongFileMode = tarLongFileMode;
239    }
240
241    public void setTarBigNumberMode(String tarBigNumberMode) {
242        this.tarBigNumberMode = tarBigNumberMode;
243    }
244
245    /**
246     * Validates the input parameters.
247     */
248    public void validate() throws PackagingException {
249        if (control == null || !control.isDirectory()) {
250            throw new PackagingException("The 'control' attribute doesn't point to a directory. " + control);
251        }
252
253        if (changesIn != null) {
254
255            if (changesIn.exists() && (!changesIn.isFile() || !changesIn.canRead())) {
256                throw new PackagingException("The 'changesIn' setting needs to point to a readable file. " + changesIn + " was not found/readable.");
257            }
258
259            if (changesOut != null && !isWritableFile(changesOut)) {
260                throw new PackagingException("Cannot write the output for 'changesOut' to " + changesOut);
261            }
262
263            if (changesSave != null && !isWritableFile(changesSave)) {
264                throw new PackagingException("Cannot write the output for 'changesSave' to " + changesSave);
265            }
266
267        } else {
268            if (changesOut != null || changesSave != null) {
269                throw new PackagingException("The 'changesOut' or 'changesSave' settings may only be used when there is a 'changesIn' specified.");
270            }
271        }
272
273        if (Compression.toEnum(compression) == null) {
274            throw new PackagingException("The compression method '" + compression + "' is not supported (expected 'none', 'gzip', 'bzip2' or 'xz')");
275        }
276
277        if (deb == null) {
278            throw new PackagingException("You need to specify where the deb file is supposed to be created.");
279        }
280
281        getDigestCode(digest);
282    }
283
284    static int getDigestCode(String digestName) throws PackagingException {
285        if ("SHA1".equals(digestName)) {
286            return HashAlgorithmTags.SHA1;
287        } else if ("MD2".equals(digestName)) {
288            return HashAlgorithmTags.MD2;
289        } else if ("MD5".equals(digestName)) {
290            return HashAlgorithmTags.MD5;
291        } else if ("RIPEMD160".equals(digestName)) {
292            return HashAlgorithmTags.RIPEMD160;
293        } else if ("SHA256".equals(digestName)) {
294            return HashAlgorithmTags.SHA256;
295        } else if ("SHA384".equals(digestName)) {
296            return HashAlgorithmTags.SHA384;
297        } else if ("SHA512".equals(digestName)) {
298            return HashAlgorithmTags.SHA512;
299        } else if ("SHA224".equals(digestName)) {
300            return HashAlgorithmTags.SHA224;
301        } else {
302            throw new PackagingException("unknown hash algorithm tag in digestName: " + digestName);
303        }
304    }
305
306    public void makeDeb() throws PackagingException {
307        BinaryPackageControlFile packageControlFile;
308        try {
309            console.info("Creating debian package: " + deb);
310
311            // If we should sign the package
312            boolean doSign = signPackage;
313
314            if (doSign) {
315
316                if (keyring == null || !keyring.exists()) {
317                    doSign = false;
318                    console.warn("Signing requested, but no keyring supplied");
319                }
320
321                if (key == null) {
322                    doSign = false;
323                    console.warn("Signing requested, but no key supplied");
324                }
325
326                if (passphrase == null) {
327                    doSign = false;
328                    console.warn("Signing requested, but no passphrase supplied");
329                }
330
331                FileInputStream keyRingInput = new FileInputStream(keyring);
332                PGPSigner signer = null;
333                try {
334                    signer = new PGPSigner(new FileInputStream(keyring), key, passphrase, getDigestCode(digest));
335                } finally {
336                    keyRingInput.close();
337                }
338
339                PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(new BcPGPContentSignerBuilder(signer.getSecretKey().getPublicKey().getAlgorithm(), getDigestCode(digest)));
340                signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, signer.getPrivateKey());
341
342                packageControlFile = createSignedDeb(Compression.toEnum(compression), signatureGenerator, signer);
343            } else {
344                packageControlFile = createDeb(Compression.toEnum(compression));
345            }
346
347        } catch (Exception e) {
348            throw new PackagingException("Failed to create debian package " + deb, e);
349        }
350
351        makeChangesFiles(packageControlFile);
352    }
353
354    private void makeChangesFiles(final BinaryPackageControlFile packageControlFile) throws PackagingException {
355        if (changesOut == null) {
356            changesOut = new File(deb.getParentFile(), FilenameUtils.getBaseName(deb.getName()) + ".changes");
357        }
358
359        ChangesProvider changesProvider;
360        FileOutputStream out = null;
361
362        try {
363            console.info("Creating changes file: " + changesOut);
364
365            out = new FileOutputStream(changesOut);
366
367            if (changesIn != null && changesIn.exists()) {
368                // read the changes form a textfile provider
369                changesProvider = new TextfileChangesProvider(new FileInputStream(changesIn), packageControlFile);
370            } else {
371                // create an empty changelog
372                changesProvider = new ChangesProvider() {
373                    public ChangeSet[] getChangesSets() {
374                        return new ChangeSet[] {
375                                new ChangeSet(packageControlFile.get("Package"),
376                                        packageControlFile.get("Version"),
377                                        new Date(),
378                                        packageControlFile.get("Distribution"),
379                                        packageControlFile.get("Urgency"),
380                                        packageControlFile.get("Maintainer"),
381                                        new String[0])
382                        };
383                    }
384                };
385            }
386
387            ChangesFileBuilder builder = new ChangesFileBuilder();
388            ChangesFile changesFile = builder.createChanges(packageControlFile, deb, changesProvider);
389
390            //(signChanges || signPackage) - for backward compatibility. signPackage is signing both changes and deb.
391            if ((signChanges || signPackage) && keyring != null && key != null && passphrase != null) {
392                console.info("Signing the changes file with the key " + key);
393                PGPSigner signer = new PGPSigner(new FileInputStream(keyring), key, passphrase, getDigestCode(digest));
394                signer.clearSign(changesFile.toString(), out);
395            } else {
396                out.write(changesFile.toString().getBytes("UTF-8"));
397            }
398            out.flush();
399
400        } catch (Exception e) {
401            throw new PackagingException("Failed to create the Debian changes file " + changesOut, e);
402        } finally {
403            IOUtils.closeQuietly(out);
404        }
405
406        if (changesSave == null || !(changesProvider instanceof TextfileChangesProvider)) {
407            return;
408        }
409
410        try {
411            console.info("Saving changes to file: " + changesSave);
412
413            ((TextfileChangesProvider) changesProvider).save(new FileOutputStream(changesSave));
414
415        } catch (Exception e) {
416            throw new PackagingException("Failed to save debian changes file " + changesSave, e);
417        }
418    }
419
420    private List<String> populateConffiles(Collection<DataProducer> producers) {
421        final List<String> result = new ArrayList<String>();
422
423        if (producers == null || producers.isEmpty()) {
424            return result;
425        }
426
427        final DataConsumer receiver = new DataConsumer() {
428            public void onEachFile(InputStream input, TarArchiveEntry entry)  {
429                String tempConffileItem = entry.getName();
430                if (tempConffileItem.startsWith(".")) {
431                    tempConffileItem = tempConffileItem.substring(1);
432                }
433                console.info("Adding conffile: " + tempConffileItem);
434                result.add(tempConffileItem);
435            }
436
437            public void onEachLink(TarArchiveEntry entry)  {
438            }
439
440            public void onEachDir(TarArchiveEntry tarArchiveEntry)  {
441            }
442        };
443
444        try {
445            for (DataProducer data : producers) {
446                data.produce(receiver);
447            }
448        } catch(Exception e) {
449            //
450        }
451
452        return result;
453    }
454
455    /**
456     * Create the debian archive with from the provided control files and data producers.
457     *
458     * @param compression   the compression method used for the data file
459     * @return BinaryPackageControlFile
460     * @throws PackagingException
461     */
462    public BinaryPackageControlFile createDeb(Compression compression) throws PackagingException {
463        return createSignedDeb(compression, null, null);
464    }
465    /**
466     * Create the debian archive with from the provided control files and data producers.
467     *
468     * @param compression   the compression method used for the data file (gzip, bzip2 or anything else for no compression)
469     * @param signatureGenerator   the signature generator
470     *
471     * @return PackageDescriptor
472     * @throws PackagingException
473     */
474    public BinaryPackageControlFile createSignedDeb(Compression compression, final PGPSignatureGenerator signatureGenerator, PGPSigner signer ) throws PackagingException {
475        File tempData = null;
476        File tempControl = null;
477
478        try {
479            tempData = File.createTempFile("deb", "data");
480            tempControl = File.createTempFile("deb", "control");
481
482            console.debug("Building data");
483            DataBuilder dataBuilder = new DataBuilder(console);
484            StringBuilder md5s = new StringBuilder();
485            TarOptions options = new TarOptions()
486                .compression(compression)
487                .longFileMode(tarLongFileMode)
488                .bigNumberMode(tarBigNumberMode);
489            BigInteger size = dataBuilder.buildData(dataProducers, tempData, md5s, options);
490
491            console.info("Building conffiles");
492            List<String> tempConffiles = populateConffiles(conffilesProducers);
493
494            console.debug("Building control");
495            ControlBuilder controlBuilder = new ControlBuilder(console, variableResolver, openReplaceToken, closeReplaceToken);
496            BinaryPackageControlFile packageControlFile = controlBuilder.createPackageControlFile(new File(control, "control"), size);
497            if (packageControlFile.get("Package") == null) {
498                packageControlFile.set("Package", packageName);
499            }
500            if (packageControlFile.get("Section") == null) {
501                packageControlFile.set("Section", section);
502            }
503            if (packageControlFile.get("Description") == null) {
504                packageControlFile.set("Description", description);
505            }
506            if (packageControlFile.get("Depends") == null) {
507                // Only add a depends entry to the control file if the field in this object has actually been set
508                if (depends != null && depends.length() > 0) {
509                    packageControlFile.set("Depends", depends);
510                }
511            }
512            if (packageControlFile.get("Homepage") == null) {
513                packageControlFile.set("Homepage", homepage);
514            }
515
516            controlBuilder.buildControl(packageControlFile, control.listFiles(), tempConffiles , md5s, tempControl);
517
518            if (!packageControlFile.isValid()) {
519                throw new PackagingException("Control file fields are invalid " + packageControlFile.invalidFields() +
520                        ". The following fields are mandatory: " + packageControlFile.getMandatoryFields() +
521                        ". Please check your pom.xml/build.xml and your control file.");
522            }
523
524            deb.getParentFile().mkdirs();
525
526            ArArchiveOutputStream ar = new ArArchiveOutputStream(new FileOutputStream(deb));
527
528            String binaryName = "debian-binary";
529            String binaryContent = "2.0\n";
530            String controlName = "control.tar.gz";
531            String dataName = "data.tar" + compression.getExtension();
532
533            addTo(ar, binaryName, binaryContent);
534            addTo(ar, controlName, tempControl);
535            addTo(ar, dataName, tempData);
536
537            if (signatureGenerator != null) {
538                console.info("Signing package with key " + key);
539
540                if(signRole == null) {
541                    signRole = "origin";
542                }
543
544                // Use debsig-verify as default
545                if(signMethod == null || !"dpkg-sig".equals(signMethod)) {
546                    // Sign file to verify with debsig-verify
547                    PGPSignatureOutputStream sigStream = new PGPSignatureOutputStream(signatureGenerator);
548
549                    addTo(sigStream, binaryContent);
550                    addTo(sigStream, tempControl);
551                    addTo(sigStream, tempData);
552                    addTo(ar, "_gpg" + signRole, sigStream.generateASCIISignature());
553
554                } else {
555
556                    // Sign file to verify with dpkg-sig --verify
557                    final String outputStr =
558                            "Version: 4\n" +
559                                    "Signer: \n" +
560                                    "Date: " + new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.ENGLISH).format(new Date()) + "\n" +
561                                    "Role: " + signRole +"\n" +
562                                    "Files: \n" +
563                                    addFile(binaryName, binaryContent) +
564                                    addFile(controlName, tempControl) +
565                                    addFile(dataName, tempData);
566
567                    ByteArrayOutputStream message = new ByteArrayOutputStream();
568                    signer.clearSign(outputStr, message);
569
570                    addTo(ar, "_gpg" + signRole, message.toString());
571                }
572            }
573
574            ar.close();
575
576            return packageControlFile;
577
578        } catch (Exception e) {
579            throw new PackagingException("Could not create deb package", e);
580        } finally {
581            if (tempData != null) {
582                if (!tempData.delete()) {
583                    console.warn("Could not delete the temporary file " + tempData);
584                }
585            }
586            if (tempControl != null) {
587                if (!tempControl.delete()) {
588                    console.warn("Could not delete the temporary file " + tempControl);
589                }
590            }
591        }
592    }
593
594    private String addFile(String name, String input){
595        return addLine(md5Hash(input), sha1Hash(input), input.length(), name);
596    }
597
598    private String addFile(String name, File input){
599        return addLine(md5Hash(input), sha1Hash(input), input.length(), name);
600    }
601
602    private String addLine(String md5, String sha1, long size, String name){
603        return "\t" + md5 + " " + sha1 + " " + size + " " + name + "\n";
604    }
605
606    private String md5Hash(String input){
607        return md5Hash(input.getBytes());
608    }
609
610    private String md5Hash(File input){
611        try {
612            return md5Hash(FileUtils.readFileToByteArray(input));
613        } catch (IOException e) {
614            // TODO Auto-generated catch block
615            e.printStackTrace();
616        }
617
618        return null;
619    }
620
621    private String md5Hash(byte input[]){
622        //update the input of MD5
623        MD5Digest md5 = new MD5Digest();
624        md5.update(input, 0, input.length);
625
626        //get the output/ digest size and hash it
627        byte[] digest = new byte[md5.getDigestSize()];
628        md5.doFinal(digest, 0);
629
630        return new String(Hex.encode(digest));
631    }
632
633    private String sha1Hash(String input){
634        return sha1Hash(input.getBytes());
635    }
636
637    private String sha1Hash(File input){
638        try {
639            return sha1Hash(FileUtils.readFileToByteArray(input));
640        } catch (IOException e) {
641            // TODO Auto-generated catch block
642            e.printStackTrace();
643        }
644
645        return null;
646    }
647
648    private String sha1Hash(byte input[]){
649        try
650        {
651            //prepare the input
652            MessageDigest hash = MessageDigest.getInstance(digest);
653            hash.update(input);
654
655            //proceed ....
656            byte[] digest = hash.digest();
657
658            return new String(Hex.encode(digest));
659        }
660        catch (NoSuchAlgorithmException e)
661        {
662            System.err.println("No such algorithm");
663            e.printStackTrace();
664        }
665
666        return null;
667    }
668
669    private void addTo(ArArchiveOutputStream pOutput, String pName, String pContent) throws IOException {
670        final byte[] content = pContent.getBytes();
671        pOutput.putArchiveEntry(new ArArchiveEntry(pName, content.length));
672        pOutput.write(content);
673        pOutput.closeArchiveEntry();
674    }
675
676    private void addTo(ArArchiveOutputStream pOutput, String pName, File pContent) throws IOException {
677        pOutput.putArchiveEntry(new ArArchiveEntry(pName, pContent.length()));
678
679        final InputStream input = new FileInputStream(pContent);
680        try {
681            Utils.copy(input, pOutput);
682        } finally {
683            input.close();
684        }
685
686        pOutput.closeArchiveEntry();
687    }
688
689    private void addTo(final PGPSignatureOutputStream pOutput, final String pContent) throws IOException {
690        final byte[] content = pContent.getBytes();
691        pOutput.write(content);
692    }
693
694    private void addTo(final PGPSignatureOutputStream pOutput, final File pContent) throws IOException {
695        final InputStream input = new FileInputStream(pContent);
696        try {
697            Utils.copy(input, pOutput);
698        } finally {
699            input.close();
700        }
701    }
702
703    public void setOpenReplaceToken(String openReplaceToken) {
704        this.openReplaceToken = openReplaceToken;
705    }
706
707    public void setCloseReplaceToken(String closeReplaceToken) {
708        this.closeReplaceToken = closeReplaceToken;
709    }
710}