Skip to content

Commit

Permalink
Store keys encoded rather than decoded to make adding new kinds of ke…
Browse files Browse the repository at this point in the history
…ys easier
  • Loading branch information
Ignas committed Dec 12, 2023
1 parent a85afc0 commit c963eee
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 98 deletions.
4 changes: 2 additions & 2 deletions src/main/java/lt/pow/nukagit/db/dao/PublicKeysDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public interface PublicKeysDao {
" LEFT JOIN users AS u ON u.id = user_id" +
" WHERE public_keys.not_archived = true AND user_id = :userId")
List<UserPublicKey> listPublicKeysForUser(@Bind("userId") UUID userId);
@SqlUpdate("INSERT INTO public_keys (id, user_id, fingerprint, key_type, exponent, modulus, name, x, y)" +
" VALUES (UUID(), :userId, :fingerprint, :keyType, :exponent, :modulus, :name, :x, :y)")
@SqlUpdate("INSERT INTO public_keys (id, user_id, fingerprint, key_type, key_bytes)" +
" VALUES (UUID(), :userId, :fingerprint, :keyType, :keyBytes)")
void addPublicKey(@Bind("userId") UUID userId, @Bind("fingerprint") String fingerprint, @BindMethods PublicKeyData publicKeyData);

@SqlUpdate("UPDATE public_keys SET deleted_on = NOW() WHERE id = :id")
Expand Down
68 changes: 20 additions & 48 deletions src/main/java/lt/pow/nukagit/db/entities/PublicKeyData.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,37 @@
import org.immutables.value.Value;
import org.jdbi.v3.core.annotation.JdbiProperty;

import javax.annotation.Nullable;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
import java.util.Objects;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;

@Value.Immutable
public interface PublicKeyData {
enum KeyType {
RSA,
ECDSA
EC
}

KeyType keyType();

@Nullable
BigInteger modulus();

@Nullable
BigInteger exponent();

@Nullable
String name();

@Nullable
BigInteger x();

@Nullable
BigInteger y();
byte[] keyBytes();

default PublicKey key() {
if (keyType().equals(KeyType.RSA)) {
// Generate RSAPublicKeySpec
RSAPublicKeySpec spec = new RSAPublicKeySpec(Objects.requireNonNull(modulus()), Objects.requireNonNull(exponent()));
PublicKey key;
try {
// Generate public key
key = KeyFactory.getInstance("RSA").generatePublic(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return key;
} else if (keyType().equals(KeyType.ECDSA)) {
try {
// Generate ECPoint
ECPoint ecPoint = new ECPoint(Objects.requireNonNull(x()), Objects.requireNonNull(y()));
// Generate ECParameterSpec
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec(Objects.requireNonNull(name())));
ECParameterSpec ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class);
// Generate ECPublicKeySpec
ECPublicKeySpec spec = new ECPublicKeySpec(ecPoint, ecParameterSpec);
// Generate public key
return KeyFactory.getInstance("EC").generatePublic(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidParameterSpecException e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Unknown key type");
var spec = new X509EncodedKeySpec(keyBytes());
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance(keyType().toString());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}

try {
return keyFactory.generatePublic(spec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}

Expand Down
71 changes: 49 additions & 22 deletions src/main/java/lt/pow/nukagit/db/repositories/PublicKeyDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.spec.ECPoint;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.*;
import java.util.Base64;

public class PublicKeyDecoder {
Expand All @@ -31,11 +35,18 @@ private String decodeString(ByteBuffer bb) {
return new String(bytes, StandardCharsets.UTF_8);
}

private ECPoint getECPoint(BigInteger q) {
byte[] qBytes = q.toByteArray();
if (qBytes[0] != 0x04) {
private ECPoint decodeECPoint(ByteBuffer bb) {
int len = bb.getInt();
var format = bb.get();

// validating that the key is uncompressed
if (format != 0x04) {
throw new IllegalArgumentException("Only uncompressed points are supported");
}

byte[] qBytes = new byte[len - 1];
bb.get(qBytes);

byte[] xBytes = new byte[qBytes.length / 2];
byte[] yBytes = new byte[qBytes.length / 2];
System.arraycopy(qBytes, 0, xBytes, 0, xBytes.length);
Expand All @@ -45,6 +56,9 @@ private ECPoint getECPoint(BigInteger q) {

public PublicKeyData decodePublicKey(String keyLine) throws InvalidKeyStringException {
String[] parts = keyLine.split(" ");
PublicKey key;
PublicKeyData.KeyType keyType;

for (String part : parts) {
if (part.startsWith("AAAA")) {
byte[] decodeBuffer;
Expand All @@ -57,34 +71,47 @@ public PublicKeyData decodePublicKey(String keyLine) throws InvalidKeyStringExce
ByteBuffer bb = ByteBuffer.wrap(decodeBuffer);
String typeString = decodeString(bb);
if ("ssh-rsa".equals(typeString)) {
keyType = PublicKeyData.KeyType.RSA;
// extracting exponent and modulus from remaining byte-buffer
BigInteger exponent = decodeBigInt(bb);
BigInteger modulus = decodeBigInt(bb);
PublicKeyData keyData = ImmutablePublicKeyData.builder()
.keyType(PublicKeyData.KeyType.RSA)
.modulus(modulus)
.exponent(exponent).build();
// Make sure a key can be constructed from the data
keyData.key();
return keyData;

RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
try {
// Generate public key
key = KeyFactory.getInstance("RSA").generatePublic(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new InvalidKeyStringException("Failed to construct a valid RSA key", e);
}
} else if (typeString.startsWith("ecdsa-sha2-")) {
keyType = PublicKeyData.KeyType.EC;
// extracting curve name from remaining byte-buffer
String nistNameString = decodeString(bb);
String nameString = nistNameString.replace("nist", "sec") + "r1";
// extracting q from remaining byte-buffer
BigInteger q = decodeBigInt(bb);
PublicKeyData keyData = ImmutablePublicKeyData.builder()
.keyType(PublicKeyData.KeyType.ECDSA)
.name(nameString)
.x(getECPoint(q).getAffineX())
.y(getECPoint(q).getAffineY())
.build();
// Make sure a key can be constructed from the data
keyData.key();
return keyData;

// Generate ECPoint
ECPoint ecPoint = decodeECPoint(bb);

try {
// Generate ECParameterSpec
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec(nameString));
ECParameterSpec ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class);
// Generate ECPublicKeySpec
ECPublicKeySpec spec = new ECPublicKeySpec(ecPoint, ecParameterSpec);
// Generate public key
key = KeyFactory.getInstance("EC").generatePublic(spec);
} catch (InvalidParameterSpecException | InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new InvalidKeyStringException("Failed to construct a valid ECDSA key", e);
}

} else {
throw new InvalidPublicKeyTypeException(String.format("Only supports RSA and ECDSA keys, received %s key", typeString));
}
return ImmutablePublicKeyData.builder()
.keyType(keyType)
.keyBytes(key.getEncoded())
.build();
}
}
throw new InvalidKeyStringException("Key string missing the actual key");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
import com.google.common.base.Suppliers;
import lt.pow.nukagit.db.dao.PublicKeysDao;
import lt.pow.nukagit.db.dao.UsersDao;
import lt.pow.nukagit.db.entities.ImmutablePublicKeyData;
import lt.pow.nukagit.db.entities.PublicKeyData;
import lt.pow.nukagit.db.entities.UserPublicKey;

import javax.inject.Inject;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.List;
Expand All @@ -26,20 +23,6 @@ public class PublicKeyRepository {

private final Supplier<List<UserPublicKey>> publicKeyCache;

static private BigInteger generateRandomBigInteger(int bitLength) {
var bytes = new byte[bitLength / 8];
random.nextBytes(bytes);
return new BigInteger(bytes).abs();
}

static public PublicKeyData generateRandomPublicKeyData() {
return ImmutablePublicKeyData.builder()
.keyType(PublicKeyData.KeyType.RSA)
.modulus(generateRandomBigInteger(1024))
.exponent(generateRandomBigInteger(64))
.build();
}

@Inject
public PublicKeyRepository(UsersDao usersDao, PublicKeysDao publicKeysDao, UsernameValidator usernameValidator, PublicKeyDecoder publicKeyDecoder) {
this.usersDao = usersDao;
Expand Down
6 changes: 1 addition & 5 deletions src/main/resources/db/migration/V001__users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ CREATE TABLE public_keys
user_id CHAR(36) NOT NULL,
fingerprint CHAR(59) NOT NULL,
key_type VARCHAR(8) NOT NULL,
modulus VARBINARY(8000),
exponent VARBINARY(8000),
name VARCHAR(255),
x VARBINARY(8000),
y VARBINARY(8000),
key_bytes VARBINARY(4096) NOT NULL,
created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_on TIMESTAMP NULL DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class PublicKeyDecoderTest extends Specification {
def "test valid keys"() {
expect:
decoder.decodePublicKey(key).key() != null
// def spec = new X509EncodedKeySpec(decoder.decodePublicKey(key).key().getEncoded());
where:
key << validKeys
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,36 @@ package lt.pow.nukagit.db.repositories
import lt.pow.nukagit.db.DatabaseTestBase
import lt.pow.nukagit.db.dao.PublicKeysDao
import lt.pow.nukagit.db.dao.UsersDao
import lt.pow.nukagit.db.entities.ImmutablePublicKeyData
import lt.pow.nukagit.db.entities.PublicKeyData
import org.jeasy.random.EasyRandom
import org.testcontainers.spock.Testcontainers
import spock.lang.Shared

import java.security.KeyPairGenerator

@Testcontainers
class PublicKeyRepositoryDaoTest extends DatabaseTestBase {
@Shared
EasyRandom random = new EasyRandom()
PublicKeyRepository repository
PublicKeyDecoder publicKeyDecoder

def generateRandomRSAKeyData() {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048)
def keyPair = keyGen.generateKeyPair()
return ImmutablePublicKeyData.builder()
.keyType(PublicKeyData.KeyType.RSA)
.keyBytes(keyPair.public.encoded)
.build()
}

def "you can add a user with public key"() {
given:
def username = random.nextObject(String.class)
def publicKey = random.nextObject(String.class)
def testKeyData = repository.generateRandomPublicKeyData()
def testKeyData = generateRandomRSAKeyData()
publicKeyDecoder.decodePublicKey(publicKey) >> testKeyData
when:
repository.addUserWithKey(username, publicKey)
Expand All @@ -35,7 +48,7 @@ class PublicKeyRepositoryDaoTest extends DatabaseTestBase {
def publicKeysDao = jdbi.onDemand(PublicKeysDao.class)
def usersDao = jdbi.onDemand(UsersDao.class)
def usernameValidator = Mock(UsernameValidator.class)
usernameValidator.isValid(_) >> true
usernameValidator.isValid(_ as String) >> true
publicKeyDecoder = Mock(PublicKeyDecoder.class)
// Create repository
repository = new PublicKeyRepository(usersDao, publicKeysDao, usernameValidator, publicKeyDecoder)
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/fixtures/fingerprints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
myfingerprint: F2:65:2C:DD:90:7C:98:D8:E1:91:DF:00:A5:2B:46:0D:41:D4:A4:17
- key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE0Wi6suWaO7sdaTVNjyu3ApwA1KACCItX1R6WUPV01OOVH7080Jc/WD0KhMlEGxfF//8nZAzWTjJWymnQIo6lk=
fingerprint: 7a:2c:d0:4c:53:2f:4f:4e:be:51:cb:a1:2b:9e:39:34
myfingerprint: 90:EB:26:E7:3E:CC:0B:9C:97:86:FB:D3:C2:26:CC:3B:1C:FD:01:85
myfingerprint: FD:B6:32:D7:C9:C4:8C:95:F6:81:B5:9F:E6:04:DB:C2:8F:99:03:10

0 comments on commit c963eee

Please sign in to comment.