mirror of
https://codeberg.org/PGPainless/cert-d-java.git
synced 2024-11-23 16:02:06 +01:00
Initial commit
This commit is contained in:
commit
b142f310be
46 changed files with 2494 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
libs/
|
||||||
|
|
||||||
|
*.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
|
259
build.gradle
Normal file
259
build.gradle
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
// Only generate jar for submodules
|
||||||
|
// without this we would generate an empty .jar for the project root
|
||||||
|
// https://stackoverflow.com/a/25445035
|
||||||
|
jar {
|
||||||
|
onlyIf { !sourceSets.main.allSource.files.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// For library modules, enable android api compatibility check
|
||||||
|
if (it.name != 'cli') {
|
||||||
|
// 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 = "Shared PGP Certificate Directory 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'
|
||||||
|
sopJavaVersion = '1.2.0'
|
||||||
|
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 = 'PGPainless'
|
||||||
|
description = 'Simple to use OpenPGP API for Java based on Bouncycastle'
|
||||||
|
url = 'https://github.com/pgpainless/pgpainless'
|
||||||
|
inceptionYear = '2018'
|
||||||
|
|
||||||
|
scm {
|
||||||
|
url = 'https://github.com/pgpainless/pgpainless'
|
||||||
|
connection = 'scm:https://github.com/pgpainless/pgpainless'
|
||||||
|
developerConnection = 'scm:git://github.com/pgpainless/pgpainless.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>
|
29
pgp-cert-d-java-jdbc-sqlite-lookup/build.gradle
Normal file
29
pgp-cert-d-java-jdbc-sqlite-lookup/build.gradle
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
|
||||||
|
implementation project(":pgp-cert-d-java")
|
||||||
|
api 'org.xerial:sqlite-jdbc:3.36.0.3'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import pgp.certificate_store.SubkeyLookup;
|
||||||
|
|
||||||
|
public class DatabaseSubkeyLookup implements SubkeyLookup {
|
||||||
|
|
||||||
|
private final SubkeyLookupDao dao;
|
||||||
|
|
||||||
|
public DatabaseSubkeyLookup(SubkeyLookupDao dao) {
|
||||||
|
this.dao = dao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException {
|
||||||
|
try {
|
||||||
|
List<Entry> entries = dao.selectValues(subkeyId);
|
||||||
|
Set<String> certificates = new HashSet<>();
|
||||||
|
for (Entry entry : entries) {
|
||||||
|
certificates.add(entry.getCertificate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableSet(certificates);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Cannot query for subkey lookup entries.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException {
|
||||||
|
try {
|
||||||
|
dao.insertValues(certificate, subkeyIds);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Cannot store subkey lookup entries in database.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subkey-ID database entry.
|
||||||
|
*/
|
||||||
|
public class Entry {
|
||||||
|
|
||||||
|
private final int id;
|
||||||
|
private final String certificate;
|
||||||
|
private final long subkeyId;
|
||||||
|
|
||||||
|
public Entry(int id, long subkeyId, String certificate) {
|
||||||
|
this.id = id;
|
||||||
|
this.subkeyId = subkeyId;
|
||||||
|
this.certificate = certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the internal ID of this entry in the database.
|
||||||
|
*
|
||||||
|
* @return internal id
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the key-ID of the subkey.
|
||||||
|
*
|
||||||
|
* @return subkey id
|
||||||
|
*/
|
||||||
|
public long getSubkeyId() {
|
||||||
|
return subkeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the fingerprint of the certificate the subkey belongs to.
|
||||||
|
* @return fingerprint
|
||||||
|
*/
|
||||||
|
public String getCertificate() {
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
import org.sqlite.SQLiteErrorCode;
|
||||||
|
import org.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SqliteSubkeyLookupDaoImpl implements SubkeyLookupDao {
|
||||||
|
|
||||||
|
private final String databaseUrl;
|
||||||
|
|
||||||
|
private static final String CREATE_TABLE_STMT = "" +
|
||||||
|
"CREATE TABLE IF NOT EXISTS subkey_lookup (\n" +
|
||||||
|
" id integer PRIMARY KEY,\n" + // id (internal to the database)
|
||||||
|
" certificate text NOT NULL,\n" + // certificate fingerprint
|
||||||
|
" subkey_id integer NOT NULL,\n" + // subkey id
|
||||||
|
" UNIQUE(certificate, subkey_id)\n" +
|
||||||
|
")";
|
||||||
|
|
||||||
|
private static final String INSERT_STMT = "" +
|
||||||
|
"INSERT INTO subkey_lookup(certificate, subkey_id) " +
|
||||||
|
"VALUES (?,?)";
|
||||||
|
private static final String QUERY_STMT = "" +
|
||||||
|
"SELECT * FROM subkey_lookup " +
|
||||||
|
"WHERE subkey_id=?";
|
||||||
|
|
||||||
|
public SqliteSubkeyLookupDaoImpl(String databaseURL) throws SQLException {
|
||||||
|
this.databaseUrl = databaseURL;
|
||||||
|
try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute(CREATE_TABLE_STMT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return DriverManager.getConnection(databaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SqliteSubkeyLookupDaoImpl forDatabaseFile(File databaseFile) throws SQLException {
|
||||||
|
return new SqliteSubkeyLookupDaoImpl("jdbc:sqlite:" + databaseFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int insertValues(String certificate, List<Long> subkeyIds) throws SQLException {
|
||||||
|
int inserted = 0;
|
||||||
|
try (Connection connection = getConnection(); PreparedStatement statement = connection.prepareStatement(INSERT_STMT)) {
|
||||||
|
for (long subkeyId : subkeyIds) {
|
||||||
|
try {
|
||||||
|
statement.setString(1, certificate);
|
||||||
|
statement.setLong(2, subkeyId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
inserted++;
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
// throw any exception, except:
|
||||||
|
// ignore unique constraint-related exceptions if we ignoreDuplicates
|
||||||
|
if (e.getResultCode().code == SQLiteErrorCode.SQLITE_CONSTRAINT_UNIQUE.code) {
|
||||||
|
// ignore duplicates
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Entry> selectValues(long subkeyId) throws SQLException {
|
||||||
|
List<Entry> results = new ArrayList<>();
|
||||||
|
try (Connection connection = getConnection(); PreparedStatement statement = connection.prepareStatement(QUERY_STMT)) {
|
||||||
|
statement.setLong(1, subkeyId);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
while (resultSet.next()) {
|
||||||
|
Entry entry = new Entry(
|
||||||
|
resultSet.getInt("id"),
|
||||||
|
resultSet.getLong("subkey_id"),
|
||||||
|
resultSet.getString("certificate"));
|
||||||
|
results.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface SubkeyLookupDao {
|
||||||
|
|
||||||
|
int insertValues(String certificate, List<Long> subkeyIds) throws SQLException;
|
||||||
|
|
||||||
|
List<Entry> selectValues(long subkeyId) throws SQLException;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a {@link pgp.certificate_store.SubkeyLookup} mechanism using an SQLite Database.
|
||||||
|
*/
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
|
@ -0,0 +1,21 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class EntryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleGetterTest() {
|
||||||
|
Entry entry = new Entry(1, 123L, "eb85bb5fa33a75e15e944e63f231550c4f47e38e");
|
||||||
|
|
||||||
|
assertEquals(1, entry.getId());
|
||||||
|
assertEquals(123L, entry.getSubkeyId());
|
||||||
|
assertEquals("eb85bb5fa33a75e15e944e63f231550c4f47e38e", entry.getCertificate());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class SqliteSubkeyLookupTest {
|
||||||
|
|
||||||
|
private File databaseFile;
|
||||||
|
private DatabaseSubkeyLookup lookup;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setupLookup() throws IOException, SQLException {
|
||||||
|
databaseFile = Files.createTempFile("pgp.cert.d-", "lookup.db").toFile();
|
||||||
|
databaseFile.createNewFile();
|
||||||
|
databaseFile.deleteOnExit();
|
||||||
|
lookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleInsertAndGet() throws IOException {
|
||||||
|
store("eb85bb5fa33a75e15e944e63f231550c4f47e38e", 123L, 234L);
|
||||||
|
store("d1a66e1a23b182c9980f788cfbfcc82a015e7330", 234L);
|
||||||
|
|
||||||
|
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(123L));
|
||||||
|
assertEquals(
|
||||||
|
new HashSet<>(Arrays.asList("eb85bb5fa33a75e15e944e63f231550c4f47e38e", "d1a66e1a23b182c9980f788cfbfcc82a015e7330")),
|
||||||
|
lookup.getCertificateFingerprintsForSubkeyId(234L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getNonExistingSubkeyYieldsNull() throws IOException {
|
||||||
|
assertTrue(lookup.getCertificateFingerprintsForSubkeyId(6666666).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void secondInstanceLookupTest() throws IOException, SQLException {
|
||||||
|
store("eb85bb5fa33a75e15e944e63f231550c4f47e38e", 1337L);
|
||||||
|
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337));
|
||||||
|
|
||||||
|
// do the lookup using a second db instance on the same file
|
||||||
|
DatabaseSubkeyLookup secondInstance = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile));
|
||||||
|
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ignoreInsertDuplicates() throws IOException {
|
||||||
|
store("d1a66e1a23b182c9980f788cfbfcc82a015e7330", 123L, 234L);
|
||||||
|
// per default we ignore duplicates
|
||||||
|
store("d1a66e1a23b182c9980f788cfbfcc82a015e7330", 123L, 512L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void store(String cert, long... ids) throws IOException {
|
||||||
|
List<Long> idList = new ArrayList<>();
|
||||||
|
for (long id : ids) {
|
||||||
|
idList.add(id);
|
||||||
|
}
|
||||||
|
lookup.storeCertificateSubkeyIds(cert, idList);
|
||||||
|
}
|
||||||
|
}
|
16
pgp-cert-d-java/README.md
Normal file
16
pgp-cert-d-java/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Shared PGP Certificate Directory for Java
|
||||||
|
|
||||||
|
Backend-agnostic implementation of the [Shared PGP Certificate Directory Specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/).
|
||||||
|
This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths,
|
||||||
|
locking the directory for writes etc.
|
||||||
|
|
||||||
|
To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide
|
||||||
|
support for reading and merging certificates.
|
||||||
|
|
||||||
|
`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store`.
|
30
pgp-cert-d-java/build.gradle
Normal file
30
pgp-cert-d-java/build.gradle
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
|
||||||
|
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
|
||||||
|
|
||||||
|
api project(":pgp-certificate-store")
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import pgp.certificate_store.CertificateReaderBackend;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public abstract class BackendProvider {
|
||||||
|
|
||||||
|
public abstract CertificateReaderBackend provideCertificateReaderBackend();
|
||||||
|
|
||||||
|
public abstract MergeCallback provideDefaultMergeCallback();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public class BaseDirectoryProvider {
|
||||||
|
|
||||||
|
public static File getDefaultBaseDir() {
|
||||||
|
// Check for environment variable
|
||||||
|
String baseDirFromEnv = System.getenv("PGP_CERT_D");
|
||||||
|
if (baseDirFromEnv != null) {
|
||||||
|
return new File(baseDirFromEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return OS-specific default dir
|
||||||
|
String osName = System.getProperty("os.name", "generic")
|
||||||
|
.toLowerCase();
|
||||||
|
return getDefaultBaseDirForOS(osName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getDefaultBaseDirForOS(String osName) {
|
||||||
|
String STORE_NAME = "pgp.cert.d";
|
||||||
|
if (osName.contains("win")) {
|
||||||
|
// %APPDATA%\Roaming\pgp.cert.d
|
||||||
|
return Paths.get(System.getenv("APPDATA"), "Roaming", STORE_NAME).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osName.contains("nux")) {
|
||||||
|
// $XDG_DATA_HOME/pgp.cert.d
|
||||||
|
String xdg_data_home = System.getenv("XDG_DATA_HOME");
|
||||||
|
if (xdg_data_home != null) {
|
||||||
|
return Paths.get(xdg_data_home, STORE_NAME).toFile();
|
||||||
|
}
|
||||||
|
// $HOME/.local/share/pgp.cert.d
|
||||||
|
return Paths.get(System.getProperty("user.home"), ".local", "share", STORE_NAME).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osName.contains("mac")) {
|
||||||
|
return Paths.get(System.getenv("HOME"), "Library", "Application Support", STORE_NAME).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unknown OS " + osName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caching wrapper for {@link SharedPGPCertificateDirectory} implementations.
|
||||||
|
*/
|
||||||
|
public class CachingSharedPGPCertificateDirectoryWrapper
|
||||||
|
implements SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
private static final Map<String, String> tagMap = new HashMap<>();
|
||||||
|
private static final Map<String, Certificate> certificateMap = new HashMap<>();
|
||||||
|
private final SharedPGPCertificateDirectory underlyingCertificateDirectory;
|
||||||
|
|
||||||
|
public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) {
|
||||||
|
this.underlyingCertificateDirectory = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the given certificate under the given identifier into the cache.
|
||||||
|
*
|
||||||
|
* @param identifier fingerprint or special name
|
||||||
|
* @param certificate certificate
|
||||||
|
*/
|
||||||
|
private void remember(String identifier, Certificate certificate) {
|
||||||
|
certificateMap.put(identifier, certificate);
|
||||||
|
try {
|
||||||
|
tagMap.put(identifier, certificate.getTag());
|
||||||
|
} catch (IOException e) {
|
||||||
|
tagMap.put(identifier, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true, if the cached tag differs from the provided tag.
|
||||||
|
*
|
||||||
|
* @param identifier fingerprint or special name
|
||||||
|
* @param tag tag
|
||||||
|
* @return true if cached tag differs, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean tagChanged(String identifier, String tag) {
|
||||||
|
String tack = tagMap.get(identifier);
|
||||||
|
return !tagEquals(tag, tack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true, if tag and tack are equal, false otherwise.
|
||||||
|
* @param tag tag
|
||||||
|
* @param tack other tag
|
||||||
|
* @return true if equal
|
||||||
|
*/
|
||||||
|
private static boolean tagEquals(String tag, String tack) {
|
||||||
|
return (tag == null && tack == null)
|
||||||
|
|| tag != null && tag.equals(tack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache.
|
||||||
|
*/
|
||||||
|
public void invalidate() {
|
||||||
|
certificateMap.clear();
|
||||||
|
tagMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LockingMechanism getLock() {
|
||||||
|
return underlyingCertificateDirectory.getLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = certificateMap.get(fingerprint);
|
||||||
|
if (certificate == null) {
|
||||||
|
certificate = underlyingCertificateDirectory.getByFingerprint(fingerprint);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(fingerprint, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = certificateMap.get(specialName);
|
||||||
|
if (certificate == null) {
|
||||||
|
certificate = underlyingCertificateDirectory.getBySpecialName(specialName);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(specialName, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (tagChanged(fingerprint, tag)) {
|
||||||
|
return getByFingerprint(fingerprint);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (tagChanged(specialName, tag)) {
|
||||||
|
return getBySpecialName(specialName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.insert(data, merge);
|
||||||
|
remember(certificate.getFingerprint(), certificate);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(certificate.getFingerprint(), certificate);
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException, InterruptedException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.insertWithSpecialName(specialName, data, merge);
|
||||||
|
remember(specialName, certificate);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.tryInsertWithSpecialName(specialName, data, merge);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(specialName, certificate);
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Certificate> items() {
|
||||||
|
|
||||||
|
Iterator<Certificate> iterator = underlyingCertificateDirectory.items();
|
||||||
|
|
||||||
|
return new Iterator<Certificate>() {
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return iterator.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate next() {
|
||||||
|
Certificate certificate = iterator.next();
|
||||||
|
remember(certificate.getFingerprint(), certificate);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<String> fingerprints() {
|
||||||
|
return underlyingCertificateDirectory.fingerprints();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.channels.FileLock;
|
||||||
|
import java.nio.channels.OverlappingFileLockException;
|
||||||
|
|
||||||
|
public class FileLockingMechanism implements LockingMechanism {
|
||||||
|
|
||||||
|
private final File lockFile;
|
||||||
|
private RandomAccessFile randomAccessFile;
|
||||||
|
private FileLock fileLock;
|
||||||
|
|
||||||
|
public FileLockingMechanism(File lockFile) {
|
||||||
|
this.lockFile = lockFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
|
||||||
|
return new FileLockingMechanism(new File(baseDirectory, "writelock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void lockDirectory() throws IOException, InterruptedException {
|
||||||
|
if (randomAccessFile != null) {
|
||||||
|
// we own the lock already. Let's wait...
|
||||||
|
this.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
lockFile.createNewFile();
|
||||||
|
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLock = randomAccessFile.getChannel().lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean tryLockDirectory() throws IOException {
|
||||||
|
if (randomAccessFile != null) {
|
||||||
|
// We already locked the directory for another write operation.
|
||||||
|
// We fail, since we have not yet released the lock from the other operation.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
lockFile.createNewFile();
|
||||||
|
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileLock = randomAccessFile.getChannel().tryLock();
|
||||||
|
if (fileLock == null) {
|
||||||
|
// try-lock failed, file is locked by another process.
|
||||||
|
randomAccessFile.close();
|
||||||
|
randomAccessFile = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (OverlappingFileLockException e) {
|
||||||
|
// Some other object is holding the lock.
|
||||||
|
randomAccessFile.close();
|
||||||
|
randomAccessFile = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void releaseDirectory() throws IOException {
|
||||||
|
// unlock file
|
||||||
|
if (fileLock != null) {
|
||||||
|
fileLock.release();
|
||||||
|
fileLock = null;
|
||||||
|
}
|
||||||
|
// close file
|
||||||
|
if (randomAccessFile != null) {
|
||||||
|
randomAccessFile.close();
|
||||||
|
randomAccessFile = null;
|
||||||
|
}
|
||||||
|
// delete file
|
||||||
|
if (lockFile.exists()) {
|
||||||
|
lockFile.delete();
|
||||||
|
}
|
||||||
|
// notify waiters
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class FilenameResolver {
|
||||||
|
|
||||||
|
private final File baseDirectory;
|
||||||
|
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
|
||||||
|
|
||||||
|
public FilenameResolver(File baseDirectory) {
|
||||||
|
this.baseDirectory = baseDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getBaseDirectory() {
|
||||||
|
return baseDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the file location for the certificate addressed by the given
|
||||||
|
* lowercase hexadecimal OpenPGP fingerprint.
|
||||||
|
*
|
||||||
|
* @param fingerprint fingerprint
|
||||||
|
* @return absolute certificate file location
|
||||||
|
* @throws BadNameException
|
||||||
|
*/
|
||||||
|
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
|
||||||
|
if (!isFingerprint(fingerprint)) {
|
||||||
|
throw new BadNameException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// is fingerprint
|
||||||
|
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
|
||||||
|
File file = new File(subdirectory, fingerprint.substring(2));
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getCertFileBySpecialName(String specialName) throws BadNameException {
|
||||||
|
if (!isSpecialName(specialName)) {
|
||||||
|
throw new BadNameException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File(getBaseDirectory(), specialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFingerprint(String fingerprint) {
|
||||||
|
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSpecialName(String specialName) {
|
||||||
|
return SpecialNames.lookupSpecialName(specialName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import pgp.certificate_store.SubkeyLookup;
|
||||||
|
|
||||||
|
public class InMemorySubkeyLookup implements SubkeyLookup {
|
||||||
|
|
||||||
|
private static final Map<Long, Set<String>> subkeyMap = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) {
|
||||||
|
Set<String> identifiers = subkeyMap.get(subkeyId);
|
||||||
|
if (identifiers == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableSet(identifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) {
|
||||||
|
for (long subkeyId : subkeyIds) {
|
||||||
|
Set<String> certificates = subkeyMap.get(subkeyId);
|
||||||
|
if (certificates == null) {
|
||||||
|
certificates = new HashSet<>();
|
||||||
|
subkeyMap.put(subkeyId, certificates);
|
||||||
|
}
|
||||||
|
certificates.add(certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
subkeyMap.clear();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface LockingMechanism {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the store for writes.
|
||||||
|
* Readers can continue to use the store and will always see consistent certs.
|
||||||
|
*/
|
||||||
|
void lockDirectory() throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try top lock the store for writes.
|
||||||
|
* Return false without locking the store in case the store was already locked.
|
||||||
|
*
|
||||||
|
* @return true if locking succeeded, false otherwise
|
||||||
|
*/
|
||||||
|
boolean tryLockDirectory() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the directory write-lock acquired via {@link #lockDirectory()}.
|
||||||
|
*/
|
||||||
|
void releaseDirectory() throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public interface SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
LockingMechanism getLock();
|
||||||
|
|
||||||
|
Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException;
|
||||||
|
|
||||||
|
Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException;
|
||||||
|
|
||||||
|
Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException, InterruptedException;
|
||||||
|
|
||||||
|
Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
Iterator<Certificate> items();
|
||||||
|
|
||||||
|
Iterator<String> fingerprints();
|
||||||
|
}
|
|
@ -0,0 +1,314 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.exception.NotAStoreException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.CertificateReaderBackend;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
private final FilenameResolver resolver;
|
||||||
|
private final LockingMechanism writeLock;
|
||||||
|
private final CertificateReaderBackend certificateReaderBackend;
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(backendProvider.provideCertificateReaderBackend());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(
|
||||||
|
BaseDirectoryProvider.getDefaultBaseDir(),
|
||||||
|
certificateReaderBackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(
|
||||||
|
certificateReaderBackend,
|
||||||
|
new FilenameResolver(baseDirectory),
|
||||||
|
FileLockingMechanism.defaultDirectoryFileLock(baseDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(
|
||||||
|
CertificateReaderBackend certificateReaderBackend,
|
||||||
|
FilenameResolver filenameResolver,
|
||||||
|
LockingMechanism writeLock)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this.certificateReaderBackend = certificateReaderBackend;
|
||||||
|
this.resolver = filenameResolver;
|
||||||
|
this.writeLock = writeLock;
|
||||||
|
|
||||||
|
File baseDirectory = resolver.getBaseDirectory();
|
||||||
|
if (!baseDirectory.exists()) {
|
||||||
|
if (!baseDirectory.mkdirs()) {
|
||||||
|
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (baseDirectory.isFile()) {
|
||||||
|
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LockingMechanism getLock() {
|
||||||
|
return writeLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
File certFile = resolver.getCertFileByFingerprint(fingerprint);
|
||||||
|
if (!certFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream fileIn = new FileInputStream(certFile);
|
||||||
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
|
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
|
||||||
|
|
||||||
|
if (!certificate.getFingerprint().equals(fingerprint)) {
|
||||||
|
// TODO: Figure out more suitable exception
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException {
|
||||||
|
File certFile = resolver.getCertFileBySpecialName(specialName);
|
||||||
|
if (!certFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream fileIn = new FileInputStream(certFile);
|
||||||
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
|
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = getByFingerprint(fingerprint);
|
||||||
|
if (certificate.getTag().equals(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException {
|
||||||
|
Certificate certificate = getBySpecialName(specialName);
|
||||||
|
if (certificate.getTag().equals(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException {
|
||||||
|
writeLock.lockDirectory();
|
||||||
|
|
||||||
|
Certificate certificate = _insert(data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
if (!writeLock.tryLockDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Certificate certificate = _insert(data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate _insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
|
||||||
|
Certificate existingCertificate;
|
||||||
|
File certFile;
|
||||||
|
try {
|
||||||
|
existingCertificate = getByFingerprint(newCertificate.getFingerprint());
|
||||||
|
certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
|
||||||
|
} catch (BadNameException e) {
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
|
||||||
|
newCertificate = merge.merge(newCertificate, existingCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCertificate(newCertificate, certFile);
|
||||||
|
|
||||||
|
return newCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCertificate(Certificate certificate, File certFile)
|
||||||
|
throws IOException {
|
||||||
|
certFile.getParentFile().mkdirs();
|
||||||
|
if (!certFile.exists() && !certFile.createNewFile()) {
|
||||||
|
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream certIn = certificate.getInputStream();
|
||||||
|
FileOutputStream fileOut = new FileOutputStream(certFile);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
while ((read = certIn.read(buffer)) != -1) {
|
||||||
|
fileOut.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
certIn.close();
|
||||||
|
fileOut.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException, InterruptedException {
|
||||||
|
writeLock.lockDirectory();
|
||||||
|
|
||||||
|
Certificate certificate = _insertSpecial(specialName, data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (!writeLock.tryLockDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Certificate certificate = _insertSpecial(specialName, data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate _insertSpecial(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
|
||||||
|
Certificate existingCertificate = getBySpecialName(specialName);
|
||||||
|
File certFile = resolver.getCertFileBySpecialName(specialName);
|
||||||
|
|
||||||
|
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
|
||||||
|
newCertificate = merge.merge(newCertificate, existingCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCertificate(newCertificate, certFile);
|
||||||
|
|
||||||
|
return newCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Certificate> items() {
|
||||||
|
return new Iterator<Certificate>() {
|
||||||
|
|
||||||
|
private final List<Lazy<Certificate>> certificateQueue = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
// Constructor... wtf.
|
||||||
|
{
|
||||||
|
File[] subdirectories = resolver.getBaseDirectory().listFiles(new FileFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File file) {
|
||||||
|
return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (File subdirectory : subdirectories) {
|
||||||
|
File[] files = subdirectory.listFiles(new FileFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File file) {
|
||||||
|
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (File certFile : files) {
|
||||||
|
certificateQueue.add(new Lazy<Certificate>() {
|
||||||
|
@Override
|
||||||
|
Certificate get() throws BadDataException {
|
||||||
|
try {
|
||||||
|
Certificate certificate = certificateReaderBackend.readCertificate(new FileInputStream(certFile));
|
||||||
|
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("File got deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return !certificateQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate next() {
|
||||||
|
try {
|
||||||
|
return certificateQueue.remove(0).get();
|
||||||
|
} catch (BadDataException e) {
|
||||||
|
throw new AssertionError("Could not retrieve item: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class Lazy<E> {
|
||||||
|
abstract E get() throws BadDataException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<String> fingerprints() {
|
||||||
|
Iterator<Certificate> certificates = items();
|
||||||
|
return new Iterator<String>() {
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return certificates.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String next() {
|
||||||
|
return certificates.next().getFingerprint();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
22
pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java
Normal file
22
pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SpecialNames {
|
||||||
|
|
||||||
|
private static final Map<String, String> SPECIAL_NAMES = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
SPECIAL_NAMES.put("TRUST-ROOT", "trust-root"); // TODO: Remove
|
||||||
|
SPECIAL_NAMES.put("trust-root", "trust-root");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String lookupSpecialName(String specialName) {
|
||||||
|
return SPECIAL_NAMES.get(specialName);
|
||||||
|
}
|
||||||
|
}
|
10
pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java
Normal file
10
pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the Shared PGP Certificate Directory for java.
|
||||||
|
*
|
||||||
|
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory</a>
|
||||||
|
*/
|
||||||
|
package pgp.cert_d;
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
public class BaseDirectoryProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDefaultBaseDir_Linux() {
|
||||||
|
assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux"));
|
||||||
|
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux");
|
||||||
|
assertTrue(baseDir.getAbsolutePath().endsWith("/.local/share/pgp.cert.d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDefaultBaseDir_Windows() {
|
||||||
|
assumeTrue(System.getProperty("os.name").toLowerCase().contains("win"));
|
||||||
|
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("Windows");
|
||||||
|
assertTrue(baseDir.getAbsolutePath().endsWith("\\Roaming\\pgp.cert.d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDefaultBaseDir_Mac() {
|
||||||
|
assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac"));
|
||||||
|
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("Mac");
|
||||||
|
assertTrue(baseDir.getAbsolutePath().endsWith("/Library/Application Support/pgp.cert.d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDefaultBaseDirNotNull() {
|
||||||
|
File baseDir = BaseDirectoryProvider.getDefaultBaseDir();
|
||||||
|
assertNotNull(baseDir);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
public class FilenameResolverTest {
|
||||||
|
|
||||||
|
private File baseDir;
|
||||||
|
private FilenameResolver resolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() throws IOException {
|
||||||
|
baseDir = Files.createTempDirectory("filenameresolver").toFile();
|
||||||
|
baseDir.deleteOnExit();
|
||||||
|
resolver = new FilenameResolver(baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForFingerprint1() throws BadNameException {
|
||||||
|
String fingerprint = "d1a66e1a23b182c9980f788cfbfcc82a015e7330";
|
||||||
|
|
||||||
|
File subDir = new File(baseDir, "d1");
|
||||||
|
File expected = new File(subDir, "a66e1a23b182c9980f788cfbfcc82a015e7330");
|
||||||
|
|
||||||
|
assertEquals(expected.getAbsolutePath(), resolver.getCertFileByFingerprint(fingerprint).getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForFingerprint2() throws BadNameException {
|
||||||
|
String fingerprint = "eb85bb5fa33a75e15e944e63f231550c4f47e38e";
|
||||||
|
|
||||||
|
File subDir = new File(baseDir, "eb");
|
||||||
|
File expected = new File(subDir, "85bb5fa33a75e15e944e63f231550c4f47e38e");
|
||||||
|
|
||||||
|
assertEquals(expected.getAbsolutePath(), resolver.getCertFileByFingerprint(fingerprint).getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForInvalidNonHexFingerprint() {
|
||||||
|
String invalidFingerprint = "thisisnothexadecimalthisisnothexadecimal";
|
||||||
|
assertThrows(BadNameException.class, () -> resolver.getCertFileByFingerprint(invalidFingerprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForInvalidWrongLengthFingerprint() {
|
||||||
|
String invalidFingerprint = "d1a66e1a23b182c9980f788cfbfcc82a015e73301234";
|
||||||
|
assertThrows(BadNameException.class, () -> resolver.getCertFileByFingerprint(invalidFingerprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForNullFingerprint() {
|
||||||
|
assertThrows(NullPointerException.class, () -> resolver.getCertFileByFingerprint(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForSpecialName() throws BadNameException {
|
||||||
|
String specialName = "trust-root";
|
||||||
|
File expected = new File(baseDir, "trust-root");
|
||||||
|
|
||||||
|
assertEquals(expected, resolver.getCertFileBySpecialName(specialName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetFileForInvalidSpecialName() {
|
||||||
|
String invalidSpecialName = "invalid";
|
||||||
|
assertThrows(BadNameException.class, () -> resolver.getCertFileBySpecialName(invalidSpecialName));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
public class SpecialNamesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bothTrustRootNotationsAreRecognized() {
|
||||||
|
assertEquals("trust-root", SpecialNames.lookupSpecialName("trust-root"));
|
||||||
|
assertEquals("trust-root", SpecialNames.lookupSpecialName("TRUST-ROOT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidSpecialNameReturnsNull() {
|
||||||
|
assertNull(SpecialNames.lookupSpecialName("invalid"));
|
||||||
|
assertNull(SpecialNames.lookupSpecialName("trust root"));
|
||||||
|
assertNull(SpecialNames.lookupSpecialName("writelock"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup;
|
||||||
|
import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl;
|
||||||
|
import pgp.certificate_store.SubkeyLookup;
|
||||||
|
|
||||||
|
public class SubkeyLookupTest {
|
||||||
|
|
||||||
|
private static final List<SubkeyLookup> testSubjects = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setupLookupTestSubjects() throws IOException, SQLException {
|
||||||
|
InMemorySubkeyLookup inMemorySubkeyLookup = new InMemorySubkeyLookup();
|
||||||
|
testSubjects.add(inMemorySubkeyLookup);
|
||||||
|
|
||||||
|
File sqliteDatabase = Files.createTempFile("subkeyLookupTest", ".db").toFile();
|
||||||
|
sqliteDatabase.createNewFile();
|
||||||
|
sqliteDatabase.deleteOnExit();
|
||||||
|
DatabaseSubkeyLookup sqliteSubkeyLookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(sqliteDatabase));
|
||||||
|
testSubjects.add(sqliteSubkeyLookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void tearDownLookupTestSubjects() {
|
||||||
|
((InMemorySubkeyLookup) testSubjects.get(0)).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<SubkeyLookup> provideSubkeyLookupsForTest() {
|
||||||
|
return testSubjects.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideSubkeyLookupsForTest")
|
||||||
|
public void testInsertGet(SubkeyLookup subject) throws IOException {
|
||||||
|
// Initially all null
|
||||||
|
|
||||||
|
assertTrue(subject.getCertificateFingerprintsForSubkeyId(123).isEmpty());
|
||||||
|
assertTrue(subject.getCertificateFingerprintsForSubkeyId(1337).isEmpty());
|
||||||
|
assertTrue(subject.getCertificateFingerprintsForSubkeyId(420).isEmpty());
|
||||||
|
|
||||||
|
// Store one val, others still null
|
||||||
|
|
||||||
|
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(123L));
|
||||||
|
|
||||||
|
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(123));
|
||||||
|
assertTrue(subject.getCertificateFingerprintsForSubkeyId(1337).isEmpty());
|
||||||
|
assertTrue(subject.getCertificateFingerprintsForSubkeyId(420).isEmpty());
|
||||||
|
|
||||||
|
// Store other val, first stays intact
|
||||||
|
|
||||||
|
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(1337L));
|
||||||
|
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(420L));
|
||||||
|
|
||||||
|
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(123));
|
||||||
|
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(1337));
|
||||||
|
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(420));
|
||||||
|
|
||||||
|
// add additional entry for subkey
|
||||||
|
|
||||||
|
subject.storeCertificateSubkeyIds("eb85bb5fa33a75e15e944e63f231550c4f47e38e", Collections.singletonList(123L));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
new HashSet<>(Arrays.asList("eb85bb5fa33a75e15e944e63f231550c4f47e38e", "d1a66e1a23b182c9980f788cfbfcc82a015e7330")),
|
||||||
|
subject.getCertificateFingerprintsForSubkeyId(123));
|
||||||
|
}
|
||||||
|
}
|
10
pgp-certificate-store/README.md
Normal file
10
pgp-certificate-store/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# PGP Certificate Store Definitions
|
||||||
|
|
||||||
|
This module contains API definitions for a certificate store for PGPainless.
|
||||||
|
A certificate store is used to store public key certificates only.
|
26
pgp-certificate-store/build.gradle
Normal file
26
pgp-certificate-store/build.gradle
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
api "org.slf4j:slf4j-api:$slf4jVersion"
|
||||||
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public abstract class AbstractCertificateStore implements CertificateStore {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCertificateStore.class);
|
||||||
|
|
||||||
|
public Set<Certificate> getCertificatesBySubkeyId(long subkeyId)
|
||||||
|
throws IOException {
|
||||||
|
Set<String> identifiers = getCertificateFingerprintsForSubkeyId(subkeyId);
|
||||||
|
if (identifiers.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Certificate> certificates = new HashSet<>();
|
||||||
|
for (String identifier : identifiers) {
|
||||||
|
try {
|
||||||
|
certificates.add(getCertificate(identifier));
|
||||||
|
} catch (BadNameException | BadDataException e) {
|
||||||
|
LOGGER.warn("Could not read certificate.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificates;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public abstract class Certificate {
|
||||||
|
/**
|
||||||
|
* Return the fingerprint of the certificate as 40 lowercase hex characters.
|
||||||
|
* TODO: Allow OpenPGP V5 fingerprints
|
||||||
|
*
|
||||||
|
* @return fingerprint
|
||||||
|
*/
|
||||||
|
public abstract String getFingerprint();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link InputStream} of the binary representation of the certificate.
|
||||||
|
*
|
||||||
|
* @return input stream
|
||||||
|
*/
|
||||||
|
public abstract InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a tag of the certificate.
|
||||||
|
* The tag is a checksum calculated over the binary representation of the certificate.
|
||||||
|
*
|
||||||
|
* @return tag
|
||||||
|
*/
|
||||||
|
public abstract String getTag() throws IOException;
|
||||||
|
|
||||||
|
public abstract Set<Long> getSubkeyIds() throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate storage definition.
|
||||||
|
* This interface defines methods to insert and retrieve {@link Certificate Certificates} to and from a store.
|
||||||
|
*
|
||||||
|
* {@link Certificate Certificates} are hereby identified by identifiers. An identifier can either be a fingerprint
|
||||||
|
* or a special name. Special names are implementation-defined identifiers for certificates.
|
||||||
|
*
|
||||||
|
* Fingerprints are expected to be hexadecimal lowercase character sequences.
|
||||||
|
*/
|
||||||
|
public interface CertificateDirectory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the certificate that matches the given identifier.
|
||||||
|
* If no matching certificate can be found, return null.
|
||||||
|
*
|
||||||
|
* @param identifier identifier for a certificate.
|
||||||
|
* @return certificate or null
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate getCertificate(String identifier)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the certificate that matches the given identifier, but only iff it changed since the last invocation.
|
||||||
|
* To compare the certificate against its last returned result, the given tag is used.
|
||||||
|
* If the tag of the currently found certificate matches the given argument, return null.
|
||||||
|
*
|
||||||
|
* @param identifier identifier for a certificate
|
||||||
|
* @param tag tag to compare freshness
|
||||||
|
* @return changed certificate or null
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate getCertificateIfChanged(String identifier, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
|
||||||
|
* consider to use {@link #tryInsertCertificate(InputStream, MergeCallback)} instead.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
* @throws InterruptedException in case the inserting thread gets interrupted
|
||||||
|
*/
|
||||||
|
Certificate insertCertificate(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, InterruptedException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
|
||||||
|
* without any writing.
|
||||||
|
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
|
||||||
|
* and return the written certificate.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate or null if the store cannot be locked
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate tryInsertCertificate(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* The certificate will be stored under the given special name instead of its fingerprint.
|
||||||
|
*
|
||||||
|
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
|
||||||
|
* consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, MergeCallback)} instead.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate or null if the store cannot be locked
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate insertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, InterruptedException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* The certificate will be stored under the given special name instead of its fingerprint.
|
||||||
|
*
|
||||||
|
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
|
||||||
|
* without any writing.
|
||||||
|
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
|
||||||
|
* and return the written certificate.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate or null if the store cannot be locked
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link Iterator} containing all certificates in the store.
|
||||||
|
* The iterator will contain both certificates addressed by special names and by fingerprints.
|
||||||
|
*
|
||||||
|
* @return certificates
|
||||||
|
*/
|
||||||
|
Iterator<Certificate> getCertificates();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link Iterator} containing all certificate fingerprints from the store.
|
||||||
|
* Note that this only includes the fingerprints of certificate primary keys, not those of subkeys.
|
||||||
|
*
|
||||||
|
* @return fingerprints
|
||||||
|
*/
|
||||||
|
Iterator<String> getFingerprints();
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface definition for a class that can read {@link Certificate Certificates} from binary
|
||||||
|
* {@link InputStream InputStreams}.
|
||||||
|
*/
|
||||||
|
public interface CertificateReaderBackend {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a {@link Certificate} from the given {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @param inputStream input stream containing the binary representation of the certificate.
|
||||||
|
* @return certificate object
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
Certificate readCertificate(InputStream inputStream) throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
public interface CertificateStore extends CertificateDirectory, SubkeyLookup {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge a given certificate (update) with an existing certificate.
|
||||||
|
*/
|
||||||
|
public interface MergeCallback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge the given certificate data with the existing certificate and return the result.
|
||||||
|
*
|
||||||
|
* If no existing certificate is found (i.e. existing is null), this method returns the unmodified data.
|
||||||
|
*
|
||||||
|
* @param data certificate
|
||||||
|
* @param existing optional already existing copy of the certificate
|
||||||
|
* @return merged certificate
|
||||||
|
*/
|
||||||
|
Certificate merge(Certificate data, Certificate existing) throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface SubkeyLookup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup the fingerprint of the certificate that contains the given subkey.
|
||||||
|
* If no record is found, return null.
|
||||||
|
*
|
||||||
|
* @param subkeyId subkey id
|
||||||
|
* @return fingerprint of the certificate
|
||||||
|
*/
|
||||||
|
Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record, which certificate the subkey-ids in the list belong to.
|
||||||
|
* This method does not change the affiliation of subkey-ids not contained in the provided list.
|
||||||
|
*
|
||||||
|
* @param certificate certificate fingerprint
|
||||||
|
* @param subkeyIds subkey ids
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data was not a valid OpenPGP cert or key in binary format.
|
||||||
|
*/
|
||||||
|
public class BadDataException extends Exception {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provided name was neither a valid fingerprint, nor a known special name.
|
||||||
|
*/
|
||||||
|
public class BadNameException extends Exception {
|
||||||
|
|
||||||
|
public BadNameException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadNameException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base dir cannot possibly contain a store.
|
||||||
|
*/
|
||||||
|
public class NotAStoreException extends Exception {
|
||||||
|
|
||||||
|
public NotAStoreException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotAStoreException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exceptions defined by the Shared PGP Certificate Directory.
|
||||||
|
*
|
||||||
|
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
|
||||||
|
*/
|
||||||
|
package pgp.certificate_store.exception;
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract definitions of an OpenPGP certificate store.
|
||||||
|
*/
|
||||||
|
package pgp.certificate_store;
|
|
@ -0,0 +1,16 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class DummyTest {
|
||||||
|
@Test
|
||||||
|
public void test() {
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
10
settings.gradle
Normal file
10
settings.gradle
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
rootProject.name = 'cert-d-java'
|
||||||
|
|
||||||
|
include 'pgpainless-core',
|
||||||
|
'pgpainless-sop',
|
||||||
|
'pgpainless-cli'
|
||||||
|
|
13
version.gradle
Normal file
13
version.gradle
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
ext {
|
||||||
|
shortVersion = '1.1.2'
|
||||||
|
isSnapshot = true
|
||||||
|
minAndroidSdk = 10
|
||||||
|
javaSourceCompatibility = 1.8
|
||||||
|
bouncyCastleVersion = '1.70'
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue