blob: 55e419fddf24409037be447cf9b5cc97c624be5e [file] [log] [blame]
/*
* 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.util.buffer;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.function.IntUnaryOperator;
import java.util.logging.Level;
import org.apache.sshd.common.PropertyResolver;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.OpenSshCertificate;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.Readable;
import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
import org.apache.sshd.common.util.logging.SimplifiedLog;
import org.apache.sshd.common.util.security.SecurityUtils;
/**
* Provides an abstract message buffer for encoding SSH messages
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public abstract class Buffer implements Readable {
protected final byte[] workBuf = new byte[Long.BYTES];
protected Buffer() {
super();
}
/**
* @return Current reading position
*/
public abstract int rpos();
/**
* @param rpos Set current reading position
*/
public abstract void rpos(int rpos);
/**
* @return Current writing position
*/
public abstract int wpos();
/**
* @param wpos Set current writing position - <B>Note:</B> if necessary, the underlying data buffer will be
* increased so as to allow writing from the new position
*/
public abstract void wpos(int wpos);
/**
* @return Number of bytes that can still be written without re-sizing the internal buffer
*/
public abstract int capacity();
/**
* @return The <U>raw</U> underlying data bytes
*/
public abstract byte[] array();
/**
* @return The bytes consumed so far
*/
public abstract byte[] getBytesConsumed();
/**
* @param pos A position in the <U>raw</U> underlying data bytes
* @return The byte at the specified position without changing the current {@link #rpos() read position}.
* <B>Note:</B> no validation is made whether the position lies within array boundaries
*/
public byte rawByte(int pos) {
byte[] data = array();
return data[pos];
}
/**
* @param pos A position in the <U>raw</U> underlying data bytes
* @return The unsigned 32 bit integer at the specified position without changing the current {@link #rpos()
* read position}. <B>Note:</B> no validation is made whether the position and the required extra 4
* bytes lie within array boundaries
*/
public long rawUInt(int pos) {
byte[] data = array();
return BufferUtils.getUInt(data, pos, Integer.BYTES);
}
/**
* &quot;Shift&quot; the internal data so that reading starts from position zero.
*/
public abstract void compact();
public byte[] getCompactData() {
int l = available();
if (l > 0) {
byte[] b = new byte[l];
System.arraycopy(array(), rpos(), b, 0, l);
return b;
} else {
return GenericUtils.EMPTY_BYTE_ARRAY;
}
}
/**
* Reset read/write positions to zero - <B>Note:</B> zeroes any previously existing data
*
* @return Reference to this buffer
* @see #clear(boolean)
*/
public Buffer clear() {
return clear(true);
}
/**
* Reset read/write positions to zero
*
* @param wipeData Whether to zero any previously existing data
* @return Reference to this buffer
*/
public abstract Buffer clear(boolean wipeData);
public boolean isValidMessageStructure(Class<?>... fieldTypes) {
return isValidMessageStructure(GenericUtils.isEmpty(fieldTypes) ? Collections.emptyList() : Arrays.asList(fieldTypes));
}
public boolean isValidMessageStructure(Collection<Class<?>> fieldTypes) {
if (GenericUtils.isEmpty(fieldTypes)) {
return true;
}
int remainLen = available();
int readOffset = 0;
for (Class<?> ft : fieldTypes) {
if ((ft == boolean.class) || (ft == Boolean.class)
|| (ft == byte.class) || (ft == Byte.class)) {
if (remainLen < Byte.BYTES) {
return false;
}
remainLen -= Byte.BYTES;
readOffset += Byte.BYTES;
} else if ((ft == short.class) || (ft == Short.class)) {
if (remainLen < Short.BYTES) {
return false;
}
remainLen -= Short.BYTES;
readOffset += Short.BYTES;
} else if ((ft == int.class) || (ft == Integer.class)) {
if (remainLen < Integer.BYTES) {
return false;
}
remainLen -= Integer.BYTES;
readOffset += Integer.BYTES;
} else if ((ft == long.class) || (ft == Long.class)) {
if (remainLen < Long.BYTES) {
return false;
}
remainLen -= Long.BYTES;
readOffset += Long.BYTES;
} else if ((ft == byte[].class) || (ft == String.class)) {
if (remainLen < Integer.BYTES) {
return false;
}
copyRawBytes(readOffset, workBuf, 0, Integer.BYTES);
remainLen -= Integer.BYTES;
readOffset += Integer.BYTES;
long length = BufferUtils.getUInt(workBuf, 0, Integer.BYTES);
if (length > remainLen) {
return false;
}
remainLen -= (int) length;
readOffset += (int) length;
}
}
return true;
}
protected abstract void copyRawBytes(int offset, byte[] buf, int pos, int len);
public String toHex() {
return BufferUtils.toHex(array(), rpos(), available());
}
public void dumpHex(SimplifiedLog logger, String prefix, PropertyResolver resolver) {
dumpHex(logger, BufferUtils.DEFAULT_HEXDUMP_LEVEL, prefix, resolver);
}
public void dumpHex(SimplifiedLog logger, Level level, String prefix, PropertyResolver resolver) {
BufferUtils.dumpHex(
logger, level, prefix, resolver, BufferUtils.DEFAULT_HEX_SEPARATOR, array(), rpos(), available());
}
/*
* ====================== Read methods ======================
*/
public int getUByte() {
return getByte() & 0xFF;
}
public byte getByte() {
ensureAvailable(Byte.BYTES);
getRawBytes(workBuf, 0, Byte.BYTES);
return workBuf[0];
}
public short getShort() {
ensureAvailable(Short.BYTES);
getRawBytes(workBuf, 0, Short.BYTES);
short v = (short) ((workBuf[1] << Byte.SIZE) & 0xFF00);
v |= (short) (workBuf[0] & 0xF);
return v;
}
public int getInt() {
return (int) getUInt();
}
public long getUInt() {
ensureAvailable(Integer.BYTES);
getRawBytes(workBuf, 0, Integer.BYTES);
return BufferUtils.getUInt(workBuf, 0, Integer.BYTES);
}
public long getLong() {
ensureAvailable(Long.BYTES);
getRawBytes(workBuf, 0, Long.BYTES);
long l = ((long) workBuf[0] << 56) & 0xff00000000000000L;
l |= ((long) workBuf[1] << 48) & 0x00ff000000000000L;
l |= ((long) workBuf[2] << 40) & 0x0000ff0000000000L;
l |= ((long) workBuf[3] << 32) & 0x000000ff00000000L;
l |= ((long) workBuf[4] << 24) & 0x00000000ff000000L;
l |= ((long) workBuf[5] << 16) & 0x0000000000ff0000L;
l |= ((long) workBuf[6] << 8) & 0x000000000000ff00L;
l |= (workBuf[7]) & 0x00000000000000ffL;
return l;
}
@SuppressWarnings("PMD.BooleanGetMethodName")
public boolean getBoolean() {
return getByte() != 0;
}
/**
* @return Reads a UTF-8 encoded string
*/
public String getString() {
return getString(StandardCharsets.UTF_8);
}
/**
* According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10">RFC 4251</A>:
*
* A name-list is represented as a uint32 containing its length (number of bytes that follow) followed by a
* comma-separated list of zero or more names.
*
* @return The parsed result
*/
public List<String> getNameList() {
return getNameList(StandardCharsets.UTF_8);
}
public List<String> getNameList(Charset charset) {
return getNameList(charset, ',');
}
public List<String> getNameList(char separator) {
return getNameList(StandardCharsets.UTF_8, separator);
}
/**
* Parses a string that contains values separated by a delimiter
*
* @param charset The {@link Charset} to use to read the string
* @param separator The separator
* @return A {@link List} of the parsed values
*/
public List<String> getNameList(Charset charset, char separator) {
String list = getString(charset);
String[] values = GenericUtils.split(list, separator);
return GenericUtils.isEmpty(values) ? Collections.emptyList() : Arrays.asList(values);
}
public List<OpenSshCertificate.CertificateOption> getCertificateOptions() {
return getCertificateOptions(StandardCharsets.UTF_8);
}
/**
* According to <A HREF=
* "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys#L222-L262">PROTOCOL.certkeys</A>:
*
* Critical Options is a set of bytes that is
*
* [overall length][name(string)][data(string)]...
*
* Where each Critical Option is encoded as a name (string) and data (string)
*
* Then the entire name + data strings are added as bytes (which will get a length prefix)
*
* @param charset {@link Charset} to use for converting bytes to characters
* @return the parsed result, never {@code null}, but possibly empty
*/
public List<OpenSshCertificate.CertificateOption> getCertificateOptions(Charset charset) {
// pull out entire Critical Options section
final ByteArrayBuffer optionBuffer = new ByteArrayBuffer(getBytes());
List<OpenSshCertificate.CertificateOption> list = new ArrayList<>();
while (optionBuffer.available() > 0) {
String name = optionBuffer.getString(charset);
String data = GenericUtils.trimToEmpty(optionBuffer.getString(charset));
list.add(new OpenSshCertificate.CertificateOption(name, data.length() > 0 ? data : null));
}
return list;
}
/**
* @param usePrependedLength If {@code true} then there is a 32-bit value indicating the number of strings to read.
* Otherwise, the method will use a &quot;greedy&quot; reading of strings while more data
* available.
* @return A {@link Collection} of the read strings
* @see #getStringList(boolean, Charset)
*/
public Collection<String> getStringList(boolean usePrependedLength) {
return getStringList(usePrependedLength, StandardCharsets.UTF_8);
}
/**
* @param usePrependedLength If {@code true} then there is a 32-bit value indicating the number of strings to read.
* Otherwise, the method will use a &quot;greedy&quot; reading of strings while more data
* available.
* @param charset The {@link Charset} to use for the strings
* @return A {@link Collection} of the read strings
* @see #getStringList(int, Charset)
* @see #getAvailableStrings()
*/
public Collection<String> getStringList(boolean usePrependedLength, Charset charset) {
if (usePrependedLength) {
int count = getInt();
return getStringList(count, charset);
} else {
return getAvailableStrings(charset);
}
}
/**
* @return The remaining data as a list of strings
* @see #getAvailableStrings(Charset)
*/
public Collection<String> getAvailableStrings() {
return getAvailableStrings(StandardCharsets.UTF_8);
}
/**
* @param charset The {@link Charset} to use for the strings
* @return The remaining data as a list of strings
* @see #available()
* @see #getString(Charset)
*/
public Collection<String> getAvailableStrings(Charset charset) {
Collection<String> list = new LinkedList<>();
while (available() > 0) {
String s = getString(charset);
list.add(s);
}
return list;
}
/**
* @param count The <U>exact</U> number of strings to read - can be zero
* @return A {@link List} with the specified number of strings
* @see #getStringList(int, Charset)
*/
public List<String> getStringList(int count) {
return getStringList(count, StandardCharsets.UTF_8);
}
/**
* @param count The <U>exact</U> number of strings to read - can be zero
* @param charset The {@link Charset} of the strings
* @return A {@link List} with the specified number of strings
* @see #getString(Charset)
*/
public List<String> getStringList(int count, Charset charset) {
if ((count < 0) || (count > SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT)) {
throw new IndexOutOfBoundsException("Illogical string list length: " + count);
}
if (count == 0) {
return Collections.emptyList();
}
List<String> list = new ArrayList<>(count);
for (int index = 1; index <= count; index++) {
String s = getString(charset);
list.add(s);
}
return list;
}
/**
* Reads a string using a given charset.
*
* @param charset The {@link Charset} to use for the string bytes
* @return The read string
*/
public abstract String getString(Charset charset);
public BigInteger getMPInt() {
return new BigInteger(getMPIntAsBytes());
}
public byte[] getMPIntAsBytes() {
return getBytes();
}
public byte[] getBytes() {
int reqLen = getInt();
int len = ensureAvailable(reqLen);
byte[] b = new byte[len];
getRawBytes(b);
return b;
}
public void getRawBytes(byte[] buf) {
getRawBytes(buf, 0, buf.length);
}
public PublicKey getPublicKey() throws SshException {
return getPublicKey(BufferPublicKeyParser.DEFAULT);
}
/**
* @param parser A {@link BufferPublicKeyParser} to extract the key from the buffer - never {@code null}
* @return The extracted {@link PublicKey} - may be {@code null} if the parser so decided
* @throws SshException If failed to extract the key
* @see #getRawPublicKey(BufferPublicKeyParser)
*/
public PublicKey getPublicKey(BufferPublicKeyParser<? extends PublicKey> parser) throws SshException {
int ow = wpos();
int len = getInt();
if (len < 0) {
throw new SshException("Illogical public key length: " + len);
}
wpos(rpos() + len);
try {
return getRawPublicKey(parser);
} finally {
wpos(ow);
}
}
public PublicKey getRawPublicKey() throws SshException {
return getRawPublicKey(BufferPublicKeyParser.DEFAULT);
}
/**
* @param parser A {@link BufferPublicKeyParser} to extract the key from the buffer - never {@code null}
* @return The extracted {@link PublicKey} - may be {@code null} if the parser so decided
* @throws SshException If failed to extract the key
*/
public PublicKey getRawPublicKey(BufferPublicKeyParser<? extends PublicKey> parser) throws SshException {
Objects.requireNonNull(parser, "No key data parser");
try {
String keyType = getString();
if (!parser.isKeyTypeSupported(keyType)) {
throw new NoSuchAlgorithmException("Key type=" + keyType + ") not supported by parser=" + parser);
}
return parser.getRawPublicKey(keyType, this);
} catch (GeneralSecurityException e) {
throw new SshException(e);
}
}
public KeyPair getKeyPair() throws SshException {
try {
PublicKey pub;
PrivateKey prv;
String keyAlg = getString();
if (KeyPairProvider.SSH_RSA.equals(keyAlg)) {
BigInteger e = getMPInt();
BigInteger n = getMPInt();
BigInteger d = getMPInt();
BigInteger qInv = getMPInt();
BigInteger q = getMPInt();
BigInteger p = getMPInt();
BigInteger dP = d.remainder(p.subtract(BigInteger.valueOf(1)));
BigInteger dQ = d.remainder(q.subtract(BigInteger.valueOf(1)));
KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM);
pub = keyFactory.generatePublic(new RSAPublicKeySpec(n, e));
prv = keyFactory.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, dP, dQ, qInv));
} else if (KeyPairProvider.SSH_DSS.equals(keyAlg)) {
BigInteger p = getMPInt();
BigInteger q = getMPInt();
BigInteger g = getMPInt();
BigInteger y = getMPInt();
BigInteger x = getMPInt();
KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.DSS_ALGORITHM);
pub = keyFactory.generatePublic(new DSAPublicKeySpec(y, p, q, g));
prv = keyFactory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g));
} else if (KeyPairProvider.SSH_ED25519.equals(keyAlg)) {
return SecurityUtils.extractEDDSAKeyPair(this, keyAlg);
} else {
ECCurves curve = ECCurves.fromKeyType(keyAlg);
if (curve == null) {
throw new NoSuchAlgorithmException("Unsupported key pair algorithm: " + keyAlg);
}
String curveName = curve.getName();
ECParameterSpec params = curve.getParameters();
return extractEC(curveName, params);
}
return new KeyPair(pub, prv);
} catch (GeneralSecurityException e) {
throw new SshException(e);
}
}
protected KeyPair extractEC(String expectedCurveName, ECParameterSpec spec) throws GeneralSecurityException {
String curveName = getString();
if (!expectedCurveName.equals(curveName)) {
throw new InvalidKeySpecException("extractEC(" + expectedCurveName + ") mismatched curve name: " + curveName);
}
byte[] groupBytes = getBytes();
BigInteger exponent = getMPInt();
if (spec == null) {
throw new InvalidKeySpecException("extractEC(" + expectedCurveName + ") missing parameters for curve");
}
ECPoint group;
try {
group = ECCurves.octetStringToEcPoint(groupBytes);
} catch (RuntimeException e) {
throw new InvalidKeySpecException(
"extractEC(" + expectedCurveName + ")"
+ " failed (" + e.getClass().getSimpleName() + ")"
+ " to decode EC group for curve: " + e.getMessage(),
e);
}
KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM);
PublicKey pubKey = keyFactory.generatePublic(new ECPublicKeySpec(group, spec));
PrivateKey privKey = keyFactory.generatePrivate(new ECPrivateKeySpec(exponent, spec));
return new KeyPair(pubKey, privKey);
}
/**
* Makes sure the buffer contains enough data to accommodate the requested length
*
* @param reqLen Requested data in bytes
* @return Same as input if validation successful
* @throws BufferException If negative length or beyond available requested
*/
public int ensureAvailable(int reqLen) throws BufferException {
if (reqLen < 0) {
throw new BufferException("Bad item length: " + reqLen);
}
int availLen = available();
if (availLen < reqLen) {
throw new BufferException("Underflow: requested=" + reqLen + ", available=" + availLen);
}
return reqLen;
}
/*
* ====================== Write methods ======================
*/
public void putByte(byte b) {
ensureCapacity(Byte.BYTES);
workBuf[0] = b;
putRawBytes(workBuf, 0, Byte.BYTES);
}
/**
* Checks if the <tt>buffer</tt> argument is an array of bytes, a {@link Readable} instance or a {@link ByteBuffer}
* and invokes the appropriate {@code putXXX} method. If {@code null} then puts an empty byte array value
*
* @param buffer The buffered data object to inspect
* @see #putBufferedData(Object)
*/
public void putOptionalBufferedData(Object buffer) {
if (buffer == null) {
putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
} else {
putBufferedData(buffer);
}
}
/**
* Checks if the <tt>buffer</tt> argument is an array of bytes, a {@link Readable} instance or a {@link ByteBuffer}
* and invokes the appropriate {@code putXXX} method.
*
* @param buffer The (never {@code null}) buffer object to put
* @throws IllegalArgumentException If <tt>buffer</tt> is none of the supported types
*/
public void putBufferedData(Object buffer) {
Objects.requireNonNull(buffer, "No buffered data to encode");
if (buffer instanceof byte[]) {
putBytes((byte[]) buffer);
} else if (buffer instanceof Readable) {
putBuffer((Readable) buffer);
} else if (buffer instanceof ByteBuffer) {
putBuffer((ByteBuffer) buffer);
} else {
throw new IllegalArgumentException(
"No buffered overload found for "
+ ((buffer == null) ? null : buffer.getClass().getName()));
}
}
public void putBuffer(Readable buffer) {
putBuffer(buffer, true);
}
public abstract int putBuffer(Readable buffer, boolean expand);
public abstract void putBuffer(ByteBuffer buffer);
/**
* Writes 16 bits
*
* @param i The 16-bit value
*/
public void putShort(int i) {
ensureCapacity(Short.BYTES);
workBuf[0] = (byte) (i >> 8);
workBuf[1] = (byte) i;
putRawBytes(workBuf, 0, Short.BYTES);
}
/**
* Writes 32 bits
*
* @param i The 32-bit value
*/
public void putInt(long i) {
BufferUtils.validateInt32Value(i, "Invalid 32-bit value: %d");
ensureCapacity(Integer.BYTES);
BufferUtils.putUInt(i, workBuf, 0, Integer.BYTES);
putRawBytes(workBuf, 0, Integer.BYTES);
}
/**
* Writes 64 bits
*
* @param i The 64-bit value
*/
public void putLong(long i) {
ensureCapacity(Long.BYTES);
workBuf[0] = (byte) (i >> 56);
workBuf[1] = (byte) (i >> 48);
workBuf[2] = (byte) (i >> 40);
workBuf[3] = (byte) (i >> 32);
workBuf[4] = (byte) (i >> 24);
workBuf[5] = (byte) (i >> 16);
workBuf[6] = (byte) (i >> 8);
workBuf[7] = (byte) i;
putRawBytes(workBuf, 0, Long.BYTES);
}
public void putBoolean(boolean b) {
putByte(b ? (byte) 1 : (byte) 0);
}
/**
* Adds the bytes to the buffer and wipes the data from the input buffer <U>after</U> having added it - useful for
* sensitive information such as password
*
* @param b The buffer to add - OK if {@code null}
*/
public void putAndWipeBytes(byte[] b) {
putAndWipeBytes(b, 0, NumberUtils.length(b));
}
public void putAndWipeBytes(byte[] b, int off, int len) {
putBytes(b, off, len);
for (int pos = off, index = 0; index < len; pos++, index++) {
b[pos] = (byte) 0;
}
}
public void putBytes(byte[] b) {
putBytes(b, 0, NumberUtils.length(b));
}
public void putBytes(byte[] b, int off, int len) {
putInt(len);
putRawBytes(b, off, len);
}
/**
* Encodes the {@link Objects#toString(Object, String) toString} value of each member.
*
* @param objects The objects to be encoded in the buffer - OK if {@code null}/empty
* @param prependLength If {@code true} then the list is preceded by a 32-bit count of the number of members in the
* list
* @see #putStringList(Collection, Charset, boolean)
*/
public void putStringList(Collection<?> objects, boolean prependLength) {
putStringList(objects, StandardCharsets.UTF_8, prependLength);
}
/**
* Encodes the {@link Objects#toString(Object, String) toString} value of each member
*
* @param objects The objects to be encoded in the buffer - OK if {@code null}/empty
* @param charset The {@link Charset} to use for encoding
* @param prependLength If {@code true} then the list is preceded by a 32-bit count of the number of members in the
* list
* @see #putString(String, Charset)
*/
public void putStringList(Collection<?> objects, Charset charset, boolean prependLength) {
int numObjects = GenericUtils.size(objects);
if (prependLength) {
putInt(numObjects);
}
if (numObjects <= 0) {
return;
}
for (Object o : objects) {
String s = Objects.toString(o, null);
putString(s, charset);
}
}
public void putCertificateOptions(List<OpenSshCertificate.CertificateOption> options) {
putCertificateOptions(options, StandardCharsets.UTF_8);
}
/**
* According to <A HREF=
* "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys#L222-L262">PROTOCOL.certkeys</A>:
*
* Critical Options is a set of bytes that is
*
* [overall length][name(string)][data(string)]...
*
* Where each Critical Option is encoded as a name (string) and data (string)
*
* Then the entire name + data strings are added as bytes (which will get a length prefix)
*
* @param options to write into the buffer, may be {@code null} or empty but must not contain {@code null} elements
* @param charset The {@link Charset} to use for string options
*/
public void putCertificateOptions(List<OpenSshCertificate.CertificateOption> options, Charset charset) {
int numObjects = GenericUtils.size(options);
if (numObjects <= 0) {
putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
return;
}
ByteArrayBuffer tmpBuffer = new ByteArrayBuffer();
for (OpenSshCertificate.CertificateOption option : options) {
tmpBuffer.putString(option.getName(), charset);
tmpBuffer.putString(option.getData(), charset);
}
putBytes(tmpBuffer.getCompactData());
}
/**
* According to <A HREF="https://tools.ietf.org/html/rfc4251#page-10">RFC 4251</A>: <CODE>
* A name-list is represented as a uint32 containing its length (number of bytes
* that follow) followed by a comma-separated list of zero or more names.
* </CODE>
*
* @param names The name list to put
*/
public void putNameList(Collection<String> names) {
putNameList(names, StandardCharsets.UTF_8);
}
public void putNameList(Collection<String> names, Charset charset) {
putNameList(names, charset, ',');
}
public void putNameList(Collection<String> names, char separator) {
putNameList(names, StandardCharsets.UTF_8, separator);
}
/**
* Adds a string that contains values separated by a delimiter
*
* @param names The names to set
* @param charset The {@link Charset} to use to encode the string
* @param separator The separator
*/
public void putNameList(Collection<String> names, Charset charset, char separator) {
String list = GenericUtils.join(names, separator);
putString(list, charset);
}
public void putString(String string) {
putString(string, StandardCharsets.UTF_8);
}
public void putString(String string, Charset charset) {
if (GenericUtils.isEmpty(string)) {
putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
} else {
byte[] bytes = string.getBytes(charset);
putBytes(bytes);
}
}
/**
* Zeroes the input array <U>after</U> having put the characters in the buffer - useful for sensitive information
* such as passwords
*
* @param chars The characters to put in the buffer - may be {@code null}/empty
* @see #putAndWipeChars(char[], Charset)
* @see #putChars(char[], Charset)
*/
public void putAndWipeChars(char[] chars) {
putAndWipeChars(chars, 0, GenericUtils.length(chars));
}
public void putAndWipeChars(char[] chars, int offset, int len) {
putAndWipeChars(chars, offset, len, StandardCharsets.UTF_8);
}
public void putAndWipeChars(char[] chars, Charset charset) {
putAndWipeChars(chars, 0, GenericUtils.length(chars), charset);
}
public void putAndWipeChars(char[] chars, int offset, int len, Charset charset) {
putChars(chars, offset, len, charset);
for (int pos = offset, index = 0; index < len; index++, pos++) {
chars[pos] = '\0';
}
}
public void putChars(char[] chars) {
putChars(chars, 0, GenericUtils.length(chars));
}
public void putChars(char[] chars, int offset, int len) {
putChars(chars, offset, len, StandardCharsets.UTF_8);
}
public void putChars(char[] chars, Charset charset) {
putChars(chars, 0, GenericUtils.length(chars), charset);
}
public void putChars(char[] chars, int offset, int len, Charset charset) {
if (len <= 0) {
putBytes(GenericUtils.EMPTY_BYTE_ARRAY);
} else {
CharBuffer charBuf = CharBuffer.wrap(chars, offset, len);
ByteBuffer byteBuf = charset.encode(charBuf);
putBuffer(byteBuf);
}
}
public void putMPInt(BigInteger bigint) {
putMPInt(bigint.toByteArray());
}
public void putMPInt(byte[] mpInt) {
if ((mpInt[0] & 0x80) != 0) {
putInt(mpInt.length + 1 /* padding */);
putByte((byte) 0);
} else {
putInt(mpInt.length);
}
putRawBytes(mpInt);
}
public void putRawBytes(byte[] d) {
putRawBytes(d, 0, d.length);
}
public abstract void putRawBytes(byte[] d, int off, int len);
public void putPublicKey(PublicKey key) {
int ow = wpos();
putInt(0);
int ow1 = wpos();
putRawPublicKey(key);
int ow2 = wpos();
wpos(ow);
putInt(ow2 - ow1);
wpos(ow2);
}
public void putRawPublicKey(PublicKey key) {
putString(KeyUtils.getKeyType(key));
putRawPublicKeyBytes(key);
}
public void putRawPublicKeyBytes(PublicKey key) {
Objects.requireNonNull(key, "No key");
if (key instanceof RSAPublicKey) {
RSAPublicKey rsaPub = (RSAPublicKey) key;
putMPInt(rsaPub.getPublicExponent());
putMPInt(rsaPub.getModulus());
} else if (key instanceof DSAPublicKey) {
DSAPublicKey dsaPub = (DSAPublicKey) key;
DSAParams dsaParams = dsaPub.getParams();
putMPInt(dsaParams.getP());
putMPInt(dsaParams.getQ());
putMPInt(dsaParams.getG());
putMPInt(dsaPub.getY());
} else if (key instanceof ECPublicKey) {
ECPublicKey ecKey = (ECPublicKey) key;
ECParameterSpec ecParams = ecKey.getParams();
ECCurves curve = ECCurves.fromCurveParameters(ecParams);
if (curve == null) {
throw new BufferException("Unsupported EC curve parameters");
}
byte[] ecPoint = ECCurves.encodeECPoint(ecKey.getW(), ecParams);
putString(curve.getName());
putBytes(ecPoint);
} else if (SecurityUtils.EDDSA.equals(key.getAlgorithm())) {
SecurityUtils.putRawEDDSAPublicKey(this, key);
} else if (key instanceof OpenSshCertificate) {
OpenSshCertificate cert = (OpenSshCertificate) key;
putBytes(cert.getNonce());
putRawPublicKeyBytes(cert.getCertPubKey());
putLong(cert.getSerial());
putInt(cert.getType().getCode());
putString(cert.getId());
ByteArrayBuffer tmpBuffer = new ByteArrayBuffer();
tmpBuffer.putStringList(cert.getPrincipals(), false);
putBytes(tmpBuffer.getCompactData());
putLong(cert.getValidAfter());
putLong(cert.getValidBefore());
putCertificateOptions(cert.getCriticalOptions());
putCertificateOptions(cert.getExtensions());
// must always be an empty string
putString(cert.getReserved());
tmpBuffer = new ByteArrayBuffer(); // TODO tmpBuffer.clear() instead of allocate new buffer
tmpBuffer.putRawPublicKey(cert.getCaPubKey());
putBytes(tmpBuffer.getCompactData());
// only append signature when present, it's useful to *not* append a sig when building a buf for signing
if (cert.getSignature() != null) {
putBytes(cert.getSignature());
}
} else {
throw new BufferException("Unsupported raw public key algorithm: " + key.getAlgorithm());
}
}
public void putKeyPair(KeyPair kp) {
PublicKey pubKey = kp.getPublic();
PrivateKey prvKey = kp.getPrivate();
if (prvKey instanceof RSAPrivateCrtKey) {
RSAPublicKey rsaPub = (RSAPublicKey) pubKey;
RSAPrivateCrtKey rsaPrv = (RSAPrivateCrtKey) prvKey;
putString(KeyPairProvider.SSH_RSA);
putMPInt(rsaPub.getPublicExponent());
putMPInt(rsaPub.getModulus());
putMPInt(rsaPrv.getPrivateExponent());
putMPInt(rsaPrv.getCrtCoefficient());
putMPInt(rsaPrv.getPrimeQ());
putMPInt(rsaPrv.getPrimeP());
} else if (pubKey instanceof DSAPublicKey) {
DSAPublicKey dsaPub = (DSAPublicKey) pubKey;
DSAParams dsaParams = dsaPub.getParams();
DSAPrivateKey dsaPrv = (DSAPrivateKey) prvKey;
putString(KeyPairProvider.SSH_DSS);
putMPInt(dsaParams.getP());
putMPInt(dsaParams.getQ());
putMPInt(dsaParams.getG());
putMPInt(dsaPub.getY());
putMPInt(dsaPrv.getX());
} else if (pubKey instanceof ECPublicKey) {
ECPublicKey ecPub = (ECPublicKey) pubKey;
ECPrivateKey ecPriv = (ECPrivateKey) prvKey;
ECParameterSpec ecParams = ecPub.getParams();
ECCurves curve = ECCurves.fromCurveParameters(ecParams);
if (curve == null) {
throw new BufferException("Unsupported EC curve parameters");
}
byte[] ecPoint = ECCurves.encodeECPoint(ecPub.getW(), ecParams);
putString(curve.getKeyType());
putString(curve.getName());
putBytes(ecPoint);
putMPInt(ecPriv.getS());
} else if (SecurityUtils.EDDSA.equals(pubKey.getAlgorithm())) {
SecurityUtils.putEDDSAKeyPair(this, pubKey, prvKey);
} else {
throw new BufferException("Unsupported key pair algorithm: " + pubKey.getAlgorithm());
}
}
public Buffer ensureCapacity(int capacity) {
return ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR);
}
/**
* @param capacity The required capacity
* @param growthFactor An {@link IntUnaryOperator} that is invoked if the current capacity is insufficient. The
* argument is the minimum required new data length, the function result should be the
* effective new data length to be allocated - if less than minimum then an exception is thrown
* @return This buffer instance
*/
public abstract Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor);
/**
* @return Current size of underlying backing data bytes array
*/
protected abstract int size();
@Override
public String toString() {
return getClass().getSimpleName()
+ "[rpos=" + rpos()
+ ", wpos=" + wpos()
+ ", size=" + size()
+ "]";
}
}