133 lines
3.1 KiB
Go
133 lines
3.1 KiB
Go
package buildblob
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
const (
|
|
keyLen = 32
|
|
defaultTime = 3
|
|
defaultMem = 64 * 1024
|
|
defaultCPU = 4
|
|
)
|
|
|
|
type Manifest struct {
|
|
OAuthClientID string `json:"oauthClientID"`
|
|
OAuthClientSecret string `json:"oauthClientSecret"`
|
|
Tags []string `json:"tags"`
|
|
Users []ManifestUser `json:"users"`
|
|
}
|
|
|
|
type ManifestUser struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type Blob struct {
|
|
Version int `json:"version"`
|
|
Users []BlobUser `json:"users"`
|
|
KDF KDFParams `json:"kdf"`
|
|
}
|
|
|
|
type BlobUser struct {
|
|
Username string `json:"username"`
|
|
Salt []byte `json:"salt"`
|
|
Nonce []byte `json:"nonce"`
|
|
Ciphertext []byte `json:"ciphertext"`
|
|
}
|
|
|
|
type KDFParams struct {
|
|
Time uint32 `json:"time"`
|
|
Memory uint32 `json:"memory"`
|
|
Threads uint8 `json:"threads"`
|
|
KeyLen uint32 `json:"keyLen"`
|
|
}
|
|
|
|
type payload struct {
|
|
OAuthClientID string `json:"oauthClientID"`
|
|
OAuthClientSecret string `json:"oauthClientSecret"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
func BuildFromFile(path string) ([]byte, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var manifest Manifest
|
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
return Build(manifest)
|
|
}
|
|
|
|
func Build(manifest Manifest) ([]byte, error) {
|
|
if manifest.OAuthClientID == "" || manifest.OAuthClientSecret == "" {
|
|
return nil, errors.New("manifest must include oauthClientID and oauthClientSecret")
|
|
}
|
|
if len(manifest.Users) == 0 {
|
|
return nil, errors.New("manifest must include at least one user")
|
|
}
|
|
pt, err := json.Marshal(payload{
|
|
OAuthClientID: manifest.OAuthClientID,
|
|
OAuthClientSecret: manifest.OAuthClientSecret,
|
|
Tags: manifest.Tags,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := KDFParams{Time: defaultTime, Memory: defaultMem, Threads: defaultCPU, KeyLen: keyLen}
|
|
blob := Blob{Version: 1, KDF: params}
|
|
seen := map[string]struct{}{}
|
|
for _, user := range manifest.Users {
|
|
if user.Username == "" || user.Password == "" {
|
|
return nil, errors.New("each user must include username and password")
|
|
}
|
|
if _, ok := seen[user.Username]; ok {
|
|
return nil, fmt.Errorf("duplicate username %q", user.Username)
|
|
}
|
|
seen[user.Username] = struct{}{}
|
|
|
|
salt := make([]byte, 16)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return nil, err
|
|
}
|
|
key := argon2.IDKey([]byte(user.Password), salt, params.Time, params.Memory, params.Threads, params.KeyLen)
|
|
block, err := aes.NewCipher(key)
|
|
zero(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aead, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nonce := make([]byte, aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
blob.Users = append(blob.Users, BlobUser{
|
|
Username: user.Username,
|
|
Salt: salt,
|
|
Nonce: nonce,
|
|
Ciphertext: aead.Seal(nil, nonce, pt, nil),
|
|
})
|
|
}
|
|
return json.Marshal(blob)
|
|
}
|
|
|
|
func zero(buf []byte) {
|
|
for i := range buf {
|
|
buf[i] = 0
|
|
}
|
|
}
|