001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package org.pgpainless.sop; 006 007import java.io.ByteArrayOutputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.OutputStream; 011import java.util.ArrayList; 012import java.util.List; 013 014import org.bouncycastle.openpgp.PGPException; 015import org.bouncycastle.openpgp.PGPSecretKeyRing; 016import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; 017import org.bouncycastle.openpgp.PGPSignature; 018import org.bouncycastle.util.io.Streams; 019import org.pgpainless.PGPainless; 020import org.pgpainless.algorithm.DocumentSignatureType; 021import org.pgpainless.encryption_signing.EncryptionResult; 022import org.pgpainless.encryption_signing.EncryptionStream; 023import org.pgpainless.encryption_signing.ProducerOptions; 024import org.pgpainless.encryption_signing.SigningOptions; 025import org.pgpainless.key.SubkeyIdentifier; 026import org.pgpainless.key.info.KeyRingInfo; 027import org.pgpainless.key.protection.SecretKeyRingProtector; 028import org.pgpainless.util.ArmoredOutputStreamFactory; 029import sop.Ready; 030import sop.enums.SignAs; 031import sop.exception.SOPGPException; 032import sop.operation.Sign; 033 034public class SignImpl implements Sign { 035 036 private boolean armor = true; 037 private SignAs mode = SignAs.Binary; 038 private final SigningOptions signingOptions = new SigningOptions(); 039 040 @Override 041 public Sign noArmor() { 042 armor = false; 043 return this; 044 } 045 046 @Override 047 public Sign mode(SignAs mode) { 048 this.mode = mode; 049 return this; 050 } 051 052 @Override 053 public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { 054 try { 055 PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); 056 if (keys.size() != 1) { 057 throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); 058 } 059 060 PGPSecretKeyRing key = keys.iterator().next(); 061 KeyRingInfo info = new KeyRingInfo(key); 062 if (!info.isFullyDecrypted()) { 063 throw new SOPGPException.KeyIsProtected(); 064 } 065 signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); 066 } catch (PGPException e) { 067 throw new SOPGPException.BadData(e); 068 } 069 return this; 070 } 071 072 @Override 073 public Ready data(InputStream data) throws IOException { 074 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 075 try { 076 EncryptionStream signingStream = PGPainless.encryptAndOrSign() 077 .onOutputStream(buffer) 078 .withOptions(ProducerOptions.sign(signingOptions) 079 .setAsciiArmor(armor)); 080 081 return new Ready() { 082 @Override 083 public void writeTo(OutputStream outputStream) throws IOException { 084 085 if (signingStream.isClosed()) { 086 throw new IllegalStateException("EncryptionStream is already closed."); 087 } 088 089 Streams.pipeAll(data, signingStream); 090 signingStream.close(); 091 EncryptionResult encryptionResult = signingStream.getResult(); 092 093 List<PGPSignature> signatures = new ArrayList<>(); 094 for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { 095 signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); 096 } 097 098 OutputStream out; 099 if (armor) { 100 out = ArmoredOutputStreamFactory.get(outputStream); 101 } else { 102 out = outputStream; 103 } 104 for (PGPSignature sig : signatures) { 105 sig.encode(out); 106 } 107 out.close(); 108 outputStream.close(); // armor out does not close underlying stream 109 } 110 }; 111 112 } catch (PGPException e) { 113 throw new RuntimeException(e); 114 } 115 116 } 117 118 private static DocumentSignatureType modeToSigType(SignAs mode) { 119 return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT 120 : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; 121 } 122}