mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-11-25 16:42:07 +01:00
Initial commit
This commit is contained in:
commit
8e3ee6c284
90 changed files with 6086 additions and 0 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
libs/
|
||||||
|
|
||||||
|
*/build
|
||||||
|
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.jar
|
||||||
|
|
||||||
|
gradle.properties
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
pgpainless-core/.classpath
|
||||||
|
pgpainless-core/.project
|
||||||
|
pgpainless-core/.settings/
|
||||||
|
|
||||||
|
push_html.sh
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
- Initial release from new repository
|
||||||
|
- Implement SOP specification version 3
|
23
README.md
Normal file
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# SOP for Java
|
||||||
|
|
||||||
|
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification
|
||||||
|
defines a generic stateless CLI for dealing with OpenPGP messages.
|
||||||
|
Its goal is to provide a minimal, yet powerful API for the most common OpenPGP related operations.
|
||||||
|
|
||||||
|
`sop-java` defines a set of Java interfaces describing said API.
|
||||||
|
|
||||||
|
`sop-java-picocli` contains a wrapper application that transforms the `sop-java` API into a command line application
|
||||||
|
compatible with the SOP-CLI specification.
|
||||||
|
|
||||||
|
## Known Implementations
|
||||||
|
(Please expand!)
|
||||||
|
|
||||||
|
| Project | Description |
|
||||||
|
|---------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||||
|
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/master/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
|
||||||
|
|
||||||
|
### Implementations in other languages
|
||||||
|
| Project | Language |
|
||||||
|
|-------------------------------------------------|----------|
|
||||||
|
| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust |
|
||||||
|
| [SOP for python](https://pypi.org/project/sop/) | Python |
|
252
build.gradle
Normal file
252
build.gradle
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
|
||||||
|
maven {
|
||||||
|
url "https://plugins.gradle.org/m2/"
|
||||||
|
}
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "gradle.plugin.org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'version.gradle'
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'idea'
|
||||||
|
apply plugin: 'eclipse'
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
apply plugin: 'checkstyle'
|
||||||
|
|
||||||
|
// For non-cli modules enable android api compatibility check
|
||||||
|
if (it.name.equals('sop-java')) {
|
||||||
|
// animalsniffer
|
||||||
|
apply plugin: 'ru.vyarus.animalsniffer'
|
||||||
|
dependencies {
|
||||||
|
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
|
||||||
|
}
|
||||||
|
animalsniffer {
|
||||||
|
sourceSets = [sourceSets.main]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkstyle
|
||||||
|
checkstyle {
|
||||||
|
toolVersion = '8.18'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
description = "Stateless OpenPGP Protocol API for Java"
|
||||||
|
version = shortVersion
|
||||||
|
|
||||||
|
sourceCompatibility = javaSourceCompatibility
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproducible Builds
|
||||||
|
tasks.withType(AbstractArchiveTask) {
|
||||||
|
preserveFileTimestamps = false
|
||||||
|
reproducibleFileOrder = true
|
||||||
|
}
|
||||||
|
|
||||||
|
project.ext {
|
||||||
|
slf4jVersion = '1.7.32'
|
||||||
|
logbackVersion = '1.2.9'
|
||||||
|
junitVersion = '5.8.2'
|
||||||
|
picocliVersion = '4.6.2'
|
||||||
|
rootConfigDir = new File(rootDir, 'config')
|
||||||
|
gitCommit = getGitCommit()
|
||||||
|
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
|
||||||
|
isReleaseVersion = !isSnapshot
|
||||||
|
signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment)
|
||||||
|
sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')
|
||||||
|
sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots'
|
||||||
|
sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSnapshot) {
|
||||||
|
version = version + '-SNAPSHOT'
|
||||||
|
}
|
||||||
|
def projectDirFile = new File("$projectDir")
|
||||||
|
if (!project.ext.isSnapshot && !'git describe --exact-match HEAD'.execute(null, projectDirFile).text.trim().equals(ext.shortVersion)) {
|
||||||
|
throw new InvalidUserDataException('Untagged version detected! Please tag every release.')
|
||||||
|
}
|
||||||
|
if (!version.endsWith('-SNAPSHOT') && version != 'git tag --points-at HEAD'.execute(null, projectDirFile).text.trim()) {
|
||||||
|
throw new InvalidUserDataException(
|
||||||
|
'Tag mismatch detected, version is ' + version + ' but should be ' +
|
||||||
|
'git tag --points-at HEAD'.execute(null, projectDirFile).text.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion = "0.8.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
jacocoTestReport {
|
||||||
|
dependsOn test
|
||||||
|
sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs))
|
||||||
|
classDirectories.setFrom(project.files(sourceSets.main.output))
|
||||||
|
reports {
|
||||||
|
xml.enabled true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed"
|
||||||
|
exceptionFormat "full"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'maven-publish'
|
||||||
|
apply plugin: 'signing'
|
||||||
|
|
||||||
|
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||||
|
classifier = 'sources'
|
||||||
|
from sourceSets.main.allSource
|
||||||
|
}
|
||||||
|
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||||
|
classifier = 'javadoc'
|
||||||
|
from javadoc.destinationDir
|
||||||
|
}
|
||||||
|
task testsJar(type: Jar, dependsOn: testClasses) {
|
||||||
|
classifier = 'tests'
|
||||||
|
from sourceSets.test.output
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
mavenJava(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
artifact sourcesJar
|
||||||
|
artifact javadocJar
|
||||||
|
artifact testsJar
|
||||||
|
pom {
|
||||||
|
name = 'SOP for Java'
|
||||||
|
description = 'Stateless OpenPGP Protocol API for Java'
|
||||||
|
url = 'https://github.com/pgpainless/sop-java'
|
||||||
|
inceptionYear = '2020'
|
||||||
|
|
||||||
|
scm {
|
||||||
|
url = 'https://github.com/pgpainless/sop-java'
|
||||||
|
connection = 'scm:https://github.com/pgpainless/sop-java'
|
||||||
|
developerConnection = 'scm:git://github.com/pgpainless/sop-java.git'
|
||||||
|
}
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = 'The Apache Software License, Version 2.0'
|
||||||
|
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||||
|
distribution = 'repo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = 'vanitasvitae'
|
||||||
|
name = 'Paul Schaub'
|
||||||
|
email = 'vanitasvitae@fsfe.org'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
if (sonatypeCredentialsAvailable) {
|
||||||
|
maven {
|
||||||
|
url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl
|
||||||
|
credentials {
|
||||||
|
username = sonatypeUsername
|
||||||
|
password = sonatypePassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
required { signingRequired }
|
||||||
|
sign publishing.publications.mavenJava
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGitCommit() {
|
||||||
|
def projectDirFile = new File("$projectDir")
|
||||||
|
def dotGit = new File("$projectDir/.git")
|
||||||
|
if (!dotGit.isDirectory()) return 'non-git build'
|
||||||
|
|
||||||
|
def cmd = 'git describe --always --tags --dirty=+'
|
||||||
|
def proc = cmd.execute(null, projectDirFile)
|
||||||
|
def gitCommit = proc.text.trim()
|
||||||
|
assert !gitCommit.isEmpty()
|
||||||
|
|
||||||
|
def srCmd = 'git symbolic-ref --short HEAD'
|
||||||
|
def srProc = srCmd.execute(null, projectDirFile)
|
||||||
|
srProc.waitForOrKill(10 * 1000)
|
||||||
|
if (srProc.exitValue() == 0) {
|
||||||
|
// Only add the information if the git command was
|
||||||
|
// successful. There may be no symbolic reference for HEAD if
|
||||||
|
// e.g. in detached mode.
|
||||||
|
def symbolicReference = srProc.text.trim()
|
||||||
|
assert !symbolicReference.isEmpty()
|
||||||
|
gitCommit += "-$symbolicReference"
|
||||||
|
}
|
||||||
|
|
||||||
|
gitCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "com.github.kt3k.coveralls"
|
||||||
|
coveralls {
|
||||||
|
sourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs).files.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
task jacocoRootReport(type: JacocoReport) {
|
||||||
|
dependsOn = subprojects.jacocoTestReport
|
||||||
|
sourceDirectories.setFrom(files(subprojects.sourceSets.main.allSource.srcDirs))
|
||||||
|
classDirectories.setFrom(files(subprojects.sourceSets.main.output))
|
||||||
|
executionData.setFrom(files(subprojects.jacocoTestReport.executionData))
|
||||||
|
reports {
|
||||||
|
xml.enabled true
|
||||||
|
xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
|
||||||
|
}
|
||||||
|
// We could remove the following setOnlyIf line, but then
|
||||||
|
// jacocoRootReport would silently be SKIPPED if something with
|
||||||
|
// the projectsWithUnitTests is wrong (e.g. a project is missing
|
||||||
|
// in there).
|
||||||
|
setOnlyIf { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
task javadocAll(type: Javadoc) {
|
||||||
|
def currentJavaVersion = JavaVersion.current()
|
||||||
|
if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) {
|
||||||
|
options.addStringOption("-release", "8");
|
||||||
|
}
|
||||||
|
source subprojects.collect {project ->
|
||||||
|
project.sourceSets.main.allJava }
|
||||||
|
destinationDir = new File(buildDir, 'javadoc')
|
||||||
|
// Might need a classpath
|
||||||
|
classpath = files(subprojects.collect {project ->
|
||||||
|
project.sourceSets.main.compileClasspath})
|
||||||
|
options.linkSource = true
|
||||||
|
options.use = true
|
||||||
|
options.links = [
|
||||||
|
"https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/",
|
||||||
|
] as String[]
|
||||||
|
}
|
232
config/checkstyle/checkstyle.xml
Normal file
232
config/checkstyle/checkstyle.xml
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||||
|
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
||||||
|
<module name="Checker">
|
||||||
|
|
||||||
|
<!-- Suppressions -->
|
||||||
|
<module name="SuppressionFilter">
|
||||||
|
<property name="file" value="${config_loc}/suppressions.xml"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="NewlineAtEndOfFile">
|
||||||
|
<property name="lineSeparator" value="lf"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<!--
|
||||||
|
Matches StringBuilder.append(String) calls where the
|
||||||
|
argument is a String of length one. Those should be replaced
|
||||||
|
with append(char) for performance reasons.
|
||||||
|
|
||||||
|
TODO: This could be more advanced in order to match also
|
||||||
|
- .append("\u1234")
|
||||||
|
-->
|
||||||
|
<property name="format" value="\.append\("(.|\\.)"\)"/>
|
||||||
|
<property name="message" value="Don't use StringBuilder.append(String) when you can use StringBuilder.append(char). Solution: Replace double quotes of append's argument with single quotes."/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Whitespace only lines -->
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<property name="format" value="^\s+$"/>
|
||||||
|
<property name="message" value="Line containing only whitespace character(s)"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Mixed spaces/tabs -->
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<!-- We use {2,} instead of + here to address the typical case where a file was written
|
||||||
|
with tabs but javadoc is causing '\t *' -->
|
||||||
|
<property name="format" value="^\t+ {2,}"/>
|
||||||
|
<property name="message" value="Line containing space(s) after tab(s)"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Trailing whitespaces -->
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<!--
|
||||||
|
Explaining the following Regex
|
||||||
|
|
||||||
|
\s+ $
|
||||||
|
| +- End of Line (2)
|
||||||
|
+- At least one whitespace (1)
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
Matches trailing whitespace (2) in lines containing at least one (1) non-whitespace character
|
||||||
|
-->
|
||||||
|
<property name="format" value="\s+$"/>
|
||||||
|
<property name="message" value="Line containing trailing whitespace character(s)"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- <module name="RegexpSingleline"> -->
|
||||||
|
<!-- <property name="format" value="fqdn"/> -->
|
||||||
|
<!-- </module> -->
|
||||||
|
|
||||||
|
<!-- Space after // -->
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<property name="format" value="^\s*//[^\s]"/>
|
||||||
|
<property name="message" value="Comment start ('//') followed by non-space character. You would not continue after a punctuation without a space, would you?"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocPackage">
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<module name="SuppressionCommentFilter"/>
|
||||||
|
<module name="FinalClass"/>
|
||||||
|
<module name="UnusedImports">
|
||||||
|
<property name="processJavadoc" value="true"/>
|
||||||
|
</module>
|
||||||
|
<module name="AvoidStarImport"/>
|
||||||
|
<module name="IllegalImport"/>
|
||||||
|
<module name="RedundantImport"/>
|
||||||
|
<module name="RedundantModifier"/>
|
||||||
|
<module name="ModifierOrder"/>
|
||||||
|
<module name="UpperEll"/>
|
||||||
|
<module name="ArrayTypeStyle"/>
|
||||||
|
<module name="GenericWhitespace"/>
|
||||||
|
<module name="EmptyStatement"/>
|
||||||
|
<module name="PackageDeclaration"/>
|
||||||
|
<module name="LeftCurly"/>
|
||||||
|
|
||||||
|
<!-- printStackTrace -->
|
||||||
|
<module name="RegexpSinglelineJava">
|
||||||
|
<property name="format" value="printStackTrace"/>
|
||||||
|
<property name="message" value="Usage of printStackTrace. Either rethrow exception, or log using Logger."/>
|
||||||
|
<property name="ignoreComments" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- println -->
|
||||||
|
<module name="RegexpSinglelineJava">
|
||||||
|
<property name="format" value="println"/>
|
||||||
|
<property name="message" value="Usage of println"/>
|
||||||
|
<property name="ignoreComments" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Spaces instead of Tabs -->
|
||||||
|
<module name="RegexpSinglelineJava">
|
||||||
|
<property name="format" value="^\t+"/>
|
||||||
|
<property name="message" value="Indent must not use tab characters. Use space instead."/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocMethod">
|
||||||
|
<!-- TODO stricten those checks -->
|
||||||
|
<property name="scope" value="public"/>
|
||||||
|
<!--<property name="allowUndeclaredRTE" value="true"/>-->
|
||||||
|
<property name="allowMissingParamTags" value="true"/>
|
||||||
|
<property name="allowMissingThrowsTags" value="true"/>
|
||||||
|
<property name="allowMissingReturnTag" value="true"/>
|
||||||
|
<property name="allowMissingJavadoc" value="true"/>
|
||||||
|
<property name="suppressLoadErrors" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="JavadocStyle">
|
||||||
|
<property name="scope" value="public"/>
|
||||||
|
<property name="checkEmptyJavadoc" value="true"/>
|
||||||
|
<property name="checkHtml" value="false"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="ParenPad">
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Whitespace after key tokens -->
|
||||||
|
<module name="NoWhitespaceAfter">
|
||||||
|
<property name="tokens" value="INC
|
||||||
|
, DEC
|
||||||
|
, UNARY_MINUS
|
||||||
|
, UNARY_PLUS
|
||||||
|
, BNOT, LNOT
|
||||||
|
, DOT
|
||||||
|
, ARRAY_DECLARATOR
|
||||||
|
, INDEX_OP
|
||||||
|
"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Whitespace after key words -->
|
||||||
|
<module name="WhitespaceAfter">
|
||||||
|
<property name="tokens" value="TYPECAST
|
||||||
|
, LITERAL_IF
|
||||||
|
, LITERAL_ELSE
|
||||||
|
, LITERAL_WHILE
|
||||||
|
, LITERAL_DO
|
||||||
|
, LITERAL_FOR
|
||||||
|
, DO_WHILE
|
||||||
|
"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="WhitespaceAround">
|
||||||
|
<property
|
||||||
|
name="ignoreEnhancedForColon"
|
||||||
|
value="false"
|
||||||
|
/>
|
||||||
|
<!-- Currently disabled tokens: LCURLY, RCURLY, WILDCARD_TYPE, GENERIC_START, GENERIC_END -->
|
||||||
|
<property
|
||||||
|
name="tokens"
|
||||||
|
value="ASSIGN
|
||||||
|
, ARRAY_INIT
|
||||||
|
, BAND
|
||||||
|
, BAND_ASSIGN
|
||||||
|
, BOR
|
||||||
|
, BOR_ASSIGN
|
||||||
|
, BSR
|
||||||
|
, BSR_ASSIGN
|
||||||
|
, BXOR
|
||||||
|
, BXOR_ASSIGN
|
||||||
|
, COLON
|
||||||
|
, DIV
|
||||||
|
, DIV_ASSIGN
|
||||||
|
, DO_WHILE
|
||||||
|
, EQUAL
|
||||||
|
, GE
|
||||||
|
, GT
|
||||||
|
, LAMBDA
|
||||||
|
, LAND
|
||||||
|
, LE
|
||||||
|
, LITERAL_CATCH
|
||||||
|
, LITERAL_DO
|
||||||
|
, LITERAL_ELSE
|
||||||
|
, LITERAL_FINALLY
|
||||||
|
, LITERAL_FOR
|
||||||
|
, LITERAL_IF
|
||||||
|
, LITERAL_RETURN
|
||||||
|
, LITERAL_SWITCH
|
||||||
|
, LITERAL_SYNCHRONIZED
|
||||||
|
, LITERAL_TRY
|
||||||
|
, LITERAL_WHILE
|
||||||
|
, LOR
|
||||||
|
, LT
|
||||||
|
, MINUS
|
||||||
|
, MINUS_ASSIGN
|
||||||
|
, MOD
|
||||||
|
, MOD_ASSIGN
|
||||||
|
, NOT_EQUAL
|
||||||
|
, PLUS
|
||||||
|
, PLUS_ASSIGN
|
||||||
|
, QUESTION
|
||||||
|
, SL
|
||||||
|
, SLIST
|
||||||
|
, SL_ASSIGN
|
||||||
|
, SR
|
||||||
|
, SR_ASSIGN
|
||||||
|
, STAR
|
||||||
|
, STAR_ASSIGN
|
||||||
|
, LITERAL_ASSERT
|
||||||
|
, TYPE_EXTENSION_AND
|
||||||
|
"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<module name="CustomImportOrder">
|
||||||
|
<property name="customImportOrderRules"
|
||||||
|
value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE"/>
|
||||||
|
<property name="specialImportsRegExp" value="^org\.org.pgpainless.core\.org.pgpainless.core"/>
|
||||||
|
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||||
|
<property name="separateLineBetweenGroups" value="true"/>
|
||||||
|
</module>
|
||||||
|
-->
|
||||||
|
</module>
|
||||||
|
</module>
|
19
config/checkstyle/suppressions.xml
Normal file
19
config/checkstyle/suppressions.xml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE suppressions PUBLIC
|
||||||
|
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||||
|
"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||||
|
<suppressions>
|
||||||
|
<!-- GenericWhitespace has some problems with false postive, leave
|
||||||
|
it disabled until gradle uses a checkstyle version where this is fixed
|
||||||
|
-->
|
||||||
|
<suppress checks="GenericWhitespace"
|
||||||
|
files="Protocol.java" />
|
||||||
|
<!-- Suppress JavadocPackage in the test packages -->
|
||||||
|
<suppress checks="JavadocPackage" files="[\\/]test[\\/]"/>
|
||||||
|
</suppressions>
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
185
gradlew
vendored
Executable file
185
gradlew
vendored
Executable file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MSYS* | MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
9
settings.gradle
Normal file
9
settings.gradle
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
rootProject.name = 'PGPainless'
|
||||||
|
|
||||||
|
include 'sop-java',
|
||||||
|
'sop-java-picocli'
|
||||||
|
|
34
sop-java-picocli/README.md
Normal file
34
sop-java-picocli/README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
# SOP-Java-Picocli
|
||||||
|
|
||||||
|
Implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification.
|
||||||
|
This terminal application allows generation of OpenPGP keys, extraction of public key certificates,
|
||||||
|
armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures.
|
||||||
|
|
||||||
|
## Install a SOP backend
|
||||||
|
|
||||||
|
This module comes without a SOP backend, so in order to function you need to extend it with an implementation of the interfaces defined in `sop-java`.
|
||||||
|
An implementation using PGPainless can be found in the module `pgpainless-sop`, but it is of course possible to provide your
|
||||||
|
own implementation.
|
||||||
|
|
||||||
|
Just install your SOP backend by calling
|
||||||
|
```java
|
||||||
|
// static method call prior to execution of the main method
|
||||||
|
SopCLI.setSopInstance(yourSopImpl);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To get an overview of available commands of the application, execute
|
||||||
|
```shell
|
||||||
|
java -jar sop-java-picocli-XXX.jar help
|
||||||
|
```
|
||||||
|
|
||||||
|
If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes
|
||||||
|
`sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up.
|
||||||
|
|
||||||
|
Enjoy!
|
39
sop-java-picocli/build.gradle
Normal file
39
sop-java-picocli/build.gradle
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'application'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
// https://todd.ginsberg.com/post/testing-system-exit/
|
||||||
|
testImplementation 'com.ginsberg:junit5-system-exit:1.1.1'
|
||||||
|
testImplementation 'org.mockito:mockito-core:4.2.0'
|
||||||
|
|
||||||
|
implementation(project(":sop-java"))
|
||||||
|
implementation "info.picocli:picocli:$picocliVersion"
|
||||||
|
|
||||||
|
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
|
||||||
|
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
mainClassName = 'sop.cli.picocli.SopCLI'
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes 'Main-Class': "$mainClassName"
|
||||||
|
}
|
||||||
|
|
||||||
|
from {
|
||||||
|
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||||
|
} {
|
||||||
|
exclude "META-INF/*.SF"
|
||||||
|
exclude "META-INF/*.DSA"
|
||||||
|
exclude "META-INF/*.RSA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
public class DateParser {
|
||||||
|
|
||||||
|
public static final Date BEGINNING_OF_TIME = new Date(0);
|
||||||
|
public static final Date END_OF_TIME = new Date(8640000000000000L);
|
||||||
|
|
||||||
|
public static Date parseNotAfter(String notAfter) {
|
||||||
|
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
|
||||||
|
if (date == null) {
|
||||||
|
Print.errln("Invalid date string supplied as value of --not-after.");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Date parseNotBefore(String notBefore) {
|
||||||
|
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
|
||||||
|
if (date == null) {
|
||||||
|
Print.errln("Invalid date string supplied as value of --not-before.");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
98
sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java
Normal file
98
sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public class FileUtil {
|
||||||
|
|
||||||
|
private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem.";
|
||||||
|
private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set.";
|
||||||
|
private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists.";
|
||||||
|
private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist.";
|
||||||
|
private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s";
|
||||||
|
|
||||||
|
public static final String PRFX_ENV = "@ENV:";
|
||||||
|
public static final String PRFX_FD = "@FD:";
|
||||||
|
|
||||||
|
private static EnvironmentVariableResolver envResolver = System::getenv;
|
||||||
|
|
||||||
|
public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
|
||||||
|
if (envResolver == null) {
|
||||||
|
throw new NullPointerException("Variable envResolver cannot be null.");
|
||||||
|
}
|
||||||
|
FileUtil.envResolver = envResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface EnvironmentVariableResolver {
|
||||||
|
/**
|
||||||
|
* Resolve the value of the given environment variable.
|
||||||
|
* Return null if the variable is not present.
|
||||||
|
*
|
||||||
|
* @param name name of the variable
|
||||||
|
* @return variable value or null
|
||||||
|
*/
|
||||||
|
String resolveEnvironmentVariable(String name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getFile(String fileName) {
|
||||||
|
if (fileName == null) {
|
||||||
|
throw new NullPointerException("File name cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName.startsWith(PRFX_ENV)) {
|
||||||
|
|
||||||
|
if (new File(fileName).exists()) {
|
||||||
|
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
String envName = fileName.substring(PRFX_ENV.length());
|
||||||
|
String envValue = envResolver.resolveEnvironmentVariable(envName);
|
||||||
|
if (envValue == null) {
|
||||||
|
throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName));
|
||||||
|
}
|
||||||
|
return new File(envValue);
|
||||||
|
} else if (fileName.startsWith(PRFX_FD)) {
|
||||||
|
|
||||||
|
if (new File(fileName).exists()) {
|
||||||
|
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("File descriptors not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileInputStream getFileInputStream(String fileName) {
|
||||||
|
File file = getFile(fileName);
|
||||||
|
try {
|
||||||
|
FileInputStream inputStream = new FileInputStream(file);
|
||||||
|
return inputStream;
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File createNewFileOrThrow(File file) throws IOException {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException("File cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!file.createNewFile()) {
|
||||||
|
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage()));
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
26
sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
Normal file
26
sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
public class Print {
|
||||||
|
|
||||||
|
public static void errln(String string) {
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
|
System.err.println(string);
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void trace(Throwable e) {
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
|
e.printStackTrace();
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void outln(String string) {
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
|
System.out.println(string);
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode(Throwable exception) {
|
||||||
|
if (exception instanceof SOPGPException) {
|
||||||
|
return ((SOPGPException) exception).getExitCode();
|
||||||
|
}
|
||||||
|
if (exception instanceof CommandLine.UnmatchedArgumentException) {
|
||||||
|
CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception;
|
||||||
|
// Unmatched option of subcommand (eg. `generate-key -k`)
|
||||||
|
if (ex.isUnknownOption()) {
|
||||||
|
return SOPGPException.UnsupportedOption.EXIT_CODE;
|
||||||
|
}
|
||||||
|
// Unmatched subcommand
|
||||||
|
return SOPGPException.UnsupportedSubcommand.EXIT_CODE;
|
||||||
|
}
|
||||||
|
// Invalid option (eg. `--label Invalid`)
|
||||||
|
if (exception instanceof CommandLine.ParameterException) {
|
||||||
|
return SOPGPException.UnsupportedOption.EXIT_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Others, like IOException etc.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) {
|
||||||
|
int exitCode = commandLine.getExitCodeExceptionMapper() != null ?
|
||||||
|
commandLine.getExitCodeExceptionMapper().getExitCode(ex) :
|
||||||
|
commandLine.getCommandSpec().exitCodeOnExecutionException();
|
||||||
|
CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme();
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
|
if (ex.getMessage() != null) {
|
||||||
|
commandLine.getErr().println(colorScheme.errorText(ex.getMessage()));
|
||||||
|
}
|
||||||
|
ex.printStackTrace(commandLine.getErr());
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
68
sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
Normal file
68
sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.commands.ArmorCmd;
|
||||||
|
import sop.cli.picocli.commands.DearmorCmd;
|
||||||
|
import sop.cli.picocli.commands.DecryptCmd;
|
||||||
|
import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd;
|
||||||
|
import sop.cli.picocli.commands.EncryptCmd;
|
||||||
|
import sop.cli.picocli.commands.ExtractCertCmd;
|
||||||
|
import sop.cli.picocli.commands.GenerateKeyCmd;
|
||||||
|
import sop.cli.picocli.commands.SignCmd;
|
||||||
|
import sop.cli.picocli.commands.VerifyCmd;
|
||||||
|
import sop.cli.picocli.commands.VersionCmd;
|
||||||
|
|
||||||
|
@CommandLine.Command(
|
||||||
|
exitCodeOnInvalidInput = 69,
|
||||||
|
subcommands = {
|
||||||
|
CommandLine.HelpCommand.class,
|
||||||
|
ArmorCmd.class,
|
||||||
|
DearmorCmd.class,
|
||||||
|
DecryptCmd.class,
|
||||||
|
DetachInbandSignatureAndMessageCmd.class,
|
||||||
|
EncryptCmd.class,
|
||||||
|
ExtractCertCmd.class,
|
||||||
|
GenerateKeyCmd.class,
|
||||||
|
SignCmd.class,
|
||||||
|
VerifyCmd.class,
|
||||||
|
VersionCmd.class
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public class SopCLI {
|
||||||
|
// Singleton
|
||||||
|
static SOP SOP_INSTANCE;
|
||||||
|
|
||||||
|
public static String EXECUTABLE_NAME = "sop";
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int exitCode = execute(args);
|
||||||
|
if (exitCode != 0) {
|
||||||
|
System.exit(exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int execute(String[] args) {
|
||||||
|
return new CommandLine(SopCLI.class)
|
||||||
|
.setCommandName(EXECUTABLE_NAME)
|
||||||
|
.setExecutionExceptionHandler(new SOPExecutionExceptionHandler())
|
||||||
|
.setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper())
|
||||||
|
.setCaseInsensitiveEnumValuesAllowed(true)
|
||||||
|
.execute(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SOP getSop() {
|
||||||
|
if (SOP_INSTANCE == null) {
|
||||||
|
throw new IllegalStateException("No SOP backend set.");
|
||||||
|
}
|
||||||
|
return SOP_INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSopInstance(SOP instance) {
|
||||||
|
SOP_INSTANCE = instance;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.enums.ArmorLabel;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Armor;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "armor",
|
||||||
|
description = "Add ASCII Armor to standard input",
|
||||||
|
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||||
|
public class ArmorCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}")
|
||||||
|
ArmorLabel label;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Armor armor = SopCLI.getSop().armor();
|
||||||
|
if (armor == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label != null) {
|
||||||
|
try {
|
||||||
|
armor.label(label);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
Print.errln("Armor labels not supported.");
|
||||||
|
System.exit(unsupportedOption.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Ready ready = armor.data(System.in);
|
||||||
|
ready.writeTo(System.out);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
Print.errln("Bad data.");
|
||||||
|
Print.trace(badData);
|
||||||
|
System.exit(badData.getExitCode());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Dearmor;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "dearmor",
|
||||||
|
description = "Remove ASCII Armor from standard input",
|
||||||
|
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||||
|
public class DearmorCmd implements Runnable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Dearmor dearmor = SopCLI.getSop().dearmor();
|
||||||
|
if (dearmor == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SopCLI.getSop()
|
||||||
|
.dearmor()
|
||||||
|
.data(System.in)
|
||||||
|
.writeTo(System.out);
|
||||||
|
} catch (SOPGPException.BadData e) {
|
||||||
|
Print.errln("Bad data.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(e.getExitCode());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.DecryptionResult;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SessionKey;
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.cli.picocli.DateParser;
|
||||||
|
import sop.cli.picocli.FileUtil;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Decrypt;
|
||||||
|
import sop.util.HexUtil;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "decrypt",
|
||||||
|
description = "Decrypt a message from standard input",
|
||||||
|
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||||
|
public class DecryptCmd implements Runnable {
|
||||||
|
|
||||||
|
private static final String SESSION_KEY_OUT = "--session-key-out";
|
||||||
|
private static final String VERIFY_OUT = "--verify-out";
|
||||||
|
|
||||||
|
private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported.";
|
||||||
|
private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist.";
|
||||||
|
private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists.";
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = {SESSION_KEY_OUT},
|
||||||
|
description = "Can be used to learn the session key on successful decryption",
|
||||||
|
paramLabel = "SESSIONKEY")
|
||||||
|
File sessionKeyOut;
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = {"--with-session-key"},
|
||||||
|
description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet",
|
||||||
|
paramLabel = "SESSIONKEY")
|
||||||
|
List<String> withSessionKey = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = {"--with-password"},
|
||||||
|
description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
|
||||||
|
paramLabel = "PASSWORD")
|
||||||
|
List<String> withPassword = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {VERIFY_OUT},
|
||||||
|
description = "Produces signature verification status to the designated file",
|
||||||
|
paramLabel = "VERIFICATIONS")
|
||||||
|
File verifyOut;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--verify-with"},
|
||||||
|
description = "Certificates whose signatures would be acceptable for signatures over this message",
|
||||||
|
paramLabel = "CERT")
|
||||||
|
List<File> certs = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--not-before"},
|
||||||
|
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||||
|
"Reject signatures with a creation date not in range.\n" +
|
||||||
|
"Defaults to beginning of time (\"-\").",
|
||||||
|
paramLabel = "DATE")
|
||||||
|
String notBefore = "-";
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--not-after"},
|
||||||
|
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||||
|
"Reject signatures with a creation date not in range.\n" +
|
||||||
|
"Defaults to current system time (\"now\").\n" +
|
||||||
|
"Accepts special value \"-\" for end of time.",
|
||||||
|
paramLabel = "DATE")
|
||||||
|
String notAfter = "now";
|
||||||
|
|
||||||
|
@CommandLine.Parameters(index = "0..*",
|
||||||
|
description = "Secret keys to attempt decryption with",
|
||||||
|
paramLabel = "KEY")
|
||||||
|
List<File> keys = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
throwIfOutputExists(verifyOut, VERIFY_OUT);
|
||||||
|
throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT);
|
||||||
|
|
||||||
|
Decrypt decrypt = SopCLI.getSop().decrypt();
|
||||||
|
if (decrypt == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotAfter(notAfter, decrypt);
|
||||||
|
setNotBefore(notBefore, decrypt);
|
||||||
|
setWithPasswords(withPassword, decrypt);
|
||||||
|
setWithSessionKeys(withSessionKey, decrypt);
|
||||||
|
setVerifyWith(certs, decrypt);
|
||||||
|
setDecryptWith(keys, decrypt);
|
||||||
|
|
||||||
|
if (verifyOut != null && certs.isEmpty()) {
|
||||||
|
String errorMessage = "Option %s is requested, but no option %s was provided.";
|
||||||
|
throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
|
||||||
|
DecryptionResult result = ready.writeTo(System.out);
|
||||||
|
writeSessionKeyOut(result);
|
||||||
|
writeVerifyOut(result);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new RuntimeException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwIfOutputExists(File outputFile, String optionName) {
|
||||||
|
if (outputFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputFile.exists()) {
|
||||||
|
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeVerifyOut(DecryptionResult result) throws IOException {
|
||||||
|
if (verifyOut != null) {
|
||||||
|
FileUtil.createNewFileOrThrow(verifyOut);
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) {
|
||||||
|
PrintWriter writer = new PrintWriter(outputStream);
|
||||||
|
for (Verification verification : result.getVerifications()) {
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
|
writer.println(verification.toString());
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
}
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeSessionKeyOut(DecryptionResult result) throws IOException {
|
||||||
|
if (sessionKeyOut != null) {
|
||||||
|
FileUtil.createNewFileOrThrow(sessionKeyOut);
|
||||||
|
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) {
|
||||||
|
if (!result.getSessionKey().isPresent()) {
|
||||||
|
throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported.");
|
||||||
|
} else {
|
||||||
|
SessionKey sessionKey = result.getSessionKey().get();
|
||||||
|
outputStream.write(sessionKey.getAlgorithm());
|
||||||
|
outputStream.write(sessionKey.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDecryptWith(List<File> keys, Decrypt decrypt) {
|
||||||
|
for (File key : keys) {
|
||||||
|
try (FileInputStream keyIn = new FileInputStream(key)) {
|
||||||
|
decrypt.withKey(keyIn);
|
||||||
|
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
||||||
|
throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVerifyWith(List<File> certs, Decrypt decrypt) {
|
||||||
|
for (File cert : certs) {
|
||||||
|
try (FileInputStream certIn = new FileInputStream(cert)) {
|
||||||
|
decrypt.verifyWithCert(certIn);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new RuntimeException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
|
||||||
|
Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$");
|
||||||
|
for (String sessionKey : withSessionKey) {
|
||||||
|
if (!sessionKeyPattern.matcher(sessionKey).matches()) {
|
||||||
|
throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'.");
|
||||||
|
}
|
||||||
|
String[] split = sessionKey.split(":");
|
||||||
|
byte algorithm = (byte) Integer.parseInt(split[0]);
|
||||||
|
byte[] key = HexUtil.hexToBytes(split[1]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
decrypt.withSessionKey(new SessionKey(algorithm, key));
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
|
||||||
|
for (String password : withPassword) {
|
||||||
|
try {
|
||||||
|
decrypt.withPassword(password);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNotAfter(String notAfter, Decrypt decrypt) {
|
||||||
|
Date notAfterDate = DateParser.parseNotAfter(notAfter);
|
||||||
|
try {
|
||||||
|
decrypt.verifyNotAfter(notAfterDate);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNotBefore(String notBefore, Decrypt decrypt) {
|
||||||
|
Date notBeforeDate = DateParser.parseNotBefore(notBefore);
|
||||||
|
try {
|
||||||
|
decrypt.verifyNotBefore(notBeforeDate);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Signatures;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "detach-inband-signature-and-message",
|
||||||
|
description = "Split a clearsigned message",
|
||||||
|
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||||
|
public class DetachInbandSignatureAndMessageCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = {"--signatures-out"},
|
||||||
|
description = "Destination to which a detached signatures block will be written",
|
||||||
|
paramLabel = "SIGNATURES")
|
||||||
|
File signaturesOut;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--no-armor",
|
||||||
|
description = "ASCII armor the output",
|
||||||
|
negatable = true)
|
||||||
|
boolean armor = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage();
|
||||||
|
if (detach == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signaturesOut == null) {
|
||||||
|
throw new SOPGPException.MissingArg("--signatures-out is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
detach.noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Signatures signatures = detach
|
||||||
|
.message(System.in).writeTo(System.out);
|
||||||
|
if (!signaturesOut.createNewFile()) {
|
||||||
|
throw new SOPGPException.OutputExists("Destination of --signatures-out already exists.");
|
||||||
|
}
|
||||||
|
signatures.writeTo(new FileOutputStream(signaturesOut));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.enums.EncryptAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Encrypt;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "encrypt",
|
||||||
|
description = "Encrypt a message from standard input",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class EncryptCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--no-armor",
|
||||||
|
description = "ASCII armor the output",
|
||||||
|
negatable = true)
|
||||||
|
boolean armor = true;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--as"},
|
||||||
|
description = "Type of the input data. Defaults to 'binary'",
|
||||||
|
paramLabel = "{binary|text|mime}")
|
||||||
|
EncryptAs type;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--with-password",
|
||||||
|
description = "Encrypt the message with a password",
|
||||||
|
paramLabel = "PASSWORD")
|
||||||
|
List<String> withPassword = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--sign-with",
|
||||||
|
description = "Sign the output with a private key",
|
||||||
|
paramLabel = "KEY")
|
||||||
|
List<File> signWith = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Parameters(description = "Certificates the message gets encrypted to",
|
||||||
|
index = "0..*",
|
||||||
|
paramLabel = "CERTS")
|
||||||
|
List<File> certs = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Encrypt encrypt = SopCLI.getSop().encrypt();
|
||||||
|
if (encrypt == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
try {
|
||||||
|
encrypt.mode(type);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withPassword.isEmpty() && certs.isEmpty()) {
|
||||||
|
throw new SOPGPException.MissingArg("At least one password or cert file required for encryption.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String password : withPassword) {
|
||||||
|
try {
|
||||||
|
encrypt.withPassword(password);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File keyFile : signWith) {
|
||||||
|
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
|
||||||
|
encrypt.signWith(keyIn);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
||||||
|
throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected);
|
||||||
|
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||||
|
throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
|
||||||
|
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
|
||||||
|
throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File certFile : certs) {
|
||||||
|
try (FileInputStream certIn = new FileInputStream(certFile)) {
|
||||||
|
encrypt.withCert(certIn);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||||
|
throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
|
||||||
|
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
|
||||||
|
throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
encrypt.noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Ready ready = encrypt.plaintext(System.in);
|
||||||
|
ready.writeTo(System.out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.ExtractCert;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "extract-cert",
|
||||||
|
description = "Extract a public key certificate from a secret key from standard input",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class ExtractCertCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--no-armor",
|
||||||
|
description = "ASCII armor the output",
|
||||||
|
negatable = true)
|
||||||
|
boolean armor = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ExtractCert extractCert = SopCLI.getSop().extractCert();
|
||||||
|
if (extractCert == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
extractCert.noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Ready ready = extractCert.key(System.in);
|
||||||
|
ready.writeTo(System.out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.GenerateKey;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "generate-key",
|
||||||
|
description = "Generate a secret key",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class GenerateKeyCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--no-armor",
|
||||||
|
description = "ASCII armor the output",
|
||||||
|
negatable = true)
|
||||||
|
boolean armor = true;
|
||||||
|
|
||||||
|
@CommandLine.Parameters(description = "User-ID, eg. \"Alice <alice@example.com>\"")
|
||||||
|
List<String> userId = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
GenerateKey generateKey = SopCLI.getSop().generateKey();
|
||||||
|
if (generateKey == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String userId : userId) {
|
||||||
|
generateKey.userId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
generateKey.noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Ready ready = generateKey.generate();
|
||||||
|
ready.writeTo(System.out);
|
||||||
|
} catch (SOPGPException.MissingArg missingArg) {
|
||||||
|
Print.errln("Missing argument.");
|
||||||
|
Print.trace(missingArg);
|
||||||
|
System.exit(missingArg.getExitCode());
|
||||||
|
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||||
|
Print.errln("Unsupported asymmetric algorithm.");
|
||||||
|
Print.trace(unsupportedAsymmetricAlgo);
|
||||||
|
System.exit(unsupportedAsymmetricAlgo.getExitCode());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.MicAlg;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SigningResult;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.enums.SignAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Sign;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "sign",
|
||||||
|
description = "Create a detached signature on the data from standard input",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class SignCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--no-armor",
|
||||||
|
description = "ASCII armor the output",
|
||||||
|
negatable = true)
|
||||||
|
boolean armor = true;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.",
|
||||||
|
paramLabel = "{binary|text}")
|
||||||
|
SignAs type;
|
||||||
|
|
||||||
|
@CommandLine.Parameters(description = "Secret keys used for signing",
|
||||||
|
paramLabel = "KEYS")
|
||||||
|
List<File> secretKeyFile = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)",
|
||||||
|
paramLabel = "MICALG")
|
||||||
|
File micAlgOut;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Sign sign = SopCLI.getSop().sign();
|
||||||
|
if (sign == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
try {
|
||||||
|
sign.mode(type);
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
Print.errln("Unsupported option '--as'");
|
||||||
|
Print.trace(unsupportedOption);
|
||||||
|
System.exit(unsupportedOption.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (micAlgOut != null && micAlgOut.exists()) {
|
||||||
|
throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretKeyFile.isEmpty()) {
|
||||||
|
Print.errln("Missing required parameter 'KEYS'.");
|
||||||
|
System.exit(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File keyFile : secretKeyFile) {
|
||||||
|
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
|
||||||
|
sign.key(keyIn);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("Cannot access file " + keyFile.getAbsolutePath());
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.KeyIsProtected e) {
|
||||||
|
Print.errln("Key " + keyFile.getName() + " is password protected.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":");
|
||||||
|
Print.trace(badData);
|
||||||
|
System.exit(badData.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
sign.noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ReadyWithResult<SigningResult> ready = sign.data(System.in);
|
||||||
|
SigningResult result = ready.writeTo(System.out);
|
||||||
|
|
||||||
|
MicAlg micAlg = result.getMicAlg();
|
||||||
|
if (micAlgOut != null) {
|
||||||
|
// Write micalg out
|
||||||
|
micAlgOut.createNewFile();
|
||||||
|
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut);
|
||||||
|
micAlg.writeTo(micAlgOutStream);
|
||||||
|
micAlgOutStream.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.ExpectedText expectedText) {
|
||||||
|
Print.errln("Expected text input, but got binary data.");
|
||||||
|
Print.trace(expectedText);
|
||||||
|
System.exit(expectedText.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.cli.picocli.DateParser;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Verify;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "verify",
|
||||||
|
description = "Verify a detached signature over the data from standard input",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class VerifyCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.Parameters(index = "0",
|
||||||
|
description = "Detached signature",
|
||||||
|
paramLabel = "SIGNATURE")
|
||||||
|
File signature;
|
||||||
|
|
||||||
|
@CommandLine.Parameters(index = "1..*",
|
||||||
|
arity = "1..*",
|
||||||
|
description = "Public key certificates",
|
||||||
|
paramLabel = "CERT")
|
||||||
|
List<File> certificates = new ArrayList<>();
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--not-before"},
|
||||||
|
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||||
|
"Reject signatures with a creation date not in range.\n" +
|
||||||
|
"Defaults to beginning of time (\"-\").",
|
||||||
|
paramLabel = "DATE")
|
||||||
|
String notBefore = "-";
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--not-after"},
|
||||||
|
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||||
|
"Reject signatures with a creation date not in range.\n" +
|
||||||
|
"Defaults to current system time (\"now\").\n" +
|
||||||
|
"Accepts special value \"-\" for end of time.",
|
||||||
|
paramLabel = "DATE")
|
||||||
|
String notAfter = "now";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Verify verify = SopCLI.getSop().verify();
|
||||||
|
if (verify == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notAfter != null) {
|
||||||
|
try {
|
||||||
|
verify.notAfter(DateParser.parseNotAfter(notAfter));
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
Print.errln("Unsupported option '--not-after'.");
|
||||||
|
Print.trace(unsupportedOption);
|
||||||
|
System.exit(unsupportedOption.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (notBefore != null) {
|
||||||
|
try {
|
||||||
|
verify.notBefore(DateParser.parseNotBefore(notBefore));
|
||||||
|
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||||
|
Print.errln("Unsupported option '--not-before'.");
|
||||||
|
Print.trace(unsupportedOption);
|
||||||
|
System.exit(unsupportedOption.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File certFile : certificates) {
|
||||||
|
try (FileInputStream certIn = new FileInputStream(certFile)) {
|
||||||
|
verify.cert(certIn);
|
||||||
|
} catch (FileNotFoundException fileNotFoundException) {
|
||||||
|
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
|
||||||
|
|
||||||
|
Print.trace(fileNotFoundException);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(ioException);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate.");
|
||||||
|
Print.trace(badData);
|
||||||
|
System.exit(badData.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signature != null) {
|
||||||
|
try (FileInputStream sigIn = new FileInputStream(signature)) {
|
||||||
|
verify.signatures(sigIn);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature.");
|
||||||
|
Print.trace(badData);
|
||||||
|
System.exit(badData.getExitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Verification> verifications = null;
|
||||||
|
try {
|
||||||
|
verifications = verify.data(System.in);
|
||||||
|
} catch (SOPGPException.NoSignature e) {
|
||||||
|
Print.errln("No verifiable signature found.");
|
||||||
|
Print.trace(e);
|
||||||
|
System.exit(e.getExitCode());
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
Print.errln("IO Error.");
|
||||||
|
Print.trace(ioException);
|
||||||
|
System.exit(1);
|
||||||
|
} catch (SOPGPException.BadData badData) {
|
||||||
|
Print.errln("Standard Input appears not to contain a valid OpenPGP message.");
|
||||||
|
Print.trace(badData);
|
||||||
|
System.exit(badData.getExitCode());
|
||||||
|
}
|
||||||
|
for (Verification verification : verifications) {
|
||||||
|
Print.outln(verification.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import sop.cli.picocli.Print;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Version;
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "version", description = "Display version information about the tool",
|
||||||
|
exitCodeOnInvalidInput = 37)
|
||||||
|
public class VersionCmd implements Runnable {
|
||||||
|
|
||||||
|
@CommandLine.ArgGroup()
|
||||||
|
Exclusive exclusive;
|
||||||
|
|
||||||
|
static class Exclusive {
|
||||||
|
@CommandLine.Option(names = "--extended", description = "Print an extended version string.")
|
||||||
|
boolean extended;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.")
|
||||||
|
boolean backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Version version = SopCLI.getSop().version();
|
||||||
|
if (version == null) {
|
||||||
|
throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusive == null) {
|
||||||
|
Print.outln(version.getName() + " " + version.getVersion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusive.extended) {
|
||||||
|
Print.outln(version.getExtendedVersion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusive.backend) {
|
||||||
|
Print.outln(version.getBackendVersion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subcommands of the PGPainless SOP.
|
||||||
|
*/
|
||||||
|
package sop.cli.picocli.commands;
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the Stateless OpenPGP Command Line Interface using Picocli.
|
||||||
|
*/
|
||||||
|
package sop.cli.picocli;
|
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
public class DateParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotAfterDashReturnsEndOfTime() {
|
||||||
|
assertEquals(DateParser.END_OF_TIME, DateParser.parseNotAfter("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotBeforeDashReturnsBeginningOfTime() {
|
||||||
|
assertEquals(DateParser.BEGINNING_OF_TIME, DateParser.parseNotBefore("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotAfterNowReturnsNow() {
|
||||||
|
assertEquals(new Date().getTime(), DateParser.parseNotAfter("now").getTime(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotBeforeNowReturnsNow() {
|
||||||
|
assertEquals(new Date().getTime(), DateParser.parseNotBefore("now").getTime(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotAfterTimestamp() {
|
||||||
|
String timestamp = "2019-10-24T23:48:29Z";
|
||||||
|
Date date = DateParser.parseNotAfter(timestamp);
|
||||||
|
assertEquals(timestamp, UTCUtil.formatUTCDate(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseNotBeforeTimestamp() {
|
||||||
|
String timestamp = "2019-10-29T18:36:45Z";
|
||||||
|
Date date = DateParser.parseNotBefore(timestamp);
|
||||||
|
assertEquals(timestamp, UTCUtil.formatUTCDate(date));
|
||||||
|
}
|
||||||
|
}
|
123
sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java
Normal file
123
sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public class FileUtilTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setup() {
|
||||||
|
FileUtil.setEnvironmentVariableResolver(new FileUtil.EnvironmentVariableResolver() {
|
||||||
|
@Override
|
||||||
|
public String resolveEnvironmentVariable(String name) {
|
||||||
|
if (name.equals("test123")) {
|
||||||
|
return "test321";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_ThrowsForNull() {
|
||||||
|
assertThrows(NullPointerException.class, () -> FileUtil.getFile(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_prfxEnvAlreadyExists() throws IOException {
|
||||||
|
File tempFile = new File("@ENV:test");
|
||||||
|
tempFile.createNewFile();
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
|
||||||
|
assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@ENV:test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_EnvironmentVariable() {
|
||||||
|
File file = FileUtil.getFile("@ENV:test123");
|
||||||
|
assertEquals("test321", file.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_nonExistentEnvVariable() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@ENV:INVALID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_prfxFdAlreadyExists() throws IOException {
|
||||||
|
File tempFile = new File("@FD:1");
|
||||||
|
tempFile.createNewFile();
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
|
||||||
|
assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@FD:1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFile_prfxFdNotSupported() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@FD:2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createNewFileOrThrow_throwsForNull() {
|
||||||
|
assertThrows(NullPointerException.class, () -> FileUtil.createNewFileOrThrow(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createNewFileOrThrow_success() throws IOException {
|
||||||
|
File dir = Files.createTempDirectory("test").toFile();
|
||||||
|
dir.deleteOnExit();
|
||||||
|
File file = new File(dir, "file");
|
||||||
|
|
||||||
|
assertFalse(file.exists());
|
||||||
|
FileUtil.createNewFileOrThrow(file);
|
||||||
|
assertTrue(file.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createNewFileOrThrow_alreadyExists() throws IOException {
|
||||||
|
File dir = Files.createTempDirectory("test").toFile();
|
||||||
|
dir.deleteOnExit();
|
||||||
|
File file = new File(dir, "file");
|
||||||
|
|
||||||
|
FileUtil.createNewFileOrThrow(file);
|
||||||
|
assertTrue(file.exists());
|
||||||
|
assertThrows(SOPGPException.OutputExists.class, () -> FileUtil.createNewFileOrThrow(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFileInputStream_success() throws IOException {
|
||||||
|
File dir = Files.createTempDirectory("test").toFile();
|
||||||
|
dir.deleteOnExit();
|
||||||
|
File file = new File(dir, "file");
|
||||||
|
|
||||||
|
FileUtil.createNewFileOrThrow(file);
|
||||||
|
FileInputStream inputStream = FileUtil.getFileInputStream(file.getAbsolutePath());
|
||||||
|
assertNotNull(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFileInputStream_fileNotFound() throws IOException {
|
||||||
|
File dir = Files.createTempDirectory("test").toFile();
|
||||||
|
dir.deleteOnExit();
|
||||||
|
File file = new File(dir, "file");
|
||||||
|
|
||||||
|
assertThrows(SOPGPException.MissingInput.class,
|
||||||
|
() -> FileUtil.getFileInputStream(file.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
}
|
119
sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java
Normal file
119
sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.operation.Armor;
|
||||||
|
import sop.operation.Dearmor;
|
||||||
|
import sop.operation.Decrypt;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
|
import sop.operation.Encrypt;
|
||||||
|
import sop.operation.ExtractCert;
|
||||||
|
import sop.operation.GenerateKey;
|
||||||
|
import sop.operation.Sign;
|
||||||
|
import sop.operation.Verify;
|
||||||
|
import sop.operation.Version;
|
||||||
|
|
||||||
|
public class SOPTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(69)
|
||||||
|
public void assertExitOnInvalidSubcommand() {
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void assertThrowsIfNoSOPBackendSet() {
|
||||||
|
SopCLI.SOP_INSTANCE = null;
|
||||||
|
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1)
|
||||||
|
SopCLI.main(new String[] {"armor"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void UnsupportedSubcommandsTest() {
|
||||||
|
SOP nullCommandSOP = new SOP() {
|
||||||
|
@Override
|
||||||
|
public Version version() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GenerateKey generateKey() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtractCert extractCert() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sign sign() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Verify verify() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Encrypt encrypt() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Decrypt decrypt() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Armor armor() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dearmor dearmor() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SopCLI.setSopInstance(nullCommandSOP);
|
||||||
|
|
||||||
|
List<String[]> commands = new ArrayList<>();
|
||||||
|
commands.add(new String[] {"armor"});
|
||||||
|
commands.add(new String[] {"dearmor"});
|
||||||
|
commands.add(new String[] {"decrypt"});
|
||||||
|
commands.add(new String[] {"detach-inband-signature-and-message"});
|
||||||
|
commands.add(new String[] {"encrypt"});
|
||||||
|
commands.add(new String[] {"extract-cert"});
|
||||||
|
commands.add(new String[] {"generate-key"});
|
||||||
|
commands.add(new String[] {"sign"});
|
||||||
|
commands.add(new String[] {"verify", "signature.asc", "cert.asc"});
|
||||||
|
commands.add(new String[] {"version"});
|
||||||
|
|
||||||
|
for (String[] command : commands) {
|
||||||
|
int exit = SopCLI.execute(command);
|
||||||
|
assertEquals(69, exit, "Unexpected exit code for non-implemented command " + Arrays.toString(command) + ": " + exit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import com.ginsberg.junit.exit.FailOnSystemExit;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.enums.ArmorLabel;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Armor;
|
||||||
|
|
||||||
|
public class ArmorCmdTest {
|
||||||
|
|
||||||
|
private Armor armor;
|
||||||
|
private SOP sop;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws SOPGPException.BadData {
|
||||||
|
armor = mock(Armor.class);
|
||||||
|
sop = mock(SOP.class);
|
||||||
|
when(sop.armor()).thenReturn(armor);
|
||||||
|
when(armor.data((InputStream) any())).thenReturn(nopReady());
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"armor"});
|
||||||
|
verify(armor, never()).label(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption {
|
||||||
|
for (ArmorLabel label : ArmorLabel.values()) {
|
||||||
|
SopCLI.main(new String[] {"armor", "--label", label.name()});
|
||||||
|
verify(armor, times(1)).label(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData {
|
||||||
|
SopCLI.main(new String[] {"armor"});
|
||||||
|
verify(armor, times(1)).data((InputStream) any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertThrowsForInvalidLabel() {
|
||||||
|
SopCLI.main(new String[] {"armor", "--label", "Invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported."));
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"armor", "--label", "Sig"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void ifBadDataExit41() throws SOPGPException.BadData {
|
||||||
|
when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"armor"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@FailOnSystemExit
|
||||||
|
public void ifNoErrorsNoExit() {
|
||||||
|
when(sop.armor()).thenReturn(armor);
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"armor"});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ready nopReady() {
|
||||||
|
return new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Dearmor;
|
||||||
|
|
||||||
|
public class DearmorCmdTest {
|
||||||
|
|
||||||
|
private SOP sop;
|
||||||
|
private Dearmor dearmor;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws IOException, SOPGPException.BadData {
|
||||||
|
sop = mock(SOP.class);
|
||||||
|
dearmor = mock(Dearmor.class);
|
||||||
|
when(dearmor.data((InputStream) any())).thenReturn(nopReady());
|
||||||
|
when(sop.dearmor()).thenReturn(dearmor);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ready nopReady() {
|
||||||
|
return new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
|
||||||
|
SopCLI.main(new String[] {"dearmor"});
|
||||||
|
verify(dearmor, times(1)).data((InputStream) any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
|
||||||
|
when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
|
||||||
|
SopCLI.main(new String[] {"dearmor"});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,344 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentMatcher;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import sop.DecryptionResult;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.SessionKey;
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.cli.picocli.DateParser;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Decrypt;
|
||||||
|
import sop.util.HexUtil;
|
||||||
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
public class DecryptCmdTest {
|
||||||
|
|
||||||
|
private Decrypt decrypt;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws SOPGPException.UnsupportedOption, SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.PasswordNotHumanReadable, SOPGPException.CannotDecrypt {
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
decrypt = mock(Decrypt.class);
|
||||||
|
|
||||||
|
when(decrypt.verifyNotAfter(any())).thenReturn(decrypt);
|
||||||
|
when(decrypt.verifyNotBefore(any())).thenReturn(decrypt);
|
||||||
|
when(decrypt.withPassword(any())).thenReturn(decrypt);
|
||||||
|
when(decrypt.withSessionKey(any())).thenReturn(decrypt);
|
||||||
|
when(decrypt.withKey((InputStream) any())).thenReturn(decrypt);
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenReturn(nopReadyWithResult());
|
||||||
|
|
||||||
|
when(sop.decrypt()).thenReturn(decrypt);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadyWithResult<DecryptionResult> nopReadyWithResult() {
|
||||||
|
return new ReadyWithResult<DecryptionResult>() {
|
||||||
|
@Override
|
||||||
|
public DecryptionResult writeTo(OutputStream outputStream) {
|
||||||
|
return new DecryptionResult(null, Collections.emptyList());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(19)
|
||||||
|
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt {
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
|
||||||
|
SopCLI.main(new String[] {"decrypt"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt {
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"decrypt"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(31)
|
||||||
|
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
|
||||||
|
SOPGPException.UnsupportedOption {
|
||||||
|
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--with-password", "pretendThisIsNotReadable"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--with-password", "orange"});
|
||||||
|
verify(decrypt, times(1)).withPassword("orange");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
||||||
|
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported."));
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date now = new Date();
|
||||||
|
SopCLI.main(new String[] {"decrypt"});
|
||||||
|
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME);
|
||||||
|
verify(decrypt, times(1)).verifyNotAfter(
|
||||||
|
ArgumentMatchers.argThat(argument -> {
|
||||||
|
// allow 1-second difference
|
||||||
|
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"});
|
||||||
|
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME);
|
||||||
|
verify(decrypt, times(1)).verifyNotAfter(DateParser.END_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertVerifyNotAfterAndBeforeNowResultsInMinTimeRange() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date now = new Date();
|
||||||
|
ArgumentMatcher<Date> isMaxOneSecOff = argument -> {
|
||||||
|
// Allow less than 1-second difference
|
||||||
|
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-before", "now", "--not-after", "now"});
|
||||||
|
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
|
||||||
|
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void assertMalformedDateInNotBeforeCausesExit1() {
|
||||||
|
// ParserException causes exit(1)
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-before", "invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void assertMalformedDateInNotAfterCausesExit1() {
|
||||||
|
// ParserException causes exit(1)
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-after", "invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-after", "now"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--not-before", "now"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(59)
|
||||||
|
public void assertExistingSessionKeyOutFileCausesExit59() throws IOException {
|
||||||
|
File tempFile = File.createTempFile("existing-session-key-", ".tmp");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
|
||||||
|
Path tempDir = Files.createTempDirectory("session-key-out-dir");
|
||||||
|
File tempFile = new File(tempDir.toFile(), "session-key");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertSessionKeyIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
|
||||||
|
byte[] key = "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137".getBytes(StandardCharsets.UTF_8);
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
|
||||||
|
@Override
|
||||||
|
public DecryptionResult writeTo(OutputStream outputStream) {
|
||||||
|
return new DecryptionResult(
|
||||||
|
new SessionKey((byte) 9, key),
|
||||||
|
Collections.emptyList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Path tempDir = Files.createTempDirectory("session-key-out-dir");
|
||||||
|
File tempFile = new File(tempDir.toFile(), "session-key");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
|
||||||
|
try (FileInputStream fileIn = new FileInputStream(tempFile)) {
|
||||||
|
byte[] buf = new byte[32];
|
||||||
|
int read = fileIn.read(buf);
|
||||||
|
while (read != -1) {
|
||||||
|
bytesInFile.write(buf, 0, read);
|
||||||
|
read = fileIn.read(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] algAndKey = new byte[key.length + 1];
|
||||||
|
algAndKey[0] = (byte) 9;
|
||||||
|
System.arraycopy(key, 0, algAndKey, 1, key.length);
|
||||||
|
assertArrayEquals(algAndKey, bytesInFile.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(29)
|
||||||
|
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt());
|
||||||
|
SopCLI.main(new String[] {"decrypt"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(3)
|
||||||
|
public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
|
||||||
|
@Override
|
||||||
|
public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature {
|
||||||
|
throw new SOPGPException.NoSignature();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SopCLI.main(new String[] {"decrypt"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
|
||||||
|
when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
File tempFile = File.createTempFile("verify-with-", ".tmp");
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(61)
|
||||||
|
public void unexistentCertFileCausesExit61() {
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(59)
|
||||||
|
public void existingVerifyOutCausesExit59() throws IOException {
|
||||||
|
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
|
||||||
|
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
|
||||||
|
File certFile = File.createTempFile("verify-out-cert", ".asc");
|
||||||
|
File verifyOut = new File(certFile.getParent(), "verify-out.txt");
|
||||||
|
if (verifyOut.exists()) {
|
||||||
|
verifyOut.delete();
|
||||||
|
}
|
||||||
|
verifyOut.deleteOnExit();
|
||||||
|
Date date = UTCUtil.parseUTCDate("2021-07-11T20:58:23Z");
|
||||||
|
when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
|
||||||
|
@Override
|
||||||
|
public DecryptionResult writeTo(OutputStream outputStream) {
|
||||||
|
return new DecryptionResult(null, Collections.singletonList(
|
||||||
|
new Verification(
|
||||||
|
date,
|
||||||
|
"1B66A707819A920925BC6777C3E0AFC0B2DFF862",
|
||||||
|
"C8CD564EBF8D7BBA90611D8D071773658BF6BF86"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()});
|
||||||
|
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
|
||||||
|
String line = reader.readLine();
|
||||||
|
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption {
|
||||||
|
SessionKey key1 = new SessionKey((byte) 9, HexUtil.hexToBytes("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"));
|
||||||
|
SessionKey key2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
|
||||||
|
SopCLI.main(new String[] {"decrypt",
|
||||||
|
"--with-session-key", "9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137",
|
||||||
|
"--with-session-key", "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"});
|
||||||
|
verify(decrypt).withSessionKey(key1);
|
||||||
|
verify(decrypt).withSessionKey(key2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void assertMalformedSessionKeysResultInExit1() {
|
||||||
|
SopCLI.main(new String[] {"decrypt",
|
||||||
|
"--with-session-key", "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
||||||
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
|
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(61)
|
||||||
|
public void assertKeyFileNotFoundCausesExit61() {
|
||||||
|
SopCLI.main(new String[] {"decrypt", "nonexistent-key"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(67)
|
||||||
|
public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
|
||||||
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
|
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(13)
|
||||||
|
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
||||||
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException()));
|
||||||
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
|
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(23)
|
||||||
|
public void verifyOutWithoutVerifyWithCausesExit23() {
|
||||||
|
SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.enums.EncryptAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Encrypt;
|
||||||
|
|
||||||
|
public class EncryptCmdTest {
|
||||||
|
|
||||||
|
Encrypt encrypt;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws IOException {
|
||||||
|
encrypt = mock(Encrypt.class);
|
||||||
|
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
when(sop.encrypt()).thenReturn(encrypt);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(19)
|
||||||
|
public void missingBothPasswordAndCertFileCauseExit19() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--no-armor"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported."));
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--as", "Binary"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void as_invalidModeOptionCausesExit37() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--as", "invalid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption {
|
||||||
|
for (EncryptAs mode : EncryptAs.values()) {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", "0rbit"});
|
||||||
|
verify(encrypt, times(1)).mode(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(31)
|
||||||
|
public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
||||||
|
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "pretendThisIsNotReadable"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
||||||
|
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported."));
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "orange"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
|
||||||
|
File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
|
||||||
|
File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "password", "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()});
|
||||||
|
verify(encrypt, times(2)).signWith((InputStream) any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(61)
|
||||||
|
public void signWith_nonExistentKeyFileCausesExit61() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(67)
|
||||||
|
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(13)
|
||||||
|
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(79)
|
||||||
|
public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
|
||||||
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
|
||||||
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(61)
|
||||||
|
public void cert_nonExistentCertFileCausesExit61() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "invalid.asc"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(13)
|
||||||
|
public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(17)
|
||||||
|
public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception()));
|
||||||
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
|
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_notCalledByDefault() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "clownfish"});
|
||||||
|
verify(encrypt, never()).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_callGetsPassedDown() {
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "monkey", "--no-armor"});
|
||||||
|
verify(encrypt, times(1)).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void writeTo_ioExceptionCausesExit1() throws IOException {
|
||||||
|
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"encrypt", "--with-password", "wildcat"});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.ExtractCert;
|
||||||
|
|
||||||
|
public class ExtractCertCmdTest {
|
||||||
|
|
||||||
|
ExtractCert extractCert;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws IOException, SOPGPException.BadData {
|
||||||
|
extractCert = mock(ExtractCert.class);
|
||||||
|
when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
when(sop.extractCert()).thenReturn(extractCert);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_notCalledByDefault() {
|
||||||
|
SopCLI.main(new String[] {"extract-cert"});
|
||||||
|
verify(extractCert, never()).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_passedDown() {
|
||||||
|
SopCLI.main(new String[] {"extract-cert", "--no-armor"});
|
||||||
|
verify(extractCert, times(1)).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData {
|
||||||
|
when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SopCLI.main(new String[] {"extract-cert"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData {
|
||||||
|
when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"extract-cert"});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.GenerateKey;
|
||||||
|
|
||||||
|
public class GenerateKeyCmdTest {
|
||||||
|
|
||||||
|
GenerateKey generateKey;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
|
generateKey = mock(GenerateKey.class);
|
||||||
|
when(generateKey.generate()).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
when(sop.generateKey()).thenReturn(generateKey);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_notCalledByDefault() {
|
||||||
|
SopCLI.main(new String[] {"generate-key", "Alice"});
|
||||||
|
verify(generateKey, never()).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_passedDown() {
|
||||||
|
SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"});
|
||||||
|
verify(generateKey, times(1)).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void userId_multipleUserIdsPassedDownInProperOrder() {
|
||||||
|
SopCLI.main(new String[] {"generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"});
|
||||||
|
|
||||||
|
InOrder inOrder = Mockito.inOrder(generateKey);
|
||||||
|
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
|
||||||
|
inOrder.verify(generateKey).userId("Bob <bob@pgpainless.org>");
|
||||||
|
|
||||||
|
verify(generateKey, times(2)).userId(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(19)
|
||||||
|
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
|
// TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids,
|
||||||
|
// so we might want to change this test in the future.
|
||||||
|
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
|
||||||
|
SopCLI.main(new String[] {"generate-key"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(13)
|
||||||
|
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
|
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
|
SopCLI.main(new String[] {"generate-key", "Alice"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
|
when(generateKey.generate()).thenReturn(new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SopCLI.main(new String[] {"generate-key", "Alice"});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.SigningResult;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Sign;
|
||||||
|
|
||||||
|
public class SignCmdTest {
|
||||||
|
|
||||||
|
Sign sign;
|
||||||
|
File keyFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() throws IOException, SOPGPException.ExpectedText {
|
||||||
|
sign = mock(Sign.class);
|
||||||
|
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
|
||||||
|
@Override
|
||||||
|
public SigningResult writeTo(OutputStream outputStream) {
|
||||||
|
return SigningResult.builder().build();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
when(sop.sign()).thenReturn(sign);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
|
||||||
|
keyFile = File.createTempFile("sign-", ".asc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void as_optionsAreCaseInsensitive() {
|
||||||
|
SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()});
|
||||||
|
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
|
||||||
|
SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void as_invalidOptionCausesExit37() {
|
||||||
|
SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported."));
|
||||||
|
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void key_nonExistentKeyFileCausesExit1() {
|
||||||
|
SopCLI.main(new String[] {"sign", "invalid.asc"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void key_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
||||||
|
when(sign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
|
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
||||||
|
when(sign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(19)
|
||||||
|
public void key_missingKeyFileCausesExit19() {
|
||||||
|
SopCLI.main(new String[] {"sign"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_notCalledByDefault() {
|
||||||
|
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
||||||
|
verify(sign, never()).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noArmor_passedDown() {
|
||||||
|
SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()});
|
||||||
|
verify(sign, times(1)).noArmor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
|
||||||
|
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
|
||||||
|
@Override
|
||||||
|
public SigningResult writeTo(OutputStream outputStream) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(53)
|
||||||
|
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
|
||||||
|
when(sign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
|
||||||
|
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.cli.picocli.DateParser;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Verify;
|
||||||
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
public class VerifyCmdTest {
|
||||||
|
|
||||||
|
Verify verify;
|
||||||
|
File signature;
|
||||||
|
File cert;
|
||||||
|
|
||||||
|
PrintStream originalSout;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException {
|
||||||
|
originalSout = System.out;
|
||||||
|
|
||||||
|
verify = mock(Verify.class);
|
||||||
|
when(verify.notBefore(any())).thenReturn(verify);
|
||||||
|
when(verify.notAfter(any())).thenReturn(verify);
|
||||||
|
when(verify.cert((InputStream) any())).thenReturn(verify);
|
||||||
|
when(verify.signatures((InputStream) any())).thenReturn(verify);
|
||||||
|
when(verify.data((InputStream) any())).thenReturn(
|
||||||
|
Collections.singletonList(
|
||||||
|
new Verification(
|
||||||
|
UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
|
||||||
|
"EB85BB5FA33A75E15E944E63F231550C4F47E38E",
|
||||||
|
"EB85BB5FA33A75E15E944E63F231550C4F47E38E")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
when(sop.verify()).thenReturn(verify);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
|
||||||
|
signature = File.createTempFile("signature-", ".asc");
|
||||||
|
cert = File.createTempFile("cert-", ".asc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void restoreSout() {
|
||||||
|
System.setOut(originalSout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notAfter(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notAfter_now() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date now = new Date();
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notAfter(dateMatcher(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notAfter(DateParser.END_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notBefore(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notBefore_now() throws SOPGPException.UnsupportedOption {
|
||||||
|
Date now = new Date();
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notBefore(dateMatcher(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
|
when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
||||||
|
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
verify(verify, times(1)).notAfter(dateMatcher(new Date()));
|
||||||
|
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Date dateMatcher(Date date) {
|
||||||
|
return ArgumentMatchers.argThat(argument -> Math.abs(argument.getTime() - date.getTime()) < 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void cert_fileNotFoundCausesExit1() {
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void cert_badDataCausesExit41() throws SOPGPException.BadData {
|
||||||
|
when(verify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(1)
|
||||||
|
public void signature_fileNotFoundCausesExit1() {
|
||||||
|
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void signature_badDataCausesExit41() throws SOPGPException.BadData {
|
||||||
|
when(verify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(3)
|
||||||
|
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
||||||
|
when(verify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature());
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(41)
|
||||||
|
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
||||||
|
when(verify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
||||||
|
when(verify.data((InputStream) any())).thenReturn(Arrays.asList(
|
||||||
|
new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
|
||||||
|
"EB85BB5FA33A75E15E944E63F231550C4F47E38E",
|
||||||
|
"EB85BB5FA33A75E15E944E63F231550C4F47E38E"),
|
||||||
|
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
|
||||||
|
"C90E6D36200A1B922A1509E77618196529AE5FF8",
|
||||||
|
"C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
|
||||||
|
));
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
System.setOut(new PrintStream(out));
|
||||||
|
|
||||||
|
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
||||||
|
|
||||||
|
System.setOut(originalSout);
|
||||||
|
|
||||||
|
String expected = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E\n" +
|
||||||
|
"2019-10-24T23:48:29Z C90E6D36200A1B922A1509E77618196529AE5FF8 C4BC2DDB38CCE96485EBE9C2F20691179038E5C6\n";
|
||||||
|
|
||||||
|
assertEquals(expected, out.toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.operation.Version;
|
||||||
|
|
||||||
|
public class VersionCmdTest {
|
||||||
|
|
||||||
|
private Version version;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void mockComponents() {
|
||||||
|
SOP sop = mock(SOP.class);
|
||||||
|
version = mock(Version.class);
|
||||||
|
when(version.getName()).thenReturn("MockSop");
|
||||||
|
when(version.getVersion()).thenReturn("1.0");
|
||||||
|
when(sop.version()).thenReturn(version);
|
||||||
|
|
||||||
|
SopCLI.setSopInstance(sop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertVersionCommandWorks() {
|
||||||
|
SopCLI.main(new String[] {"version"});
|
||||||
|
verify(version, times(1)).getVersion();
|
||||||
|
verify(version, times(1)).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ExpectSystemExitWithStatus(37)
|
||||||
|
public void assertInvalidOptionResultsInExit37() {
|
||||||
|
SopCLI.main(new String[] {"version", "--invalid"});
|
||||||
|
}
|
||||||
|
}
|
80
sop-java/README.md
Normal file
80
sop-java/README.md
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# SOP-Java
|
||||||
|
|
||||||
|
[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03)
|
||||||
|
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java)
|
||||||
|
[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/sop/SOP.html)
|
||||||
|
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless)
|
||||||
|
|
||||||
|
Stateless OpenPGP Protocol for Java.
|
||||||
|
|
||||||
|
This module contains interfaces that model the API described by the
|
||||||
|
[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification.
|
||||||
|
|
||||||
|
This module is not a command line application! For that, see `sop-java-picocli`.
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
The API defined by `sop-java` is super straight forward:
|
||||||
|
```java
|
||||||
|
SOP sop = ... // e.g. new org.pgpainless.sop.SOPImpl();
|
||||||
|
|
||||||
|
// Generate an OpenPGP key
|
||||||
|
byte[] key = sop.generateKey()
|
||||||
|
.userId("Alice <alice@example.org>")
|
||||||
|
.generate()
|
||||||
|
.getBytes();
|
||||||
|
|
||||||
|
// Extract the certificate (public key)
|
||||||
|
byte[] cert = sop.extractCert()
|
||||||
|
.key(key)
|
||||||
|
.getBytes();
|
||||||
|
|
||||||
|
// Encrypt a message
|
||||||
|
byte[] message = ...
|
||||||
|
byte[] encrypted = sop.encrypt()
|
||||||
|
.withCert(cert)
|
||||||
|
.signWith(key)
|
||||||
|
.plaintext(message)
|
||||||
|
.getBytes();
|
||||||
|
|
||||||
|
// Decrypt a message
|
||||||
|
ByteArrayAndResult<DecryptionResult> messageAndVerifications = sop.decrypt()
|
||||||
|
.verifyWith(cert)
|
||||||
|
.withKey(key)
|
||||||
|
.ciphertext(encrypted)
|
||||||
|
.toByteArrayAndResult();
|
||||||
|
byte[] decrypted = messageAndVerifications.getBytes();
|
||||||
|
// Signature Verifications
|
||||||
|
DecryptionResult messageInfo = messageAndVerifications.getResult();
|
||||||
|
List<Verification> signatureVerifications = messageInfo.getVerifications();
|
||||||
|
```
|
||||||
|
|
||||||
|
Furthermore, the API is capable of signing messages and verifying unencrypted signed data, as well as adding and removing ASCII armor.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
As per the spec, sop-java does not (yet) deal with encrypted OpenPGP keys.
|
||||||
|
|
||||||
|
## Why should I use this?
|
||||||
|
|
||||||
|
If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying
|
||||||
|
signatures inside your application, you probably don't want to start from scratch and instead reuse some library.
|
||||||
|
|
||||||
|
Instead of locking yourselves in by depending hard on that one library, you can simply depend on the interfaces from
|
||||||
|
`sop-java` and plug in a library (such as `pgpainless-sop`) that implements said interfaces.
|
||||||
|
|
||||||
|
That way you don't make yourself dependent from a single OpenPGP library and stay flexible.
|
||||||
|
Should another library emerge, that better suits your needs (and implements `sop-java`), you can easily switch
|
||||||
|
by swapping out the dependency with minimal changes to your code.
|
||||||
|
|
||||||
|
## Why should I *implement* this?
|
||||||
|
|
||||||
|
Did you create an [OpenPGP](https://datatracker.ietf.org/doc/html/rfc4880) implementation that can be used in the Java ecosystem?
|
||||||
|
By implementing the `sop-java` interface, you can turn your library into a command line interface (see `sop-java-picocli`).
|
||||||
|
This allows you to plug your library into the [OpenPGP interoperability test suite](https://tests.sequoia-pgp.org/)
|
||||||
|
of the [Sequoia-PGP](https://sequoia-pgp.org/) project.
|
22
sop-java/build.gradle
Normal file
22
sop-java/build.gradle
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
50
sop-java/src/main/java/sop/ByteArrayAndResult.java
Normal file
50
sop-java/src/main/java/sop/ByteArrayAndResult.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tuple of a byte array and associated result object.
|
||||||
|
* @param <T> type of result
|
||||||
|
*/
|
||||||
|
public class ByteArrayAndResult<T> {
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
private final T result;
|
||||||
|
|
||||||
|
public ByteArrayAndResult(byte[] bytes, T result) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the byte array part.
|
||||||
|
*
|
||||||
|
* @return bytes
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the result part.
|
||||||
|
*
|
||||||
|
* @return result
|
||||||
|
*/
|
||||||
|
public T getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the byte array part as an {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @return input stream
|
||||||
|
*/
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return new ByteArrayInputStream(getBytes());
|
||||||
|
}
|
||||||
|
}
|
29
sop-java/src/main/java/sop/DecryptionResult.java
Normal file
29
sop-java/src/main/java/sop/DecryptionResult.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import sop.util.Optional;
|
||||||
|
|
||||||
|
public class DecryptionResult {
|
||||||
|
|
||||||
|
private final Optional<SessionKey> sessionKey;
|
||||||
|
private final List<Verification> verifications;
|
||||||
|
|
||||||
|
public DecryptionResult(SessionKey sessionKey, List<Verification> verifications) {
|
||||||
|
this.sessionKey = Optional.ofNullable(sessionKey);
|
||||||
|
this.verifications = Collections.unmodifiableList(verifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SessionKey> getSessionKey() {
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Verification> getVerifications() {
|
||||||
|
return new ArrayList<>(verifications);
|
||||||
|
}
|
||||||
|
}
|
55
sop-java/src/main/java/sop/MicAlg.java
Normal file
55
sop-java/src/main/java/sop/MicAlg.java
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
public class MicAlg {
|
||||||
|
|
||||||
|
private final String micAlg;
|
||||||
|
|
||||||
|
public MicAlg(String micAlg) {
|
||||||
|
if (micAlg == null) {
|
||||||
|
throw new IllegalArgumentException("MicAlg String cannot be null.");
|
||||||
|
}
|
||||||
|
this.micAlg = micAlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MicAlg empty() {
|
||||||
|
return new MicAlg("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MicAlg fromHashAlgorithmId(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case 1:
|
||||||
|
return new MicAlg("pgp-md5");
|
||||||
|
case 2:
|
||||||
|
return new MicAlg("pgp-sha1");
|
||||||
|
case 3:
|
||||||
|
return new MicAlg("pgp-ripemd160");
|
||||||
|
case 8:
|
||||||
|
return new MicAlg("pgp-sha256");
|
||||||
|
case 9:
|
||||||
|
return new MicAlg("pgp-sha384");
|
||||||
|
case 10:
|
||||||
|
return new MicAlg("pgp-sha512");
|
||||||
|
case 11:
|
||||||
|
return new MicAlg("pgp-sha224");
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMicAlg() {
|
||||||
|
return micAlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(OutputStream outputStream) {
|
||||||
|
PrintWriter pw = new PrintWriter(outputStream);
|
||||||
|
pw.write(getMicAlg());
|
||||||
|
pw.close();
|
||||||
|
}
|
||||||
|
}
|
45
sop-java/src/main/java/sop/Ready.java
Normal file
45
sop-java/src/main/java/sop/Ready.java
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public abstract class Ready {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the data to the provided output stream.
|
||||||
|
*
|
||||||
|
* @param outputStream output stream
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
public abstract void writeTo(OutputStream outputStream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning
|
||||||
|
* the array.
|
||||||
|
*
|
||||||
|
* @return data as byte array
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
writeTo(bytes);
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an input stream containing the data.
|
||||||
|
*
|
||||||
|
* @return input stream
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return new ByteArrayInputStream(getBytes());
|
||||||
|
}
|
||||||
|
}
|
41
sop-java/src/main/java/sop/ReadyWithResult.java
Normal file
41
sop-java/src/main/java/sop/ReadyWithResult.java
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public abstract class ReadyWithResult<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the data e.g. decrypted plaintext to the provided output stream and return the result of the
|
||||||
|
* processing operation.
|
||||||
|
*
|
||||||
|
* @param outputStream output stream
|
||||||
|
* @return result, eg. signatures
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
* @throws SOPGPException.NoSignature if there are no valid signatures found
|
||||||
|
*/
|
||||||
|
public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data as a {@link ByteArrayAndResult}.
|
||||||
|
* Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while
|
||||||
|
* {@link ByteArrayAndResult#getResult()} will grant access to the appended result.
|
||||||
|
*
|
||||||
|
* @return byte array and result
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
* @throws SOPGPException.NoSignature if there are no valid signatures found
|
||||||
|
*/
|
||||||
|
public ByteArrayAndResult<T> toByteArrayAndResult() throws IOException, SOPGPException.NoSignature {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
T result = writeTo(bytes);
|
||||||
|
return new ByteArrayAndResult<>(bytes.toByteArray(), result);
|
||||||
|
}
|
||||||
|
}
|
95
sop-java/src/main/java/sop/SOP.java
Normal file
95
sop-java/src/main/java/sop/SOP.java
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import sop.operation.Armor;
|
||||||
|
import sop.operation.Dearmor;
|
||||||
|
import sop.operation.Decrypt;
|
||||||
|
import sop.operation.DetachInbandSignatureAndMessage;
|
||||||
|
import sop.operation.Encrypt;
|
||||||
|
import sop.operation.ExtractCert;
|
||||||
|
import sop.operation.GenerateKey;
|
||||||
|
import sop.operation.Sign;
|
||||||
|
import sop.operation.Verify;
|
||||||
|
import sop.operation.Version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless OpenPGP Interface.
|
||||||
|
*/
|
||||||
|
public interface SOP {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the implementations name and version.
|
||||||
|
*
|
||||||
|
* @return version
|
||||||
|
*/
|
||||||
|
Version version();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a secret key.
|
||||||
|
* Customize the operation using the builder {@link GenerateKey}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
GenerateKey generateKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a certificate (public key) from a secret key.
|
||||||
|
* Customize the operation using the builder {@link ExtractCert}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
ExtractCert extractCert();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create detached signatures.
|
||||||
|
* Customize the operation using the builder {@link Sign}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Sign sign();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify detached signatures.
|
||||||
|
* Customize the operation using the builder {@link Verify}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Verify verify();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a message.
|
||||||
|
* Customize the operation using the builder {@link Encrypt}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt encrypt();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a message.
|
||||||
|
* Customize the operation using the builder {@link Decrypt}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt decrypt();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert binary OpenPGP data to ASCII.
|
||||||
|
* Customize the operation using the builder {@link Armor}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Armor armor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ASCII armored OpenPGP data to binary.
|
||||||
|
* Customize the operation using the builder {@link Dearmor}.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Dearmor dearmor();
|
||||||
|
|
||||||
|
DetachInbandSignatureAndMessage detachInbandSignatureAndMessage();
|
||||||
|
}
|
79
sop-java/src/main/java/sop/SessionKey.java
Normal file
79
sop-java/src/main/java/sop/SessionKey.java
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import sop.util.HexUtil;
|
||||||
|
|
||||||
|
public class SessionKey {
|
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9a-fA-F]+)$");
|
||||||
|
|
||||||
|
private final byte algorithm;
|
||||||
|
private final byte[] sessionKey;
|
||||||
|
|
||||||
|
public SessionKey(byte algorithm, byte[] sessionKey) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.sessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the symmetric algorithm octet.
|
||||||
|
*
|
||||||
|
* @return algorithm id
|
||||||
|
*/
|
||||||
|
public byte getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the session key.
|
||||||
|
*
|
||||||
|
* @return session key
|
||||||
|
*/
|
||||||
|
public byte[] getKey() {
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getAlgorithm() * 17 + Arrays.hashCode(getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this == other) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof SessionKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionKey otherKey = (SessionKey) other;
|
||||||
|
return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SessionKey fromString(String string) {
|
||||||
|
Matcher matcher = PATTERN.matcher(string);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new IllegalArgumentException("Provided session key does not match expected format.");
|
||||||
|
}
|
||||||
|
byte algorithm = Byte.parseByte(matcher.group(1));
|
||||||
|
String key = matcher.group(2);
|
||||||
|
|
||||||
|
return new SessionKey(algorithm, HexUtil.hexToBytes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey);
|
||||||
|
}
|
||||||
|
}
|
21
sop-java/src/main/java/sop/Signatures.java
Normal file
21
sop-java/src/main/java/sop/Signatures.java
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public abstract class Signatures extends Ready {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write OpenPGP signatures to the provided output stream.
|
||||||
|
*
|
||||||
|
* @param signatureOutputStream output stream
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public abstract void writeTo(OutputStream signatureOutputStream) throws IOException;
|
||||||
|
|
||||||
|
}
|
50
sop-java/src/main/java/sop/SigningResult.java
Normal file
50
sop-java/src/main/java/sop/SigningResult.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains various information about a signed message.
|
||||||
|
*/
|
||||||
|
public final class SigningResult {
|
||||||
|
|
||||||
|
private final MicAlg micAlg;
|
||||||
|
|
||||||
|
private SigningResult(MicAlg micAlg) {
|
||||||
|
this.micAlg = micAlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a string identifying the digest mechanism used to create the signed message.
|
||||||
|
* This is useful for setting the micalg= parameter for the multipart/signed
|
||||||
|
* content type of a PGP/MIME object as described in section 5 of [RFC3156].
|
||||||
|
*
|
||||||
|
* If more than one signature was generated and different digest mechanisms were used,
|
||||||
|
* the value of the micalg object is an empty string.
|
||||||
|
*
|
||||||
|
* @return micalg
|
||||||
|
*/
|
||||||
|
public MicAlg getMicAlg() {
|
||||||
|
return micAlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private MicAlg micAlg;
|
||||||
|
|
||||||
|
public Builder setMicAlg(MicAlg micAlg) {
|
||||||
|
this.micAlg = micAlg;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SigningResult build() {
|
||||||
|
SigningResult signingResult = new SigningResult(micAlg);
|
||||||
|
return signingResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
sop-java/src/main/java/sop/Verification.java
Normal file
58
sop-java/src/main/java/sop/Verification.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
public class Verification {
|
||||||
|
|
||||||
|
private final Date creationTime;
|
||||||
|
private final String signingKeyFingerprint;
|
||||||
|
private final String signingCertFingerprint;
|
||||||
|
|
||||||
|
public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) {
|
||||||
|
this.creationTime = creationTime;
|
||||||
|
this.signingKeyFingerprint = signingKeyFingerprint;
|
||||||
|
this.signingCertFingerprint = signingCertFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the signatures' creation time.
|
||||||
|
*
|
||||||
|
* @return signature creation time
|
||||||
|
*/
|
||||||
|
public Date getCreationTime() {
|
||||||
|
return creationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the fingerprint of the signing (sub)key.
|
||||||
|
*
|
||||||
|
* @return signing key fingerprint
|
||||||
|
*/
|
||||||
|
public String getSigningKeyFingerprint() {
|
||||||
|
return signingKeyFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the fingerprint fo the signing certificate.
|
||||||
|
*
|
||||||
|
* @return signing certificate fingerprint
|
||||||
|
*/
|
||||||
|
public String getSigningCertFingerprint() {
|
||||||
|
return signingCertFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return UTCUtil.formatUTCDate(getCreationTime()) +
|
||||||
|
' ' +
|
||||||
|
getSigningKeyFingerprint() +
|
||||||
|
' ' +
|
||||||
|
getSigningCertFingerprint();
|
||||||
|
}
|
||||||
|
}
|
13
sop-java/src/main/java/sop/enums/ArmorLabel.java
Normal file
13
sop-java/src/main/java/sop/enums/ArmorLabel.java
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.enums;
|
||||||
|
|
||||||
|
public enum ArmorLabel {
|
||||||
|
Auto,
|
||||||
|
Sig,
|
||||||
|
Key,
|
||||||
|
Cert,
|
||||||
|
Message
|
||||||
|
}
|
11
sop-java/src/main/java/sop/enums/EncryptAs.java
Normal file
11
sop-java/src/main/java/sop/enums/EncryptAs.java
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.enums;
|
||||||
|
|
||||||
|
public enum EncryptAs {
|
||||||
|
Binary,
|
||||||
|
Text,
|
||||||
|
MIME
|
||||||
|
}
|
10
sop-java/src/main/java/sop/enums/SignAs.java
Normal file
10
sop-java/src/main/java/sop/enums/SignAs.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.enums;
|
||||||
|
|
||||||
|
public enum SignAs {
|
||||||
|
Binary,
|
||||||
|
Text
|
||||||
|
}
|
9
sop-java/src/main/java/sop/enums/package-info.java
Normal file
9
sop-java/src/main/java/sop/enums/package-info.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless OpenPGP Interface for Java.
|
||||||
|
* Enumerations.
|
||||||
|
*/
|
||||||
|
package sop.enums;
|
316
sop-java/src/main/java/sop/exception/SOPGPException.java
Normal file
316
sop-java/src/main/java/sop/exception/SOPGPException.java
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.exception;
|
||||||
|
|
||||||
|
public abstract class SOPGPException extends RuntimeException {
|
||||||
|
|
||||||
|
public SOPGPException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SOPGPException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SOPGPException(Throwable e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SOPGPException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract int getExitCode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No acceptable signatures found (sop verify).
|
||||||
|
*/
|
||||||
|
public static class NoSignature extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 3;
|
||||||
|
|
||||||
|
public NoSignature() {
|
||||||
|
super("No verifiable signature found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asymmetric algorithm unsupported (sop encrypt).
|
||||||
|
*/
|
||||||
|
public static class UnsupportedAsymmetricAlgo extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 13;
|
||||||
|
|
||||||
|
public UnsupportedAsymmetricAlgo(String message, Throwable e) {
|
||||||
|
super(message, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate not encryption capable (e,g, expired, revoked, unacceptable usage).
|
||||||
|
*/
|
||||||
|
public static class CertCannotEncrypt extends SOPGPException {
|
||||||
|
public static final int EXIT_CODE = 17;
|
||||||
|
|
||||||
|
public CertCannotEncrypt(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Missing required argument.
|
||||||
|
*/
|
||||||
|
public static class MissingArg extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 19;
|
||||||
|
|
||||||
|
public MissingArg(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incomplete verification instructions (sop decrypt).
|
||||||
|
*/
|
||||||
|
public static class IncompleteVerification extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 23;
|
||||||
|
|
||||||
|
public IncompleteVerification(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unable to decrypt (sop decrypt).
|
||||||
|
*/
|
||||||
|
public static class CannotDecrypt extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 29;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-UTF-8 or otherwise unreliable password (sop encrypt).
|
||||||
|
*/
|
||||||
|
public static class PasswordNotHumanReadable extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 31;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsupported option.
|
||||||
|
*/
|
||||||
|
public static class UnsupportedOption extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 37;
|
||||||
|
|
||||||
|
public UnsupportedOption(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsupportedOption(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalid data type (no secret key where KEYS expected, etc.).
|
||||||
|
*/
|
||||||
|
public static class BadData extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 41;
|
||||||
|
|
||||||
|
public BadData(Throwable e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadData(String message, BadData badData) {
|
||||||
|
super(message, badData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-Text input where text expected.
|
||||||
|
*/
|
||||||
|
public static class ExpectedText extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 53;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output file already exists.
|
||||||
|
*/
|
||||||
|
public static class OutputExists extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 59;
|
||||||
|
|
||||||
|
public OutputExists(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input file does not exist.
|
||||||
|
*/
|
||||||
|
public static class MissingInput extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 61;
|
||||||
|
|
||||||
|
public MissingInput(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A KEYS input is protected (locked) with a password, and sop cannot unlock it.
|
||||||
|
*/
|
||||||
|
public static class KeyIsProtected extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 67;
|
||||||
|
|
||||||
|
public KeyIsProtected() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyIsProtected(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsupported subcommand.
|
||||||
|
*/
|
||||||
|
public static class UnsupportedSubcommand extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 69;
|
||||||
|
|
||||||
|
public UnsupportedSubcommand(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix.
|
||||||
|
*/
|
||||||
|
public static class UnsupportedSpecialPrefix extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 71;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A indirect input parameter is a special designator (it starts with @),
|
||||||
|
* and a filename matching the designator is actually present.
|
||||||
|
*/
|
||||||
|
public static class AmbiguousInput extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 73;
|
||||||
|
|
||||||
|
public AmbiguousInput(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key not signature-capable (e.g. expired, revoked, unacceptable usage flags)
|
||||||
|
* (sop sign and sop encrypt with --sign-with).
|
||||||
|
*/
|
||||||
|
public static class KeyCannotSign extends SOPGPException {
|
||||||
|
|
||||||
|
public static final int EXIT_CODE = 79;
|
||||||
|
|
||||||
|
public KeyCannotSign() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyCannotSign(String s, KeyCannotSign keyCannotSign) {
|
||||||
|
super(s, keyCannotSign);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getExitCode() {
|
||||||
|
return EXIT_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
sop-java/src/main/java/sop/exception/package-info.java
Normal file
9
sop-java/src/main/java/sop/exception/package-info.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless OpenPGP Interface for Java.
|
||||||
|
* Exception classes.
|
||||||
|
*/
|
||||||
|
package sop.exception;
|
41
sop-java/src/main/java/sop/operation/Armor.java
Normal file
41
sop-java/src/main/java/sop/operation/Armor.java
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.enums.ArmorLabel;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Armor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides automatic detection of label.
|
||||||
|
*
|
||||||
|
* @param label armor label
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Armor the provided data.
|
||||||
|
*
|
||||||
|
* @param data input stream of unarmored OpenPGP data
|
||||||
|
* @return armored data
|
||||||
|
*/
|
||||||
|
Ready data(InputStream data) throws SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Armor the provided data.
|
||||||
|
*
|
||||||
|
* @param data unarmored OpenPGP data
|
||||||
|
* @return armored data
|
||||||
|
*/
|
||||||
|
default Ready data(byte[] data) throws SOPGPException.BadData {
|
||||||
|
return data(new ByteArrayInputStream(data));
|
||||||
|
}
|
||||||
|
}
|
33
sop-java/src/main/java/sop/operation/Dearmor.java
Normal file
33
sop-java/src/main/java/sop/operation/Dearmor.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Dearmor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dearmor armored OpenPGP data.
|
||||||
|
*
|
||||||
|
* @param data armored OpenPGP data
|
||||||
|
* @return input stream of unarmored data
|
||||||
|
*/
|
||||||
|
Ready data(InputStream data) throws SOPGPException.BadData, IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dearmor armored OpenPGP data.
|
||||||
|
*
|
||||||
|
* @param data armored OpenPGP data
|
||||||
|
* @return input stream of unarmored data
|
||||||
|
*/
|
||||||
|
default Ready data(byte[] data) throws SOPGPException.BadData, IOException {
|
||||||
|
return data(new ByteArrayInputStream(data));
|
||||||
|
}
|
||||||
|
}
|
118
sop-java/src/main/java/sop/operation/Decrypt.java
Normal file
118
sop-java/src/main/java/sop/operation/Decrypt.java
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import sop.DecryptionResult;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SessionKey;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Decrypt {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the SOP consider signatures before this date invalid.
|
||||||
|
*
|
||||||
|
* @param timestamp timestamp
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt verifyNotBefore(Date timestamp)
|
||||||
|
throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the SOP consider signatures after this date invalid.
|
||||||
|
*
|
||||||
|
* @param timestamp timestamp
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt verifyNotAfter(Date timestamp)
|
||||||
|
throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one or more verification cert.
|
||||||
|
*
|
||||||
|
* @param cert input stream containing the cert(s)
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt verifyWithCert(InputStream cert)
|
||||||
|
throws SOPGPException.BadData,
|
||||||
|
IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one or more verification cert.
|
||||||
|
*
|
||||||
|
* @param cert byte array containing the cert(s)
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Decrypt verifyWithCert(byte[] cert)
|
||||||
|
throws SOPGPException.BadData, IOException {
|
||||||
|
return verifyWithCert(new ByteArrayInputStream(cert));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to decrypt with the given session key.
|
||||||
|
*
|
||||||
|
* @param sessionKey session key
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt withSessionKey(SessionKey sessionKey)
|
||||||
|
throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to decrypt with the given password.
|
||||||
|
*
|
||||||
|
* @param password password
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt withPassword(String password)
|
||||||
|
throws SOPGPException.PasswordNotHumanReadable,
|
||||||
|
SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one or more decryption key.
|
||||||
|
*
|
||||||
|
* @param key input stream containing the key(s)
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Decrypt withKey(InputStream key)
|
||||||
|
throws SOPGPException.KeyIsProtected,
|
||||||
|
SOPGPException.BadData,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one or more decryption key.
|
||||||
|
*
|
||||||
|
* @param key byte array containing the key(s)
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Decrypt withKey(byte[] key)
|
||||||
|
throws SOPGPException.KeyIsProtected,
|
||||||
|
SOPGPException.BadData,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo {
|
||||||
|
return withKey(new ByteArrayInputStream(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the given ciphertext, returning verification results and plaintext.
|
||||||
|
* @param ciphertext ciphertext
|
||||||
|
* @return ready with result
|
||||||
|
*/
|
||||||
|
ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
|
||||||
|
throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the given ciphertext, returning verification results and plaintext.
|
||||||
|
* @param ciphertext ciphertext
|
||||||
|
* @return ready with result
|
||||||
|
*/
|
||||||
|
default ReadyWithResult<DecryptionResult> ciphertext(byte[] ciphertext)
|
||||||
|
throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt {
|
||||||
|
return ciphertext(new ByteArrayInputStream(ciphertext));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.Signatures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split cleartext signed messages up into data and signatures.
|
||||||
|
*/
|
||||||
|
public interface DetachInbandSignatureAndMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not wrap the signatures in ASCII armor.
|
||||||
|
* @return builder
|
||||||
|
*/
|
||||||
|
DetachInbandSignatureAndMessage noArmor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach the provided cleartext signed message from its signatures.
|
||||||
|
*
|
||||||
|
* @param messageInputStream input stream containing the signed message
|
||||||
|
* @return result containing the detached message
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
ReadyWithResult<Signatures> message(InputStream messageInputStream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach the provided cleartext signed message from its signatures.
|
||||||
|
*
|
||||||
|
* @param message byte array containing the signed message
|
||||||
|
* @return result containing the detached message
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
default ReadyWithResult<Signatures> message(byte[] message) throws IOException {
|
||||||
|
return message(new ByteArrayInputStream(message));
|
||||||
|
}
|
||||||
|
}
|
109
sop-java/src/main/java/sop/operation/Encrypt.java
Normal file
109
sop-java/src/main/java/sop/operation/Encrypt.java
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.enums.EncryptAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Encrypt {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable ASCII armor encoding.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt noArmor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets encryption mode.
|
||||||
|
*
|
||||||
|
* @param mode mode
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt mode(EncryptAs mode)
|
||||||
|
throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the signer key.
|
||||||
|
*
|
||||||
|
* @param key input stream containing the encoded signer key
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt signWith(InputStream key)
|
||||||
|
throws SOPGPException.KeyIsProtected,
|
||||||
|
SOPGPException.KeyCannotSign,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo,
|
||||||
|
SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the signer key.
|
||||||
|
*
|
||||||
|
* @param key byte array containing the encoded signer key
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Encrypt signWith(byte[] key)
|
||||||
|
throws SOPGPException.KeyIsProtected,
|
||||||
|
SOPGPException.KeyCannotSign,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo,
|
||||||
|
SOPGPException.BadData {
|
||||||
|
return signWith(new ByteArrayInputStream(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt with the given password.
|
||||||
|
*
|
||||||
|
* @param password password
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt withPassword(String password)
|
||||||
|
throws SOPGPException.PasswordNotHumanReadable,
|
||||||
|
SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt with the given cert.
|
||||||
|
*
|
||||||
|
* @param cert input stream containing the encoded cert.
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Encrypt withCert(InputStream cert)
|
||||||
|
throws SOPGPException.CertCannotEncrypt,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo,
|
||||||
|
SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt with the given cert.
|
||||||
|
*
|
||||||
|
* @param cert byte array containing the encoded cert.
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Encrypt withCert(byte[] cert)
|
||||||
|
throws SOPGPException.CertCannotEncrypt,
|
||||||
|
SOPGPException.UnsupportedAsymmetricAlgo,
|
||||||
|
SOPGPException.BadData {
|
||||||
|
return withCert(new ByteArrayInputStream(cert));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt the given data yielding the ciphertext.
|
||||||
|
* @param plaintext plaintext
|
||||||
|
* @return input stream containing the ciphertext
|
||||||
|
*/
|
||||||
|
Ready plaintext(InputStream plaintext)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt the given data yielding the ciphertext.
|
||||||
|
* @param plaintext plaintext
|
||||||
|
* @return input stream containing the ciphertext
|
||||||
|
*/
|
||||||
|
default Ready plaintext(byte[] plaintext) throws IOException {
|
||||||
|
return plaintext(new ByteArrayInputStream(plaintext));
|
||||||
|
}
|
||||||
|
}
|
40
sop-java/src/main/java/sop/operation/ExtractCert.java
Normal file
40
sop-java/src/main/java/sop/operation/ExtractCert.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface ExtractCert {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable ASCII armor encoding.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
ExtractCert noArmor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the cert(s) from the provided key(s).
|
||||||
|
*
|
||||||
|
* @param keyInputStream input stream containing the encoding of one or more OpenPGP keys
|
||||||
|
* @return result containing the encoding of the keys certs
|
||||||
|
*/
|
||||||
|
Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the cert(s) from the provided key(s).
|
||||||
|
*
|
||||||
|
* @param key byte array containing the encoding of one or more OpenPGP key
|
||||||
|
* @return result containing the encoding of the keys certs
|
||||||
|
*/
|
||||||
|
default Ready key(byte[] key) throws IOException, SOPGPException.BadData {
|
||||||
|
return key(new ByteArrayInputStream(key));
|
||||||
|
}
|
||||||
|
}
|
36
sop-java/src/main/java/sop/operation/GenerateKey.java
Normal file
36
sop-java/src/main/java/sop/operation/GenerateKey.java
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.Ready;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface GenerateKey {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable ASCII armor encoding.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
GenerateKey noArmor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a user-id.
|
||||||
|
*
|
||||||
|
* @param userId user-id
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
GenerateKey userId(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the OpenPGP key and return it encoded as an {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @return key
|
||||||
|
*/
|
||||||
|
Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException;
|
||||||
|
}
|
69
sop-java/src/main/java/sop/operation/Sign.java
Normal file
69
sop-java/src/main/java/sop/operation/Sign.java
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SigningResult;
|
||||||
|
import sop.enums.SignAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Sign {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable ASCII armor encoding.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Sign noArmor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the signature mode.
|
||||||
|
* Note: This method has to be called before {@link #key(InputStream)} is called.
|
||||||
|
*
|
||||||
|
* @param mode signature mode
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one or more signing keys.
|
||||||
|
*
|
||||||
|
* @param key input stream containing encoded keys
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one or more signing keys.
|
||||||
|
*
|
||||||
|
* @param key byte array containing encoded keys
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
|
||||||
|
return key(new ByteArrayInputStream(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs data.
|
||||||
|
*
|
||||||
|
* @param data input stream containing data
|
||||||
|
* @return ready
|
||||||
|
*/
|
||||||
|
ReadyWithResult<SigningResult> data(InputStream data) throws IOException, SOPGPException.ExpectedText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs data.
|
||||||
|
*
|
||||||
|
* @param data byte array containing data
|
||||||
|
* @return ready
|
||||||
|
*/
|
||||||
|
default ReadyWithResult<SigningResult> data(byte[] data) throws IOException, SOPGPException.ExpectedText {
|
||||||
|
return data(new ByteArrayInputStream(data));
|
||||||
|
}
|
||||||
|
}
|
67
sop-java/src/main/java/sop/operation/Verify.java
Normal file
67
sop-java/src/main/java/sop/operation/Verify.java
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface Verify extends VerifySignatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the SOP implementation consider signatures before this date invalid.
|
||||||
|
*
|
||||||
|
* @param timestamp timestamp
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the SOP implementation consider signatures after this date invalid.
|
||||||
|
*
|
||||||
|
* @param timestamp timestamp
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one or more verification cert.
|
||||||
|
*
|
||||||
|
* @param cert input stream containing the encoded certs
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
Verify cert(InputStream cert) throws SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add one or more verification cert.
|
||||||
|
*
|
||||||
|
* @param cert byte array containing the encoded certs
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default Verify cert(byte[] cert) throws SOPGPException.BadData {
|
||||||
|
return cert(new ByteArrayInputStream(cert));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the signatures.
|
||||||
|
* @param signatures input stream containing encoded, detached signatures.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the signatures.
|
||||||
|
* @param signatures byte array containing encoded, detached signatures.
|
||||||
|
*
|
||||||
|
* @return builder instance
|
||||||
|
*/
|
||||||
|
default VerifySignatures signatures(byte[] signatures) throws SOPGPException.BadData {
|
||||||
|
return signatures(new ByteArrayInputStream(signatures));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
sop-java/src/main/java/sop/operation/VerifySignatures.java
Normal file
40
sop-java/src/main/java/sop/operation/VerifySignatures.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public interface VerifySignatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the signed data (without signatures).
|
||||||
|
*
|
||||||
|
* @param data signed data
|
||||||
|
* @return list of signature verifications
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
* @throws SOPGPException.NoSignature when no signature is found
|
||||||
|
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
|
||||||
|
*/
|
||||||
|
List<Verification> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the signed data (without signatures).
|
||||||
|
*
|
||||||
|
* @param data signed data
|
||||||
|
* @return list of signature verifications
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
* @throws SOPGPException.NoSignature when no signature is found
|
||||||
|
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
|
||||||
|
*/
|
||||||
|
default List<Verification> data(byte[] data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
|
||||||
|
return data(new ByteArrayInputStream(data));
|
||||||
|
}
|
||||||
|
}
|
49
sop-java/src/main/java/sop/operation/Version.java
Normal file
49
sop-java/src/main/java/sop/operation/Version.java
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.operation;
|
||||||
|
|
||||||
|
public interface Version {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the implementations name.
|
||||||
|
* e.g. "SOP",
|
||||||
|
*
|
||||||
|
* @return implementation name
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the implementations short version string.
|
||||||
|
* e.g. "1.0"
|
||||||
|
*
|
||||||
|
* @return version string
|
||||||
|
*/
|
||||||
|
String getVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return version information about the used OpenPGP backend.
|
||||||
|
* e.g. "Bouncycastle 1.70"
|
||||||
|
*
|
||||||
|
* @return backend version string
|
||||||
|
*/
|
||||||
|
String getBackendVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an extended version string containing multiple lines of version information.
|
||||||
|
* The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text
|
||||||
|
* has no defined structure.
|
||||||
|
* Example:
|
||||||
|
* <pre>
|
||||||
|
* "SOP 1.0
|
||||||
|
* Awesome PGP!
|
||||||
|
* Using Bouncycastle 1.70
|
||||||
|
* LibFoo 1.2.2
|
||||||
|
* See https://pgp.example.org/sop/ for more information"
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @return extended version string
|
||||||
|
*/
|
||||||
|
String getExtendedVersion();
|
||||||
|
}
|
9
sop-java/src/main/java/sop/operation/package-info.java
Normal file
9
sop-java/src/main/java/sop/operation/package-info.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless OpenPGP Interface for Java.
|
||||||
|
* Different cryptographic operations.
|
||||||
|
*/
|
||||||
|
package sop.operation;
|
8
sop-java/src/main/java/sop/package-info.java
Normal file
8
sop-java/src/main/java/sop/package-info.java
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless OpenPGP Interface for Java.
|
||||||
|
*/
|
||||||
|
package sop;
|
47
sop-java/src/main/java/sop/util/HexUtil.java
Normal file
47
sop-java/src/main/java/sop/util/HexUtil.java
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L.
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
public class HexUtil {
|
||||||
|
|
||||||
|
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a byte array to a hex string.
|
||||||
|
*
|
||||||
|
* @see <a href="https://stackoverflow.com/a/9855338">
|
||||||
|
* How to convert a byte array to a hex string in Java?</a>
|
||||||
|
* @param bytes bytes
|
||||||
|
* @return hex encoding
|
||||||
|
*/
|
||||||
|
public static String bytesToHex(byte[] bytes) {
|
||||||
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
|
for (int j = 0; j < bytes.length; j++) {
|
||||||
|
int v = bytes[j] & 0xFF;
|
||||||
|
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||||
|
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(hexChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a hex string into a byte array.
|
||||||
|
*
|
||||||
|
* @see <a href="https://stackoverflow.com/a/140861">
|
||||||
|
* Convert a string representation of a hex dump to a byte array using Java?</a>
|
||||||
|
* @param s hex string
|
||||||
|
* @return decoded byte array
|
||||||
|
*/
|
||||||
|
public static byte[] hexToBytes(String s) {
|
||||||
|
int len = s.length();
|
||||||
|
byte[] data = new byte[len / 2];
|
||||||
|
for (int i = 0; i < len; i += 2) {
|
||||||
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||||
|
+ Character.digit(s.charAt(i + 1), 16));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
50
sop-java/src/main/java/sop/util/Optional.java
Normal file
50
sop-java/src/main/java/sop/util/Optional.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backport of java.util.Optional for older Android versions.
|
||||||
|
*
|
||||||
|
* @param <T> item type
|
||||||
|
*/
|
||||||
|
public class Optional<T> {
|
||||||
|
|
||||||
|
private final T item;
|
||||||
|
|
||||||
|
public Optional() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional(T item) {
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Optional<T> of(T item) {
|
||||||
|
if (item == null) {
|
||||||
|
throw new NullPointerException("Item cannot be null.");
|
||||||
|
}
|
||||||
|
return new Optional<>(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Optional<T> ofNullable(T item) {
|
||||||
|
return new Optional<>(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Optional<T> ofEmpty() {
|
||||||
|
return new Optional<>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get() {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPresent() {
|
||||||
|
return item != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return item == null;
|
||||||
|
}
|
||||||
|
}
|
80
sop-java/src/main/java/sop/util/ProxyOutputStream.java
Normal file
80
sop-java/src/main/java/sop/util/ProxyOutputStream.java
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced.
|
||||||
|
* At that point, first all the buffered data is being written to the underlying stream, followed by any successive
|
||||||
|
* data that may get written to the {@link ProxyOutputStream}.
|
||||||
|
*
|
||||||
|
* This class is useful if we need to provide an {@link OutputStream} at one point in time when the final
|
||||||
|
* target output stream is not yet known.
|
||||||
|
*/
|
||||||
|
public class ProxyOutputStream extends OutputStream {
|
||||||
|
|
||||||
|
private final ByteArrayOutputStream buffer;
|
||||||
|
private OutputStream swapped;
|
||||||
|
|
||||||
|
public ProxyOutputStream() {
|
||||||
|
this.buffer = new ByteArrayOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void replaceOutputStream(OutputStream underlying) throws IOException {
|
||||||
|
if (underlying == null) {
|
||||||
|
throw new NullPointerException("Underlying OutputStream cannot be null.");
|
||||||
|
}
|
||||||
|
this.swapped = underlying;
|
||||||
|
|
||||||
|
byte[] bufferBytes = buffer.toByteArray();
|
||||||
|
swapped.write(bufferBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void write(byte[] b) throws IOException {
|
||||||
|
if (swapped == null) {
|
||||||
|
buffer.write(b);
|
||||||
|
} else {
|
||||||
|
swapped.write(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (swapped == null) {
|
||||||
|
buffer.write(b, off, len);
|
||||||
|
} else {
|
||||||
|
swapped.write(b, off, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void flush() throws IOException {
|
||||||
|
buffer.flush();
|
||||||
|
if (swapped != null) {
|
||||||
|
swapped.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() throws IOException {
|
||||||
|
buffer.close();
|
||||||
|
if (swapped != null) {
|
||||||
|
swapped.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void write(int i) throws IOException {
|
||||||
|
if (swapped == null) {
|
||||||
|
buffer.write(i);
|
||||||
|
} else {
|
||||||
|
swapped.write(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
sop-java/src/main/java/sop/util/UTCUtil.java
Normal file
56
sop-java/src/main/java/sop/util/UTCUtil.java
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to parse and format dates as ISO-8601 UTC timestamps.
|
||||||
|
*/
|
||||||
|
public class UTCUtil {
|
||||||
|
|
||||||
|
public static final SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||||
|
public static final SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] {
|
||||||
|
UTC_FORMATTER,
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"),
|
||||||
|
new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||||
|
};
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (SimpleDateFormat f : UTC_PARSERS) {
|
||||||
|
f.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse an ISO-8601 UTC timestamp from a string.
|
||||||
|
*
|
||||||
|
* @param dateString string
|
||||||
|
* @return date
|
||||||
|
*/
|
||||||
|
public static Date parseUTCDate(String dateString) {
|
||||||
|
for (SimpleDateFormat parser : UTC_PARSERS) {
|
||||||
|
try {
|
||||||
|
return parser.parse(dateString);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
// Try next parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as ISO-8601 UTC timestamp.
|
||||||
|
*
|
||||||
|
* @param date date
|
||||||
|
* @return timestamp string
|
||||||
|
*/
|
||||||
|
public static String formatUTCDate(Date date) {
|
||||||
|
return UTC_FORMATTER.format(date);
|
||||||
|
}
|
||||||
|
}
|
8
sop-java/src/main/java/sop/util/package-info.java
Normal file
8
sop-java/src/main/java/sop/util/package-info.java
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility classes.
|
||||||
|
*/
|
||||||
|
package sop.util;
|
33
sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java
Normal file
33
sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.ByteArrayAndResult;
|
||||||
|
import sop.Verification;
|
||||||
|
|
||||||
|
public class ByteArrayAndResultTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreationAndGetters() {
|
||||||
|
byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
List<Verification> result = Collections.singletonList(
|
||||||
|
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
|
||||||
|
"C90E6D36200A1B922A1509E77618196529AE5FF8",
|
||||||
|
"C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
|
||||||
|
);
|
||||||
|
ByteArrayAndResult<List<Verification>> bytesAndResult = new ByteArrayAndResult<>(bytes, result);
|
||||||
|
|
||||||
|
assertArrayEquals(bytes, bytesAndResult.getBytes());
|
||||||
|
assertEquals(result, bytesAndResult.getResult());
|
||||||
|
}
|
||||||
|
}
|
63
sop-java/src/test/java/sop/util/HexUtilTest.java
Normal file
63
sop-java/src/test/java/sop/util/HexUtilTest.java
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test using some test vectors from RFC4648.
|
||||||
|
*
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-10">RFC-4648 §10: Test Vectors</a>
|
||||||
|
*/
|
||||||
|
public class HexUtilTest {
|
||||||
|
|
||||||
|
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||||
|
private static final Charset ASCII = Charset.forName("US-ASCII");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyHexEncodeTest() {
|
||||||
|
assertHexEquals("", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeF() {
|
||||||
|
assertHexEquals("66", "f");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeFo() {
|
||||||
|
assertHexEquals("666F", "fo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeFoo() {
|
||||||
|
assertHexEquals("666F6F", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeFoob() {
|
||||||
|
assertHexEquals("666F6F62", "foob");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeFooba() {
|
||||||
|
assertHexEquals("666F6F6261", "fooba");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeFoobar() {
|
||||||
|
assertHexEquals("666F6F626172", "foobar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHexEquals(String hex, String ascii) {
|
||||||
|
assertEquals(hex, HexUtil.bytesToHex(ascii.getBytes(ASCII)));
|
||||||
|
assertArrayEquals(ascii.getBytes(ASCII), HexUtil.hexToBytes(hex));
|
||||||
|
}
|
||||||
|
}
|
53
sop-java/src/test/java/sop/util/MicAlgTest.java
Normal file
53
sop-java/src/test/java/sop/util/MicAlgTest.java
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.MicAlg;
|
||||||
|
|
||||||
|
public class MicAlgTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorNullArgThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> new MicAlg(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyMicAlgIsEmptyString() {
|
||||||
|
MicAlg empty = MicAlg.empty();
|
||||||
|
assertNotNull(empty.getMicAlg());
|
||||||
|
assertTrue(empty.getMicAlg().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromInvalidAlgorithmIdThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> MicAlg.fromHashAlgorithmId(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromHashAlgorithmIdsKnownAlgsMatch() {
|
||||||
|
Map<Integer, String> knownAlgorithmMicalgs = new HashMap<>();
|
||||||
|
knownAlgorithmMicalgs.put(1, "pgp-md5");
|
||||||
|
knownAlgorithmMicalgs.put(2, "pgp-sha1");
|
||||||
|
knownAlgorithmMicalgs.put(3, "pgp-ripemd160");
|
||||||
|
knownAlgorithmMicalgs.put(8, "pgp-sha256");
|
||||||
|
knownAlgorithmMicalgs.put(9, "pgp-sha384");
|
||||||
|
knownAlgorithmMicalgs.put(10, "pgp-sha512");
|
||||||
|
knownAlgorithmMicalgs.put(11, "pgp-sha224");
|
||||||
|
|
||||||
|
for (Integer id : knownAlgorithmMicalgs.keySet()) {
|
||||||
|
MicAlg micAlg = MicAlg.fromHashAlgorithmId(id);
|
||||||
|
assertEquals(knownAlgorithmMicalgs.get(id), micAlg.getMicAlg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
sop-java/src/test/java/sop/util/OptionalTest.java
Normal file
78
sop-java/src/test/java/sop/util/OptionalTest.java
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class OptionalTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmpty() {
|
||||||
|
Optional<String> optional = new Optional<>();
|
||||||
|
assertEmpty(optional);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testArg() {
|
||||||
|
String string = "foo";
|
||||||
|
Optional<String> optional = new Optional<>(string);
|
||||||
|
assertFalse(optional.isEmpty());
|
||||||
|
assertTrue(optional.isPresent());
|
||||||
|
assertEquals(string, optional.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOfEmpty() {
|
||||||
|
Optional<String> optional = Optional.ofEmpty();
|
||||||
|
assertEmpty(optional);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNullArg() {
|
||||||
|
Optional<String> optional = new Optional<>(null);
|
||||||
|
assertEmpty(optional);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOfWithNullArgThrows() {
|
||||||
|
assertThrows(NullPointerException.class, () -> Optional.of(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOf() {
|
||||||
|
String string = "Hello, World!";
|
||||||
|
Optional<String> optional = Optional.of(string);
|
||||||
|
assertFalse(optional.isEmpty());
|
||||||
|
assertTrue(optional.isPresent());
|
||||||
|
assertEquals(string, optional.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOfNullableWithNull() {
|
||||||
|
Optional<String> optional = Optional.ofNullable(null);
|
||||||
|
assertEmpty(optional);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOfNullableWithArg() {
|
||||||
|
Optional<String> optional = Optional.ofNullable("bar");
|
||||||
|
assertEquals("bar", optional.get());
|
||||||
|
assertFalse(optional.isEmpty());
|
||||||
|
assertTrue(optional.isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> void assertEmpty(Optional<T> optional) {
|
||||||
|
assertTrue(optional.isEmpty());
|
||||||
|
assertFalse(optional.isPresent());
|
||||||
|
|
||||||
|
assertNull(optional.get());
|
||||||
|
}
|
||||||
|
}
|
40
sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java
Normal file
40
sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class ProxyOutputStreamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replaceOutputStreamThrowsNPEForNull() {
|
||||||
|
ProxyOutputStream proxy = new ProxyOutputStream();
|
||||||
|
assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSwappingStreamPreservesWrittenBytes() throws IOException {
|
||||||
|
byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
ProxyOutputStream proxy = new ProxyOutputStream();
|
||||||
|
proxy.write(firstSection);
|
||||||
|
|
||||||
|
ByteArrayOutputStream swappedStream = new ByteArrayOutputStream();
|
||||||
|
proxy.replaceOutputStream(swappedStream);
|
||||||
|
|
||||||
|
proxy.write(secondSection);
|
||||||
|
proxy.close();
|
||||||
|
|
||||||
|
assertEquals("Foo\nBar\nBaz\n", swappedStream.toString());
|
||||||
|
}
|
||||||
|
}
|
30
sop-java/src/test/java/sop/util/ReadyTest.java
Normal file
30
sop-java/src/test/java/sop/util/ReadyTest.java
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.Ready;
|
||||||
|
|
||||||
|
public class ReadyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void readyTest() throws IOException {
|
||||||
|
byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Ready ready = new Ready() {
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
|
outputStream.write(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertArrayEquals(data, ready.getBytes());
|
||||||
|
}
|
||||||
|
}
|
44
sop-java/src/test/java/sop/util/ReadyWithResultTest.java
Normal file
44
sop-java/src/test/java/sop/util/ReadyWithResultTest.java
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.ByteArrayAndResult;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.Verification;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
public class ReadyWithResultTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadyWithResult() throws SOPGPException.NoSignature, IOException {
|
||||||
|
byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
List<Verification> result = Collections.singletonList(
|
||||||
|
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),
|
||||||
|
"C90E6D36200A1B922A1509E77618196529AE5FF8",
|
||||||
|
"C4BC2DDB38CCE96485EBE9C2F20691179038E5C6")
|
||||||
|
);
|
||||||
|
ReadyWithResult<List<Verification>> readyWithResult = new ReadyWithResult<List<Verification>>() {
|
||||||
|
@Override
|
||||||
|
public List<Verification> writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
||||||
|
outputStream.write(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ByteArrayAndResult<List<Verification>> bytesAndResult = readyWithResult.toByteArrayAndResult();
|
||||||
|
assertArrayEquals(data, bytesAndResult.getBytes());
|
||||||
|
assertEquals(result, bytesAndResult.getResult());
|
||||||
|
}
|
||||||
|
}
|
61
sop-java/src/test/java/sop/util/SessionKeyTest.java
Normal file
61
sop-java/src/test/java/sop/util/SessionKeyTest.java
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.SessionKey;
|
||||||
|
|
||||||
|
public class SessionKeyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromStringTest() {
|
||||||
|
String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD";
|
||||||
|
SessionKey sessionKey = SessionKey.fromString(string);
|
||||||
|
assertEquals(string, sessionKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void toStringTest() {
|
||||||
|
SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
|
||||||
|
assertEquals("9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", sessionKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equalsTest() {
|
||||||
|
SessionKey s1 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
|
||||||
|
SessionKey s2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
|
||||||
|
SessionKey s3 = new SessionKey((byte) 4, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));
|
||||||
|
SessionKey s4 = new SessionKey((byte) 9, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB"));
|
||||||
|
SessionKey s5 = new SessionKey((byte) 4, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB"));
|
||||||
|
|
||||||
|
assertEquals(s1, s1);
|
||||||
|
assertEquals(s1, s2);
|
||||||
|
assertEquals(s1.hashCode(), s2.hashCode());
|
||||||
|
assertNotEquals(s1, s3);
|
||||||
|
assertNotEquals(s1.hashCode(), s3.hashCode());
|
||||||
|
assertNotEquals(s1, s4);
|
||||||
|
assertNotEquals(s1.hashCode(), s4.hashCode());
|
||||||
|
assertNotEquals(s4, s5);
|
||||||
|
assertNotEquals(s4.hashCode(), s5.hashCode());
|
||||||
|
assertNotEquals(s1, null);
|
||||||
|
assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromString_missingAlgorithmIdThrows() {
|
||||||
|
String missingAlgorithId = "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD";
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(missingAlgorithId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromString_wrongDivider() {
|
||||||
|
String semicolonDivider = "9;FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD";
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(semicolonDivider));
|
||||||
|
}
|
||||||
|
}
|
23
sop-java/src/test/java/sop/util/SigningResultTest.java
Normal file
23
sop-java/src/test/java/sop/util/SigningResultTest.java
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.MicAlg;
|
||||||
|
import sop.SigningResult;
|
||||||
|
|
||||||
|
public class SigningResultTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void basicBuilderTest() {
|
||||||
|
SigningResult result = SigningResult.builder()
|
||||||
|
.setMicAlg(MicAlg.fromHashAlgorithmId(10))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertEquals("pgp-sha512", result.getMicAlg().getMicAlg());
|
||||||
|
}
|
||||||
|
}
|
48
sop-java/src/test/java/sop/util/UTCUtilTest.java
Normal file
48
sop-java/src/test/java/sop/util/UTCUtilTest.java
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test parsing some date examples from the stateless OpenPGP CLI spec.
|
||||||
|
*
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-01#section-4.1">OpenPGP Stateless CLI §4.1. Date</a>
|
||||||
|
*/
|
||||||
|
public class UTCUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseExample1() {
|
||||||
|
String timestamp = "2019-10-29T12:11:04+00:00";
|
||||||
|
Date date = UTCUtil.parseUTCDate(timestamp);
|
||||||
|
assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseExample2() {
|
||||||
|
String timestamp = "2019-10-24T23:48:29Z";
|
||||||
|
Date date = UTCUtil.parseUTCDate(timestamp);
|
||||||
|
assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseExample3() {
|
||||||
|
String timestamp = "20191029T121104Z";
|
||||||
|
Date date = UTCUtil.parseUTCDate(timestamp);
|
||||||
|
assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidDateReturnsNull() {
|
||||||
|
String invalidTimestamp = "foobar";
|
||||||
|
Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp);
|
||||||
|
assertNull(expectNull);
|
||||||
|
}
|
||||||
|
}
|
12
version.gradle
Normal file
12
version.gradle
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
ext {
|
||||||
|
shortVersion = '1.0.1'
|
||||||
|
isSnapshot = true
|
||||||
|
minAndroidSdk = 10
|
||||||
|
javaSourceCompatibility = 1.8
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue