All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sshd.common.config.keys.AuthorizedKeyEntry Maven / Gradle / Ivy

There is a newer version: 2.4.1.Final
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.sshd.common.config.keys;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StreamCorruptedException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.NoCloseReader;

/**
 * Represents an entry in the user's {@code authorized_keys} file according to the
 * OpenSSH
 * format. Note: {@code equals/hashCode} check only the key type and data - the comment and/or login options
 * are not considered part of equality
 *
 * @author Apache MINA SSHD Project
 * @see    sshd(8) - AUTHORIZED_KEYS_FILE_FORMAT
 */
public class AuthorizedKeyEntry extends PublicKeyEntry {
    public static final char BOOLEAN_OPTION_NEGATION_INDICATOR = '!';

    private static final long serialVersionUID = -9007505285002809156L;

    private String comment;
    // for options that have no value, "true" is used
    private Map loginOptions = Collections.emptyMap();

    public AuthorizedKeyEntry() {
        super();
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String value) {
        this.comment = value;
    }

    public Map getLoginOptions() {
        return loginOptions;
    }

    public void setLoginOptions(Map value) {
        if (value == null) {
            this.loginOptions = Collections.emptyMap();
        } else {
            this.loginOptions = value;
        }
    }

    /**
     * @param  session                  The {@link SessionContext} for invoking this load command - may be {@code null}
     *                                  if not invoked within a session context (e.g., offline tool or session unknown).
     * @param  fallbackResolver         The {@link PublicKeyEntryResolver} to consult if none of the built-in ones can
     *                                  be used. If {@code null} and no built-in resolver can be used then an
     *                                  {@link InvalidKeySpecException} is thrown.
     * @return                          The resolved {@link PublicKey} - or {@code null} if could not be resolved.
     *                                  Note: may be called only after key type and data bytes have been set or
     *                                  exception(s) may be thrown
     * @throws IOException              If failed to decode the key
     * @throws GeneralSecurityException If failed to generate the key
     * @see                             PublicKeyEntry#resolvePublicKey(SessionContext, Map, PublicKeyEntryResolver)
     */
    public PublicKey resolvePublicKey(
            SessionContext session, PublicKeyEntryResolver fallbackResolver)
            throws IOException, GeneralSecurityException {
        return resolvePublicKey(session, getLoginOptions(), fallbackResolver);
    }

    @Override
    public PublicKey appendPublicKey(
            SessionContext session, Appendable sb, PublicKeyEntryResolver fallbackResolver)
            throws IOException, GeneralSecurityException {
        Map options = getLoginOptions();
        if (MapEntryUtils.isNotEmpty(options)) {
            int index = 0;
            // Cannot use forEach because the index value is not effectively final
            for (Map.Entry oe : options.entrySet()) {
                String key = oe.getKey();
                String value = oe.getValue();
                if (index > 0) {
                    sb.append(',');
                }
                sb.append(key);
                // TODO figure out a way to remember which options where quoted
                // TODO figure out a way to remember which options had no value
                if (!"true".equals(value)) {
                    sb.append('=').append(value);
                }
                index++;
            }

            if (index > 0) {
                sb.append(' ');
            }
        }

        PublicKey key = super.appendPublicKey(session, sb, fallbackResolver);
        String kc = getComment();
        if (!GenericUtils.isEmpty(kc)) {
            sb.append(' ').append(kc);
        }

        return key;
    }

    @Override // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
    public int hashCode() {
        return super.hashCode();
    }

    @Override // to avoid Findbugs[EQ_DOESNT_OVERRIDE_EQUALS]
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Override
    public String toString() {
        String entry = super.toString();
        String kc = getComment();
        Map ko = getLoginOptions();
        return (MapEntryUtils.isEmpty(ko) ? "" : ko.toString() + " ")
               + entry
               + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
    }

    /**
     * Reads read the contents of an {@code authorized_keys} file
     *
     * @param  url         The {@link URL} to read from
     * @return             A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
     * @throws IOException If failed to read or parse the entries
     * @see                #readAuthorizedKeys(InputStream, boolean)
     */
    public static List readAuthorizedKeys(URL url) throws IOException {
        try (InputStream in = url.openStream()) {
            return readAuthorizedKeys(in, true);
        }
    }

