001/*
002 * Copyright 2007-2018 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("Homepage") == null) {
507                packageControlFile.set("Homepage", homepage);
508            }
509
510            controlBuilder.buildControl(packageControlFile, control.listFiles(), tempConffiles , md5s, tempControl);
511
512            if (!packageControlFile.isValid()) {
513                throw new PackagingException("Control file fields are invalid " + packageControlFile.invalidFields() +
514                        ". The following fields are mandatory: " + packageControlFile.getMandatoryFields() +
515                        ". Please check your pom.xml/build.xml and your control file.");
516            }
517
518            deb.getParentFile().mkdirs();
519
520            ArArchiveOutputStream ar = new ArArchiveOutputStream(new FileOutputStream(deb));
521
522            String binaryName = "debian-binary";
523            String binaryContent = "2.0\n";
524            String controlName = "control.tar.gz";
525            String dataName = "data.tar" + compression.getExtension();
526
527            addTo(ar, binaryName, binaryContent);
528            addTo(ar, controlName, tempControl);
529            addTo(ar, dataName, tempData);
530
531            if (signatureGenerator != null) {
532                console.info("Signing package with key " + key);
533
534                if(signRole == null) {
535                    signRole = "origin";
536                }
537
538                // Use debsig-verify as default
539                if(signMethod == null || !"dpkg-sig".equals(signMethod)) {
540                    // Sign file to verify with debsig-verify
541                    PGPSignatureOutputStream sigStream = new PGPSignatureOutputStream(signatureGenerator);
542
543                    addTo(sigStream, binaryContent);
544                    addTo(sigStream, tempControl);
545                    addTo(sigStream, tempData);
546                    addTo(ar, "_gpg" + signRole, sigStream.generateASCIISignature());
547
548                } else {
549
550                    // Sign file to verify with dpkg-sig --verify
551                    final String outputStr =
552                            "Version: 4\n" +
553                                    "Signer: \n" +
554                                    "Date: " + new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.ENGLISH).format(new Date()) + "\n" +
555                                    "Role: " + signRole +"\n" +
556                                    "Files: \n" +
557                                    addFile(binaryName, binaryContent) +
558                                    addFile(controlName, tempControl) +
559                                    addFile(dataName, tempData);
560
561                    ByteArrayOutputStream message = new ByteArrayOutputStream();
562                    signer.clearSign(outputStr, message);
563
564                    addTo(ar, "_gpg" + signRole, message.toString());
565                }
566            }
567
568            ar.close();
569
570            return packageControlFile;
571
572        } catch (Exception e) {
573            throw new PackagingException("Could not create deb package", e);
574        } finally {
575            if (tempData != null) {
576                if (!tempData.delete()) {
577                    console.warn("Could not delete the temporary file " + tempData);
578                }
579            }
580            if (tempControl != null) {
581                if (!tempControl.delete()) {
582                    console.warn("Could not delete the temporary file " + tempControl);
583                }
584            }
585        }
586    }
587
588    private String addFile(String name, String input){
589        return addLine(md5Hash(input), sha1Hash(input), input.length(), name);
590    }
591
592    private String addFile(String name, File input){
593        return addLine(md5Hash(input), sha1Hash(input), input.length(), name);
594    }
595
596    private String addLine(String md5, String sha1, long size, String name){
597        return "\t" + md5 + " " + sha1 + " " + size + " " + name + "\n";
598    }
599
600    private String md5Hash(String input){
601        return md5Hash(input.getBytes());
602    }
603
604    private String md5Hash(File input){
605        try {
606            return md5Hash(FileUtils.readFileToByteArray(input));
607        } catch (IOException e) {
608            // TODO Auto-generated catch block
609            e.printStackTrace();
610        }
611
612        return null;
613    }
614
615    private String md5Hash(byte input[]){
616        //update the input of MD5
617        MD5Digest md5 = new MD5Digest();
618        md5.update(input, 0, input.length);
619
620        //get the output/ digest size and hash it
621        byte[] digest = new byte[md5.getDigestSize()];
622        md5.doFinal(digest, 0);
623
624        return new String(Hex.encode(digest));
625    }
626
627    private String sha1Hash(String input){
628        return sha1Hash(input.getBytes());
629    }
630
631    private String sha1Hash(File input){
632        try {
633            return sha1Hash(FileUtils.readFileToByteArray(input));
634        } catch (IOException e) {
635            // TODO Auto-generated catch block
636            e.printStackTrace();
637        }
638
639        return null;
640    }
641
642    private String sha1Hash(byte input[]){
643        try
644        {
645            //prepare the input
646            MessageDigest hash = MessageDigest.getInstance(digest);
647            hash.update(input);
648
649            //proceed ....
650            byte[] digest = hash.digest();
651
652            return new String(Hex.encode(digest));
653        }
654        catch (NoSuchAlgorithmException e)
655        {
656            System.err.println("No such algorithm");
657            e.printStackTrace();
658        }
659
660        return null;
661    }
662
663    private void addTo(ArArchiveOutputStream pOutput, String pName, String pContent) throws IOException {
664        final byte[] content = pContent.getBytes();
665        pOutput.putArchiveEntry(new ArArchiveEntry(pName, content.length));
666        pOutput.write(content);
667        pOutput.closeArchiveEntry();
668    }
669
670    private void addTo(ArArchiveOutputStream pOutput, String pName, File pContent) throws IOException {
671        pOutput.putArchiveEntry(new ArArchiveEntry(pName, pContent.length()));
672
673        final InputStream input = new FileInputStream(pContent);
674        try {
675            Utils.copy(input, pOutput);
676        } finally {
677            input.close();
678        }
679
680        pOutput.closeArchiveEntry();
681    }
682
683    private void addTo(final PGPSignatureOutputStream pOutput, final String pContent) throws IOException {
684        final byte[] content = pContent.getBytes();
685        pOutput.write(content);
686    }
687
688    private void addTo(final PGPSignatureOutputStream pOutput, final File pContent) throws IOException {
689        final InputStream input = new FileInputStream(pContent);
690        try {
691            Utils.copy(input, pOutput);
692        } finally {
693            input.close();
694        }
695    }
696
697    public void setOpenReplaceToken(String openReplaceToken) {
698        this.openReplaceToken = openReplaceToken;
699    }
700
701    public void setCloseReplaceToken(String closeReplaceToken) {
702        this.closeReplaceToken = closeReplaceToken;
703    }
704}