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}