    /**
     * Reads read the contents of an {@code authorized_keys} file
     *
     * @param  path        {@link Path} to read from
     * @param  options     The {@link OpenOption}s to use - if unspecified then appropriate defaults assumed
     * @return             A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
     * @throws IOException If failed to read or parse the entries
     * @see                #readAuthorizedKeys(InputStream, boolean)
     * @see                Files#newInputStream(Path, OpenOption...)
     */
    public static List readAuthorizedKeys(Path path, OpenOption... options) throws IOException {
        try (InputStream in = Files.newInputStream(path, options)) {
            return readAuthorizedKeys(in, true);
        }
    }

    /**
     * Reads read the contents of an {@code authorized_keys} file
     *
     * @param  in          The {@link InputStream} to use to read the contents of an {@code authorized_keys} file
     * @param  okToClose   {@code true} if method may close the input regardless success or failure
     * @return             A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
     * @throws IOException If failed to read or parse the entries
     * @see                #readAuthorizedKeys(Reader, boolean)
     */
    public static List readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException {
        try (Reader rdr = new InputStreamReader(
                NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) {
            return readAuthorizedKeys(rdr, true);
        }
    }

    /**
     * Reads read the contents of an {@code authorized_keys} file
     *
     * @param  rdr         The {@link Reader} to use to read the contents of an {@code authorized_keys} file
     * @param  okToClose   {@code true} if method may close the input regardless success or failure
     * @return             A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
     * @throws IOException If failed to read or parse the entries
     * @see                #readAuthorizedKeys(BufferedReader)
     */
    public static List readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException {
        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
            return readAuthorizedKeys(buf);
        }
    }

    /**
     * @param  rdr         The {@link BufferedReader} to use to read the contents of an {@code authorized_keys} file
     * @return             A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
     * @throws IOException If failed to read or parse the entries
     * @see                #parseAuthorizedKeyEntry(String)
     */
    public static List readAuthorizedKeys(BufferedReader rdr) throws IOException {
        List entries = null;
        for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
            AuthorizedKeyEntry entry;
            try {
                entry = parseAuthorizedKeyEntry(line);
                if (entry == null) {
                    continue; // null, empty or comment line
                }
            } catch (RuntimeException | Error e) {
                throw new StreamCorruptedException(
                        "Failed (" + e.getClass().getSimpleName() + ")"
                                                   + " to parse key entry=" + line + ": " + e.getMessage());
            }

            if (entries == null) {
                entries = new ArrayList<>();
            }

            entries.add(entry);
        }

