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}