Documentation
¶
Overview ¶
Package libcipher provides a collection of cryptographic utilities for encryption, decryption, integrity verification, and secure key generation. It includes implementations for AES-GCM (authenticated encryption) and AES-CBC combined with HMAC (for encryption with integrity verification), as well as functions for sealed HMAC hash creation and constant-time comparison.
The package offers the following functionalities:
- AES-GCM based encryption/decryption, which provides both confidentiality and authenticity.
- AES-CBC with HMAC for scenarios where nonce collisions are a concern, especially in high-volume or distributed environments. This mode encrypts data using AES-CBC (with PKCS#7 padding) and ensures integrity via an HMAC over the encrypted payload and additional data.
- Sealed HMAC hash creation and comparison, where a unique salt is automatically added and the resulting JSON-encoded object encapsulates both the computed HMAC digest and the salt.
- Cryptographically secure key generation.
- Ed25519 signing keys: generation, seed and public-key parsing/formatting, signing and verification. These live here rather than at the call site so that one encoding and one set of length checks serve every component that handles a key.
Security Considerations:
- The encryption key and integrity key must be kept secret and must be distinct. Reusing keys for different purposes can compromise security.
- An Ed25519 keypair is an identity, not a transport secret: what it signs is defined by the protocol using it, and that definition belongs in that protocol's package. This package signs the bytes it is given and nothing more.
- When using AES-CBC with HMAC, ensure that the entire ciphertext fits in memory as the HMAC is computed over the complete message. For high-volume systems, AES-GCM may be preferred.
Index ¶
- Constants
- Variables
- func CheckHash(signingKey string, salt string, shouldBe string, hash []byte) (bool, error)
- func Equal(sealedHash1, sealedHash2 []byte) bool
- func EqualSigningKey(a, b SigningPrivateKey) bool
- func FormatPublicKey(pub SigningPublicKey) string
- func FormatSigningSeed(priv SigningPrivateKey) (string, error)
- func GenerateKey(keyLength int) (string, error)
- func GenerateSigningKey() (SigningPublicKey, SigningPrivateKey, error)
- func NewHash(args GenerateHashArgs, hashfn func() hash.Hash) ([]byte, error)
- func ParseSigningSeed(s string) (SigningPublicKey, SigningPrivateKey, error)
- func Sign(priv SigningPrivateKey, message []byte) ([]byte, error)
- func Verify(pub SigningPublicKey, message, sig []byte) bool
- type CipherTextError
- type Decryptor
- type EncryptionKeyError
- type Encryptor
- type GenerateHashArgs
- type HashError
- type IntegrityKeyError
- type InvalidUsageError
- type KeyGenerationError
- type MessageError
- type SigningKeyError
- type SigningPrivateKey
- type SigningPublicKey
Constants ¶
const ( SigningSeedSize = ed25519.SeedSize // 32, the private half's canonical storage form SigningPublicKeySize = ed25519.PublicKeySize // 32 SigningPrivateKeySize = ed25519.PrivateKeySize // 64, seed ‖ public key SignatureSize = ed25519.SignatureSize // 64 )
Ed25519 signing keys live in this package so that every component that has to read, write, or check one agrees on a single set of rules. The alternative — each caller reaching for crypto/ed25519 and encoding/base64 on its own — is how two ends of the same protocol end up with two encodings, and a key that round-trips locally but fails every connection in the field.
Sizes are re-exported so callers can validate lengths without importing crypto/ed25519 themselves.
const ( ErrBadPublicKey = SigningKeyError("not an ed25519 public key") ErrBadPrivateKey = SigningKeyError("not an ed25519 private key") ErrBadSeed = SigningKeyError("not an ed25519 seed") ErrKeyGeneration = SigningKeyError("error generating signing key") )
Signing-key failures. They are all "this is not usable key material", which a caller treats as fatal: a malformed key does not become well-formed on retry.
Variables ¶
var SigningKeyEncoding = base64.StdEncoding
SigningKeyEncoding is the one encoding this package writes.
Standard base64 with padding, because that is what encoding/json already produces for a []byte field: a service that marshals its key into an enrolment payload and a tool that prints one agree without a second convention, and no call site has to guess. Note that this deliberately differs from GenerateKey, which hex-encodes symmetric key material; hex doubles the length, and these keys travel in JSON documents and command lines where the shorter form is what people already see.
Security Considerations:
- The encoding is not a security property. ParsePublicKey and ParseSigningSeed therefore read the unpadded and URL-safe base64 variants and lowercase hex as well, since a key is text a human may have moved between machines. Widening what is *read* is safe in a way that widening what is *accepted as valid* is not: the signature check is unchanged by how the key was spelled, and every accepted spelling must still decode to exactly the right number of bytes.
Functions ¶
func CheckHash ¶
CheckHash verifies the shouldBe string against the hash. params: - signingKey (string) - the signing key for the hash - salt (string) - the salt used for the hash - password (string) - the password to verify - hash ([]byte) - the stored hash to compare against returns: (bool, error) - whether the password matches the hash and an error if any
Note: Think twice, maybe bycrypt is what you need.
func Equal ¶
Equal compares two JSON-encoded sealed hashes in constant time. It unmarshals each sealed hash into a SealedHash struct and then compares both the Hash and Salt fields using hmac.Equal.
Usage:
ok := Equal(sealedHash1, sealedHash2)
if !ok {
// the sealed hashes do not match
}
Returns true when the two sealed hashes are byte-identical. Since a sealed hash is the JSON encoding of both the digest and its salt, comparing the encoded forms compares both components — no unmarshalling is needed, and not unmarshalling is what keeps the comparison constant-time end to end.
func EqualSigningKey ¶ added in v0.38.0
func EqualSigningKey(a, b SigningPrivateKey) bool
EqualSigningKey compares two private keys in constant time.
Usage:
if !EqualSigningKey(configured, loaded) {
// the process was handed two different identities
}
hmac.Equal, not bytes.Equal, for the same reason Equal uses it: the inputs are secret, so the comparison must not leak where they first differ. Keys of differing length simply compare unequal.
func FormatPublicKey ¶ added in v0.38.0
func FormatPublicKey(pub SigningPublicKey) string
FormatPublicKey renders a public key for storage or for handing to a peer, in SigningKeyEncoding. It returns the empty string for a key of the wrong length rather than emitting text that would never parse back.
func FormatSigningSeed ¶ added in v0.38.0
func FormatSigningSeed(priv SigningPrivateKey) (string, error)
FormatSigningSeed renders the private half of a keypair as its 32-byte seed in SigningKeyEncoding. The seed is the storage form: the full 64-byte private key is the seed with the public key appended, so persisting the seed loses nothing and halves what has to be kept secret.
Security Considerations:
- The returned string is secret material. Do not log it, and prefer passing it through a channel that does not end up in shell history or a crash dump.
func GenerateKey ¶
GenerateKey generates a cryptographically random key with the specified length.
func GenerateSigningKey ¶ added in v0.38.0
func GenerateSigningKey() (SigningPublicKey, SigningPrivateKey, error)
GenerateSigningKey returns a fresh Ed25519 keypair from the system CSPRNG.
When to use: Use it when a component needs an identity to sign with — a relay proving itself to the instances paired with it, for example. The public half is meant to be published with FormatPublicKey; the private half is a secret and must be stored with FormatSigningSeed somewhere only that component can read.
Security Considerations:
- The keypair is long-lived by intent, so where the seed is written matters more than how it was generated. This package deliberately does not read or write files or environment variables; that is a deployment concern.
func NewHash ¶
func NewHash(args GenerateHashArgs, hashfn func() hash.Hash) ([]byte, error)
NewHash generates a sealed hash from the provided arguments and hash function. It computes an HMAC digest over the concatenation of the input data and a unique salt, and packages the result along with the salt in a JSON-encoded SealedHash object.
Usage:
sealed, err := NewHash(GenerateHashArgs{
Hash: data,
SigningKey: key,
}, sha256.New)
if err != nil {
// handle error
}
When to use: Use NewHash when you need to securely generate a hash that verifies the integrity and authenticity of data. A unique salt is automatically added so that identical inputs produce distinct outputs, and the signing key is applied during the HMAC computation. The signing key is not required during verification since it is already embedded in the computed HMAC digest.
func ParseSigningSeed ¶ added in v0.38.0
func ParseSigningSeed(s string) (SigningPublicKey, SigningPrivateKey, error)
ParseSigningSeed reconstructs a keypair from a seed produced by FormatSigningSeed, accepting the same spellings ParsePublicKey does.
When to use: Use it at start-up, on whatever a deployment hands the process as its signing identity. It is the counterpart of FormatSigningSeed and the only supported way to turn stored text back into a key — parsing a seed at the call site is exactly the duplication this package exists to remove.
The seed determines the public key, so a caller that also received a public key out of band should compare the two and refuse to start on a mismatch: signing with a key nobody pinned produces signatures nobody can verify.
func Sign ¶ added in v0.38.0
func Sign(priv SigningPrivateKey, message []byte) ([]byte, error)
Sign returns a detached signature over message. The message is signed as it is given: any domain separation or framing is the caller's protocol and belongs in the caller, not here.
It returns an error wrapping ErrBadPrivateKey rather than panicking on a key of the wrong length, which is what crypto/ed25519 would do — a key usually arrives from configuration, and bad configuration should surface as an error an operator can read.
func Verify ¶ added in v0.38.0
func Verify(pub SigningPublicKey, message, sig []byte) bool
Verify reports whether sig is a valid signature of message by pub. It is total: a key, message, or signature of any length or contents yields false rather than a panic, because everything it is handed came off a wire or out of a config file and none of it is trusted.
Security Considerations:
- A false result must be treated as an authentication failure, never as a transient error to retry past.
Types ¶
type CipherTextError ¶
type CipherTextError string
func (CipherTextError) Error ¶
func (e CipherTextError) Error() string
type Decryptor ¶
type Decryptor interface {
// Encrypts/Decrypts a message, misuse may lead to a panic.
Crypt(cipherpackage []byte) ([]byte, []byte, error)
}
provides a method to crypt a cipher package. Misuse of this method may lead to a panic.
func NewCBCHMACDecryptor ¶
func NewCBCHMACDecryptor(encryptionKey []byte, integrityKey []byte, calculateMAC func() hash.Hash) (Decryptor, error)
Configure & init the AES-CBC+HMAC cryptor in decryption mode.
func NewGCMDecryptor ¶
NewGCMDecryptor creates a new Decryptor using AES-GCM with the given key.
type EncryptionKeyError ¶
type EncryptionKeyError string
func (EncryptionKeyError) Error ¶
func (e EncryptionKeyError) Error() string
type Encryptor ¶
type Encryptor interface {
// Encrypts/Decrypts a message, misuse may lead to a panic.
Crypt(message []byte, additionalData []byte) ([]byte, error)
}
provides a method to crypt a message with additional data. Misuse of this method may lead to a panic.
func NewCBCHMACEncryptor ¶
func NewCBCHMACEncryptor(encryptionKey []byte, integrityKey []byte, calculateMAC func() hash.Hash, rand io.Reader) (Encryptor, error)
Configure & init the AES-CBC+HMAC cryptor in encryption mode. AES-CBC with PKCS7 padding HMAC for integrity.
The final encrypted string format: [ MAC | AD-Length | AD | Initialization Vector | Block 1 | Block 2 | ... ] uses rand from the arguments for introducing randomness.
Don't use this for big messages, the whole cypher has to be in mem for computing the Hmac.
The encryption key and integrity key must be distinct. Both keys have to be kept secret. Rotating must be done to both keys simultaneously.
Compromised Encryption Key:
An attacker, possessing the encryption key, could decrypt sensitive data. If you rotate only the integrity key, they still have access to the previously encrypted data.
Compromised Integrity Key:
An attacker with the integrity key could potentially modify encrypted data, forge HMACs, and tamper with the system without detection. Even if you rotate the encryption key, the integrity of past data is compromised.
the MAC is calculated from ( AD-Length | AD | Initialization Vector | Block 1 | Block 2 | ... )
GCM Comparison:
Use CBC with HMAC over GCM (or any stream cipher) when avoiding nonce collisions can be challenging is a problem.
This is the case if you deal with:
- high-volume systems (the probability of nonce collisions increases, especially if the nonce space is limited).
- distributed environments (coordinating nonce generation across nodes and ensuring uniqueness becomes even more complex).
- or scenarios where encrypted data needs to be stored persistently. For example, if encrypted data is stored in a database.
and later retrieved and re-encrypted, ensuring that a new, unique nonce is used each time can be challenging.
Since this is a one-person project, ensure you review the code before using it to validate its security and correctness.
type GenerateHashArgs ¶
GenerateHashArgs contains the input parameters for generating a sealed hash. The Payload field is the data to be hashed, and SigningKey is the key used to compute the HMAC digest. The SigningKey should be kept secret.
type IntegrityKeyError ¶
type IntegrityKeyError string
func (IntegrityKeyError) Error ¶
func (e IntegrityKeyError) Error() string
type InvalidUsageError ¶
type InvalidUsageError string
func (InvalidUsageError) Error ¶
func (e InvalidUsageError) Error() string
type KeyGenerationError ¶
type KeyGenerationError string
func (KeyGenerationError) Error ¶
func (e KeyGenerationError) Error() string
type MessageError ¶
type MessageError string
func (MessageError) Error ¶
func (e MessageError) Error() string
type SigningKeyError ¶ added in v0.38.0
type SigningKeyError string
SigningKeyError represents an error while generating, parsing, or using an Ed25519 key. Every value is a fixed sentinel wrapped with detail, so callers can match with errors.Is and still print something an operator can act on.
func (SigningKeyError) Error ¶ added in v0.38.0
func (e SigningKeyError) Error() string
type SigningPrivateKey ¶ added in v0.38.0
type SigningPrivateKey = ed25519.PrivateKey
SigningPublicKey and SigningPrivateKey are aliases, not new types, so a value obtained from crypto/ed25519 elsewhere stays assignable here and callers are never forced to convert. The alias exists to spell the intent — these are signing keys, distinct from the symmetric material GenerateKey produces — not to wrap the standard library.
type SigningPublicKey ¶ added in v0.38.0
SigningPublicKey and SigningPrivateKey are aliases, not new types, so a value obtained from crypto/ed25519 elsewhere stays assignable here and callers are never forced to convert. The alias exists to spell the intent — these are signing keys, distinct from the symmetric material GenerateKey produces — not to wrap the standard library.
func ParsePublicKey ¶ added in v0.38.0
func ParsePublicKey(s string) (SigningPublicKey, error)
ParsePublicKey reads a key produced by FormatPublicKey. It also accepts the unpadded and URL-safe base64 variants and lowercase hex — see SigningKeyEncoding for why reading is lenient where writing is not. The result is always exactly SigningPublicKeySize bytes or an error wrapping ErrBadPublicKey; it never panics, whatever the input.