├── .travis.yml ├── docs ├── console.png ├── session.png ├── ssh_logo.png └── ssh_logo.svg ├── .gitignore ├── src └── main │ ├── resources │ ├── motd.txt │ ├── plugin.yml │ └── config.yml │ └── java │ ├── com │ └── ryanmichela │ │ └── sshd │ │ ├── SshTerminal.java │ │ ├── PermissionUtil.java │ │ ├── FlushyOutputStream.java │ │ ├── Waitable.java │ │ ├── FlushyStreamHandler.java │ │ ├── ConsoleCommandCompleter.java │ │ ├── ConsoleCommandFactory.java │ │ ├── StreamHandlerAppender.java │ │ ├── implementations │ │ ├── SSHDConversationTracker.java │ │ └── SSHDCommandSender.java │ │ ├── ConfigPasswordAuthenticator.java │ │ ├── PublicKeyAuthenticator.java │ │ ├── ReflectionUtil.java │ │ ├── MkpasswdCommand.java │ │ ├── ConsoleLogFormatter.java │ │ ├── Cryptography.java │ │ ├── SshdPlugin.java │ │ ├── ConsoleShellFactory.java │ │ └── BCrypt.java │ └── org │ └── slf4j │ └── impl │ ├── StaticLoggerBinder.java │ └── PluginSlf4jFactory.java ├── .clangformat ├── README.md ├── pom.xml └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: java 4 | jdk: 5 | - oraclejdk8 6 | -------------------------------------------------------------------------------- /docs/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justasic/Minecraft-SSHD/HEAD/docs/console.png -------------------------------------------------------------------------------- /docs/session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justasic/Minecraft-SSHD/HEAD/docs/session.png -------------------------------------------------------------------------------- /docs/ssh_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justasic/Minecraft-SSHD/HEAD/docs/ssh_logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | .idea 8 | *.iml 9 | target 10 | -------------------------------------------------------------------------------- /src/main/resources/motd.txt: -------------------------------------------------------------------------------- 1 | §l§4 _____ _____ _ _ _____§r 2 | §l§6 / ____/ ____| | | | __ \§r 3 | §l§2| (___| (___ | |__| | | | |§r 4 | §l§3 \___ \\___ \| __ | | | |§r 5 | §l§9 ____) |___) | | | | |__| |§r 6 | §l§5|_____/_____/|_| |_|_____/§r 7 | 8 | =============================================== -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: SSHD 2 | version: ${project.version} 3 | author: Ryan Michela, Haarolean, toxuin, Justin Crawford, Zachery Coleman 4 | main: com.ryanmichela.sshd.SshdPlugin 5 | load: STARTUP 6 | commands: 7 | mkpasswd: 8 | description: Make a SSHD password hash 9 | usage: /mkpasswd 10 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/SshTerminal.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import jline.TerminalSupport; 4 | 5 | /** 6 | * Copyright 2013 Ryan Michela 7 | */ 8 | public class SshTerminal extends TerminalSupport 9 | { 10 | protected SshTerminal() 11 | { 12 | super(true); 13 | } 14 | 15 | @Override 16 | public void init() throws Exception 17 | { 18 | setAnsiSupported(true); 19 | setEchoEnabled(true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/PermissionUtil.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import java.util.Optional; 4 | 5 | import com.ryanmichela.sshd.SshdPlugin; 6 | 7 | public class PermissionUtil 8 | { 9 | public static Optional GetCredential(String username, String credential) 10 | { 11 | String Default = SshdPlugin.instance.getConfig().getString("Credentials.$default." + credential); 12 | String cred = SshdPlugin.instance.getConfig().getString("Credentials." + username + "." + credential, Default); 13 | 14 | if (cred == null) 15 | return Optional.empty(); 16 | 17 | else if (cred.isEmpty()) 18 | return Optional.empty(); 19 | 20 | else 21 | return Optional.of(cred); 22 | } 23 | }; -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/FlushyOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.sshd.common.SshException; 4 | import org.apache.sshd.common.channel.exception.SshChannelClosedException; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.math.BigInteger; 9 | 10 | /** 11 | * Copyright 2013 Ryan Michela 12 | */ 13 | public class FlushyOutputStream extends OutputStream 14 | { 15 | 16 | private OutputStream base; 17 | private boolean isClosed = false; 18 | 19 | public FlushyOutputStream(OutputStream base) 20 | { 21 | this.base = base; 22 | } 23 | 24 | @Override 25 | public void write(int b) throws IOException 26 | { 27 | this.write(BigInteger.valueOf(b).toByteArray()); 28 | } 29 | 30 | @Override 31 | public void write(byte[] b) throws IOException 32 | { 33 | this.write(b, 0, b.length); 34 | } 35 | 36 | @Override 37 | public void write(byte[] b, int off, int len) throws IOException 38 | { 39 | if (isClosed) 40 | return; 41 | 42 | try 43 | { 44 | base.write(b, off, len); 45 | base.flush(); 46 | } 47 | catch (SshChannelClosedException e) 48 | { 49 | // ignored. 50 | } 51 | } 52 | 53 | @Override 54 | public void close() throws IOException 55 | { 56 | isClosed = true; 57 | base.close(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/Waitable.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | 5 | /** 6 | * Copyright 2013 Ryan Michela 7 | */ 8 | public abstract class Waitable implements Runnable 9 | { 10 | 11 | private enum Status 12 | { 13 | WAITING, 14 | RUNNING, 15 | FINISHED, 16 | } 17 | 18 | Throwable t = null; 19 | T value = null; 20 | Status status = Status.WAITING; 21 | 22 | public final void run() 23 | { 24 | synchronized (this) 25 | { 26 | if (status != Status.WAITING) 27 | throw new IllegalStateException("Invalid state " + status); 28 | 29 | status = Status.RUNNING; 30 | } 31 | 32 | try 33 | { 34 | value = evaluate(); 35 | } 36 | catch (Throwable t) 37 | { 38 | this.t = t; 39 | } 40 | finally 41 | { 42 | synchronized (this) 43 | { 44 | status = Status.FINISHED; 45 | this.notifyAll(); 46 | } 47 | } 48 | } 49 | 50 | protected abstract T evaluate(); 51 | 52 | public synchronized T get() throws InterruptedException, ExecutionException 53 | { 54 | while (status != Status.FINISHED) 55 | this.wait(); 56 | 57 | if (t != null) 58 | throw new ExecutionException(t); 59 | 60 | return value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/FlushyStreamHandler.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import jline.console.ConsoleReader; 4 | import org.apache.sshd.common.SshException; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.util.logging.*; 9 | 10 | /** 11 | * Copyright 2013 Ryan Michela 12 | */ 13 | public class FlushyStreamHandler extends StreamHandler 14 | { 15 | private ConsoleReader reader; 16 | 17 | public FlushyStreamHandler(OutputStream out, Formatter formatter, ConsoleReader reader) 18 | { 19 | super(out, formatter); 20 | this.reader = reader; 21 | setLevel(Level.INFO); 22 | } 23 | 24 | @Override 25 | public synchronized void publish(LogRecord record) 26 | { 27 | record.setMessage(record.getMessage().replace("\n", "\n\r")); 28 | super.publish(record); 29 | flush(); 30 | } 31 | 32 | @Override 33 | public synchronized void flush() 34 | { 35 | try 36 | { 37 | reader.print(ConsoleReader.RESET_LINE + ""); 38 | reader.flush(); 39 | super.flush(); 40 | try 41 | { 42 | reader.drawLine(); 43 | } 44 | catch (Throwable ex) 45 | { 46 | reader.getCursorBuffer().clear(); 47 | } 48 | 49 | reader.flush(); 50 | super.flush(); 51 | } 52 | catch (SshException ex) 53 | { 54 | // do nothing 55 | } 56 | catch (IOException ex) 57 | { 58 | Logger.getLogger(FlushyStreamHandler.class.getName()).log(Level.SEVERE, null, ex); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ConsoleCommandCompleter.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | /** 4 | * Copyright 2013 Ryan Michela 5 | */ 6 | 7 | import jline.console.completer.Completer; 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.command.CommandMap; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.ExecutionException; 13 | import java.util.logging.Level; 14 | 15 | public class ConsoleCommandCompleter implements Completer 16 | { 17 | public int complete(final String buffer, final int cursor, final List candidates) 18 | { 19 | Waitable> waitable = new Waitable>() 20 | { 21 | @Override 22 | protected List evaluate() 23 | { 24 | CommandMap commandMap = ReflectionUtil.getProtectedValue(Bukkit.getServer(), "commandMap"); 25 | return commandMap.tabComplete(Bukkit.getServer().getConsoleSender(), buffer); 26 | } 27 | }; 28 | 29 | Bukkit.getScheduler().runTask(SshdPlugin.instance, waitable); 30 | try 31 | { 32 | List offers = waitable.get(); 33 | if (offers == null) 34 | return cursor; 35 | 36 | candidates.addAll(offers); 37 | 38 | final int lastSpace = buffer.lastIndexOf(' '); 39 | if (lastSpace == -1) 40 | return cursor - buffer.length(); 41 | else 42 | return cursor - (buffer.length() - lastSpace - 1); 43 | } 44 | catch (ExecutionException e) 45 | { 46 | SshdPlugin.instance.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); 47 | } 48 | catch (InterruptedException e) 49 | { 50 | Thread.currentThread().interrupt(); 51 | } 52 | return cursor; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /.clangformat: -------------------------------------------------------------------------------- 1 | --- 2 | #BasedOnStyle: WebKit 3 | TabWidth: '4' 4 | IndentWidth: '4' 5 | UseTab: 'Always' 6 | AlignOperands: 'true' 7 | AlignAfterOpenBracket: 'Align' 8 | AlignConsecutiveAssignments: 'true' 9 | AlignConsecutiveDeclarations: 'true' 10 | AlignEscapedNewlines: 'Left' 11 | AlignTrailingComments: 'true' 12 | AllowAllParametersOfDeclarationOnNextLine: 'true' 13 | AllowShortBlocksOnASingleLine: 'false' 14 | AllowShortCaseLabelsOnASingleLine: 'false' 15 | AllowShortFunctionsOnASingleLine: 'All' 16 | AllowShortIfStatementsOnASingleLine: 'false' 17 | AllowShortLoopsOnASingleLine: 'false' 18 | AlwaysBreakAfterReturnType: 'None' 19 | AlwaysBreakTemplateDeclarations: 'true' 20 | AlwaysBreakBeforeMultilineStrings: 'false' 21 | BinPackArguments: 'false' 22 | BinPackParameters: 'false' 23 | BreakBeforeBraces: 'Custom' 24 | BraceWrapping: 25 | AfterEnum: 'true' 26 | AfterClass: 'true' 27 | AfterControlStatement: 'true' 28 | AfterStruct: 'true' 29 | AfterFunction: 'true' 30 | AfterNamespace: 'true' 31 | AfterUnion: 'true' 32 | AfterExternBlock: 'true' 33 | BeforeCatch: 'true' 34 | BeforeElse: 'true' 35 | SplitEmptyRecord: 'false' 36 | SplitEmptyNamespace: 'false' 37 | SplitEmptyFunction: 'false' 38 | BreakBeforeBinaryOperators: 'true' 39 | BreakBeforeTernaryOperators: 'false' 40 | BreakConstructorInitializersBeforeComma: 'false' 41 | BreakBeforeInheritanceComma: 'false' 42 | BreakStringLiterals: 'true' 43 | ColumnLimit: '140' 44 | CompactNamespaces: 'false' 45 | Cpp11BracedListStyle: 'true' 46 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' 47 | DerivePointerAlignment: 'false' 48 | IndentCaseLabels: 'true' 49 | IndentPPDirectives: 'AfterHash' 50 | KeepEmptyLinesAtTheStartOfBlocks: 'true' 51 | Language: 'Java' 52 | NamespaceIndentation: 'All' 53 | PointerAlignment: 'Right' 54 | ReflowComments: 'true' 55 | SortIncludes: 'true' 56 | SortUsingDeclarations: 'true' 57 | SpaceAfterCStyleCast: 'false' 58 | SpaceAfterTemplateKeyword: 'false' 59 | SpaceBeforeAssignmentOperators: 'true' 60 | SpaceBeforeParens: ControlStatements 61 | SpaceInEmptyParentheses: 'false' 62 | SpacesInAngles: 'false' 63 | SpacesInCStyleCastParentheses: 'false' 64 | SpacesInContainerLiterals: 'false' 65 | SpacesInParentheses: 'false' 66 | SpacesInSquareBrackets: 'false' 67 | Standard: 'Auto' 68 | ... 69 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ConsoleCommandFactory.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.sshd.server.command.Command; 4 | import org.apache.sshd.server.command.CommandFactory; 5 | import org.apache.sshd.server.channel.ChannelSession; 6 | import org.apache.sshd.server.Environment; 7 | import org.apache.sshd.server.ExitCallback; 8 | import org.bukkit.Bukkit; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | 14 | /** 15 | * Copyright 2013 Ryan Michela 16 | */ 17 | public class ConsoleCommandFactory implements CommandFactory 18 | { 19 | 20 | @Override 21 | public Command createCommand(ChannelSession cs, String command) 22 | { 23 | return new ConsoleCommand(command); 24 | } 25 | 26 | public class ConsoleCommand implements Command 27 | { 28 | private String command; 29 | private InputStream in; 30 | private OutputStream out; 31 | private OutputStream err; 32 | private ExitCallback callback; 33 | 34 | public ConsoleCommand(String command) 35 | { 36 | this.command = command; 37 | } 38 | 39 | public void setInputStream(InputStream in) 40 | { 41 | this.in = in; 42 | } 43 | 44 | public void setOutputStream(OutputStream out) 45 | { 46 | this.out = out; 47 | } 48 | 49 | public void setErrorStream(OutputStream err) 50 | { 51 | this.err = err; 52 | } 53 | 54 | public void setExitCallback(ExitCallback callback) 55 | { 56 | this.callback = callback; 57 | } 58 | 59 | @Override 60 | public void start(ChannelSession cs, Environment environment) throws IOException 61 | { 62 | try 63 | { 64 | SshdPlugin.instance.getLogger() 65 | .info("[U: " + environment.getEnv().get(Environment.ENV_USER) + "] " + command); 66 | Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); 67 | } 68 | catch (Exception e) 69 | { 70 | SshdPlugin.instance.getLogger().severe("Error processing command from SSH -" + e.getMessage()); 71 | } 72 | finally 73 | { 74 | callback.onExit(0); 75 | } 76 | } 77 | 78 | @Override 79 | public void destroy(ChannelSession cn) {} 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/StreamHandlerAppender.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.logging.log4j.core.Appender; 4 | import org.apache.logging.log4j.core.ErrorHandler; 5 | import org.apache.logging.log4j.core.Layout; 6 | import org.apache.logging.log4j.core.LogEvent; 7 | 8 | import java.io.Serializable; 9 | import java.util.UUID; 10 | import java.util.logging.LogRecord; 11 | import java.util.logging.StreamHandler; 12 | 13 | /** 14 | * Copyright 2014 Ryan Michela 15 | */ 16 | public class StreamHandlerAppender implements Appender 17 | { 18 | 19 | private StreamHandler streamHandler; 20 | private UUID uuid; 21 | 22 | public StreamHandlerAppender(StreamHandler streamHandler) 23 | { 24 | this.streamHandler = streamHandler; 25 | uuid = UUID.randomUUID(); 26 | } 27 | 28 | @Override 29 | public void append(LogEvent logEvent) 30 | { 31 | java.util.logging.Level level; 32 | 33 | if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.DEBUG)) 34 | level = java.util.logging.Level.FINE; 35 | else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.INFO)) 36 | level = java.util.logging.Level.INFO; 37 | else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.WARN)) 38 | level = java.util.logging.Level.WARNING; 39 | else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.ERROR)) 40 | level = java.util.logging.Level.SEVERE; 41 | else 42 | level = java.util.logging.Level.INFO; 43 | 44 | 45 | String message = logEvent.getMessage().getFormattedMessage(); 46 | LogRecord logRecord = new LogRecord(level, message); 47 | streamHandler.publish(logRecord); 48 | } 49 | 50 | @Override 51 | public String getName() 52 | { 53 | return "StreamHandlerAppender:" + uuid.toString(); 54 | } 55 | 56 | @Override 57 | public Layout getLayout() 58 | { 59 | return null; 60 | } 61 | 62 | @Override 63 | public boolean ignoreExceptions() 64 | { 65 | return false; 66 | } 67 | 68 | @Override 69 | public ErrorHandler getHandler() 70 | { 71 | return null; 72 | } 73 | 74 | @Override 75 | public void setHandler(ErrorHandler errorHandler) 76 | { 77 | } 78 | 79 | @Override 80 | public void start() 81 | { 82 | } 83 | 84 | @Override 85 | public void stop() 86 | { 87 | } 88 | 89 | @Override 90 | public boolean isStarted() 91 | { 92 | return true; 93 | } 94 | 95 | @Override 96 | public boolean isStopped() 97 | { 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/implementations/SSHDConversationTracker.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd.implementations; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.conversations.Conversation; 5 | import org.bukkit.conversations.ConversationAbandonedEvent; 6 | import org.bukkit.conversations.ManuallyAbandonedConversationCanceller; 7 | 8 | import java.util.LinkedList; 9 | import java.util.logging.Level; 10 | 11 | public class SSHDConversationTracker 12 | { 13 | private LinkedList conversationQueue = new LinkedList<>(); 14 | 15 | synchronized boolean beginConversation(Conversation conversation) 16 | { 17 | if (!this.conversationQueue.contains(conversation)) 18 | { 19 | this.conversationQueue.addLast(conversation); 20 | if (this.conversationQueue.getFirst() == conversation) 21 | { 22 | conversation.begin(); 23 | conversation.outputNextPrompt(); 24 | return true; 25 | } 26 | } 27 | 28 | return true; 29 | } 30 | 31 | synchronized void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) 32 | { 33 | if (!this.conversationQueue.isEmpty()) 34 | { 35 | if (this.conversationQueue.getFirst() == conversation) 36 | conversation.abandon(details); 37 | 38 | if (this.conversationQueue.contains(conversation)) 39 | this.conversationQueue.remove(conversation); 40 | 41 | if (!this.conversationQueue.isEmpty()) 42 | this.conversationQueue.getFirst().outputNextPrompt(); 43 | } 44 | 45 | } 46 | 47 | public synchronized void abandonAllConversations() 48 | { 49 | LinkedList oldQueue = this.conversationQueue; 50 | this.conversationQueue = new LinkedList<>(); 51 | 52 | for (Conversation conversation : oldQueue) 53 | { 54 | try 55 | { 56 | conversation.abandon(new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller())); 57 | } 58 | catch (Throwable var5) 59 | { 60 | Bukkit.getLogger().log(Level.SEVERE, "Unexpected exception while abandoning a conversation", var5); 61 | } 62 | } 63 | 64 | } 65 | 66 | synchronized void acceptConversationInput(String input) 67 | { 68 | if (this.isConversing()) 69 | { 70 | Conversation conversation = this.conversationQueue.getFirst(); 71 | 72 | try 73 | { 74 | conversation.acceptInput(input); 75 | } 76 | catch (Throwable var4) 77 | { 78 | conversation.getContext().getPlugin().getLogger().log(Level.WARNING, String.format("Plugin %s generated an exception whilst handling conversation input", conversation.getContext().getPlugin().getDescription().getFullName()), var4); 79 | } 80 | } 81 | 82 | } 83 | 84 | synchronized boolean isConversing() 85 | { 86 | return !this.conversationQueue.isEmpty(); 87 | } 88 | 89 | public synchronized boolean isConversingModaly() 90 | { 91 | return this.isConversing() && this.conversationQueue.getFirst().isModal(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ConfigPasswordAuthenticator.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.sshd.server.auth.password.PasswordAuthenticator; 4 | import org.apache.sshd.server.session.ServerSession; 5 | 6 | import com.ryanmichela.sshd.Cryptography; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Copyright 2013 Ryan Michela 13 | */ 14 | public class ConfigPasswordAuthenticator implements PasswordAuthenticator 15 | { 16 | private Map FailCounts = new HashMap(); 17 | 18 | @Override 19 | public boolean authenticate(String username, String password, ServerSession ss) 20 | { 21 | // Depending on our hash type, we have to try and figure out what we're doing. 22 | String HashType = SshdPlugin.instance.getConfig().getString("PasswordType"); 23 | String ConfigHash = SshdPlugin.instance.getConfig().getString("Credentials." + username.trim() + ".password"); 24 | 25 | if (ConfigHash == null) 26 | SshdPlugin.instance.getLogger().warning("Config has no such user: " + username); 27 | else 28 | { 29 | try 30 | { 31 | if (HashType.equalsIgnoreCase("PLAIN")) 32 | { 33 | if (ConfigHash.equals(password)) 34 | { 35 | FailCounts.put(username, 0); 36 | return true; 37 | } 38 | } 39 | else if (HashType.equalsIgnoreCase("bcrypt")) 40 | { 41 | if (Cryptography.BCrypt_ValidatePassword(password, ConfigHash)) 42 | { 43 | FailCounts.put(username, 0); 44 | return true; 45 | } 46 | } 47 | else if (HashType.equalsIgnoreCase("pbkdf2")) 48 | { 49 | if (Cryptography.PBKDF2_ValidateHash(password, ConfigHash)) 50 | { 51 | FailCounts.put(username, 0); 52 | return true; 53 | } 54 | } 55 | else if (HashType.equalsIgnoreCase("sha256")) 56 | { 57 | if (Cryptography.SHA256_ValidatePassword(password, ConfigHash)) 58 | { 59 | FailCounts.put(username, 0); 60 | return true; 61 | } 62 | } 63 | } 64 | catch (Exception e) 65 | { 66 | // report it to the console. 67 | e.printStackTrace(); 68 | // But continue as though there was a password failure. 69 | } 70 | } 71 | 72 | SshdPlugin.instance.getLogger().info("Failed login for " + username + " using " + HashType + "-based password authentication."); 73 | Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries", 3); 74 | 75 | try 76 | { 77 | Thread.sleep(3000); 78 | if (this.FailCounts.containsKey(username)) 79 | this.FailCounts.put(username, this.FailCounts.get(username) + 1); 80 | else 81 | this.FailCounts.put(username, 1); 82 | 83 | if (this.FailCounts.get(username) >= tries) 84 | { 85 | this.FailCounts.put(username, 0); 86 | SshdPlugin.instance.getLogger().info("Too many failures for " + username + ", disconnecting."); 87 | ss.close(true); 88 | } 89 | } 90 | catch (InterruptedException e) 91 | { 92 | // do nothing 93 | } 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/PublicKeyAuthenticator.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.commons.lang.ArrayUtils; 4 | import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; 5 | import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; 6 | import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; 7 | import org.apache.sshd.server.session.ServerSession; 8 | 9 | import java.io.File; 10 | import java.util.List; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.io.FileReader; 14 | import java.security.PublicKey; 15 | 16 | /** 17 | * Copyright 2013 Ryan Michela 18 | */ 19 | public class PublicKeyAuthenticator implements PublickeyAuthenticator 20 | { 21 | private File authorizedKeysDir; 22 | private Map FailCounts = new HashMap(); 23 | 24 | public PublicKeyAuthenticator(File authorizedKeysDir) { this.authorizedKeysDir = authorizedKeysDir; } 25 | 26 | @Override public boolean authenticate(String username, PublicKey key, ServerSession session) 27 | { 28 | byte[] keyBytes = key.getEncoded(); 29 | File keyFile = new File(authorizedKeysDir, username); 30 | Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries", 3); 31 | 32 | if (keyFile.exists()) 33 | { 34 | try 35 | { 36 | // Read all the public key entries 37 | List pklist = AuthorizedKeyEntry.readAuthorizedKeys(keyFile.toPath()); 38 | // Get an authenticator 39 | PublickeyAuthenticator auth = PublickeyAuthenticator.fromAuthorizedEntries(username, session, pklist, 40 | PublicKeyEntryResolver.IGNORING); 41 | 42 | // Validate that the logging in user has the same valid SSH key 43 | if (auth.authenticate(username, key, session)) 44 | { 45 | FailCounts.put(username, 0); 46 | return true; 47 | } 48 | else 49 | { 50 | SshdPlugin.instance.getLogger().info( 51 | username + " failed authentication via SSH session using key file " + keyFile.getAbsolutePath()); 52 | } 53 | 54 | // If the user fails with several SSH keys, then terminate the connection. 55 | if (this.FailCounts.containsKey(username)) 56 | this.FailCounts.put(username, this.FailCounts.get(username) + 1); 57 | else 58 | this.FailCounts.put(username, 1); 59 | 60 | if (this.FailCounts.get(username) >= tries) 61 | { 62 | this.FailCounts.put(username, 0); 63 | SshdPlugin.instance.getLogger().info("Too many failures for " + username + ", disconnecting."); 64 | session.close(true); 65 | } 66 | 67 | return false; 68 | } 69 | catch (Exception e) 70 | { 71 | e.printStackTrace(); 72 | SshdPlugin.instance.getLogger().severe("Failed to process public key " + keyFile.getAbsolutePath()); 73 | } 74 | } 75 | else 76 | { 77 | SshdPlugin.instance.getLogger().warning("Could not locate public key for " + username 78 | + ". Make sure the user's key is named the same as their user name " 79 | + "without a file extension."); 80 | } 81 | 82 | return false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/org/slf4j/impl/StaticLoggerBinder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2004-2011 QOS.ch 3 | * All rights reserved. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining 6 | * a copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to 11 | * the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | * 24 | */ 25 | package org.slf4j.impl; 26 | 27 | import org.slf4j.ILoggerFactory; 28 | import org.slf4j.LoggerFactory; 29 | import org.slf4j.spi.LoggerFactoryBinder; 30 | 31 | /** 32 | * The binding of {@link LoggerFactory} class with an actual instance of 33 | * {@link ILoggerFactory} is performed using information returned by this class. 34 | * 35 | * @author Ceki Gülcü 36 | */ 37 | public class StaticLoggerBinder implements LoggerFactoryBinder { 38 | 39 | /** 40 | * The unique instance of this class. 41 | * 42 | */ 43 | private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); 44 | 45 | /** 46 | * Return the singleton of this class. 47 | * 48 | * @return the StaticLoggerBinder singleton 49 | */ 50 | public static final StaticLoggerBinder getSingleton() { 51 | return SINGLETON; 52 | } 53 | 54 | 55 | /** 56 | * Declare the version of the SLF4J API this implementation is compiled against. 57 | * The value of this field is usually modified with each release. 58 | */ 59 | // to avoid constant folding by the compiler, this field must *not* be final 60 | public static String REQUESTED_API_VERSION = "1.6.99"; // !final 61 | 62 | 63 | private static final String loggerFactoryClassStr = PluginSlf4jFactory.class.getName(); 64 | 65 | /** The ILoggerFactory instance returned by the {@link #getLoggerFactory} method 66 | * should always be the same object 67 | */ 68 | private final ILoggerFactory loggerFactory; 69 | 70 | private StaticLoggerBinder() { 71 | // Note: JCL gets substituted at build time by an appropriate Ant task 72 | loggerFactory = new PluginSlf4jFactory(); 73 | } 74 | 75 | public ILoggerFactory getLoggerFactory() { 76 | return loggerFactory; 77 | } 78 | 79 | public String getLoggerFactoryClassStr() { 80 | return loggerFactoryClassStr; 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ReflectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | import java.lang.reflect.Modifier; 6 | 7 | /** 8 | * Copyright 2013 Ryan Michela 9 | */ 10 | public class ReflectionUtil { 11 | 12 | public static void setProtectedValue(Object o, String field, Object newValue) 13 | { 14 | setProtectedValue(o.getClass(), o, field, newValue); 15 | } 16 | 17 | public static void setProtectedValue(Class c, String field, Object newValue) 18 | { 19 | setProtectedValue(c, null, field, newValue); 20 | } 21 | 22 | public static void setProtectedValue(Class c, Object o, String field, Object newValue) 23 | { 24 | try 25 | { 26 | 27 | Field f = c.getDeclaredField(field); 28 | 29 | f.setAccessible(true); 30 | 31 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 32 | modifiersField.setAccessible(true); 33 | modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); 34 | 35 | f.set(o, newValue); 36 | } 37 | catch (NoSuchFieldException | IllegalAccessException ex) 38 | { 39 | System.out.println("*** " + c.getName() + ":" + ex); 40 | } 41 | } 42 | 43 | public static T getProtectedValue(Object obj, String fieldName) 44 | { 45 | try 46 | { 47 | Class c = obj.getClass(); 48 | while (c != Object.class) 49 | { 50 | Field[] fields = c.getDeclaredFields(); 51 | for (Field f : fields) 52 | { 53 | if (f.getName() == fieldName) 54 | { 55 | f.setAccessible(true); 56 | return (T) f.get(obj); 57 | } 58 | } 59 | c = c.getSuperclass(); 60 | } 61 | System.out.println("*** " + obj.getClass().getName() + ":No such field"); 62 | return null; 63 | } 64 | catch (Exception ex) 65 | { 66 | System.out.println("*** " + obj.getClass().getName() + ":" + ex); 67 | return null; 68 | } 69 | } 70 | 71 | public static T getProtectedValue(Class c, String field) 72 | { 73 | try 74 | { 75 | Field f = c.getDeclaredField(field); 76 | f.setAccessible(true); 77 | return (T) f.get(c); 78 | } 79 | catch (Exception ex) 80 | { 81 | System.out.println("*** " + c.getName() + ":" + ex); 82 | return null; 83 | } 84 | } 85 | 86 | public static Object invokeProtectedMethod(Class c, String method, Object... args) 87 | { 88 | return invokeProtectedMethod(c, null, method, args); 89 | } 90 | 91 | public static Object invokeProtectedMethod(Object o, String method, Object... args) 92 | { 93 | return invokeProtectedMethod(o.getClass(), o, method, args); 94 | } 95 | 96 | public static Object invokeProtectedMethod(Class c, Object o, String method, Object... args) 97 | { 98 | try 99 | { 100 | Class[] pTypes = new Class[args.length]; 101 | for (int i = 0; i < args.length; i++) 102 | { 103 | if (args[i] instanceof Integer) 104 | pTypes[i] = int.class; 105 | else 106 | pTypes[i] = args[i].getClass(); 107 | } 108 | 109 | Method m = c.getDeclaredMethod(method, pTypes); 110 | m.setAccessible(true); 111 | return m.invoke(o, args); 112 | } 113 | catch (Exception ex) 114 | { 115 | System.out.println("*** " + c.getName() + "." + method + "(): " + ex); 116 | return null; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/MkpasswdCommand.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.bukkit.command.CommandExecutor; 4 | import org.bukkit.command.CommandSender; 5 | import org.bukkit.command.Command; 6 | import org.bukkit.entity.Player; 7 | 8 | import java.util.Arrays; 9 | 10 | import net.md_5.bungee.api.chat.ClickEvent; 11 | import net.md_5.bungee.api.chat.ComponentBuilder; 12 | import net.md_5.bungee.api.chat.HoverEvent; 13 | import net.md_5.bungee.api.chat.TextComponent; 14 | 15 | import com.ryanmichela.sshd.Cryptography; 16 | import com.ryanmichela.sshd.SshdPlugin; 17 | 18 | class MkpasswdCommand implements CommandExecutor 19 | { 20 | // Because Spigot's failed syntax API is really less than ideal (you should be required to add a 21 | // SendSyntax function override), we're just always going to return true even for syntax failures 22 | // as we will handle the syntax message internally. This also lets us send the messages more 23 | // securely to the client without people knowing we're using the command. This prevents password 24 | // or hash leakages from the user to other connected users. Plus this syntax will show how 25 | // to both use the command and what hashes we support which is important for people who don't 26 | // know how to RTFM. - Justin 27 | private void SendSyntax(CommandSender sender, boolean invalid) 28 | { 29 | if (invalid) 30 | sender.sendMessage("\u00A7cInvalid Syntax\u00A7r"); 31 | sender.sendMessage("\u00A7a/mkpasswd \u00A7r"); 32 | sender.sendMessage("\u00A79Supported Hashes: SHA256, PBKDF2, BCRYPT, PLAIN\u00A7r"); 33 | } 34 | 35 | @Override 36 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) 37 | { 38 | String algoritm, password; 39 | try 40 | { 41 | // Stupid bukkit, we have to concatenate the arguments together if they're using 42 | // spaces in their passwords otherwise it won't be as strong as it should be. 43 | algoritm = args[0]; 44 | password = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); 45 | if (password.trim().isEmpty()) // Shortcut to the catch statement below. 46 | throw new ArrayIndexOutOfBoundsException(); 47 | } 48 | catch (ArrayIndexOutOfBoundsException e) 49 | { 50 | // ignore it. 51 | this.SendSyntax(sender, true); 52 | return true; 53 | } 54 | 55 | boolean hasperm = (sender instanceof Player) ? ((Player)sender).hasPermission("sshd.mkpasswd") : true; 56 | 57 | if (hasperm) 58 | { 59 | try 60 | { 61 | String hash = ""; 62 | // Dumb but whatever. Some people are really dense. 63 | if (algoritm.equalsIgnoreCase("PLAIN")) 64 | { 65 | // I mean c'mon... 66 | sender.sendMessage("\u00A79Your Hash: \u00A7cIt's literally your unhashed password."); 67 | return true; 68 | } 69 | else if (algoritm.equalsIgnoreCase("pbkdf2")) 70 | hash = Cryptography.PBKDF2_HashPassword(password); 71 | else if (algoritm.equalsIgnoreCase("bcrypt")) 72 | hash = Cryptography.BCrypt_HashPassword(password); 73 | else if (algoritm.equalsIgnoreCase("sha256")) 74 | hash = Cryptography.SHA256_HashPassword(password); 75 | else 76 | { 77 | this.SendSyntax(sender, !algoritm.equalsIgnoreCase("help")); 78 | return true; 79 | } 80 | 81 | TextComponent msg = new TextComponent("\u00A79Your Hash: " + hash + "\u00A7r"); 82 | msg.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash)); 83 | msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("Click to copy the hash!").create())); 84 | 85 | sender.spigot().sendMessage(msg); 86 | } 87 | catch (Exception e) 88 | { 89 | // We're console, just print the stack trace. 90 | e.printStackTrace(); 91 | sender.sendMessage("\u00A7cAn error occured. Please check console for details.\u00A7r"); 92 | } 93 | } 94 | else 95 | sender.sendMessage("\u00A7cPermission Denied.\u00A7r"); 96 | 97 | return true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # The IP addresses(s) the SSH server will listen on. Use a comma separated list for multiple addresses. 2 | # Leave as "all" for all addresses. 3 | ListenAddress: all 4 | # The port the SSH server will listen on. Note that anything *below* 1024 will require you to run 5 | # the whole minecraft server with elevated privileges (NOT RECOMMENDED). 6 | Port: 1025 7 | 8 | # Operational mode. Don't touch if you don't know what you're doing. Can be either DEFAULT or RPC 9 | Mode: DEFAULT 10 | 11 | # Enable built-in SFTP server or not. You'll be able to connect and upload/download files via SFTP protocol. 12 | # Might be useful for testing purposes as well , i. e. docker containers. 13 | EnableSFTP: true 14 | 15 | # Number of times a person can fail to use an SSH key or enter a password 16 | # before it terminates the connection. 17 | LoginRetries: 3 18 | 19 | ######################################################################################## 20 | # By default, only public key authentication is enabled. This is the most secure mode. 21 | # To authorize a user to login with their public key, install their key using the 22 | # OpenSSH authorized_keys file format in the authorized_users directory. Name the key 23 | # file with the user's username and no extension. Note: If you want to let a user have 24 | # many keys, you can append the keys to their file in authorized_users. 25 | ######################################################################################## 26 | 27 | # For less secure username and password based authentication, complete the sections below. 28 | 29 | # Type of hashing to use for the passwords below. 30 | # Options are: PLAIN (insecure), bcrypt, pbkdf2, sha256 31 | # 32 | # You can use the console/in-game command `/mkpasswd [hash] PASSWORD` to 33 | # generate a password hash string then copy it for your passwords below. 34 | # You can also use `/mkpasswd help` to see what algorithms are supported. 35 | PasswordType: bcrypt 36 | 37 | # Associate each username with a password hash (or the password if the PasswordType is set to PLAIN) 38 | Credentials: 39 | # The defaults for any user who does not have a specific section. 40 | # Specific user permissions override the $default pseudo-user. 41 | $default: 42 | # Whether they can read or write to the console 43 | console: RW 44 | # SFTP access for anyone. 45 | sftp: 46 | # Whether sftp is allowed at all. 47 | enabled: true 48 | # Whether to allow or deny by default 49 | default: allow 50 | # specific rules for directories 51 | rules: 52 | # Deny the SSHD config folder by default as an example. 53 | "*SSHD/*": 54 | readable: false 55 | writeable: false 56 | 57 | # Username (should match SSH key if using key-based authentication) 58 | justasic: 59 | # Password hash from /mkpasswd command 60 | password: $2a$10$Oqk83FrypRrMF35EDeoQDuidJOQEWBE0joEQ7MJFi/Oeg26wQ3fm2 61 | # Whether they can read, write, or have read/write permissions to console. 62 | console: RW 63 | # SFTP access for this user. 64 | sftp: 65 | # Whether SFTP is enabled for this user. 66 | enabled: true 67 | # Whether to deny access by default or allow access by default 68 | default: allow 69 | # Rules regarding their SFTP access. 70 | # These rules are relative to the server root. 71 | # This acts as a chroot for the server root. 72 | # Each path can be an absolute path or a regular expression. 73 | rules: 74 | "/path/to/file": 75 | # Whether the user can read the file over SFTP 76 | readable: true 77 | # Whether the user can write/modify the file over SFTP 78 | writeable: true 79 | "/path/to/regex/*": 80 | readable: true 81 | writeable: false 82 | "/path/to/directory/": 83 | readable: false 84 | writeable: true 85 | "/another/example/path": 86 | readable: false 87 | writeable: false 88 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ConsoleLogFormatter.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | /** 4 | * Copyright 2013 Ryan Michela 5 | */ 6 | 7 | import org.bukkit.ChatColor; 8 | import org.fusesource.jansi.Ansi; 9 | 10 | import java.io.PrintWriter; 11 | import java.io.StringWriter; 12 | import java.text.SimpleDateFormat; 13 | import java.util.EnumMap; 14 | import java.util.Map; 15 | import java.util.logging.Formatter; 16 | import java.util.logging.LogRecord; 17 | 18 | public class ConsoleLogFormatter extends Formatter { 19 | 20 | private SimpleDateFormat dateFormat; 21 | private static final Map replacements = new EnumMap(ChatColor.class); 22 | 23 | public ConsoleLogFormatter() { 24 | this.dateFormat = new SimpleDateFormat("HH:mm:ss"); 25 | } 26 | 27 | public static String ColorizeString(String str) 28 | { 29 | // ORIGINAL CODE FROM org.bukkit.craftbukkit.command.ColouredConsoleSender 30 | 31 | replacements.put(ChatColor.BLACK, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).boldOff().toString()); 32 | replacements.put(ChatColor.DARK_BLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).boldOff().toString()); 33 | replacements.put(ChatColor.DARK_GREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).boldOff().toString()); 34 | replacements.put(ChatColor.DARK_AQUA, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).boldOff().toString()); 35 | replacements.put(ChatColor.DARK_RED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).boldOff().toString()); 36 | replacements.put(ChatColor.DARK_PURPLE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).boldOff().toString()); 37 | replacements.put(ChatColor.GOLD, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).boldOff().toString()); 38 | replacements.put(ChatColor.GRAY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).boldOff().toString()); 39 | replacements.put(ChatColor.DARK_GRAY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).bold().toString()); 40 | replacements.put(ChatColor.BLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).bold().toString()); 41 | replacements.put(ChatColor.GREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).bold().toString()); 42 | replacements.put(ChatColor.AQUA, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).bold().toString()); 43 | replacements.put(ChatColor.RED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).bold().toString()); 44 | replacements.put(ChatColor.LIGHT_PURPLE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).bold().toString()); 45 | replacements.put(ChatColor.YELLOW, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).bold().toString()); 46 | replacements.put(ChatColor.WHITE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).bold().toString()); 47 | replacements.put(ChatColor.MAGIC, Ansi.ansi().a(Ansi.Attribute.BLINK_SLOW).toString()); 48 | replacements.put(ChatColor.BOLD, Ansi.ansi().a(Ansi.Attribute.UNDERLINE_DOUBLE).toString()); 49 | replacements.put(ChatColor.STRIKETHROUGH, Ansi.ansi().a(Ansi.Attribute.STRIKETHROUGH_ON).toString()); 50 | replacements.put(ChatColor.UNDERLINE, Ansi.ansi().a(Ansi.Attribute.UNDERLINE).toString()); 51 | replacements.put(ChatColor.ITALIC, Ansi.ansi().a(Ansi.Attribute.ITALIC).toString()); 52 | replacements.put(ChatColor.RESET, Ansi.ansi().a(Ansi.Attribute.RESET).toString()); 53 | 54 | String result = str; 55 | for (ChatColor color : ChatColor.values()) 56 | { 57 | if (replacements.containsKey(color)) 58 | { 59 | result = result.replaceAll("(?i)" + color.toString(), replacements.get(color)); 60 | } 61 | else 62 | { 63 | result = result.replaceAll("(?i)" + color.toString(), ""); 64 | } 65 | } 66 | result += Ansi.ansi().reset().toString(); 67 | return result; 68 | } 69 | 70 | public String format(LogRecord logrecord) 71 | { 72 | try 73 | { 74 | Class.forName("org.bukkit.craftbukkit.command.ColouredConsoleSender"); 75 | } 76 | catch (ClassNotFoundException ignored) 77 | { 78 | // MEANS WE'RE ON PAPER/TACO/OTHER SHIT 79 | colorize(logrecord); 80 | } 81 | StringBuilder stringbuilder = new StringBuilder(); 82 | 83 | stringbuilder.append(" ["); 84 | stringbuilder.append(this.dateFormat.format(logrecord.getMillis())).append(" "); 85 | 86 | stringbuilder.append(logrecord.getLevel().getName()).append("]: "); 87 | stringbuilder.append(this.formatMessage(logrecord)); 88 | stringbuilder.append('\n'); 89 | Throwable throwable = logrecord.getThrown(); 90 | 91 | if (throwable != null) 92 | { 93 | StringWriter stringwriter = new StringWriter(); 94 | 95 | throwable.printStackTrace(new PrintWriter(stringwriter)); 96 | stringbuilder.append(stringwriter.toString()); 97 | } 98 | 99 | return stringbuilder.toString().replace("\n", "\r\n"); 100 | } 101 | 102 | private void colorize(LogRecord logrecord) 103 | { 104 | String result = ColorizeString(logrecord.getMessage()); 105 | logrecord.setMessage(result); 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /docs/ssh_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 30 | 36 | 41 | 46 | 52 | 53 | 54 | 73 | 75 | 76 | 78 | image/svg+xml 79 | 81 | 82 | 83 | 84 | 85 | 90 | 96 | 102 | 103 | 108 | _ 120 | > 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/implementations/SSHDCommandSender.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd.implementations; 2 | 3 | import com.ryanmichela.sshd.SshdPlugin; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.ChatColor; 6 | import org.bukkit.Server; 7 | import org.bukkit.command.CommandSender; 8 | import org.bukkit.command.ConsoleCommandSender; 9 | import org.bukkit.conversations.Conversation; 10 | import org.bukkit.conversations.ConversationAbandonedEvent; 11 | import org.bukkit.conversations.ManuallyAbandonedConversationCanceller; 12 | import org.bukkit.permissions.PermissibleBase; 13 | import org.bukkit.permissions.Permission; 14 | import org.bukkit.permissions.PermissionAttachment; 15 | import org.bukkit.permissions.PermissionAttachmentInfo; 16 | import org.bukkit.plugin.Plugin; 17 | 18 | import com.ryanmichela.sshd.ConsoleShellFactory; 19 | import com.ryanmichela.sshd.ConsoleLogFormatter; 20 | 21 | import java.io.IOException; 22 | import java.util.Arrays; 23 | import java.util.Set; 24 | import java.util.logging.Level; 25 | 26 | public class SSHDCommandSender implements ConsoleCommandSender, CommandSender 27 | { 28 | private final PermissibleBase perm = new PermissibleBase(this); 29 | private final SSHDConversationTracker conversationTracker = new SSHDConversationTracker(); 30 | // Set by the upstream allocating function 31 | public ConsoleShellFactory.ConsoleShell console; 32 | 33 | public void sendMessage(String message) 34 | { 35 | this.sendRawMessage(message + "\r"); 36 | } 37 | 38 | public void sendRawMessage(String message) 39 | { 40 | // What the fuck does this code even do? Are we sending to one client or all of them? 41 | if (this.console.ConsoleReader == null) 42 | return; 43 | try 44 | { 45 | this.console.ConsoleReader.println(ConsoleLogFormatter.ColorizeString(message).replace("\n", "\n\r")); 46 | this.console.ConsoleReader.print(this.console.ConsoleReader.RESET_LINE + ""); 47 | this.console.ConsoleReader.flush(); 48 | try 49 | { 50 | this.console.ConsoleReader.drawLine(); 51 | } 52 | catch (Throwable ex) 53 | { 54 | this.console.ConsoleReader.getCursorBuffer().clear(); 55 | } 56 | this.console.ConsoleReader.flush(); 57 | } 58 | catch (IOException e) 59 | { 60 | SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error sending message to SSHDCommandSender", e); 61 | } 62 | } 63 | 64 | public void sendMessage(String[] messages) 65 | { 66 | Arrays.asList(messages).forEach(this::sendMessage); 67 | } 68 | 69 | public String getName() 70 | { 71 | return "SSHD Console"; 72 | } 73 | 74 | public boolean isOp() 75 | { 76 | return true; 77 | } 78 | 79 | public void setOp(boolean value) 80 | { 81 | throw new UnsupportedOperationException("Cannot change operator status of server console"); 82 | } 83 | 84 | public boolean beginConversation(Conversation conversation) 85 | { 86 | return this.conversationTracker.beginConversation(conversation); 87 | } 88 | 89 | public void abandonConversation(Conversation conversation) 90 | { 91 | this.conversationTracker.abandonConversation(conversation, new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller())); 92 | } 93 | 94 | public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) 95 | { 96 | this.conversationTracker.abandonConversation(conversation, details); 97 | } 98 | 99 | public void acceptConversationInput(String input) 100 | { 101 | this.conversationTracker.acceptConversationInput(input); 102 | } 103 | 104 | public boolean isConversing() 105 | { 106 | return this.conversationTracker.isConversing(); 107 | } 108 | 109 | public boolean isPermissionSet(String name) 110 | { 111 | return this.perm.isPermissionSet(name); 112 | } 113 | 114 | public boolean isPermissionSet(Permission perm) 115 | { 116 | return this.perm.isPermissionSet(perm); 117 | } 118 | 119 | public boolean hasPermission(String name) 120 | { 121 | return this.perm.hasPermission(name); 122 | } 123 | 124 | public boolean hasPermission(Permission perm) 125 | { 126 | return this.perm.hasPermission(perm); 127 | } 128 | 129 | public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) 130 | { 131 | return this.perm.addAttachment(plugin, name, value); 132 | } 133 | 134 | public PermissionAttachment addAttachment(Plugin plugin) 135 | { 136 | return this.perm.addAttachment(plugin); 137 | } 138 | 139 | public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) 140 | { 141 | return this.perm.addAttachment(plugin, name, value, ticks); 142 | } 143 | 144 | public PermissionAttachment addAttachment(Plugin plugin, int ticks) 145 | { 146 | return this.perm.addAttachment(plugin, ticks); 147 | } 148 | 149 | public void removeAttachment(PermissionAttachment attachment) 150 | { 151 | this.perm.removeAttachment(attachment); 152 | } 153 | 154 | public void recalculatePermissions() 155 | { 156 | this.perm.recalculatePermissions(); 157 | } 158 | 159 | public Set getEffectivePermissions() 160 | { 161 | return this.perm.getEffectivePermissions(); 162 | } 163 | 164 | public boolean isPlayer() 165 | { 166 | return false; 167 | } 168 | 169 | public Server getServer() 170 | { 171 | return Bukkit.getServer(); 172 | } 173 | 174 | public CommandSender.Spigot spigot() 175 | { 176 | return ((CommandSender)this).spigot(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Minecraft-SSHD 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/Justasic/Minecraft-SSHD.svg?branch=master)](https://travis-ci.org/Justasic/Minecraft-SSHD) 5 | [![Release](https://img.shields.io/github/release/Justasic/Minecraft-SSHD.svg?label=Release&maxAge=60)](https://github.com/Justasic/Minecraft-SSHD/releases/latest) 6 | [![GitHub license](https://img.shields.io/github/license/Justasic/Minecraft-SSHD)](https://github.com/Justasic/Minecraft-SSHD/blob/master/LICENSE) 7 | 8 | diskover
9 | 10 | **Have you ever wished you could remotely access your server's admin console without having to setup a complex remote access system? Now you can with Minecraft-SSHD!** 11 | 12 | Minecraft-SSHD securely exposes your Minecraft admin console and the server filesystem using the SSH protocol - the same protocol that serves as the secure foundation for nearly all remote server administration.
13 | 14 | - Compatible with all ssh clients, regardless of operating system. 15 | - Remotely view your server log in real-time. 16 | - Remotely issue commands from the server console, just as if you were on the server itself. 17 | - Supports multiple concurrent remote connections. 18 | - Strong identity support using public key authentication. 19 | - Audit history who is running commands in the console 20 | - Run Spigot without using screen or tmux (by adding `-noconsole`) 21 | - Remotely script your server by issuing one-off console commands with ssh. 22 | 23 | ### Why should I use Minecraft-SSHD? 24 | 25 | - You are in a shared hosting environment that only gives you access to the - log files. 26 | - You want to share access to your server console, but don't want to give anybody access to the machine its running on. 27 | - You always wanted to use RCON, but want to see the server log as well. 28 | - You are tired of running your server in a GNU screen or tmux session. 29 | - You just want to access your server console using SSH. 30 | 31 | Note: By default, only public key authentication is enabled. This is the most secure authentication mode! Setting a username and password will make your server less secure. 32 | 33 | Screenshots 34 | ============ 35 | 36 | console 37 | session
38 | 39 | 40 | Setting Up Public Key Authentication 41 | ==================================== 42 | 43 | Setting up public key authentication with SSH requires first generating a public and private key pair and then installing just the public key on your Spigot server. This plugin supports all modern SSH key algoritms as OpenSSH. You can paste as many public keys from the methods below into each user's authorization file if they have multiple private keys. You can read [this guide from ssh.com](https://www.ssh.com/ssh/keygen/) if you want a better explanation on different key files. 44 | 45 | ## Generating New Keys 46 | 47 | #### On Windows 48 | 49 | 1. Ensure [Putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) is installed and open up `puttygen` (you can search for it in start search). 50 | 2. Click `Generate` and follow the directions. 51 | 3. When it finishes, set your key comment (if you like) and copy the text from the big `Public key for pasting into OpenSSH authorized_keys file` 52 | 4. Create a new file inside of the `plugins/SSHD/authorized_users` folder and name the file just the username (example: `justasic`, there should ***NOT*** be a file extension or authentication does not work). 53 | 5. Paste the key you copied from step 3 into the file you just created. 54 | 6. SSH into the server and see if your key works 55 | 56 | #### On Linux/OS X 57 | 58 | 1. Open a terminal and run `ssh-keygen` then follow the prompts. 59 | 2. Copy the contents of your `id_.pub` file (example: if your key was generated with rsa, it will be named `id_rsa.pub`). This file is usually located in `/home/YOURUSERNAME/.ssh/` 60 | 3. Paste the contents of the .pub file into a new file inside the `plugins/SSHD/authorized_users` folder and name the file just the username that the user will use to login with (example: `justasic`, there should ***NOT*** be a file extension or authentication does not work). 61 | 62 | ## Using existing keys 63 | 64 | #### On Windows 65 | 66 | 1. Ensure [Putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) is installed and open up `puttygen` (you can search for it in start search). 67 | 2. Click `Conversions` then click `Import Key` and select your .ppk file. 68 | 3. Copy the text from the big `Public key for pasting into OpenSSH authorized_keys file` 69 | 4. Create a new file inside of the `plugins/SSHD/authorized_users` folder and name the file just the username (example: `justasic`, there should ***NOT*** be a file extension or authentication does not work). 70 | 5. Paste the key you copied from step 3 into the file you just created. 71 | 6. SSH into the server and see if your key works 72 | 73 | #### On Linux/OS X 74 | 75 | 1. Copy the contents of your `id_.pub` file (example: if your key was generated with rsa, it will be named `id_rsa.pub`). This file is usually located in `/home/YOURUSERNAME/.ssh/` 76 | 2. Paste the contents of the .pub file into a new file inside the `plugins/SSHD/authorized_users` folder and name the file just the username that the user will use to login with (example: `justasic`, there should ***NOT*** be a file extension or authentication does not work). 77 | 78 | Plugin Usage 79 | ============ 80 | 81 | ## Commands 82 | 83 | /mkpasswd 84 | 85 | mkpasswd supports the following hash algorithms: 86 | 87 | - bcrypt - Using the OpenBSD-style Blowfish password hash 88 | - sha256 - Using a basic salted sha256 hash 89 | - pbkdf2 - Using the [PBKDF2](https://en.wikipedia.org/wiki/Pbkdf2) password hash 90 | - PLAIN - Using plain text passwords (very insecure) 91 | 92 | ## Permissions 93 | 94 | `sshd.mkpasswd` - Checks if the in-game user has access to run the mkpasswd command. 95 | 96 | Minecraft-SSHD uses cryptographic certificates or a secure username and password to verify remote access. 97 | 98 | 99 | ## Source Code 100 | [Get the source on GitHub](https://github.com/Justasic/Minecraft-SSHD "Source Code") 101 | 102 | ## Our Discord 103 | https://discord.gg/DtrJFn 104 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/Cryptography.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import java.security.spec.InvalidKeySpecException; 4 | import java.util.Arrays; 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.SecureRandom; 8 | 9 | import javax.crypto.SecretKeyFactory; 10 | import javax.crypto.spec.PBEKeySpec; 11 | 12 | import com.ryanmichela.sshd.BCrypt; 13 | 14 | import java.math.BigInteger; 15 | 16 | // You should run `openssl speed` to see which parts of these algorithms may need 17 | // tweaking in the future as CPUs and GPUs get faster to crack these hashing algos. 18 | 19 | 20 | class Cryptography 21 | { 22 | /////////////////////////////////////////////////////////////////////////////// 23 | // BCrypt-based password hashing algorithm 24 | /////////////////////////////////////////////////////////////////////////////// 25 | 26 | public static String BCrypt_HashPassword(String password) throws NoSuchAlgorithmException 27 | { 28 | // This algo handles the salt itself. 29 | return BCrypt.hashpw(password, BCrypt.gensalt()); 30 | } 31 | 32 | public static Boolean BCrypt_ValidatePassword(String password, String ConfigPassword) throws NoSuchAlgorithmException 33 | { 34 | // Unfortunately, the BCrypt library uses String.compareTo which is not 35 | // hardened against timing attacks so we have to compare the password 36 | // ourselves otherwise it doesn't work well. 37 | String test = BCrypt.hashpw(password, ConfigPassword); 38 | return TimingSafeCmp(test.getBytes(), ConfigPassword.getBytes()); 39 | } 40 | 41 | /////////////////////////////////////////////////////////////////////////////// 42 | // SHA256-based password hashing algorithm 43 | /////////////////////////////////////////////////////////////////////////////// 44 | 45 | public static String SHA256_HashPassword(String password) throws NoSuchAlgorithmException 46 | { 47 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 48 | byte[] salt = GetSalt(); 49 | int iterations = 500000; // sha256 is a fast algo to make lots of hashes for, 50 | // try and make it kinda computationally expensive. 51 | md.update(salt); 52 | byte[] bytes = md.digest(password.getBytes()); 53 | 54 | // Hash it a few thousand times. 55 | for (int i = 0; i < iterations; i++) 56 | bytes = md.digest(bytes); 57 | 58 | StringBuilder sb = new StringBuilder(); 59 | for (int i = 0; i < bytes.length; i++) 60 | sb.append(Integer.toString((bytes[i] & 0xFF) + 0x100, 16).substring(1)); 61 | 62 | return iterations + "$" + ToHex(salt) + "$" + sb.toString(); 63 | } 64 | 65 | public static Boolean SHA256_ValidatePassword(String password, String ConfigPassword) throws NoSuchAlgorithmException 66 | { 67 | String[] hparts = ConfigPassword.split("\\$"); 68 | int iterations = Integer.parseInt(hparts[0]); 69 | byte[] salt = FromHex(hparts[1]); 70 | String hash = hparts[2]; 71 | 72 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 73 | 74 | md.update(salt); 75 | byte[] bytes = md.digest(password.getBytes()); 76 | 77 | // Hash it a few thousand times. 78 | for (int i = 0; i < iterations; i++) 79 | bytes = md.digest(bytes); 80 | 81 | StringBuilder sb = new StringBuilder(); 82 | for (int i = 0; i < bytes.length; i++) 83 | sb.append(Integer.toString((bytes[i] & 0xFF) + 0x100, 16).substring(1)); 84 | 85 | return TimingSafeCmp(hash.getBytes(), sb.toString().getBytes()); 86 | } 87 | 88 | /////////////////////////////////////////////////////////////////////////////// 89 | // PBKDF2-based password hashing algoritm 90 | /////////////////////////////////////////////////////////////////////////////// 91 | public static String PBKDF2_HashPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException 92 | { 93 | char[] passwdchars = password.toCharArray(); 94 | int iterations = 20000; // NOTE: Change this as CPUs get faster 95 | // First: Start getting 16 bytes of guaranteed random data to use for our salt 96 | byte[] salt = GetSalt(); 97 | 98 | PBEKeySpec spec = new PBEKeySpec(passwdchars, salt, iterations, 64*8); 99 | SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); 100 | byte[] hash = skf.generateSecret(spec).getEncoded(); 101 | return iterations + "$" + ToHex(salt) + "$" + ToHex(hash); 102 | } 103 | 104 | public static Boolean PBKDF2_ValidateHash(String password, String ConfigPassword) throws NoSuchAlgorithmException, InvalidKeySpecException 105 | { 106 | String[] hparts = ConfigPassword.split("\\$"); 107 | int iterations = Integer.parseInt(hparts[0]); 108 | byte[] salt = FromHex(hparts[1]); 109 | byte[] hash = FromHex(hparts[2]); 110 | 111 | PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, hash.length * 8); 112 | SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); 113 | byte[] cmphash = skf.generateSecret(spec).getEncoded(); 114 | 115 | return TimingSafeCmp(cmphash, hash); 116 | } 117 | 118 | /////////////////////////////////////////////////////////////////////////////// 119 | // Utility Functions 120 | /////////////////////////////////////////////////////////////////////////////// 121 | 122 | public static byte[] GetSalt() throws NoSuchAlgorithmException 123 | { 124 | SecureRandom sr = SecureRandom.getInstance("SHA1PRNG"); 125 | byte[] salt = new byte[16]; 126 | sr.nextBytes(salt); 127 | return salt; 128 | } 129 | 130 | // This is a string comparitor function safe against timing attacks. 131 | public static boolean TimingSafeCmp(byte[] str1, byte[] str2) 132 | { 133 | int diff = str1.length ^ str2.length; 134 | for (int i = 0; i < str1.length && i < str2.length; i++) 135 | diff |= str1[i] ^ str2[i]; 136 | 137 | return diff == 0; 138 | } 139 | 140 | private static byte[] FromHex(String hex) throws NoSuchAlgorithmException 141 | { 142 | byte[] bytes = new byte[hex.length() / 2]; 143 | 144 | for (int i = 0; i < bytes.length; i++) 145 | bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); 146 | 147 | return bytes; 148 | } 149 | 150 | private static String ToHex(byte[] array) throws NoSuchAlgorithmException 151 | { 152 | BigInteger bi = new BigInteger(1, array); 153 | String hex = bi.toString(16); 154 | int paddingLength = (array.length * 2) - hex.length(); 155 | 156 | if (paddingLength > 0) 157 | return String.format("%0" + paddingLength + "d", 0) + hex; 158 | else 159 | return hex; 160 | } 161 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ryanmichela 8 | sshd 9 | Minecraft-SSHD: The SSH daemon for Minecraft servers. 10 | 2.0.0 11 | https://github.com/Justasic/Minecraft-SSHD/ 12 | 13 | 14 | 1.8 15 | UTF-8 16 | 17 | 18 | 19 | 20 | 21 | spigot-repo 22 | https://hub.spigotmc.org/nexus/content/groups/public 23 | 24 | 25 | 26 | 27 | 28 | 29 | GPL2 30 | http://www.gnu.org/licenses/gpl-2.0.html 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.spigotmc 38 | spigot-api 39 | 1.15.2-R0.1-SNAPSHOT 40 | 41 | 42 | 43 | org.apache.sshd 44 | sshd-core 45 | 2.4.0 46 | compile 47 | jar 48 | 49 | 50 | 51 | org.apache.sshd 52 | sshd-mina 53 | 2.4.0 54 | 55 | 56 | 57 | org.apache.sshd 58 | sshd-contrib 59 | 2.4.0 60 | 61 | 62 | 63 | org.apache.sshd 64 | sshd-common 65 | 2.4.0 66 | compile 67 | jar 68 | 69 | 70 | 71 | org.apache.sshd 72 | sshd-sftp 73 | 2.4.0 74 | 75 | 76 | 77 | net.i2p.crypto 78 | eddsa 79 | 0.3.0 80 | 81 | 82 | 83 | org.apache.mina 84 | mina-core 85 | 2.1.3 86 | 87 | 88 | 89 | org.slf4j 90 | slf4j-api 91 | 1.7.28 92 | 93 | 94 | 95 | org.slf4j 96 | slf4j-jdk14 97 | 1.7.28 98 | 99 | 100 | 101 | jline 102 | jline 103 | 2.12.1 104 | 105 | 106 | 107 | org.apache.logging.log4j 108 | log4j-core 109 | 2.0 110 | provided 111 | 112 | 113 | 114 | org.apache.logging.log4j 115 | log4j-api 116 | 2.1 117 | provided 118 | 119 | 120 | 121 | commons-codec 122 | commons-codec 123 | 1.10 124 | provided 125 | 126 | 127 | 128 | 129 | 130 | clean package 131 | 132 | 133 | 134 | . 135 | true 136 | ${basedir}/src/main/resources 137 | 138 | plugin.yml 139 | config.yml 140 | motd.txt 141 | 142 | 143 | 144 | 145 | 146 | 147 | maven-assembly-plugin 148 | 3.1.1 149 | 150 | 151 | package 152 | 153 | single 154 | 155 | 156 | 157 | 158 | ${project.name}-${project.version} 159 | false 160 | 161 | jar-with-dependencies 162 | 163 | 164 | 165 | 166 | 167 | 168 | org.apache.maven.plugins 169 | maven-compiler-plugin 170 | 3.7.0 171 | 172 | 1.8 173 | 1.8 174 | true 175 | 176 | 177 | 178 | 179 | 180 | jar 181 | 182 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/SshdPlugin.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; 4 | import org.apache.sshd.common.session.helpers.AbstractSession; 5 | import org.apache.sshd.server.SshServer; 6 | import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; 7 | import org.apache.sshd.server.session.ServerSession; 8 | import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; 9 | import org.apache.sshd.server.subsystem.sftp.SimpleAccessControlSftpEventListener; 10 | import org.bukkit.configuration.ConfigurationSection; 11 | import org.bukkit.plugin.java.JavaPlugin; 12 | 13 | import com.ryanmichela.sshd.ConsoleShellFactory; 14 | import com.ryanmichela.sshd.MkpasswdCommand; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.nio.file.FileSystems; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.ArrayList; 25 | import java.util.Map; 26 | import java.util.logging.Level; 27 | import java.util.stream.Stream; 28 | 29 | /** 30 | * Copyright 2013 Ryan Michela 31 | */ 32 | public class SshdPlugin extends JavaPlugin 33 | { 34 | private SshServer sshd; 35 | public static SshdPlugin instance; 36 | 37 | public static List GetSections(ConfigurationSection source) 38 | { 39 | if (source == null) 40 | return null; 41 | 42 | List nodes = new ArrayList(); 43 | for (String key : source.getKeys(false)) 44 | { 45 | if (source.isConfigurationSection(key)) 46 | nodes.add(source.getConfigurationSection(key)); 47 | } 48 | return nodes; 49 | } 50 | 51 | @Override public void onLoad() 52 | { 53 | saveDefaultConfig(); 54 | File authorizedKeys = new File(getDataFolder(), "authorized_keys"); 55 | if (!authorizedKeys.exists()) 56 | authorizedKeys.mkdirs(); 57 | 58 | try 59 | { 60 | File motd = new File(getDataFolder(), "motd.txt"); 61 | if (!motd.exists()) 62 | { 63 | InputStream link = (getClass().getResourceAsStream("/motd.txt")); 64 | Files.copy(link, motd.getAbsoluteFile().toPath()); 65 | } 66 | } 67 | catch (IOException e) 68 | { 69 | e.printStackTrace(); 70 | } 71 | 72 | // Don't go any lower than INFO or SSHD will cause a stack overflow exception. 73 | // SSHD will log that it wrote bites to the output stream, which writes 74 | // bytes to the output stream - ad nauseaum. 75 | getLogger().setLevel(Level.INFO); 76 | } 77 | 78 | @Override public void onEnable() 79 | { 80 | instance = this; 81 | 82 | sshd = SshServer.setUpDefaultServer(); 83 | sshd.setPort(getConfig().getInt("Port", 1025)); 84 | String host = getConfig().getString("ListenAddress", "all"); 85 | sshd.setHost(host.equals("all") ? null : host); 86 | 87 | File hostKey = new File(getDataFolder(), "hostkey"); 88 | File authorizedKeys = new File(getDataFolder(), "authorized_keys"); 89 | 90 | sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKey.toPath())); 91 | sshd.setShellFactory(new ConsoleShellFactory()); 92 | sshd.setPasswordAuthenticator(new ConfigPasswordAuthenticator()); 93 | sshd.setPublickeyAuthenticator(new PublicKeyAuthenticator(authorizedKeys)); 94 | 95 | if (getConfig().getBoolean("EnableSFTP", false)) 96 | { 97 | // Handle access control for SFTP. 98 | SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder(); 99 | builder.addSftpEventListener(new SimpleAccessControlSftpEventListener() 100 | { 101 | protected boolean isAccessAllowed(ServerSession session, String remote, Path localpath) 102 | { 103 | try 104 | { 105 | ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp"); 106 | 107 | // They don't have SFTP enabled so deny them. 108 | if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled")) 109 | return false; 110 | 111 | 112 | List rules = GetSections(UsernameNamespace.getConfigurationSection("rules")); 113 | if (rules != null) 114 | { 115 | for (ConfigurationSection path : rules) 116 | { 117 | // Check if the requesting path matches 118 | if (localpath.toString().matches(path.getName())) 119 | { 120 | // Check if they have read permissions 121 | if (path.getBoolean("readable")) 122 | return true; 123 | 124 | getLogger().info(String.format("Denied %s read access to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName())); 125 | return false; 126 | } 127 | } 128 | } 129 | 130 | return UsernameNamespace.getString("default").equalsIgnoreCase("allow"); 131 | } 132 | catch (Exception e) 133 | { 134 | e.printStackTrace(); 135 | // Automatically deny. 136 | return false; 137 | } 138 | } 139 | 140 | protected boolean isModificationAllowed(ServerSession session, String remote, Path localpath) 141 | { 142 | try 143 | { 144 | boolean defaultbool = getConfig().getBoolean("Credentials.$default.sftp.enabled", false); 145 | ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp"); 146 | 147 | // They don't have SFTP enabled so deny them. 148 | if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled", defaultbool)) 149 | return false; 150 | 151 | // Check a list of files against a path trying to be accessed. 152 | List rules = GetSections(UsernameNamespace.getConfigurationSection("rules")); 153 | if (rules != null) 154 | { 155 | for (ConfigurationSection path : rules) 156 | { 157 | // Check if the requesting path matches 158 | if (localpath.toString().matches(path.getName())) 159 | { 160 | // Check if they have read permissions 161 | if (path.getBoolean("writeable")) 162 | return true; 163 | 164 | getLogger().info(String.format("Denied %s modifications to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName())); 165 | return false; 166 | } 167 | } 168 | } 169 | 170 | return UsernameNamespace.getString("default", "deny").equalsIgnoreCase("allow"); 171 | } 172 | catch (Exception e) 173 | { 174 | e.printStackTrace(); 175 | // Automatically deny. 176 | return false; 177 | } 178 | } 179 | }); 180 | 181 | sshd.setSubsystemFactories(Collections.singletonList(builder.build())); 182 | sshd.setFileSystemFactory(new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent())); 183 | } 184 | 185 | this.getCommand("mkpasswd").setExecutor(new MkpasswdCommand()); 186 | 187 | sshd.setCommandFactory(new ConsoleCommandFactory()); 188 | try 189 | { 190 | sshd.start(); 191 | } 192 | catch (IOException e) 193 | { 194 | getLogger().log(Level.SEVERE, "Failed to start SSH server! ", e); 195 | } 196 | } 197 | 198 | @Override public void onDisable() 199 | { 200 | try 201 | { 202 | // Terminate any active sessions 203 | for (AbstractSession as : sshd.getActiveSessions()) 204 | as.close(true); 205 | // Pass "true" to stop immediately! 206 | sshd.stop(true); 207 | } 208 | catch (Exception e) 209 | { 210 | // do nothing 211 | e.printStackTrace(); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/ConsoleShellFactory.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | import com.ryanmichela.sshd.ConsoleCommandCompleter; 4 | import com.ryanmichela.sshd.ConsoleLogFormatter; 5 | import com.ryanmichela.sshd.PermissionUtil; 6 | import com.ryanmichela.sshd.FlushyOutputStream; 7 | import com.ryanmichela.sshd.FlushyStreamHandler; 8 | import com.ryanmichela.sshd.SshTerminal; 9 | import com.ryanmichela.sshd.SshdPlugin; 10 | import com.ryanmichela.sshd.StreamHandlerAppender; 11 | import com.ryanmichela.sshd.implementations.SSHDCommandSender; 12 | import jline.console.ConsoleReader; 13 | import org.apache.logging.log4j.LogManager; 14 | import org.apache.logging.log4j.core.Logger; 15 | import org.apache.sshd.common.Factory; 16 | import org.apache.sshd.server.shell.ShellFactory; 17 | import org.apache.sshd.server.command.Command; 18 | import org.apache.sshd.server.channel.ChannelSession; 19 | import org.apache.sshd.server.Environment; 20 | import org.apache.sshd.server.ExitCallback; 21 | import org.bukkit.Bukkit; 22 | 23 | import java.io.File; 24 | import java.io.FileNotFoundException; 25 | import java.io.BufferedReader; 26 | import java.io.FileReader; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.net.InetAddress; 31 | import java.net.UnknownHostException; 32 | import java.util.StringTokenizer; 33 | import java.util.Optional; 34 | import java.util.logging.Level; 35 | import java.util.logging.StreamHandler; 36 | 37 | public class ConsoleShellFactory implements ShellFactory 38 | { 39 | 40 | public Command createShell(ChannelSession cs) 41 | { 42 | return new ConsoleShell(); 43 | } 44 | 45 | public class ConsoleShell implements Command, Runnable 46 | { 47 | 48 | private InputStream in; 49 | private OutputStream out; 50 | private OutputStream err; 51 | private ExitCallback callback; 52 | private Environment environment; 53 | private Thread thread; 54 | private String Username; 55 | 56 | StreamHandlerAppender streamHandlerAppender; 57 | public ConsoleReader ConsoleReader; 58 | public SSHDCommandSender SshdCommandSender; 59 | 60 | public InputStream getIn() 61 | { 62 | return in; 63 | } 64 | 65 | public OutputStream getOut() 66 | { 67 | return out; 68 | } 69 | 70 | public OutputStream getErr() 71 | { 72 | return err; 73 | } 74 | 75 | public Environment getEnvironment() 76 | { 77 | return environment; 78 | } 79 | 80 | public void setInputStream(InputStream in) 81 | { 82 | this.in = in; 83 | } 84 | 85 | public void setOutputStream(OutputStream out) 86 | { 87 | this.out = out; 88 | } 89 | 90 | public void setErrorStream(OutputStream err) 91 | { 92 | this.err = err; 93 | } 94 | 95 | public void setExitCallback(ExitCallback callback) 96 | { 97 | this.callback = callback; 98 | } 99 | 100 | @Override 101 | public void start(ChannelSession cs, Environment env) throws IOException 102 | { 103 | try 104 | { 105 | String username = env.getEnv().get(Environment.ENV_USER); 106 | Optional optcred = PermissionUtil.GetCredential(username, "console"); 107 | // They don't have access. 108 | if (optcred.isPresent() && !optcred.get().contains("R")) 109 | { 110 | cs.close(true); 111 | return; 112 | } 113 | else 114 | SshdPlugin.instance.getLogger().warning("There is no $default pseudo-user under credential, allowing unrestricted access..."); 115 | 116 | this.ConsoleReader = new ConsoleReader(in, new FlushyOutputStream(out), new SshTerminal()); 117 | this.ConsoleReader.setExpandEvents(true); 118 | this.ConsoleReader.addCompleter(new ConsoleCommandCompleter()); 119 | 120 | StreamHandler streamHandler = new FlushyStreamHandler(out, new ConsoleLogFormatter(), this.ConsoleReader); 121 | this.streamHandlerAppender = new StreamHandlerAppender(streamHandler); 122 | 123 | ((Logger)LogManager.getRootLogger()).addAppender(this.streamHandlerAppender); 124 | 125 | this.environment = env; 126 | this.Username = username; 127 | this.SshdCommandSender = new SSHDCommandSender(); 128 | this.SshdCommandSender.console = this; 129 | thread = new Thread(this, "SSHD ConsoleShell " + username); 130 | thread.start(); 131 | } 132 | catch (Exception e) 133 | { 134 | e.printStackTrace(); 135 | throw new IOException("Error starting shell", e); 136 | } 137 | } 138 | 139 | @Override 140 | public void destroy(ChannelSession cs) { ((Logger)LogManager.getRootLogger()).removeAppender(this.streamHandlerAppender); } 141 | 142 | public void run() 143 | { 144 | try 145 | { 146 | if (!SshdPlugin.instance.getConfig().getString("Mode", "DEFAULT").equals("RPC")) 147 | printPreamble(this.ConsoleReader); 148 | while (true) 149 | { 150 | String command = this.ConsoleReader.readLine("\r>", null); 151 | // The user sent CTRL+D to close the shell, terminate the session. 152 | if (command == null) 153 | break; 154 | // Skip someone spamming enter 155 | if (command.trim().isEmpty()) 156 | continue; 157 | // User wants to exit 158 | if (command.equals("exit") || command.equals("quit")) 159 | break; 160 | // Clear the text from the screen (on supported terminals) 161 | if (command.equals("cls")) 162 | { 163 | this.ConsoleReader.clearScreen(); 164 | this.ConsoleReader.drawLine(); 165 | this.ConsoleReader.flush(); 166 | continue; 167 | } 168 | // Hide the mkpasswd command input from other users. 169 | Boolean mkpasswd = command.split(" ")[0].equals("mkpasswd"); 170 | Optional optcred = PermissionUtil.GetCredential(this.Username, "console"); 171 | 172 | if (optcred.isPresent() && !optcred.get().contains("W")) 173 | continue; 174 | 175 | Bukkit.getScheduler().runTask( 176 | SshdPlugin.instance, () -> 177 | { 178 | if (SshdPlugin.instance.getConfig().getString("Mode", "DEFAULT").equals("RPC") && command.startsWith("rpc")) 179 | { 180 | // NO ECHO NO PREAMBLE AND SHIT 181 | String cmd = command.substring("rpc".length() + 1, command.length()); 182 | Bukkit.dispatchCommand(this.SshdCommandSender, cmd); 183 | } 184 | else 185 | { 186 | // Don't send our mkpasswd command output. This will echo passwords back 187 | // to the console for all to see. This command is strictly between 188 | // our plugin and the connected client. 189 | if (!mkpasswd) 190 | { 191 | SshdPlugin.instance.getLogger().info("<" + this.Username + "> " + command); 192 | Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); 193 | } 194 | else 195 | { 196 | Bukkit.dispatchCommand(this.SshdCommandSender, command); 197 | } 198 | } 199 | }); 200 | } 201 | } 202 | catch (IOException e) 203 | { 204 | e.printStackTrace(); 205 | SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error processing command from SSH", e); 206 | } 207 | finally 208 | { 209 | SshdPlugin.instance.getLogger().log(Level.INFO, this.Username + " disconnected from SSH."); 210 | callback.onExit(0); 211 | } 212 | } 213 | 214 | private String GetHostname() 215 | { 216 | try 217 | { 218 | return InetAddress.getLocalHost().getHostName(); 219 | } 220 | catch (UnknownHostException e) 221 | { 222 | e.printStackTrace(); 223 | SshdPlugin.instance.getLogger().log(Level.INFO, "The above stacktrace can be ignored, you likely have a misconfigured system hosts file."); 224 | return "Unknown"; 225 | } 226 | } 227 | 228 | private void printPreamble(ConsoleReader cr) throws IOException 229 | { 230 | File f = new File(SshdPlugin.instance.getDataFolder(), "motd.txt"); 231 | try 232 | { 233 | BufferedReader br = new BufferedReader(new FileReader(f)); 234 | 235 | String st; 236 | while ((st = br.readLine()) != null) 237 | cr.println(ConsoleLogFormatter.ColorizeString(st) + "\r"); 238 | } 239 | catch (FileNotFoundException e) 240 | { 241 | e.printStackTrace(); 242 | SshdPlugin.instance.getLogger().log(Level.WARNING, "Could not open " + f + ": File does not exist."); 243 | // Not showing the SSH motd is not a fatal failure, let the session continue. 244 | } 245 | 246 | // Doesn't really guarantee our actual system hostname but 247 | // it's better than not having one at all. 248 | cr.println("Connected to: " + this.GetHostname() + " (" + Bukkit.getServer().getName() + ")\r"); 249 | cr.println(ConsoleLogFormatter.ColorizeString(Bukkit.getServer().getMotd()).replaceAll("\n", "\r\n")); 250 | cr.println("\r"); 251 | cr.println("Type 'exit' to exit the shell." + "\r"); 252 | cr.println("===============================================" + "\r"); 253 | } 254 | } 255 | } -------------------------------------------------------------------------------- /src/main/java/org/slf4j/impl/PluginSlf4jFactory.java: -------------------------------------------------------------------------------- 1 | package org.slf4j.impl; 2 | 3 | import com.ryanmichela.sshd.SshdPlugin; 4 | import org.slf4j.ILoggerFactory; 5 | import org.slf4j.Logger; 6 | import org.slf4j.Marker; 7 | import org.slf4j.helpers.FormattingTuple; 8 | import org.slf4j.helpers.MessageFormatter; 9 | 10 | import java.util.logging.Level; 11 | 12 | /** 13 | * Copyright 2013 Ryan Michela 14 | */ 15 | public class PluginSlf4jFactory implements ILoggerFactory { 16 | @Override 17 | public Logger getLogger(String name) { 18 | return new PluginSlf4jAdapter(name); 19 | } 20 | 21 | public class PluginSlf4jAdapter implements Logger { 22 | private String name; 23 | 24 | private boolean isEnabled(Level level) { 25 | return SshdPlugin.instance != null && SshdPlugin.instance.getLogger().isLoggable(level); 26 | } 27 | 28 | private void log(Level level, String s, Object[] objects) { 29 | if (SshdPlugin.instance != null && isEnabled(level)) { 30 | FormattingTuple ft = MessageFormatter.arrayFormat(s, objects); 31 | SshdPlugin.instance.getLogger().log(level, ft.getMessage(), ft.getThrowable()); 32 | } 33 | } 34 | 35 | private void log(Level level, String s, Throwable throwable) { 36 | if (SshdPlugin.instance != null && isEnabled(level)) { 37 | SshdPlugin.instance.getLogger().log(level, s, throwable); 38 | } 39 | } 40 | 41 | public PluginSlf4jAdapter(String name) { 42 | this.name = name; 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return name; 48 | } 49 | 50 | @Override 51 | public boolean isTraceEnabled() { 52 | return isEnabled(Level.FINEST); 53 | } 54 | 55 | @Override 56 | public void trace(String s) { 57 | trace(s, new Object[]{}); 58 | } 59 | 60 | @Override 61 | public void trace(String s, Object o) { 62 | trace(s, new Object[]{o}); 63 | } 64 | 65 | @Override 66 | public void trace(String s, Object o, Object o1) { 67 | trace(s, new Object[]{o, o1}); 68 | } 69 | 70 | @Override 71 | public void trace(String s, Object[] objects) { 72 | log(Level.FINEST, s, objects); 73 | } 74 | 75 | @Override 76 | public void trace(String s, Throwable throwable) { 77 | log(Level.FINEST, s, throwable); 78 | } 79 | 80 | @Override 81 | public boolean isTraceEnabled(Marker marker) { 82 | return isTraceEnabled(); 83 | } 84 | 85 | @Override 86 | public void trace(Marker marker, String s) { 87 | trace(s); 88 | } 89 | 90 | @Override 91 | public void trace(Marker marker, String s, Object o) { 92 | trace(s, o); 93 | } 94 | 95 | @Override 96 | public void trace(Marker marker, String s, Object o, Object o1) { 97 | trace(s, o, o1); 98 | } 99 | 100 | @Override 101 | public void trace(Marker marker, String s, Object[] objects) { 102 | trace(s, objects); 103 | } 104 | 105 | @Override 106 | public void trace(Marker marker, String s, Throwable throwable) { 107 | trace(s, throwable); 108 | } 109 | 110 | @Override 111 | public boolean isDebugEnabled() { 112 | return isEnabled(Level.FINE); 113 | } 114 | 115 | @Override 116 | public void debug(String s) { 117 | debug(s, new Object[]{}); 118 | } 119 | 120 | @Override 121 | public void debug(String s, Object o) { 122 | debug(s, new Object[]{o}); 123 | } 124 | 125 | @Override 126 | public void debug(String s, Object o, Object o1) { 127 | debug(s, new Object[]{o, o1}); 128 | } 129 | 130 | @Override 131 | public void debug(String s, Object[] objects) { 132 | log(Level.FINE, s, objects); 133 | } 134 | 135 | @Override 136 | public void debug(String s, Throwable throwable) { 137 | log(Level.FINE, s, throwable); 138 | } 139 | 140 | @Override 141 | public boolean isDebugEnabled(Marker marker) { 142 | return isDebugEnabled(); 143 | } 144 | 145 | @Override 146 | public void debug(Marker marker, String s) { 147 | debug(s); 148 | } 149 | 150 | @Override 151 | public void debug(Marker marker, String s, Object o) { 152 | debug(s, o); 153 | } 154 | 155 | @Override 156 | public void debug(Marker marker, String s, Object o, Object o1) { 157 | debug(s, o, o1); 158 | } 159 | 160 | @Override 161 | public void debug(Marker marker, String s, Object[] objects) { 162 | debug(s, objects); 163 | } 164 | 165 | @Override 166 | public void debug(Marker marker, String s, Throwable throwable) { 167 | debug(s, throwable); 168 | } 169 | 170 | @Override 171 | public boolean isInfoEnabled() { 172 | return isEnabled(Level.INFO); 173 | } 174 | 175 | @Override 176 | public void info(String s) { 177 | info(s, new Object[]{}); 178 | } 179 | 180 | @Override 181 | public void info(String s, Object o) { 182 | info(s, new Object[]{o}); 183 | } 184 | 185 | @Override 186 | public void info(String s, Object o, Object o1) { 187 | info(s, new Object[]{o, o1}); 188 | } 189 | 190 | @Override 191 | public void info(String s, Object[] objects) { 192 | log(Level.INFO, s, objects); 193 | } 194 | 195 | @Override 196 | public void info(String s, Throwable throwable) { 197 | log(Level.INFO, s, throwable); 198 | } 199 | 200 | @Override 201 | public boolean isInfoEnabled(Marker marker) { 202 | return isInfoEnabled(); 203 | } 204 | 205 | @Override 206 | public void info(Marker marker, String s) { 207 | info(s); 208 | } 209 | 210 | @Override 211 | public void info(Marker marker, String s, Object o) { 212 | info(s, o); 213 | } 214 | 215 | @Override 216 | public void info(Marker marker, String s, Object o, Object o1) { 217 | info(s, o, o1); 218 | } 219 | 220 | @Override 221 | public void info(Marker marker, String s, Object[] objects) { 222 | info(s, objects); 223 | } 224 | 225 | @Override 226 | public void info(Marker marker, String s, Throwable throwable) { 227 | info(s, throwable); 228 | } 229 | 230 | @Override 231 | public boolean isWarnEnabled() { 232 | return isEnabled(Level.WARNING); 233 | } 234 | 235 | @Override 236 | public void warn(String s) { 237 | warn(s, new Object[]{}); 238 | } 239 | 240 | @Override 241 | public void warn(String s, Object o) { 242 | warn(s, new Object[]{o}); 243 | } 244 | 245 | @Override 246 | public void warn(String s, Object o, Object o1) { 247 | warn(s, new Object[]{o, o1}); 248 | } 249 | 250 | @Override 251 | public void warn(String s, Object[] objects) { 252 | log(Level.WARNING, s, objects); 253 | } 254 | 255 | @Override 256 | public void warn(String s, Throwable throwable) { 257 | log(Level.WARNING, s, throwable); 258 | } 259 | 260 | @Override 261 | public boolean isWarnEnabled(Marker marker) { 262 | return isWarnEnabled(); 263 | } 264 | 265 | @Override 266 | public void warn(Marker marker, String s) { 267 | warn(s); 268 | } 269 | 270 | @Override 271 | public void warn(Marker marker, String s, Object o) { 272 | warn(s, o); 273 | } 274 | 275 | @Override 276 | public void warn(Marker marker, String s, Object o, Object o1) { 277 | warn(s, o, o1); 278 | } 279 | 280 | @Override 281 | public void warn(Marker marker, String s, Object[] objects) { 282 | warn(s, objects); 283 | } 284 | 285 | @Override 286 | public void warn(Marker marker, String s, Throwable throwable) { 287 | warn(s, throwable); 288 | } 289 | 290 | @Override 291 | public boolean isErrorEnabled() { 292 | return isEnabled(Level.SEVERE); 293 | } 294 | 295 | @Override 296 | public void error(String s) { 297 | error(s, new Object[]{}); 298 | } 299 | 300 | @Override 301 | public void error(String s, Object o) { 302 | error(s, new Object[]{o}); 303 | } 304 | 305 | @Override 306 | public void error(String s, Object o, Object o1) { 307 | error(s, new Object[]{o, o1}); 308 | } 309 | 310 | @Override 311 | public void error(String s, Object[] objects) { 312 | log(Level.SEVERE, s, objects); 313 | } 314 | 315 | @Override 316 | public void error(String s, Throwable throwable) { 317 | log(Level.SEVERE, s, throwable); 318 | } 319 | 320 | @Override 321 | public boolean isErrorEnabled(Marker marker) { 322 | return isErrorEnabled(); 323 | } 324 | 325 | @Override 326 | public void error(Marker marker, String s) { 327 | error(s); 328 | } 329 | 330 | @Override 331 | public void error(Marker marker, String s, Object o) { 332 | error(s, o); 333 | } 334 | 335 | @Override 336 | public void error(Marker marker, String s, Object o, Object o1) { 337 | error(s, o, o1); 338 | } 339 | 340 | @Override 341 | public void error(Marker marker, String s, Object[] objects) { 342 | error(s, objects); 343 | } 344 | 345 | @Override 346 | public void error(Marker marker, String s, Throwable throwable) { 347 | error(s, throwable); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/java/com/ryanmichela/sshd/BCrypt.java: -------------------------------------------------------------------------------- 1 | package com.ryanmichela.sshd; 2 | 3 | // Copyright (c) 2006 Damien Miller 4 | // 5 | // Permission to use, copy, modify, and distribute this software for any 6 | // purpose with or without fee is hereby granted, provided that the above 7 | // copyright notice and this permission notice appear in all copies. 8 | // 9 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | import java.io.UnsupportedEncodingException; 18 | 19 | import java.security.SecureRandom; 20 | 21 | /** 22 | * BCrypt implements OpenBSD-style Blowfish password hashing using 23 | * the scheme described in "A Future-Adaptable Password Scheme" by 24 | * Niels Provos and David Mazieres. 25 | *

26 | * This password hashing system tries to thwart off-line password 27 | * cracking using a computationally-intensive hashing algorithm, 28 | * based on Bruce Schneier's Blowfish cipher. The work factor of 29 | * the algorithm is parameterised, so it can be increased as 30 | * computers get faster. 31 | *

32 | * Usage is really simple. To hash a password for the first time, 33 | * call the hashpw method with a random salt, like this: 34 | *

35 | * 36 | * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
37 | *
38 | *

39 | * To check whether a plaintext password matches one that has been 40 | * hashed previously, use the checkpw method: 41 | *

42 | * 43 | * if (BCrypt.checkpw(candidate_password, stored_hash))
44 | *     System.out.println("It matches");
45 | * else
46 | *     System.out.println("It does not match");
47 | *
48 | *

49 | * The gensalt() method takes an optional parameter (log_rounds) 50 | * that determines the computational complexity of the hashing: 51 | *

52 | * 53 | * String strong_salt = BCrypt.gensalt(10)
54 | * String stronger_salt = BCrypt.gensalt(12)
55 | *
56 | *

57 | * The amount of work increases exponentially (2**log_rounds), so 58 | * each increment is twice as much work. The default log_rounds is 59 | * 10, and the valid range is 4 to 31. 60 | * 61 | * @author Damien Miller 62 | * @version 0.2 63 | */ 64 | public class BCrypt 65 | { 66 | // BCrypt parameters 67 | private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; 68 | private static final int BCRYPT_SALT_LEN = 16; 69 | 70 | // Blowfish parameters 71 | private static final int BLOWFISH_NUM_ROUNDS = 16; 72 | 73 | // Initial contents of key schedule 74 | private static final int P_orig[] = { 75 | 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 76 | 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 77 | 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 78 | 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 79 | 0x9216d5d9, 0x8979fb1b 80 | }; 81 | private static final int S_orig[] = { 82 | 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 83 | 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 84 | 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 85 | 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 86 | 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 87 | 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 88 | 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 89 | 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 90 | 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 91 | 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 92 | 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 93 | 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 94 | 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 95 | 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 96 | 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 97 | 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 98 | 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 99 | 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 100 | 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 101 | 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 102 | 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 103 | 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 104 | 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 105 | 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 106 | 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 107 | 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 108 | 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 109 | 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 110 | 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 111 | 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 112 | 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 113 | 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 114 | 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 115 | 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 116 | 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 117 | 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 118 | 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 119 | 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 120 | 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 121 | 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 122 | 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 123 | 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 124 | 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 125 | 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 126 | 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 127 | 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 128 | 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 129 | 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 130 | 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 131 | 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 132 | 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 133 | 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 134 | 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 135 | 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 136 | 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 137 | 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 138 | 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 139 | 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 140 | 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 141 | 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 142 | 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 143 | 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 144 | 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 145 | 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 146 | 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 147 | 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 148 | 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 149 | 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 150 | 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 151 | 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 152 | 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 153 | 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 154 | 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 155 | 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 156 | 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 157 | 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 158 | 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 159 | 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 160 | 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 161 | 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 162 | 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 163 | 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 164 | 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 165 | 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 166 | 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 167 | 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 168 | 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 169 | 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 170 | 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 171 | 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 172 | 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 173 | 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 174 | 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 175 | 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 176 | 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 177 | 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 178 | 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 179 | 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 180 | 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 181 | 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 182 | 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 183 | 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 184 | 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 185 | 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 186 | 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 187 | 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 188 | 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 189 | 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 190 | 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 191 | 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 192 | 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 193 | 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 194 | 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 195 | 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 196 | 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 197 | 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 198 | 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 199 | 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 200 | 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 201 | 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 202 | 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 203 | 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 204 | 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 205 | 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 206 | 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 207 | 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 208 | 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 209 | 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 210 | 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 211 | 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 212 | 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 213 | 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 214 | 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 215 | 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 216 | 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 217 | 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 218 | 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 219 | 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 220 | 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 221 | 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 222 | 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 223 | 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 224 | 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 225 | 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 226 | 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 227 | 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 228 | 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 229 | 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 230 | 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 231 | 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 232 | 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 233 | 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 234 | 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 235 | 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 236 | 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 237 | 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 238 | 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 239 | 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 240 | 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 241 | 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 242 | 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 243 | 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 244 | 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 245 | 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 246 | 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 247 | 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 248 | 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 249 | 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 250 | 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 251 | 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 252 | 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 253 | 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 254 | 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 255 | 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 256 | 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 257 | 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 258 | 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 259 | 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 260 | 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 261 | 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 262 | 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 263 | 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 264 | 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 265 | 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 266 | 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 267 | 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 268 | 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 269 | 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 270 | 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 271 | 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 272 | 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 273 | 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 274 | 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 275 | 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 276 | 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 277 | 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 278 | 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 279 | 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 280 | 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 281 | 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 282 | 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 283 | 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 284 | 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 285 | 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 286 | 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 287 | 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 288 | 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 289 | 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 290 | 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 291 | 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 292 | 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 293 | 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 294 | 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 295 | 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 296 | 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 297 | 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 298 | 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 299 | 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 300 | 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 301 | 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 302 | 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 303 | 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 304 | 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 305 | 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 306 | 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 307 | 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 308 | 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 309 | 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 310 | 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 311 | 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 312 | 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 313 | 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 314 | 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 315 | 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 316 | 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 317 | 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 318 | 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 319 | 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 320 | 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 321 | 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 322 | 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 323 | 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 324 | 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 325 | 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 326 | 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 327 | 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 328 | 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 329 | 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 330 | 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 331 | 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 332 | 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 333 | 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 334 | 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 335 | 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 336 | 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 337 | 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 338 | }; 339 | 340 | // bcrypt IV: "OrpheanBeholderScryDoubt" 341 | static private final int bf_crypt_ciphertext[] = { 342 | 0x4f727068, 0x65616e42, 0x65686f6c, 343 | 0x64657253, 0x63727944, 0x6f756274 344 | }; 345 | 346 | // Table for Base64 encoding 347 | static private final char base64_code[] = { 348 | '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 349 | 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 350 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 351 | 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 352 | 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', 353 | '6', '7', '8', '9' 354 | }; 355 | 356 | // Table for Base64 decoding 357 | static private final byte index_64[] = { 358 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 359 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 360 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 361 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 362 | -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 363 | 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, 364 | -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 365 | 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 366 | 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 367 | -1, -1, -1, -1, -1, -1, 28, 29, 30, 368 | 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 369 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 370 | 51, 52, 53, -1, -1, -1, -1, -1 371 | }; 372 | 373 | // Expanded Blowfish key 374 | private int P[]; 375 | private int S[]; 376 | 377 | /** 378 | * Encode a byte array using bcrypt's slightly-modified base64 379 | * encoding scheme. Note that this is *not* compatible with 380 | * the standard MIME-base64 encoding. 381 | * 382 | * @param d the byte array to encode 383 | * @param len the number of bytes to encode 384 | * @return base64-encoded string 385 | * @exception IllegalArgumentException if the length is invalid 386 | */ 387 | private static String encode_base64(byte d[], int len) throws IllegalArgumentException 388 | { 389 | int off = 0; 390 | StringBuffer rs = new StringBuffer(); 391 | int c1, c2; 392 | 393 | if (len <= 0 || len > d.length) 394 | throw new IllegalArgumentException ("Invalid len"); 395 | 396 | while (off < len) 397 | { 398 | c1 = d[off++] & 0xff; 399 | rs.append(base64_code[(c1 >> 2) & 0x3f]); 400 | c1 = (c1 & 0x03) << 4; 401 | if (off >= len) 402 | { 403 | rs.append(base64_code[c1 & 0x3f]); 404 | break; 405 | } 406 | 407 | c2 = d[off++] & 0xff; 408 | c1 |= (c2 >> 4) & 0x0f; 409 | rs.append(base64_code[c1 & 0x3f]); 410 | c1 = (c2 & 0x0f) << 2; 411 | if (off >= len) 412 | { 413 | rs.append(base64_code[c1 & 0x3f]); 414 | break; 415 | } 416 | c2 = d[off++] & 0xff; 417 | c1 |= (c2 >> 6) & 0x03; 418 | rs.append(base64_code[c1 & 0x3f]); 419 | rs.append(base64_code[c2 & 0x3f]); 420 | } 421 | return rs.toString(); 422 | } 423 | 424 | /** 425 | * Look up the 3 bits base64-encoded by the specified character, 426 | * range-checking againt conversion table 427 | * @param x the base64-encoded value 428 | * @return the decoded value of x 429 | */ 430 | private static byte char64(char x) 431 | { 432 | if ((int)x < 0 || (int)x > index_64.length) 433 | return -1; 434 | 435 | return index_64[(int)x]; 436 | } 437 | 438 | /** 439 | * Decode a string encoded using bcrypt's base64 scheme to a 440 | * byte array. Note that this is *not* compatible with 441 | * the standard MIME-base64 encoding. 442 | * @param s the string to decode 443 | * @param maxolen the maximum number of bytes to decode 444 | * @return an array containing the decoded bytes 445 | * @throws IllegalArgumentException if maxolen is invalid 446 | */ 447 | private static byte[] decode_base64(String s, int maxolen) throws IllegalArgumentException 448 | { 449 | StringBuffer rs = new StringBuffer(); 450 | int off = 0, slen = s.length(), olen = 0; 451 | byte ret[]; 452 | byte c1, c2, c3, c4, o; 453 | 454 | if (maxolen <= 0) 455 | throw new IllegalArgumentException ("Invalid maxolen"); 456 | 457 | while (off < slen - 1 && olen < maxolen) 458 | { 459 | c1 = char64(s.charAt(off++)); 460 | c2 = char64(s.charAt(off++)); 461 | if (c1 == -1 || c2 == -1) 462 | break; 463 | o = (byte)(c1 << 2); 464 | o |= (c2 & 0x30) >> 4; 465 | rs.append((char)o); 466 | if (++olen >= maxolen || off >= slen) 467 | break; 468 | c3 = char64(s.charAt(off++)); 469 | if (c3 == -1) 470 | break; 471 | o = (byte)((c2 & 0x0f) << 4); 472 | o |= (c3 & 0x3c) >> 2; 473 | rs.append((char)o); 474 | if (++olen >= maxolen || off >= slen) 475 | break; 476 | c4 = char64(s.charAt(off++)); 477 | o = (byte)((c3 & 0x03) << 6); 478 | o |= c4; 479 | rs.append((char)o); 480 | ++olen; 481 | } 482 | 483 | ret = new byte[olen]; 484 | for (off = 0; off < olen; off++) 485 | ret[off] = (byte)rs.charAt(off); 486 | 487 | return ret; 488 | } 489 | 490 | /** 491 | * Blowfish encipher a single 64-bit block encoded as 492 | * two 32-bit halves 493 | * @param lr an array containing the two 32-bit half blocks 494 | * @param off the position in the array of the blocks 495 | */ 496 | private final void encipher(int lr[], int off) 497 | { 498 | int i, n, l = lr[off], r = lr[off + 1]; 499 | 500 | l ^= P[0]; 501 | for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) 502 | { 503 | // Feistel substitution on left word 504 | n = S[(l >> 24) & 0xff]; 505 | n += S[0x100 | ((l >> 16) & 0xff)]; 506 | n ^= S[0x200 | ((l >> 8) & 0xff)]; 507 | n += S[0x300 | (l & 0xff)]; 508 | r ^= n ^ P[++i]; 509 | 510 | // Feistel substitution on right word 511 | n = S[(r >> 24) & 0xff]; 512 | n += S[0x100 | ((r >> 16) & 0xff)]; 513 | n ^= S[0x200 | ((r >> 8) & 0xff)]; 514 | n += S[0x300 | (r & 0xff)]; 515 | l ^= n ^ P[++i]; 516 | } 517 | lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; 518 | lr[off + 1] = l; 519 | } 520 | 521 | /** 522 | * Cycically extract a word of key material 523 | * @param data the string to extract the data from 524 | * @param offp a "pointer" (as a one-entry array) to the 525 | * current offset into data 526 | * @return the next word of material from data 527 | */ 528 | private static int streamtoword(byte data[], int offp[]) 529 | { 530 | int i; 531 | int word = 0; 532 | int off = offp[0]; 533 | 534 | for (i = 0; i < 4; i++) 535 | { 536 | word = (word << 8) | (data[off] & 0xff); 537 | off = (off + 1) % data.length; 538 | } 539 | 540 | offp[0] = off; 541 | return word; 542 | } 543 | 544 | /** 545 | * Initialise the Blowfish key schedule 546 | */ 547 | private void init_key() 548 | { 549 | P = (int[])P_orig.clone(); 550 | S = (int[])S_orig.clone(); 551 | } 552 | 553 | /** 554 | * Key the Blowfish cipher 555 | * @param key an array containing the key 556 | */ 557 | private void key(byte key[]) 558 | { 559 | int i; 560 | int koffp[] = { 0 }; 561 | int lr[] = { 0, 0 }; 562 | int plen = P.length, slen = S.length; 563 | 564 | for (i = 0; i < plen; i++) 565 | P[i] = P[i] ^ streamtoword(key, koffp); 566 | 567 | for (i = 0; i < plen; i += 2) 568 | { 569 | encipher(lr, 0); 570 | P[i] = lr[0]; 571 | P[i + 1] = lr[1]; 572 | } 573 | 574 | for (i = 0; i < slen; i += 2) 575 | { 576 | encipher(lr, 0); 577 | S[i] = lr[0]; 578 | S[i + 1] = lr[1]; 579 | } 580 | } 581 | 582 | /** 583 | * Perform the "enhanced key schedule" step described by 584 | * Provos and Mazieres in "A Future-Adaptable Password Scheme" 585 | * http://www.openbsd.org/papers/bcrypt-paper.ps 586 | * @param data salt information 587 | * @param key password information 588 | */ 589 | private void ekskey(byte data[], byte key[]) 590 | { 591 | int i; 592 | int koffp[] = { 0 }, doffp[] = { 0 }; 593 | int lr[] = { 0, 0 }; 594 | int plen = P.length, slen = S.length; 595 | 596 | for (i = 0; i < plen; i++) 597 | P[i] = P[i] ^ streamtoword(key, koffp); 598 | 599 | for (i = 0; i < plen; i += 2) 600 | { 601 | lr[0] ^= streamtoword(data, doffp); 602 | lr[1] ^= streamtoword(data, doffp); 603 | encipher(lr, 0); 604 | P[i] = lr[0]; 605 | P[i + 1] = lr[1]; 606 | } 607 | 608 | for (i = 0; i < slen; i += 2) 609 | { 610 | lr[0] ^= streamtoword(data, doffp); 611 | lr[1] ^= streamtoword(data, doffp); 612 | encipher(lr, 0); 613 | S[i] = lr[0]; 614 | S[i + 1] = lr[1]; 615 | } 616 | } 617 | 618 | /** 619 | * Perform the central password hashing step in the 620 | * bcrypt scheme 621 | * @param password the password to hash 622 | * @param salt the binary salt to hash with the password 623 | * @param log_rounds the binary logarithm of the number 624 | * of rounds of hashing to apply 625 | * @return an array containing the binary hashed password 626 | */ 627 | private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) 628 | { 629 | int rounds, i, j; 630 | int cdata[] = (int[])bf_crypt_ciphertext.clone(); 631 | int clen = cdata.length; 632 | byte ret[]; 633 | 634 | if (log_rounds < 4 || log_rounds > 31) 635 | throw new IllegalArgumentException ("Bad number of rounds"); 636 | 637 | rounds = 1 << log_rounds; 638 | if (salt.length != BCRYPT_SALT_LEN) 639 | throw new IllegalArgumentException ("Bad salt length"); 640 | 641 | init_key(); 642 | ekskey(salt, password); 643 | for (i = 0; i < rounds; i++) 644 | { 645 | key(password); 646 | key(salt); 647 | } 648 | 649 | for (i = 0; i < 64; i++) 650 | { 651 | for (j = 0; j < (clen >> 1); j++) 652 | encipher(cdata, j << 1); 653 | } 654 | 655 | ret = new byte[clen * 4]; 656 | for (i = 0, j = 0; i < clen; i++) 657 | { 658 | ret[j++] = (byte)((cdata[i] >> 24) & 0xff); 659 | ret[j++] = (byte)((cdata[i] >> 16) & 0xff); 660 | ret[j++] = (byte)((cdata[i] >> 8) & 0xff); 661 | ret[j++] = (byte)(cdata[i] & 0xff); 662 | } 663 | return ret; 664 | } 665 | 666 | /** 667 | * Hash a password using the OpenBSD bcrypt scheme 668 | * @param password the password to hash 669 | * @param salt the salt to hash with (perhaps generated 670 | * using BCrypt.gensalt) 671 | * @return the hashed password 672 | */ 673 | public static String hashpw(String password, String salt) 674 | { 675 | BCrypt B; 676 | String real_salt; 677 | byte passwordb[], saltb[], hashed[]; 678 | char minor = (char)0; 679 | int rounds, off = 0; 680 | StringBuffer rs = new StringBuffer(); 681 | 682 | if (salt.charAt(0) != '$' || salt.charAt(1) != '2') 683 | throw new IllegalArgumentException ("Invalid salt version"); 684 | 685 | if (salt.charAt(2) == '$') 686 | off = 3; 687 | else 688 | { 689 | minor = salt.charAt(2); 690 | if (minor != 'a' || salt.charAt(3) != '$') 691 | throw new IllegalArgumentException ("Invalid salt revision"); 692 | off = 4; 693 | } 694 | 695 | // Extract number of rounds 696 | if (salt.charAt(off + 2) > '$') 697 | throw new IllegalArgumentException ("Missing salt rounds"); 698 | rounds = Integer.parseInt(salt.substring(off, off + 2)); 699 | 700 | real_salt = salt.substring(off + 3, off + 25); 701 | try 702 | { 703 | passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); 704 | } 705 | catch (UnsupportedEncodingException uee) 706 | { 707 | throw new AssertionError("UTF-8 is not supported"); 708 | } 709 | 710 | saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); 711 | 712 | B = new BCrypt(); 713 | hashed = B.crypt_raw(passwordb, saltb, rounds); 714 | 715 | rs.append("$2"); 716 | if (minor >= 'a') 717 | rs.append(minor); 718 | 719 | rs.append("$"); 720 | if (rounds < 10) 721 | rs.append("0"); 722 | 723 | rs.append(Integer.toString(rounds)); 724 | rs.append("$"); 725 | rs.append(encode_base64(saltb, saltb.length)); 726 | rs.append(encode_base64(hashed, 727 | bf_crypt_ciphertext.length * 4 - 1)); 728 | 729 | return rs.toString(); 730 | } 731 | 732 | /** 733 | * Generate a salt for use with the BCrypt.hashpw() method 734 | * @param log_rounds the log2 of the number of rounds of 735 | * hashing to apply - the work factor therefore increases as 736 | * 2**log_rounds. 737 | * @param random an instance of SecureRandom to use 738 | * @return an encoded salt value 739 | */ 740 | public static String gensalt(int log_rounds, SecureRandom random) 741 | { 742 | StringBuffer rs = new StringBuffer(); 743 | byte rnd[] = new byte[BCRYPT_SALT_LEN]; 744 | 745 | random.nextBytes(rnd); 746 | 747 | rs.append("$2a$"); 748 | if (log_rounds < 10) 749 | rs.append("0"); 750 | 751 | rs.append(Integer.toString(log_rounds)); 752 | rs.append("$"); 753 | rs.append(encode_base64(rnd, rnd.length)); 754 | return rs.toString(); 755 | } 756 | 757 | /** 758 | * Generate a salt for use with the BCrypt.hashpw() method 759 | * @param log_rounds the log2 of the number of rounds of 760 | * hashing to apply - the work factor therefore increases as 761 | * 2**log_rounds. 762 | * @return an encoded salt value 763 | */ 764 | public static String gensalt(int log_rounds) 765 | { 766 | return gensalt(log_rounds, new SecureRandom()); 767 | } 768 | 769 | /** 770 | * Generate a salt for use with the BCrypt.hashpw() method, 771 | * selecting a reasonable default for the number of hashing 772 | * rounds to apply 773 | * @return an encoded salt value 774 | */ 775 | public static String gensalt() 776 | { 777 | return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); 778 | } 779 | 780 | /** 781 | * Check that a plaintext password matches a previously hashed 782 | * one 783 | * @param plaintext the plaintext password to verify 784 | * @param hashed the previously-hashed password 785 | * @return true if the passwords match, false otherwise 786 | */ 787 | public static boolean checkpw(String plaintext, String hashed) 788 | { 789 | return (hashed.compareTo(hashpw(plaintext, hashed)) == 0); 790 | } 791 | } --------------------------------------------------------------------------------