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