mirror of
https://github.com/hanzoai/cek.git
synced 2026-10-09 02:37:18 +00:00
Open a namespace's database encrypted, one way
A key derived from the master and the namespace: not generated, not wrapped, not stored, not rotated in place. That removes the unwrap step, the rewrap step, the per-file key material, and the plaintext-to-encrypted migration — every one of which existed to carry old data forward, and none of which is a property anyone wanted. A database is born encrypted or it does not exist. The same namespace and subsystem always derive the same key, so a file reopens after a restart with nothing persisted beside it, and two databases never share a key because the subsystem is part of the binding. Losing the master loses the data. That is what encryption at rest means, and why the master lives in KMS. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Hanzo AI Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# cek
|
||||||
|
|
||||||
|
Opens a namespace's SQLite database encrypted at rest.
|
||||||
|
|
||||||
|
```go
|
||||||
|
db, err := cek.Open(master, ns, "treasury", dataDir)
|
||||||
|
```
|
||||||
|
|
||||||
|
The key is derived from the master and the namespace. It is not generated, not
|
||||||
|
wrapped, not stored, and not rotated in place — so there is no unwrap step, no
|
||||||
|
rewrap step, no per-file key material to lose, and no migration path to
|
||||||
|
maintain. A database is born encrypted or it does not exist.
|
||||||
|
|
||||||
|
Losing the master loses the data. That is the property you want from encryption
|
||||||
|
at rest, and the reason the master lives in KMS.
|
||||||
|
|
||||||
|
## Where it sits
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `hanzoai/namespace` | names the entity, and where its file lives |
|
||||||
|
| `hanzoai/cek` | turns the master + that name into the file's key, and opens it |
|
||||||
|
| `hanzoai/sqlite` | opens a file under a raw key; knows nothing about who owns it |
|
||||||
|
| `hanzoai/kms` | holds the master |
|
||||||
|
|
||||||
|
Nothing here knows about orgs, users, billing or plugins. It knows a namespace,
|
||||||
|
a subsystem, and a master key.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Package cek opens a namespace's SQLite database encrypted at rest.
|
||||||
|
//
|
||||||
|
// There is one way to do it:
|
||||||
|
//
|
||||||
|
// db, err := cek.Open(master, ns, "treasury", dataDir)
|
||||||
|
//
|
||||||
|
// The key is derived from the master and the namespace. It is not generated,
|
||||||
|
// not wrapped, not stored, and not rotated in place — so there is no unwrap
|
||||||
|
// step, no rewrap step, no per-file key material to lose, and no migration
|
||||||
|
// path to maintain. A database is born encrypted or it does not exist. Losing
|
||||||
|
// the master loses the data, which is the property you want from encryption at
|
||||||
|
// rest and the reason the master lives in KMS.
|
||||||
|
//
|
||||||
|
// The split of responsibilities is deliberate:
|
||||||
|
//
|
||||||
|
// namespace names the entity and where its file lives
|
||||||
|
// cek turns the master + that name into the file's key, and opens it
|
||||||
|
// sqlite opens a file under a raw key and knows nothing about who owns it
|
||||||
|
// kms holds the master
|
||||||
|
//
|
||||||
|
// Nothing here knows about orgs, users, billing or plugins. It knows a
|
||||||
|
// namespace, a subsystem, and a master key.
|
||||||
|
package cek
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/hanzoai/namespace"
|
||||||
|
sqlitedrv "github.com/hanzoai/sqlite"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyLen is the length of both the master key and every derived key.
|
||||||
|
const KeyLen = 32
|
||||||
|
|
||||||
|
// info binds a derived key to this scheme, this namespace and this subsystem.
|
||||||
|
// Changing any part of it changes every key, which is why the version is in
|
||||||
|
// it: a future scheme is a new prefix, not a silent reinterpretation of the
|
||||||
|
// same bytes.
|
||||||
|
const infoPrefix = "hanzo/cek/v1/"
|
||||||
|
|
||||||
|
// ErrNoMaster reports a master key that is missing or the wrong length. It is
|
||||||
|
// deliberately fatal to Open rather than falling back to plaintext: a service
|
||||||
|
// that starts unencrypted because a key was absent has failed silently at the
|
||||||
|
// only job this package has.
|
||||||
|
var ErrNoMaster = errors.New("cek: master key must be " + itoa(KeyLen) + " bytes")
|
||||||
|
|
||||||
|
// DeriveKey returns the key for one database: the master, bound to the
|
||||||
|
// namespace that owns it and the subsystem it holds.
|
||||||
|
//
|
||||||
|
// It is a pure function of its inputs. The same namespace and subsystem always
|
||||||
|
// produce the same key, so a file can be opened again after a restart with
|
||||||
|
// nothing persisted alongside it, and two different databases never share a
|
||||||
|
// key because the subsystem is part of the binding.
|
||||||
|
func DeriveKey(master []byte, ns namespace.Namespace, subsystem string) ([]byte, error) {
|
||||||
|
if len(master) != KeyLen {
|
||||||
|
return nil, ErrNoMaster
|
||||||
|
}
|
||||||
|
if ns.IsZero() {
|
||||||
|
return nil, errors.New("cek: the zero namespace owns no database")
|
||||||
|
}
|
||||||
|
if subsystem == "" {
|
||||||
|
return nil, errors.New("cek: empty subsystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := make([]byte, KeyLen)
|
||||||
|
info := []byte(infoPrefix + ns.String() + "/" + subsystem)
|
||||||
|
if _, err := io.ReadFull(hkdf.New(sha256.New, master, nil, info), key); err != nil {
|
||||||
|
return nil, fmt.Errorf("cek: derive key: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the encrypted database holding subsystem for ns, under dir,
|
||||||
|
// creating it if it does not exist. The returned handle is already keyed;
|
||||||
|
// callers use it as an ordinary *sql.DB.
|
||||||
|
//
|
||||||
|
// The error never contains the key or a DSN that holds it.
|
||||||
|
func Open(master []byte, ns namespace.Namespace, subsystem, dir string) (*sql.DB, error) {
|
||||||
|
key, err := DeriveKey(master, ns, subsystem)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := namespace.Path(dir, ns, subsystem)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sqlitedrv.OpenDB(path, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cek: open %s database for %s: %w", subsystem, ns, err)
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// itoa avoids importing strconv for one constant in an error string.
|
||||||
|
func itoa(n int) string {
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var b [4]byte
|
||||||
|
i := len(b)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
b[i] = byte('0' + n%10)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(b[i:])
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
package cek
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hanzoai/namespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func master(b byte) []byte { return bytes.Repeat([]byte{b}, KeyLen) }
|
||||||
|
|
||||||
|
func mustNS(t *testing.T, org, project string) namespace.Namespace {
|
||||||
|
t.Helper()
|
||||||
|
ns, err := namespace.OrgProject(org, project)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same inputs must always give the same key, or a database cannot be
|
||||||
|
// reopened after a restart.
|
||||||
|
func TestDeriveIsDeterministic(t *testing.T) {
|
||||||
|
ns := mustNS(t, "acme", "")
|
||||||
|
a, err := DeriveKey(master(1), ns, "treasury")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, _ := DeriveKey(master(1), ns, "treasury")
|
||||||
|
if !bytes.Equal(a, b) {
|
||||||
|
t.Fatal("same inputs produced different keys")
|
||||||
|
}
|
||||||
|
if len(a) != KeyLen {
|
||||||
|
t.Fatalf("key is %d bytes, want %d", len(a), KeyLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every axis must change the key. If any of these collide, two databases share
|
||||||
|
// a key and one account's file opens another's.
|
||||||
|
func TestDeriveSeparatesEveryAxis(t *testing.T) {
|
||||||
|
base, _ := DeriveKey(master(1), mustNS(t, "acme", ""), "treasury")
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
key func() ([]byte, error)
|
||||||
|
}{
|
||||||
|
{"different master", func() ([]byte, error) { return DeriveKey(master(2), mustNS(t, "acme", ""), "treasury") }},
|
||||||
|
{"different org", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "globex", ""), "treasury") }},
|
||||||
|
{"different subsystem", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "acme", ""), "audit") }},
|
||||||
|
{"a project of that org", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "acme", "web"), "treasury") }},
|
||||||
|
{"the system namespace", func() ([]byte, error) { return DeriveKey(master(1), namespace.System(), "treasury") }},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.key()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if bytes.Equal(base, got) {
|
||||||
|
t.Fatalf("%s produced the SAME key — two databases would share it", tc.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A missing or short master must fail, never fall back to opening in plaintext.
|
||||||
|
func TestDeriveRefusesBadInput(t *testing.T) {
|
||||||
|
ns := mustNS(t, "acme", "")
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
master []byte
|
||||||
|
ns namespace.Namespace
|
||||||
|
sub string
|
||||||
|
}{
|
||||||
|
{"no master", nil, ns, "treasury"},
|
||||||
|
{"short master", bytes.Repeat([]byte{1}, 16), ns, "treasury"},
|
||||||
|
{"zero namespace", master(1), namespace.Namespace{}, "treasury"},
|
||||||
|
{"empty subsystem", master(1), ns, ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if _, err := DeriveKey(tc.master, tc.ns, tc.sub); err == nil {
|
||||||
|
t.Fatal("expected an error, got a key")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open must write where namespace says, so the file and its durable slot agree.
|
||||||
|
func TestOpenUsesTheNamespaceLocation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
ns := mustNS(t, "acme", "")
|
||||||
|
|
||||||
|
db, err := Open(master(1), ns, "treasury", dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if _, err := db.Exec(`CREATE TABLE t (v TEXT)`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want, err := namespace.Path(dir, ns, "treasury")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := filepath.Abs(want); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An error must never carry the key or a DSN holding it.
|
||||||
|
func TestOpenErrorHoldsNoKey(t *testing.T) {
|
||||||
|
_, err := Open(master(1), namespace.Namespace{}, "treasury", t.TempDir())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error")
|
||||||
|
}
|
||||||
|
if bytes.Contains([]byte(err.Error()), master(1)) {
|
||||||
|
t.Fatal("error leaked the key")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
module github.com/hanzoai/cek
|
||||||
|
|
||||||
|
go 1.26.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hanzoai/namespace v1.2.0
|
||||||
|
github.com/hanzoai/sqlite v0.4.0
|
||||||
|
golang.org/x/crypto v0.54.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 // indirect
|
||||||
|
github.com/hanzoai/csqlite v0.1.0 // indirect
|
||||||
|
github.com/hanzoai/sqlcipher v0.1.1 // indirect
|
||||||
|
github.com/luxfi/crypto v1.20.2 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
modernc.org/libc v1.72.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.51.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
|
||||||
|
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
|
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 h1:UBg1xk+oAsIVbFuGg6hdfAm7EvCv3EL80vFxJNsslqw=
|
||||||
|
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hanzoai/csqlite v0.1.0 h1:suwC3dh0INlfP/U0Es6cDf6JNQ+2+GVLLATPWCUux6k=
|
||||||
|
github.com/hanzoai/csqlite v0.1.0/go.mod h1:H31a/O6VXuklR9UBkgY++bmAK5uzVfXPqU0F6P9Wsos=
|
||||||
|
github.com/hanzoai/namespace v1.2.0 h1:FLEHzR/GyE/OltXozMvT2n2FfUUxTuVVbILKZBkEjLI=
|
||||||
|
github.com/hanzoai/namespace v1.2.0/go.mod h1:vGuxMDHg5MdhH5q2bqqjZL/afINBWJubP4WD1YwZTm4=
|
||||||
|
github.com/hanzoai/sqlcipher v0.1.1 h1:GARjSiUEa1lwhd1/f87XRaujZBG5s1ZwxrZW2Es/ADI=
|
||||||
|
github.com/hanzoai/sqlcipher v0.1.1/go.mod h1:F0soUYM1i4sawOZUpRvVnWoUayPbeGVlGq01VXy9Aqg=
|
||||||
|
github.com/hanzoai/sqlite v0.4.0 h1:xlF6w6xzn2VSeAVBKFrZfIq0ISqRSTHz2g3QN3eCcnE=
|
||||||
|
github.com/hanzoai/sqlite v0.4.0/go.mod h1:sE5uvENQ+q+rI011AwFuAuJ7DwOoDnywyCQstZ0ekHQ=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
|
||||||
|
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
|
||||||
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
|
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
|
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||||
|
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||||
|
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
||||||
|
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
Reference in New Issue
Block a user