        if (entries == null) {
            return Collections.emptyList();
        } else {
            return entries;
        }
    }

    /**
     * @param  value                    Original line from an {@code authorized_keys} file
     * @return                          {@link AuthorizedKeyEntry} or {@code null} if the line is {@code null}/empty or
     *                                  a comment line
     * @throws IllegalArgumentException If failed to parse/decode the line
     * @see                             #parseAuthorizedKeyEntry(String, PublicKeyEntryDataResolver)
     */
    public static AuthorizedKeyEntry parseAuthorizedKeyEntry(String value) throws IllegalArgumentException {
        return parseAuthorizedKeyEntry(value, null);
    }

    /**
     * @param  value                    Original line from an {@code authorized_keys} file
     * @param  resolver                 The {@link PublicKeyEntryDataResolver} to use - if {@code null} one will be
     *                                  automatically resolved from the key type
     * @return                          {@link AuthorizedKeyEntry} or {@code null} if the line is {@code null}/empty or
     *                                  a comment line
     * @throws IllegalArgumentException If failed to parse/decode the line
     */
    public static AuthorizedKeyEntry parseAuthorizedKeyEntry(
            String value, PublicKeyEntryDataResolver resolver)
            throws IllegalArgumentException {
        String line = GenericUtils.replaceWhitespaceAndTrim(value);
        if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
            return null;
        }

        int startPos = line.indexOf(' ');
        if (startPos <= 0) {
            throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
        }

        int endPos = line.indexOf(' ', startPos + 1);
        if (endPos <= startPos) {
            endPos = line.length();
        }

        String keyType = line.substring(0, startPos);
        Object decoder = PublicKeyEntry.getKeyDataEntryResolver(keyType);
        if (decoder == null) {
            decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
        }

        AuthorizedKeyEntry entry;
        // assume this is due to the fact that it starts with login options
        if (decoder == null) {
            Map.Entry comps = resolveEntryComponents(line);
            entry = parseAuthorizedKeyEntry(comps.getValue());
            ValidateUtils.checkTrue(entry != null, "Bad format (no key data after login options): %s", line);
            entry.setLoginOptions(parseLoginOptions(comps.getKey()));
        } else {
            String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
            String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
            entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData, resolver);
            entry.setComment(comment);
        }

        return entry;
    }

    /**
     * Parses a single line from an {@code authorized_keys} file that is known to contain login options and
     * separates it to the options and the rest of the line.
     *
     * @param  entryLine The line to be parsed
     * @return           A {@link SimpleImmutableEntry} representing the parsed data where key=login options part and
     *                   value=rest of the data - {@code null} if no data in line or line starts with comment character
     * @see              sshd(8) -
     *                   AUTHORIZED_KEYS_FILE_FORMAT
     */
    public static SimpleImmutableEntry resolveEntryComponents(String entryLine) {
        String line = GenericUtils.replaceWhitespaceAndTrim(entryLine);
        if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
            return null;
        }

        for (int lastPos = 0; lastPos < line.length();) {
            int startPos = line.indexOf(' ', lastPos);
            if (startPos < lastPos) {
                throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
            }

            int quotePos = line.indexOf('"', startPos + 1);
            // If found quotes after the space then assume part of a login option
            if (quotePos > startPos) {
                lastPos = quotePos + 1;
                continue;
            }

            String loginOptions = line.substring(0, startPos).trim();
            String remainder = line.substring(startPos + 1).trim();
            return new SimpleImmutableEntry<>(loginOptions, remainder);
        }

        throw new IllegalArgumentException("Bad format (no key data contents): " + line);
    }

    /**
     * 

* Parses login options line according to * sshd(8) - AUTHORIZED_KEYS_FILE_FORMAT * guidelines. Note: *

* *
    *

    *

  • Options that have a value are automatically stripped of any surrounding double quotes./
  • *

    * *

    *

  • Options that have no value are marked as {@code true/false} - according to the * {@link #BOOLEAN_OPTION_NEGATION_INDICATOR}.
  • *

    * *

    *

  • Options that appear multiple times are simply concatenated using comma as separator.
  • *

    *
* * @param options The options line to parse - ignored if {@code null}/empty/blank * @return A {@link NavigableMap} where key=case insensitive option name and value=the parsed value. * @see #addLoginOption(Map, String) addLoginOption */ public static NavigableMap parseLoginOptions(String options) { String line = GenericUtils.replaceWhitespaceAndTrim(options); int len = GenericUtils.length(line); if (len <= 0) { return Collections.emptyNavigableMap(); } NavigableMap optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); int lastPos = 0; for (int curPos = 0; curPos < len; curPos++) { int nextPos = line.indexOf(',', curPos); if (nextPos < curPos) { break; } // check if "true" comma or one inside quotes int quotePos = line.indexOf('"', curPos); if ((quotePos >= lastPos) && (quotePos < nextPos)) { nextPos = line.indexOf('"', quotePos + 1); if (nextPos <= quotePos) { throw new IllegalArgumentException("Bad format (imbalanced quoted command): " + line); } // Make sure either comma or no more options follow the 2nd quote for (nextPos++; nextPos < len; nextPos++) { char ch = line.charAt(nextPos); if (ch == ',') { break; } if (ch != ' ') { throw new IllegalArgumentException("Bad format (incorrect list format): " + line); } } } addLoginOption(optsMap, line.substring(lastPos, nextPos)); lastPos = nextPos + 1; curPos = lastPos; } // Any leftovers at end of line ? if (lastPos < len) { addLoginOption(optsMap, line.substring(lastPos)); } return optsMap; } /** * Parses and adds a new option to the options map. If a valued option is re-specified then its value(s) are * concatenated using comma as separator. * * @param optsMap Options map to add to * @param option The option data to parse - ignored if {@code null}/empty/blank * @return The updated entry - {@code null} if no option updated in the map * @throws IllegalStateException If a boolean option is re-specified */ public static SimpleImmutableEntry addLoginOption(Map optsMap, String option) { String p = GenericUtils.trimToEmpty(option); if (GenericUtils.isEmpty(p)) { return null; } int pos = p.indexOf('='); String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos)); CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1)); value = GenericUtils.stripQuotes(value); if (value == null) { value = Boolean.toString(name.charAt(0) != BOOLEAN_OPTION_NEGATION_INDICATOR); } SimpleImmutableEntry entry = new SimpleImmutableEntry<>(name, value.toString()); String prev = optsMap.put(entry.getKey(), entry.getValue()); if (prev != null) { if (pos < 0) { throw new IllegalStateException("Bad format (boolean option (" + name + ") re-specified): " + p); } optsMap.put(entry.getKey(), prev + "," + entry.getValue()); } return entry; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy