bcrypt and null kdf done, work on ciphers next (then keytypes)

This commit is contained in:
brent s. 2022-04-25 04:27:24 -04:00
parent 91d5e99404
commit ff3f8243d1
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
43 changed files with 755 additions and 423 deletions

2
TODO
View File

@ -4,5 +4,3 @@
--- ssh-rsa (sha1), rsa-sha2-256, rsa-sha2-512 (new default)
- ciphers:
-- 3des-cbc, aes128-cbc, aes192-cbc, aes256-cbc, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com, chacha20-poly1305@openssh.com
- kdf
-- bcrypt_pbkdf

View File

@ -651,13 +651,90 @@ pre.rouge {
background-color: #f8f8f8;
}
</style>
<!-- https://stackoverflow.com/a/34481639 -->
<!-- Generate a nice TOC -->
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tocify/1.9.0/javascripts/jquery.tocify.min.js"></script>
<!-- We do not need the tocify CSS because the asciidoc CSS already provides most of what we neeed -->

<style>
.tocify-header {
font-style: italic;
}

.tocify-subheader {
font-style: normal;
font-size: 90%;
}

.tocify ul {
margin: 0;
}

.tocify-focus {
color: #7a2518;
background-color: rgba(0, 0, 0, 0.1);
}

.tocify-focus > a {
color: #7a2518;
}
</style>

<script type="text/javascript">
$(function () {
// Add a new container for the tocify toc into the existing toc so we can re-use its
// styling
$("#toc").append("<div id='generated-toc'></div>");
$("#generated-toc").tocify({
extendPage: true,
context: "#content",
highlightOnScroll: true,
hideEffect: "slideUp",
// Use the IDs that asciidoc already provides so that TOC links and intra-document
// links are the same. Anything else might confuse users when they create bookmarks.
hashGenerator: function(text, element) {
return $(element).attr("id");
},
// Smooth scrolling doesn't work properly if we use the asciidoc IDs
smoothScroll: false,
// Set to 'none' to use the tocify classes
theme: "none",
// Handle book (may contain h1) and article (only h2 deeper)
selectors: $( "#content" ).has( "h1" ).size() > 0 ? "h1,h2,h3,h4,h5" : "h2,h3,h4,h5",
ignoreSelector: ".discrete"
});

// Switch between static asciidoc toc and dynamic tocify toc based on browser size
// This is set to match the media selectors in the asciidoc CSS
// Without this, we keep the dynamic toc even if it is moved from the side to preamble
// position which will cause odd scrolling behavior
var handleTocOnResize = function() {
if ($(document).width() < 768) {
$("#generated-toc").hide();
$(".sectlevel0").show();
$(".sectlevel1").show();
}
else {
$("#generated-toc").show();
$(".sectlevel0").hide();
$(".sectlevel1").hide();
}
}

$(window).resize(handleTocOnResize);
handleTocOnResize();
});
</script>

</head>
<body class="book toc2 toc-left">
<div id="header">
<h1>OpenSSH Key Structure Guide</h1>
<div class="details">
<span id="author" class="author">brent saner &lt;bts@square-r00t.net&gt;, https://r00t2.io</span><br>
<span id="revdate">Last updated 2022-03-10 00:18:45 -0500</span>
<span id="revdate">Last updated 2022-04-25 04:27:24 -0400</span>
</div>
<div id="toc" class="toc2">
<div id="toctitle">Table of Contents</div>

77
_ref/docinfo.html Normal file
View File

@ -0,0 +1,77 @@
<!-- https://stackoverflow.com/a/34481639 -->
<!-- Generate a nice TOC -->
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tocify/1.9.0/javascripts/jquery.tocify.min.js"></script>
<!-- We do not need the tocify CSS because the asciidoc CSS already provides most of what we neeed -->

<style>
.tocify-header {
font-style: italic;
}

.tocify-subheader {
font-style: normal;
font-size: 90%;
}

.tocify ul {
margin: 0;
}

.tocify-focus {
color: #7a2518;
background-color: rgba(0, 0, 0, 0.1);
}

.tocify-focus > a {
color: #7a2518;
}
</style>

<script type="text/javascript">
$(function () {
// Add a new container for the tocify toc into the existing toc so we can re-use its
// styling
$("#toc").append("<div id='generated-toc'></div>");
$("#generated-toc").tocify({
extendPage: true,
context: "#content",
highlightOnScroll: true,
hideEffect: "slideUp",
// Use the IDs that asciidoc already provides so that TOC links and intra-document
// links are the same. Anything else might confuse users when they create bookmarks.
hashGenerator: function(text, element) {
return $(element).attr("id");
},
// Smooth scrolling doesn't work properly if we use the asciidoc IDs
smoothScroll: false,
// Set to 'none' to use the tocify classes
theme: "none",
// Handle book (may contain h1) and article (only h2 deeper)
selectors: $( "#content" ).has( "h1" ).size() > 0 ? "h1,h2,h3,h4,h5" : "h2,h3,h4,h5",
ignoreSelector: ".discrete"
});

// Switch between static asciidoc toc and dynamic tocify toc based on browser size
// This is set to match the media selectors in the asciidoc CSS
// Without this, we keep the dynamic toc even if it is moved from the side to preamble
// position which will cause odd scrolling behavior
var handleTocOnResize = function() {
if ($(document).width() < 768) {
$("#generated-toc").hide();
$(".sectlevel0").show();
$(".sectlevel1").show();
}
else {
$("#generated-toc").show();
$(".sectlevel0").hide();
$(".sectlevel1").hide();
}
}

$(window).resize(handleTocOnResize);
handleTocOnResize();
});
</script>

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package cbc

const (
Name string = "aes128-cbc"
)

View File

@ -0,0 +1,10 @@
package aes128

import (
`r00t2.io/sshkeys/cipher/aes`
)

const (
KeySize int = 16 // in bytes; AES128 is so named for its 128-bit key, thus: 128 / 8 = 16
KdfKeySize int = KeySize + aes.IvSize
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package ctr

const (
Name string = "aes128-ctr"
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package gcm

const (
Name string = "aes128-gcm@openssh.com"
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package cbc

const (
Name string = "aes192-cbc"
)

View File

@ -0,0 +1,10 @@
package aes192

import (
`r00t2.io/sshkeys/cipher/aes`
)

const (
KeySize int = 24 // in bytes; AES182 is so named for its 192-bit key, thus: 192 / 8 = 24
KdfKeySize int = KeySize + aes.IvSize
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package ctr

const (
Name string = "aes192-ctr"
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package gcm

const (
Name string = "aes192-gcm@openssh.com"
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package cbc

const (
Name string = "aes256-cbc"
)

View File

@ -0,0 +1,10 @@
package aes256

import (
`r00t2.io/sshkeys/cipher/aes`
)

const (
KeySize int = 32 // in bytes; AES256 is so named for its 256-bit key, thus: 256 / 8 = 32
KdfKeySize int = KeySize + aes.IvSize
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package ctr

const (
Name string = "aes256-ctr"
)

View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1,5 @@
package gcm

const (
Name string = "aes256-gcm@openssh.com"
)

10
cipher/aes/consts.go Normal file
View File

@ -0,0 +1,10 @@
package aes

import (
`crypto/aes`
)

const (
BlockSize int = aes.BlockSize
IvSize int = aes.BlockSize
)

View File

@ -0,0 +1 @@
TODO

1
cipher/null/TODO Normal file
View File

@ -0,0 +1 @@
TODO

View File

@ -0,0 +1 @@
TODO

50
cipher/types.go Normal file
View File

@ -0,0 +1,50 @@
package cipher

import (
`bytes`
)

type Cipher interface {
// Name returns the string form of the cipher name.
Name() (name string)
// NameBytes returns the Name result but in bytes, with a leading uint32 bytecount packed in.
NameBytes() (name []byte)
// BlockSize returns the blocksize of the cipher.Cipher. This is used for externally padding data for Cipher.Encrypt and Cipher.AllocateEncrypt.
BlockSize() (size int)
// KdfKeySize returns the desired/needed key size for use with kdf.KDF.
KdfKeySize() (size int)
// Setup initializes the Cipher with a given key.
Setup(key []byte) (err error)
/*
Encrypt takes plain data, either a:
- string
- raw byte slice ([]byte or []uint8)
- single byte (byte or uint8)
- *bytes.Buffer
and returns an encrypted *bytes.Buffer of data.
*/
Encrypt(data interface{}) (encrypted *bytes.Reader, err error)
// AllocateEncrypt is exactly like cipher.Cipher.Encrypt except that it includes a (NON-encrypted) uint32 prefix of byte allocation.
AllocateEncrypt(data interface{}) (encrypted *bytes.Reader, err error)
/*
Decrypt takes encrypted data, either a:
- raw byte slice ([]byte or []uint8)
- *bytes.Buffer
and returns a plain/decrypted *bytes.Buffer of data.
*/
Decrypt(data interface{}) (decrypted *bytes.Reader, err error)
// AllocatedDecrypt is exactly like cipher.Cipher.Decrypt except that it assumes that data includes a (NON-encrypted) uint32 prefix of byte allocation.
AllocatedDecrypt(data interface{}) (decrypted *bytes.Reader, err error)
// IsPlain returns true if this is a "null" cipher; i.e. no encryption is actually performed.
IsPlain() (plain bool)
/*
Pad returns padded bytes in a *bytes.Buffer according to the cipher's padding specification.
data can be one of either:
- string
- raw byte slice ([]byte or []uint8)
- single byte (byte or uint8)
- *bytes.Buffer
This is a prerequisite in some ciphers, and must be performed BEFORE encrypting.
*/
Pad(data interface{}) (paddedBuf *bytes.Reader, err error)
}

11
kdf/bcrypt/consts.go Normal file
View File

@ -0,0 +1,11 @@
package bcrypt

const (
Name string = "bcrypt"
// DefaultRounds is the default per OpenSSH, not per the bcrypt_pbkdf spec itself. It is recommended to use e.g. 100 rounds.
DefaultRounds uint32 = 16
// DefaultSaltLen is the default per OpenSSH, not per the bcrypt_pbkdf spec itself.
DefaultSaltLen int = 16
// DefaultKeyLen is suitable for AES256-CTR but may not be for others. TODO: revisit this and find something more flexible?
DefaultKeyLen uint32 = 48
)

246
kdf/bcrypt/funcs.go Normal file
View File

@ -0,0 +1,246 @@
package bcrypt

import (
"bytes"
"crypto/rand"
"encoding/binary"

"github.com/dchest/bcrypt_pbkdf"
"r00t2.io/sshkeys/internal"
`r00t2.io/sshkeys/kdf/errs`
)

/*
Setup must be called before DeriveKey. It configures a .

If secret is nil or an empty byte slice, ErrNoSecret will be returned.

If salt is nil or an empty byte slice, one will be randomly generated of DefaultSaltLen length.

If rounds is 0, DefaultRounds will be used.

If keyLen is 0, DefaultKeyLen will be used.
*/
func (k *KDF) Setup(secret, salt []byte, rounds, keyLen uint32) (err error) {

if secret == nil || len(secret) == 0 {
err = errs.ErrNoSecret
return
}

if salt == nil || len(salt) == 0 {
salt = make([]byte, DefaultSaltLen)
if _, err = rand.Read(salt); err != nil {
return
}
}

if rounds == 0 {
rounds = DefaultRounds
}

if keyLen == 0 {
keyLen = DefaultKeyLen
}

k.secret = secret
k.salt = salt
k.rounds = rounds
k.keyLen = keyLen
k.hasSalt = true
k.hasRounds = true

return
}

/*
SetupAuto is used to provide out-of-band configuration if the KDF options were found via kdf.UnpackKDF.

You can test this by running KDF.AutoOK.
*/
func (k *KDF) SetupAuto(secret []byte, keyLen uint32) (err error) {

if !k.AutoOK() {
err = errs.ErrUnknownKdf
return
}

if keyLen == 0 {
keyLen = DefaultKeyLen
}

k.secret = secret
k.keyLen = keyLen

return
}

/*
DeriveKey returns the derived key from a configured bcrypt.KDF.

It must be called *after* Setup.
*/
func (k *KDF) DeriveKey() (key []byte, err error) {

if k.secret == nil {
err = errs.ErrNoSecret
return
}
if k.salt == nil {
err = errs.ErrNoSalt
return
}
if k.rounds == 0 {
err = errs.ErrNoRounds
return
}
if k.keyLen == 0 {
err = errs.ErrNoKeyLen
}

if k.key, err = bcrypt_pbkdf.Key(k.secret, k.salt, int(k.rounds), int(k.keyLen)); err != nil {
return
}

key = k.key

return
}

// Name returns bcrypt.Name.
func (k *KDF) Name() (name string) {

name = Name

return
}

// NameBytes returns the byte form of bcrypt.Name with leading bytecount allocator.
func (k *KDF) NameBytes() (name []byte) {

var nb []byte
var s = k.Name()

nb = []byte(s)

name = make([]byte, 4)
binary.BigEndian.PutUint32(name, uint32(len(nb)))
name = append(name, nb...)

return
}

// PackedBytes returns block 3.0 and recursed.
func (k *KDF) PackedBytes() (buf *bytes.Reader, err error) {

var rounds = make([]byte, 4)
var packer *bytes.Reader
var w = new(bytes.Buffer)

// block 3.0.0.0 and block 3.0.0.0.0
if packer, err = internal.ReadSizeBytes(k.salt, true); err != nil {
return
}
if _, err = packer.WriteTo(w); err != nil {
return
}

// block 3.0.0.1
binary.BigEndian.PutUint32(rounds, k.rounds)
if _, err = w.Write(rounds); err != nil {
return
}

// block 3.0
if buf, err = internal.ReadSizeBytes(w, true); err != nil {
return
}

return
}

// Rounds returns the number of rounds used in derivation.
func (k *KDF) Rounds() (rounds uint32) {

rounds = k.rounds

return
}

// Salt returns the salt bytes.
func (k *KDF) Salt() (salt []byte) {

salt = k.salt

return
}

/*
AutoOK returns true if a kdf.UnpackKDF call was able to fetch the bcrypt.KDF options successfully, in which case the caller may use bcrypt.KDF.SetupAuto.

If false, it will need to be manually configured via bcrypt.KDF.Setup.
*/
func (k *KDF) AutoOK() (ok bool) {

ok = true

if k.salt == nil || len(k.salt) == 0 {
ok = false
}

if k.rounds == 0 {
ok = false
}

return
}

// IsPlain indicates if this kdf.KDF actually does derivation (false) or not (true).
func (k *KDF) IsPlain() (plain bool) {

plain = false

return
}

/*
AddSalt adds a salt as parsed from kdf.UnpackKDF.

If a salt has already been set, a no-op will be performed.
If you want to use a different salt, you will need to create a new bcrypt.KDF.
*/
func (k *KDF) AddSalt(salt []byte) (err error) {

if salt == nil || len(salt) == 0 {
err = errs.ErrNoSalt
return
}
if k.hasSalt {
return
}
k.salt = salt
k.hasSalt = true

return
}

/*
AddRounds adds the rounds as parsed from kdf.UnpackKDF.

If the number of rounds has already been set, a no-op will be performed.
If you want to use a different number of rounds, you will need to create a new bcrypt.KDF.
*/
func (k *KDF) AddRounds(rounds uint32) (err error) {

if rounds == 0 {
err = errs.ErrNoRounds
return
}
if k.hasRounds {
return
}
k.rounds = rounds
k.hasRounds = true

return
}

25
kdf/bcrypt/types.go Normal file
View File

@ -0,0 +1,25 @@
package bcrypt

/*
BcryptPbkdf combines bcrypt hashing algorithm with PBKDF2 key derivation.

(bcrypt) https://www.usenix.org/legacy/events/usenix99/provos/provos_html/node1.html
(PBKDF2) https://datatracker.ietf.org/doc/html/rfc2898
http://www.tedunangst.com/flak/post/bcrypt-pbkdf
*/
type KDF struct {
// salt is used to salt the hash for each round in rounds.
salt []byte
// rounds controls how many iterations of salting/hashing is done.
rounds uint32
// keyLen is how long the derived key should be in bytes.
keyLen uint32
// secret is the "passphrase" used to seed the key creation.
secret []byte
// key is used to store the derived key.
key []byte
// hasSalt is true if a salt has been set.
hasSalt bool
// hasRounds is true if a number of rounds have been set.
hasRounds bool
}

View File

@ -1,11 +0,0 @@
package kdf

const (
BcryptPbkdfName string = "bcrypt"
// BcryptPbkdfDefaultRounds is the default per OpenSSH, not per the bcrypt_pbkdf spec itself. It is recommended to use e.g. 100 rounds.
BcryptPbkdfDefaultRounds uint32 = 16
// BcryptPbkdfDefaultSaltLen is the default per OpenSSH, not per the bcrypt_pbkdf spec itself.
BcryptPbkdfDefaultSaltLen int = 16
// BcryptPbkdfDefaultKeyLen is suitable for AES256-CTR but may not be for others. TODO: revisit this and find something more flexible.
BcryptPbkdfDefaultKeyLen uint32 = 48
)

View File

@ -1,5 +0,0 @@
package kdf

const (
NullName string = "none"
)

View File

@ -1,4 +1,4 @@
package kdf
package errs

import (
"errors"

View File

@ -2,6 +2,10 @@ package kdf

import (
"strings"

`r00t2.io/sshkeys/kdf/bcrypt`
`r00t2.io/sshkeys/kdf/errs`
`r00t2.io/sshkeys/kdf/null`
)

/*
@ -12,12 +16,12 @@ import (
func GetKDF(name string) (k KDF, err error) {

switch strings.ToLower(name) {
case BcryptPbkdfName:
k = &BcryptPbkdf{}
case NullName, "":
k = &Null{}
case bcrypt.Name:
k = &bcrypt.KDF{}
case null.Name, "":
k = &null.KDF{}
default:
err = ErrUnknownKdf
err = errs.ErrUnknownKdf
return
}

@ -25,18 +29,21 @@ func GetKDF(name string) (k KDF, err error) {
}

/*
UnpackKDF returns a KDF from raw data as packed in a private key's bytes.
UnpackKDF returns a kdf.KDF from raw data as packed in a private key's bytes.

KDF.Setup must be called on the resulting KDF.
kdf.KDF.Setup must be called on the resulting kdf.KDF.

data can be:

- a []byte, which can be:
- the full private key bytes
- KDF section (e.g. _ref/format.ed25519 2.0 + (3.0 to 3.0.0.1) or 2.0.0 + (3.0 to 3.0.0.1))
- KDF section (e.g. ED25519 private key block 2.0 + (block 3.0 to block 3.0.0.1) OR
block 2.0.0 + (block 3.0 to block 3.0.0.1))
- a *bytes.Buffer, which supports the same as above
*/
func UnpackKDF(data interface{}) (k KDF, err error) {

// TODO

return
}

View File

@ -1,226 +0,0 @@
package kdf

import (
"bytes"
"crypto/rand"
"encoding/binary"

bcryptPbkdf "github.com/dchest/bcrypt_pbkdf"
"r00t2.io/sshkeys/internal"
)

/*
Setup must be called before DeriveKey. It configures a BcryptPbkdf.

If secret is nil or an empty byte slice, ErrNoSecret will be returned.

If salt is nil or an empty byte slice, one will be randomly generated of BcryptPbkdfDefaultSaltLen length.

If rounds is 0, BcryptPbkdfDefaultRounds will be used.

If keyLen is 0, BcryptPbkdfDefaultKeyLen will be used.
*/
func (b *BcryptPbkdf) Setup(secret, salt []byte, rounds, keyLen uint32) (err error) {

if secret == nil || len(secret) == 0 {
err = ErrNoSecret
return
}

if salt == nil || len(salt) == 0 {
salt = make([]byte, BcryptPbkdfDefaultSaltLen)
if _, err = rand.Read(salt); err != nil {
return
}
}

if rounds == 0 {
rounds = BcryptPbkdfDefaultRounds
}

if keyLen == 0 {
keyLen = BcryptPbkdfDefaultKeyLen
}

b.secret = secret
b.salt = salt
b.rounds = rounds
b.keyLen = keyLen

return
}

/*
SetupAuto is used to provide out-of-band configuration if the KDF options were found via GetKdfFromBytes.

You can test this by running KDF.AutoOK.
*/
func (b *BcryptPbkdf) SetupAuto(secret []byte, keyLen uint32) (err error) {

if !b.AutoOK() {
err = ErrUnknownKdf
return
}

if keyLen == 0 {
keyLen = BcryptPbkdfDefaultKeyLen
}

b.secret = secret
b.keyLen = keyLen

return
}

/*
DeriveKey returns the derived key from a BcryptPbkdf.

It must be called *after* Setup.
*/
func (b *BcryptPbkdf) DeriveKey() (key []byte, err error) {

if b.secret == nil {
err = ErrNoSecret
return
}
if b.salt == nil {
err = ErrNoSalt
return
}
if b.rounds == 0 {
err = ErrNoRounds
return
}
if b.keyLen == 0 {
err = ErrNoKeyLen
}

if b.key, err = bcryptPbkdf.Key(b.secret, b.salt, int(b.rounds), int(b.keyLen)); err != nil {
return
}

key = b.key

return
}

// Name returns BcryptPbkdfName.
func (b *BcryptPbkdf) Name() (name string) {

name = BcryptPbkdfName

return
}

// NameBytes returns the byte form of BcryptPbkdf.Name with leading bytecount allocator.
func (b *BcryptPbkdf) NameBytes() (name []byte) {

var nb []byte
var s = b.Name()

nb = []byte(s)

name = make([]byte, 4)
binary.BigEndian.PutUint32(name, uint32(len(nb)))
name = append(name, nb...)

return
}

// PackedBytes returns 3.0 and recursed.
func (b *BcryptPbkdf) PackedBytes() (buf *bytes.Reader, err error) {

var rounds = make([]byte, 4)
var packer *bytes.Reader
var w = new(bytes.Buffer)

// 3.0.0.0 and 3.0.0.0.0
if packer, err = internal.ReadSizeBytes(b.salt, true); err != nil {
return
}
if _, err = packer.WriteTo(w); err != nil {
return
}

// 3.0.0.1
binary.BigEndian.PutUint32(rounds, b.rounds)
if _, err = w.Write(rounds); err != nil {
return
}

// 3.0
if buf, err = internal.ReadSizeBytes(w, true); err != nil {
return
}

return
}

// Rounds returns the number of rounds used in derivation.
func (b *BcryptPbkdf) Rounds() (rounds uint32) {

rounds = b.rounds

return
}

// Salt returns the salt bytes.
func (b *BcryptPbkdf) Salt() (salt []byte) {

salt = b.salt

return
}

/*
AutoOK returns true if a GetKdfFromBytes call was able to fetch the KDF options successfully, in which case the caller may use KDF.SetupAuto.

If false, it will need to be manually configured via KDF.Setup.
*/
func (b *BcryptPbkdf) AutoOK() (ok bool) {

ok = true

if b.salt == nil || len(b.salt) == 0 {
ok = false
}

if b.rounds == 0 {
ok = false
}

return
}

// IsPlain indicates if this KDF actually does derivation (false) or not (true).
func (b *BcryptPbkdf) IsPlain() (plain bool) {

plain = false

return
}

// addSalt adds a salt as parsed from GetKdfFromBytes.
func (b *BcryptPbkdf) addSalt(salt []byte) (err error) {

if salt == nil || len(salt) == 0 {
err = ErrNoSalt
return
}
b.salt = salt

return
}

// addRounds adds the rounds as parsed from GetKdfFromBytes
func (b *BcryptPbkdf) addRounds(rounds uint32) (err error) {

if rounds == 0 {
err = ErrNoRounds
return
}

b.rounds = rounds

return
}

View File

@ -1,137 +0,0 @@
package kdf

import (
"bytes"
"encoding/binary"
)

/*
Setup must be called before DeriveKey. It configures a Null.

Note that this doesn't actually do anything, it's here for interface compat.
It is recommended to use nil/zero values.
*/
func (n *Null) Setup(secret, salt []byte, rounds, keyLen uint32) (err error) {

_, _, _, _ = secret, salt, rounds, keyLen

return
}

/*
SetupAuto is used to provide out-of-band configuration if the KDF options were found via GetKdfFromBytes.

Note that this doesn't actually do anything, it's here for interface compat.
It is recommended to use nil/zero values.
*/
func (n *Null) SetupAuto(secret []byte, keyLen uint32) (err error) {

_, _ = secret, keyLen

return
}

/*
DeriveKey returns the derived key from a Null.

Note that this doesn't actually do anything, it's here for interface compat.
key will ALWAYS be a nil byte slice.
*/
func (n *Null) DeriveKey() (key []byte, err error) {

return
}

// Name returns NullName.
func (n *Null) Name() (name string) {

name = NullName

return
}

// NameBytes returns the byte form of Null.Name with leading bytecount allocator.
func (n *Null) NameBytes() (name []byte) {

var b []byte
var s = n.Name()

b = []byte(s)

name = make([]byte, 4)
binary.BigEndian.PutUint32(name, uint32(len(b)))
name = append(name, b...)

return
}

// PackedBytes returns 3.0 and recursed.
func (n *Null) PackedBytes() (buf *bytes.Reader, err error) {

// This is static.
buf = bytes.NewReader([]byte{0x0, 0x0, 0x0, 0x0})

return
}

/*
Rounds returns the number of rounds used in derivation.

Note that this will always return 0; it's here for interface compat.
*/
func (n *Null) Rounds() (rounds uint32) {

rounds = 0

return
}

/*
Salt returns the salt bytes.

Note that this will always return nil; it's here for interface compat.
*/
func (n *Null) Salt() (salt []byte) {

salt = nil

return
}

/*
AutoOK returns true if a GetKdfFromBytes call was able to fetch the KDF options successfully, in which case the caller may use KDF.SetupAuto.

If false, it will need to be manually configured via KDF.Setup.

Note that this won't actually do anything and ok will always return as true.
*/
func (n *Null) AutoOK() (ok bool) {

ok = true

return
}

// IsPlain indicates if this KDF actually does derivation (false) or not (true).
func (n *Null) IsPlain() (plain bool) {

plain = true

return
}

// addSalt is a no-op, just here for interface compat.
func (n *Null) addSalt(salt []byte) (err error) {

_ = salt

return
}

// addRounds is a no-op; just here for interface compat.
func (n *Null) addRounds(rounds uint32) (err error) {

_ = rounds

return
}

5
kdf/null/consts.go Normal file
View File

@ -0,0 +1,5 @@
package null

const (
Name string = "none"
)

137
kdf/null/funcs.go Normal file
View File

@ -0,0 +1,137 @@
package null

import (
"bytes"
"encoding/binary"
)

/*
Setup must be called before DeriveKey. It configures a null.KDF.

Note that this doesn't actually do anything, it's here for interface compat.
It is recommended to use nil/zero values.
*/
func (k *KDF) Setup(secret, salt []byte, rounds, keyLen uint32) (err error) {

_, _, _, _ = secret, salt, rounds, keyLen

return
}

/*
SetupAuto is used to provide out-of-band configuration if the null.KDF options were found via kdf.UnpackKDF.

Note that this doesn't actually do anything, it's here for interface compat.
It is recommended to use nil/zero values.
*/
func (k *KDF) SetupAuto(secret []byte, keyLen uint32) (err error) {

_, _ = secret, keyLen

return
}

/*
DeriveKey returns the derived key from a null.KDF.

Note that this doesn't actually do anything, it's here for interface compat.
key will ALWAYS be a nil byte slice.
*/
func (k *KDF) DeriveKey() (key []byte, err error) {

return
}

// Name returns null.Name.
func (k *KDF) Name() (name string) {

name = Name

return
}

// NameBytes returns the byte form of null.Name with leading bytecount allocator.
func (k *KDF) NameBytes() (name []byte) {

var b []byte
var s = k.Name()

b = []byte(s)

name = make([]byte, 4)
binary.BigEndian.PutUint32(name, uint32(len(b)))
name = append(name, b...)

return
}

// PackedBytes returns block 3.0 and recursed.
func (k *KDF) PackedBytes() (buf *bytes.Reader, err error) {

// This is static.
buf = bytes.NewReader([]byte{0x0, 0x0, 0x0, 0x0})

return
}

/*
Rounds returns the number of rounds used in derivation.

Note that this will always return 0; it's here for interface compat.
*/
func (k *KDF) Rounds() (rounds uint32) {

rounds = 0

return
}

/*
Salt returns the salt bytes.

Note that this will always return nil; it's here for interface compat.
*/
func (k *KDF) Salt() (salt []byte) {

salt = nil

return
}

/*
AutoOK returns true if a kdf.UnpackKDF call was able to fetch the null.KDF options successfully, in which case the caller may use null.KDF.SetupAuto.

If false, it will need to be manually configured via null.KDF.Setup.

Note that this won't actually do anything and ok will always return as true.
*/
func (k *KDF) AutoOK() (ok bool) {

ok = true

return
}

// IsPlain indicates if this kdf.KDF actually does derivation (false) or not (true).
func (k *KDF) IsPlain() (plain bool) {

plain = true

return
}

// AddSalt is a no-op, just here for interface compat.
func (k *KDF) AddSalt(salt []byte) (err error) {

_ = salt

return
}

// AddRounds is a no-op; just here for interface compat.
func (k *KDF) AddRounds(rounds uint32) (err error) {

_ = rounds

return
}

4
kdf/null/types.go Normal file
View File

@ -0,0 +1,4 @@
package null

// KDF is a dummy kdf.KDF that is used for unencrypted/plain SSH private keys. It literally does nothing.
type KDF struct{}

View File

@ -26,31 +26,8 @@ type KDF interface {
IsPlain() (plain bool)
// PackedBytes returns the bytes suitable for serializing into a key file.
PackedBytes() (buf *bytes.Reader, err error)
// addSalt adds the salt as parsed from the private key.
addSalt(salt []byte) (err error)
// addRounds adds the rounds as parsed from the private key.
addRounds(rounds uint32) (err error)
// AddSalt adds the salt as parsed from the private key.
AddSalt(salt []byte) (err error)
// AddRounds adds the rounds as parsed from the private key.
AddRounds(rounds uint32) (err error)
}

/*
BcryptPbkdf combines bcrypt hashing algorithm with PBKDF2 key derivation.

(bcrypt) https://www.usenix.org/legacy/events/usenix99/provos/provos_html/node1.html
(PBKDF2) https://datatracker.ietf.org/doc/html/rfc2898
http://www.tedunangst.com/flak/post/bcrypt-pbkdf
*/
type BcryptPbkdf struct {
// salt is used to salt the hash for each round in rounds.
salt []byte
// rounds controls how many iterations of salting/hashing is done.
rounds uint32
// keyLen is how long the derived key should be in bytes.
keyLen uint32
// secret is the "passphrase" used to seed the key creation.
secret []byte
// key is used to store the derived key.
key []byte
}

// Null is a dummy KDF that is used for unencrypted/plain SSH private keys. It literally does nothing.
type Null struct{}

View File

@ -6,14 +6,14 @@ import (
`r00t2.io/sshkeys/kdf`
)

// KeyEd25519 is an ed25519 OpenSSH key.
type KeyEd25519 struct {
KeyPairs []*KeypairEd25519 `xml:"keypairs" json:"keypairs" yaml:"Keypairs"`
Cipher string // TODO: (sshkeys/cipher).Cipher
// Key is an ed25519 OpenSSH key.
type Key struct {
KeyPairs []*Keypair `xml:"keypairs" json:"keypairs" yaml:"Keypairs"`
Cipher string // TODO: (sshkeys/cipher).Cipher
KDF kdf.KDF
}

type KeypairEd25519 struct {
type Keypair struct {
Private ed25519.PrivateKey `xml:"private" json:"private_key" yaml:"Private Key"`
Public ed25519.PublicKey `xml:"public" json:"public_key" yaml:"Public Key"`
Comment string `xml:"comment,attr" json:"comment" yaml:"comment"`