Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

htpasswd #8

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ plugins {
id 'org.graalvm.buildtools.native'
id 'com.github.johnrengelman.shadow'
}

dependencies {
constraints {
implementation 'info.picocli:picocli:4.6.3'
}
}
11 changes: 11 additions & 0 deletions cli/htpasswd-cli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
id 'info.ankin.projects.app-conventions'
id 'info.ankin.projects.spring-conventions'
}

dependencies {
implementation 'info.picocli:picocli'
implementation 'org.slf4j:slf4j-api'
implementation 'org.slf4j:slf4j-simple'
implementation project(':lib:htpasswd')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package info.ankin.projects.cli.htpasswd;

import info.ankin.projects.htpasswd.Htpasswd;
import info.ankin.projects.htpasswd.HtpasswdEntry;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import picocli.CommandLine;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

@Slf4j
@CommandLine.Command(
name = "htpasswd",
description = "utility for managing htpasswd files",
mixinStandardHelpOptions = true
)
public class HtpasswdCli implements Callable<Integer> {
private static final int MIN_PARAMETERS = 2;

@CommandLine.Mixin
Options options;

@CommandLine.Parameters(arity = "2..")
List<String> parameters;

public static void main(String[] args) {
System.exit(new CommandLine(new HtpasswdCli()).execute(args));
}

@Override
public Integer call() throws Exception {
Options.OperationMode operationMode = options.operationMode();
int maxParameters = operationMode == Options.OperationMode.display ? 2 : 3;
if (parameters.size() < MIN_PARAMETERS || parameters.size() > maxParameters)
throw new IllegalArgumentException("too many parameters (" + parameters.size() + ", more than" + maxParameters + ")");

Params params = new Params(parameters);

Htpasswd htpasswd = new Htpasswd(new SecureRandom(), options.htpasswdProperties());

switch (operationMode) {
case add: {
if (params.getPasswordFile() == null)
throw new IllegalArgumentException("need passwordFile to add");

List<HtpasswdEntry> entries = Files.readAllLines(Path.of(params.getPasswordFile()))
.stream().map(htpasswd::parse)
.collect(Collectors.toList());

entries.add(htpasswd.create(params.getUsername(),
options.passwordEncryption(),
params.getPassword().toCharArray()));

Files.write(Path.of(params.getPasswordFile()),
entries.stream()
.map(HtpasswdEntry::toLine)
.collect(Collectors.joining(System.lineSeparator()))
.getBytes(StandardCharsets.UTF_8));
break;
}
case create: {
String s = htpasswd.create(params.getUsername(),
options.passwordEncryption(),
params.getPassword().toCharArray()).toLine();

Files.write(Path.of(params.getPasswordFile()), s.getBytes(StandardCharsets.UTF_8));
break;
}
case delete: {
List<HtpasswdEntry> entries = Files.readAllLines(Path.of(params.getPasswordFile()))
.stream().map(htpasswd::parse)
.collect(Collectors.toList());

entries.removeIf(e -> e.getUsername().equals(params.getUsername()));

Files.write(Path.of(params.getPasswordFile()),
entries.stream()
.map(HtpasswdEntry::toLine)
.collect(Collectors.joining(System.lineSeparator()))
.getBytes(StandardCharsets.UTF_8));
break;
}
case display: {
String s = htpasswd.create(params.getUsername(),
options.passwordEncryption(),
params.getPassword().toCharArray()).toLine();

System.out.print(s);
break;
}
case verify: {
throw new UnsupportedOperationException("not yet implemented");
}
}

log.info("{}", parameters);
return 0;
}

@AllArgsConstructor
@Data
static class Params {
private final String WRONG_PARAMS = "wrong length of parameters (not 2 or 3): ";

String passwordFile;
String username;
String password;

public Params(List<String> params) {
if (params.size() == 3) {
passwordFile = params.get(0);
username = params.get(1);
password = params.get(2);
} else if (params.size() == 2) {
username = params.get(0);
password = params.get(1);
} else {
throw new IllegalStateException(WRONG_PARAMS + params.size());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package info.ankin.projects.cli.htpasswd;

import info.ankin.projects.cli.htpasswd.exception.MultipleEncryptionException;
import info.ankin.projects.cli.htpasswd.exception.MultipleModesException;
import info.ankin.projects.htpasswd.HtpasswdProperties;
import info.ankin.projects.htpasswd.SupportedEncryption;
import lombok.Data;
import lombok.SneakyThrows;
import picocli.CommandLine;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@CommandLine.Command
public class Options {

@CommandLine.Option(names = "-c",
description = "Create a new file.")
boolean createMode;

@CommandLine.Option(names = "-n",
description = "Don't update file; display results on stdout.")
boolean displayMode;

@CommandLine.Option(names = "-b",
description = "Use the password from the command line rather than prompting for it.")
boolean noPrompt;

@CommandLine.Option(names = "-i",
description = "Read password from stdin without verification (for script usage).")
boolean stdinPassword;

@CommandLine.Option(names = "-m",
description = "Force MD5 encryption of the password (default).")
boolean useMd5;

@CommandLine.Option(names = "-B",
description = "Force bcrypt encryption of the password (very secure).")
boolean useBcrypt;

@CommandLine.Option(names = "-C",
description = "Set the computing time used for the bcrypt algorithm" +
"(higher is more secure but slower, default: 5, valid: 4 to 17).")
Integer bcryptCost = 12;

@CommandLine.Option(names = "-d",
description = "Force CRYPT encryption of the password (8 chars max, insecure).")
boolean useCrypt;

@CommandLine.Option(names = "-s",
description = "Force SHA encryption of the password (insecure).")
boolean useSha;

@CommandLine.Option(names = "-p",
description = "Do not encrypt the password (plaintext, insecure).")
boolean usePlain;

@CommandLine.Option(names = "-D",
description = "Delete the specified user.")
boolean deleteMode;

@CommandLine.Option(names = "-v",
description = "Verify password for the specified user.")
boolean verifyMode;

SupportedEncryption passwordEncryption() {
EnumMap<SupportedEncryption, Boolean> map = new EnumMap<>(SupportedEncryption.class);
map.put(SupportedEncryption.bcrypt, isUseBcrypt());
map.put(SupportedEncryption.crypt, isUseCrypt());
map.put(SupportedEncryption.md5, isUseMd5());
map.put(SupportedEncryption.plain, isUsePlain());
map.put(SupportedEncryption.sha, isUseSha());

return maxOneOrThrow(map, SupportedEncryption.bcrypt, MultipleEncryptionException::new);
}

OperationMode operationMode() {
EnumMap<OperationMode, Boolean> map = new EnumMap<>(OperationMode.class);
map.put(OperationMode.create, isCreateMode());
map.put(OperationMode.delete, isDeleteMode());
map.put(OperationMode.display, isDisplayMode());
map.put(OperationMode.verify, isVerifyMode());
return maxOneOrThrow(map, OperationMode.add, MultipleModesException::new);
}

@SneakyThrows
<T extends Enum<?>> T maxOneOrThrow(Map<T, Boolean> map, T defaultIfOne, Function<String, Exception> exceptionCreator) {
List<Map.Entry<T, Boolean>> list = map.entrySet().stream().filter(Map.Entry::getValue).collect(Collectors.toList());
if (list.size() == 0) return defaultIfOne;
if (list.size() == 1) return list.get(0).getKey();

String multiple = list.stream().map(Map.Entry::getKey).map(Enum::name).collect(Collectors.joining(", "));
throw exceptionCreator.apply("Can't select multiple: " + multiple);
}

HtpasswdProperties htpasswdProperties() {
return new HtpasswdProperties()
.setBcryptCost(getBcryptCost())
;
}

enum OperationMode {
add,
create,
delete,
display,
verify,
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package info.ankin.projects.cli.htpasswd.exception;

public class HtpasswdCliException extends RuntimeException {
public HtpasswdCliException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package info.ankin.projects.cli.htpasswd.exception;

public class MultipleEncryptionException extends HtpasswdCliException {
public MultipleEncryptionException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package info.ankin.projects.cli.htpasswd.exception;

public class MultipleModesException extends HtpasswdCliException {
public MultipleModesException(String message) {
super(message);
}
}
10 changes: 10 additions & 0 deletions lib/htpasswd/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
id 'info.ankin.projects.library-conventions'
id 'info.ankin.projects.spring-conventions'
}

dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'commons-codec:commons-codec'
implementation 'org.apache.commons:commons-lang3'
}
Loading