CRC32: Move checksum generation to a dedicated package

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-01-20 14:56:07 +01:00
parent 2df0b6e4b1
commit 01da5bdec7
19 changed files with 379 additions and 42 deletions

View file

@ -47,7 +47,7 @@ var AbortNotFound = func(c *gin.Context) {
}
values := gin.H{
"signUp": gin.H{"message": config.MsgSponsor, "url": config.SignUpURL},
"signUp": config.SignUp,
"config": conf.ClientPublic(),
"error": i18n.Msg(i18n.ErrNotFound),
"code": http.StatusNotFound,

View file

@ -7,7 +7,6 @@ import (
"github.com/manifoldco/promptui"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
@ -45,11 +44,13 @@ func placesUpdateAction(ctx *cli.Context) error {
return err
}
// Show info to non-members.
if !conf.Sponsor() && !conf.Test() {
log.Errorf(config.MsgSponsorCommand)
log.Errorf("Since running this command puts additional load on our infrastructure, we can unfortunately only offer it to members at this time.")
return nil
}
// Initialize database connection.
conf.InitDb()
defer conf.Shutdown()

View file

@ -1,10 +1,32 @@
/*
Package config provides global options, command-line flags, and user settings.
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://www.photoprism.app/trademark>
Feel free to send an email to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package config
import (
"crypto/tls"
"encoding/hex"
"fmt"
"hash/crc32"
"net/http"
"net/url"
"os"
@ -34,16 +56,14 @@ import (
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/ttl"
"github.com/photoprism/photoprism/pkg/checksum"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
)
// log points to the global logger.
var log = event.Log
var once sync.Once
var LowMem = false
var TotalMem uint64
var crc32Castagnoli = crc32.MakeTable(crc32.Castagnoli)
// Config holds database, cache and all parameters of photoprism
type Config struct {
@ -357,15 +377,7 @@ func (c *Config) Serial() string {
// SerialChecksum returns the CRC32 checksum of the storage serial.
func (c *Config) SerialChecksum() string {
var result []byte
crc := crc32.New(crc32Castagnoli)
if _, err := crc.Write([]byte(c.Serial())); err != nil {
log.Warnf("config: %s", err)
}
return hex.EncodeToString(crc.Sum(result))
return checksum.Serial([]byte(c.Serial()))
}
// Name returns the app name.
@ -402,7 +414,7 @@ func (c *Config) Version() string {
// VersionChecksum returns the application version checksum.
func (c *Config) VersionChecksum() uint32 {
return crc32.ChecksumIEEE([]byte(c.Version()))
return checksum.Crc32([]byte(c.Version()))
}
// UserAgent returns an HTTP user agent string based on the app config and version.

View file

@ -590,11 +590,18 @@ func TestConfig_Serial(t *testing.T) {
func TestConfig_SerialChecksum(t *testing.T) {
c := NewConfig(CliTestContext())
serial := "zr2g80wvjmm1zwzg"
expected := "c7dcdb1c"
c.serial = serial
result := c.SerialChecksum()
t.Logf("SerialChecksum: %s", result)
// t.Logf("Serial: %s", c.serial)
// t.Logf("SerialChecksum: %s", result)
assert.NotEmpty(t, result)
assert.Equal(t, expected, result)
}
func TestConfig_Public(t *testing.T) {

View file

@ -0,0 +1,8 @@
package config
var (
SignUpURL = "https://www.photoprism.app/membership"
MsgSponsor = "Become a member today, support our mission and enjoy our member benefits! 💎"
MsgSignUp = "Visit " + SignUpURL + " to learn more."
SignUp = Values{"message": MsgSponsor, "url": SignUpURL}
)

9
internal/config/var.go Normal file
View file

@ -0,0 +1,9 @@
package config
import (
"sync"
)
var once sync.Once
var LowMem = false
var TotalMem uint64

View file

@ -22,7 +22,7 @@ func registerPWARoutes(router *gin.Engine, conf *config.Config) {
}
values := gin.H{
"signUp": gin.H{"message": config.MsgSponsor, "url": config.SignUpURL},
"signUp": config.SignUp,
"config": conf.ClientPublic(),
}
c.HTML(http.StatusOK, conf.TemplateName(), values)

16
pkg/checksum/charset.go Normal file
View file

@ -0,0 +1,16 @@
package checksum
const (
CharsetBase36 = "abcdefghijklmnopqrstuvwxyz0123456789"
CharsetBase62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
// Char returns a simple checksum byte based on Crc32 and the 62 characters specified in CharsetBase62.
func Char(data []byte) byte {
return CharsetBase62[Crc32(data)%62]
}
// Base36 returns a simple checksum byte based on Crc32 and the 36 lower-case characters specified in CharsetBase36.
func Base36(data []byte) byte {
return CharsetBase36[Crc32(data)%36]
}

View file

@ -0,0 +1,93 @@
package checksum
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChar(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
data := ""
expected := "a"
result := string(Char([]byte(data)))
t.Logf("Char(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("One", func(t *testing.T) {
data := "1"
expected := "R"
result := string(Char([]byte(data)))
t.Logf("Char(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("Serial", func(t *testing.T) {
data := "zr2g80wvjmm1zwzg"
expected := "G"
result := string(Char([]byte(data)))
t.Logf("Char(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("HelloWorld", func(t *testing.T) {
data := "Hello World!"
expected := "X"
result := string(Char([]byte(data)))
t.Logf("Char(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
}
func TestBase36(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
data := ""
expected := "a"
result := string(Base36([]byte(data)))
t.Logf("Base36(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("One", func(t *testing.T) {
data := "1"
expected := "l"
result := string(Base36([]byte(data)))
t.Logf("Base36(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("Serial", func(t *testing.T) {
data := "zr2g80wvjmm1zwzg"
expected := "u"
result := string(Base36([]byte(data)))
t.Logf("Base36(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("HelloWorld", func(t *testing.T) {
data := "Hello World!"
expected := "x"
result := string(Base36([]byte(data)))
t.Logf("Base36(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
}

View file

@ -1,5 +1,5 @@
/*
Package config provides global options, command-line flags, and user settings.
Package checksum provides functions and abstractions to generate data checksums.
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
@ -22,10 +22,4 @@ want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package config
var SignUpURL = "https://www.photoprism.app/membership"
var MsgSponsor = "Become a member today, support our mission and enjoy our member benefits! 💎"
var MsgSignUp = "Visit " + SignUpURL + " to learn more."
var MsgSponsorCommand = "Since running this command puts additional load on our infrastructure," +
" we unfortunately can only offer it to members."
package checksum

19
pkg/checksum/crc32.go Normal file
View file

@ -0,0 +1,19 @@
package checksum
import (
"fmt"
"hash/crc32"
)
var Crc32Castagnoli = crc32.MakeTable(crc32.Castagnoli)
// Crc32 returns the CRC-32 checksum of data using the crc32.IEEE polynomial.
func Crc32(data []byte) uint32 {
return crc32.ChecksumIEEE(data)
}
// Serial returns the CRC-32 checksum as a hexadecimal encoded string using the Castagnoli polynomial,
// which has better error detection properties than crc32.IEEE.
func Serial(data []byte) string {
return fmt.Sprintf("%08x", crc32.Checksum(data, Crc32Castagnoli))
}

View file

@ -0,0 +1,98 @@
package checksum
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCrc32(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
data := ""
expected := "00000000"
crc := Crc32([]byte(data))
result := fmt.Sprintf("%08x", crc)
t.Logf("Crc32(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("One", func(t *testing.T) {
data := "1"
expected := "83dcefb7"
crc := Crc32([]byte(data))
result := fmt.Sprintf("%08x", crc)
t.Logf("Crc32(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("Serial", func(t *testing.T) {
data := "zr2g80wvjmm1zwzg"
expected := "2db41d54"
crc := Crc32([]byte(data))
result := fmt.Sprintf("%08x", crc)
t.Logf("Crc32(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("HelloWorld", func(t *testing.T) {
data := "Hello World!"
expected := "1c291ca3"
crc := Crc32([]byte(data))
result := fmt.Sprintf("%08x", crc)
t.Logf("Crc32(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
}
func TestSerial(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
data := ""
expected := "00000000"
result := Serial([]byte(data))
t.Logf("Serial(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("One", func(t *testing.T) {
data := "1"
expected := "90f599e3"
result := Serial([]byte(data))
t.Logf("Serial(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("Serial", func(t *testing.T) {
data := "zr2g80wvjmm1zwzg"
expected := "c7dcdb1c"
result := Serial([]byte(data))
t.Logf("Serial(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
t.Run("HelloWorld", func(t *testing.T) {
data := "Hello World!"
expected := "fe6cf1dc"
result := Serial([]byte(data))
t.Logf("Serial(%s): result %s, expected %s", data, result, expected)
assert.Equal(t, expected, result)
})
}

6
pkg/checksum/digit.go Normal file
View file

@ -0,0 +1,6 @@
package checksum
// Digit returns a Crc32-based checksum number between 0 and 9.
func Digit(data []byte) int {
return int(Crc32(data) % 10)
}

View file

@ -0,0 +1,70 @@
package checksum
import (
"crypto/rand"
"fmt"
"log"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDigit(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
data := ""
expected := "0"
result := Digit([]byte(data))
t.Logf("Digit(%s): result %d, expected %s", data, result, expected)
assert.Equal(t, expected, fmt.Sprintf("%d", result))
})
t.Run("One", func(t *testing.T) {
data := "1"
expected := "3"
result := Digit([]byte(data))
t.Logf("Digit(%s): result %d, expected %s", data, result, expected)
assert.Equal(t, expected, fmt.Sprintf("%d", result))
})
t.Run("Serial", func(t *testing.T) {
data := "zr2g80wvjmm1zwzg"
expected := "8"
result := Digit([]byte(data))
t.Logf("Digit(%s): result %d, expected %s", data, result, expected)
assert.Equal(t, expected, fmt.Sprintf("%d", result))
})
t.Run("HelloWorld", func(t *testing.T) {
data := "Hello World!"
expected := "5"
result := Digit([]byte(data))
t.Logf("Digit(%s): result %d, expected %s", data, result, expected)
assert.Equal(t, expected, fmt.Sprintf("%d", result))
})
t.Run("Rand", func(t *testing.T) {
for i := 0; i < 100; i++ {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
log.Fatal(err)
}
result := Digit(b)
if result < 0 {
t.Fatal("result < 0")
} else if result > 9 {
t.Fatal("result > 9")
}
}
})
}

View file

@ -6,6 +6,8 @@ import (
"hash/crc32"
"io"
"os"
"github.com/photoprism/photoprism/pkg/checksum"
)
// Hash returns the SHA1 hash of a file as string.
@ -41,7 +43,7 @@ func Checksum(fileName string) string {
defer file.Close()
hash := crc32.New(crc32.MakeTable(crc32.Castagnoli))
hash := crc32.New(checksum.Crc32Castagnoli)
if _, err := io.Copy(hash, file); err != nil {
return ""

View file

@ -3,9 +3,10 @@ package rnd
import (
"crypto/rand"
"fmt"
"hash/crc32"
"log"
"math/big"
"github.com/photoprism/photoprism/pkg/checksum"
)
const (
@ -50,7 +51,7 @@ func AppPassword() string {
if (i+1)%7 == 0 {
b = append(b, AppPasswordSeparator)
} else if i == AppPasswordLength-1 {
b = append(b, CharsetBase62[crc32.ChecksumIEEE(b)%62])
b = append(b, checksum.Char(b))
return string(b)
} else if r, err := rand.Int(rand.Reader, m); err == nil {
b = append(b, CharsetBase62[r.Int64()])
@ -85,7 +86,7 @@ func IsAppPassword(s string, verifyChecksum bool) bool {
}
// Verify token checksum.
return s[AppPasswordLength-1] == CharsetBase62[crc32.ChecksumIEEE([]byte(s[:AppPasswordLength-1]))%62]
return s[AppPasswordLength-1] == checksum.Char([]byte(s[:AppPasswordLength-1]))
}
// IsAuthAny checks if the string might be a valid auth token or app password.

View file

@ -2,8 +2,9 @@ package rnd
import (
"fmt"
"hash/crc32"
"strconv"
"github.com/photoprism/photoprism/pkg/checksum"
)
// CrcToken returns a string token with checksum.
@ -14,9 +15,8 @@ func CrcToken() string {
token = append(token, '-')
token = append(token, []byte(Base36(4))...)
checksum := crc32.ChecksumIEEE(token)
sum := strconv.FormatInt(int64(checksum), 16)
crc := checksum.Crc32(token)
sum := strconv.FormatInt(int64(crc), 16)
return fmt.Sprintf("%s-%.4s", token, sum)
}
@ -29,9 +29,8 @@ func ValidateCrcToken(s string) bool {
token := []byte(s[:9])
checksum := crc32.ChecksumIEEE(token)
sum := strconv.FormatInt(int64(checksum), 16)
crc := checksum.Crc32(token)
sum := strconv.FormatInt(int64(crc), 16)
return s == fmt.Sprintf("%s-%.4s", token, sum)
}

View file

@ -3,10 +3,12 @@ package rnd
import (
"crypto/rand"
"math/big"
"github.com/photoprism/photoprism/pkg/checksum"
)
const CharsetBase36 = "abcdefghijklmnopqrstuvwxyz0123456789"
const CharsetBase62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const CharsetBase36 = checksum.CharsetBase36
const CharsetBase62 = checksum.CharsetBase62
// Base36 generates a random token containing lowercase letters and numbers.
func Base36(length int) string {