2022-09-05 03:06:08 -04:00
|
|
|
/*
|
|
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
package cryptmapper
|
|
|
|
|
|
|
|
import (
|
2023-07-17 07:55:31 -04:00
|
|
|
"bytes"
|
2022-03-22 11:03:15 -04:00
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"testing"
|
2023-07-17 07:55:31 -04:00
|
|
|
"time"
|
2022-03-22 11:03:15 -04:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2022-06-30 09:24:36 -04:00
|
|
|
"go.uber.org/goleak"
|
2022-03-22 11:03:15 -04:00
|
|
|
)
|
|
|
|
|
2022-06-30 09:24:36 -04:00
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
goleak.VerifyTestMain(m)
|
|
|
|
}
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
func TestCloseCryptDevice(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
2022-04-26 10:54:05 -04:00
|
|
|
mapper *stubCryptDevice
|
|
|
|
wantErr bool
|
2022-03-22 11:03:15 -04:00
|
|
|
}{
|
|
|
|
"success": {
|
2022-04-26 10:54:05 -04:00
|
|
|
mapper: &stubCryptDevice{},
|
|
|
|
wantErr: false,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
2022-05-10 04:43:48 -04:00
|
|
|
"error on InitByName": {
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{initByNameErr: assert.AnError},
|
2022-04-26 10:54:05 -04:00
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error on Deactivate": {
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{deactivateErr: assert.AnError},
|
2022-04-26 10:54:05 -04:00
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper := &CryptMapper{
|
|
|
|
kms: &fakeKMS{},
|
|
|
|
mapper: tc.mapper,
|
|
|
|
}
|
|
|
|
err := mapper.closeCryptDevice("/dev/mapper/volume01", "volume01-unit-test", "crypt")
|
2022-04-26 10:54:05 -04:00
|
|
|
if tc.wantErr {
|
2022-03-22 11:03:15 -04:00
|
|
|
assert.Error(err)
|
|
|
|
} else {
|
|
|
|
assert.NoError(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-06 09:05:59 -04:00
|
|
|
mapper := &CryptMapper{
|
|
|
|
mapper: &stubCryptDevice{},
|
|
|
|
kms: &fakeKMS{},
|
|
|
|
getDiskFormat: getDiskFormat,
|
|
|
|
}
|
2022-03-22 11:03:15 -04:00
|
|
|
err := mapper.CloseCryptDevice("volume01-unit-test")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOpenCryptDevice(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
2022-05-19 02:47:17 -04:00
|
|
|
source string
|
|
|
|
volumeID string
|
|
|
|
integrity bool
|
|
|
|
mapper *stubCryptDevice
|
2023-07-17 07:55:31 -04:00
|
|
|
kms keyCreator
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo func(disk string) (string, error)
|
|
|
|
wantErr bool
|
2022-03-22 11:03:15 -04:00
|
|
|
}{
|
|
|
|
"success with Load": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
|
|
|
mapper: &stubCryptDevice{},
|
2023-07-17 07:55:31 -04:00
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: false,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"success with error on Load": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: false,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"success with integrity": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
|
|
|
integrity: true,
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: false,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error on Init": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{initErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error on Format": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError, formatErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error on Activate": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{activatePassErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error on diskInfo": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
|
|
|
diskInfo: func(disk string) (string, error) { return "", assert.AnError },
|
2022-05-19 02:47:17 -04:00
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"disk is already formatted": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "ext4", nil },
|
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
"error with integrity on wipe": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
|
|
|
integrity: true,
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError, wipeErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
2022-05-10 04:43:48 -04:00
|
|
|
},
|
|
|
|
"error on adding keyslot": {
|
2022-05-19 02:47:17 -04:00
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError, keySlotAddErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"incorrect key length": {
|
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
|
|
|
mapper: &stubCryptDevice{},
|
2023-07-17 07:55:31 -04:00
|
|
|
kms: &fakeKMS{presetKey: []byte{0x1, 0x2, 0x3}},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"incorrect key length with error on Load": {
|
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{presetKey: []byte{0x1, 0x2, 0x3}},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"getKey fails": {
|
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
|
|
|
mapper: &stubCryptDevice{},
|
2023-07-17 07:55:31 -04:00
|
|
|
kms: &fakeKMS{getDEKErr: assert.AnError},
|
2022-05-19 02:47:17 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"getKey fails with error on Load": {
|
|
|
|
source: "/dev/some-device",
|
|
|
|
volumeID: "volume0",
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper: &stubCryptDevice{loadErr: assert.AnError},
|
|
|
|
kms: &fakeKMS{getDEKErr: assert.AnError},
|
2022-05-10 04:43:48 -04:00
|
|
|
diskInfo: func(disk string) (string, error) { return "", nil },
|
|
|
|
wantErr: true,
|
2022-03-22 11:03:15 -04:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
2023-07-17 07:55:31 -04:00
|
|
|
mapper := &CryptMapper{
|
|
|
|
mapper: tc.mapper,
|
|
|
|
kms: tc.kms,
|
|
|
|
getDiskFormat: tc.diskInfo,
|
|
|
|
}
|
|
|
|
|
|
|
|
out, err := mapper.OpenCryptDevice(context.Background(), tc.source, tc.volumeID, tc.integrity)
|
2022-04-26 10:54:05 -04:00
|
|
|
if tc.wantErr {
|
2022-03-22 11:03:15 -04:00
|
|
|
assert.Error(err)
|
|
|
|
} else {
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(cryptPrefix+tc.volumeID, out)
|
2022-05-10 04:43:48 -04:00
|
|
|
|
|
|
|
if tc.mapper.loadErr == nil {
|
|
|
|
assert.False(tc.mapper.keySlotAddCalled)
|
|
|
|
} else {
|
|
|
|
assert.True(tc.mapper.keySlotAddCalled)
|
|
|
|
}
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-06 09:05:59 -04:00
|
|
|
mapper := &CryptMapper{
|
|
|
|
mapper: &stubCryptDevice{},
|
|
|
|
kms: &fakeKMS{},
|
|
|
|
getDiskFormat: getDiskFormat,
|
|
|
|
}
|
2022-03-22 11:03:15 -04:00
|
|
|
_, err := mapper.OpenCryptDevice(context.Background(), "/dev/some-device", "volume01", false)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
2022-05-02 07:48:57 -04:00
|
|
|
|
2022-05-10 04:43:48 -04:00
|
|
|
func TestResizeCryptDevice(t *testing.T) {
|
|
|
|
volumeID := "pvc-123"
|
|
|
|
someErr := errors.New("error")
|
|
|
|
testCases := map[string]struct {
|
|
|
|
volumeID string
|
|
|
|
device *stubCryptDevice
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
"success": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{},
|
|
|
|
},
|
|
|
|
"InitByName fails": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{initByNameErr: someErr},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"Load fails": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{loadErr: someErr},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"Resize fails": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{resizeErr: someErr},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"ActivateByPassphrase fails": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{activatePassErr: someErr},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
|
|
|
mapper := &CryptMapper{
|
2022-09-05 02:42:55 -04:00
|
|
|
kms: &fakeKMS{},
|
2022-05-10 04:43:48 -04:00
|
|
|
mapper: tc.device,
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := mapper.ResizeCryptDevice(context.Background(), tc.volumeID)
|
|
|
|
if tc.wantErr {
|
|
|
|
assert.Error(err)
|
|
|
|
} else {
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(cryptPrefix+tc.volumeID, res)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-11 05:08:23 -04:00
|
|
|
func TestGetDevicePath(t *testing.T) {
|
|
|
|
volumeID := "pvc-123"
|
|
|
|
someErr := errors.New("error")
|
|
|
|
testCases := map[string]struct {
|
|
|
|
volumeID string
|
|
|
|
device *stubCryptDevice
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
"success": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{deviceName: volumeID},
|
|
|
|
},
|
|
|
|
"InitByName fails": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{initByNameErr: someErr},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
"GetDeviceName returns nothing": {
|
|
|
|
volumeID: volumeID,
|
|
|
|
device: &stubCryptDevice{},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
|
|
|
mapper := &CryptMapper{
|
|
|
|
mapper: tc.device,
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := mapper.GetDevicePath(tc.volumeID)
|
|
|
|
if tc.wantErr {
|
|
|
|
assert.Error(err)
|
|
|
|
} else {
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(tc.device.deviceName, res)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-02 07:48:57 -04:00
|
|
|
func TestIsIntegrityFS(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
wantIntegrity bool
|
|
|
|
fstype string
|
|
|
|
}{
|
|
|
|
"plain ext4": {
|
|
|
|
wantIntegrity: false,
|
|
|
|
fstype: "ext4",
|
|
|
|
},
|
|
|
|
"integrity ext4": {
|
|
|
|
wantIntegrity: true,
|
|
|
|
fstype: "ext4",
|
|
|
|
},
|
|
|
|
"integrity fs": {
|
|
|
|
wantIntegrity: false,
|
|
|
|
fstype: "integrity",
|
|
|
|
},
|
|
|
|
"double integrity": {
|
|
|
|
wantIntegrity: true,
|
|
|
|
fstype: "ext4-integrity",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
|
|
|
request := tc.fstype
|
|
|
|
if tc.wantIntegrity {
|
|
|
|
request = tc.fstype + integrityFSSuffix
|
|
|
|
}
|
|
|
|
|
|
|
|
fstype, isIntegrity := IsIntegrityFS(request)
|
|
|
|
|
|
|
|
if tc.wantIntegrity {
|
|
|
|
assert.True(isIntegrity)
|
|
|
|
assert.Equal(tc.fstype, fstype)
|
|
|
|
} else {
|
|
|
|
assert.False(isIntegrity)
|
|
|
|
assert.Equal(tc.fstype, fstype)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-09-05 02:42:55 -04:00
|
|
|
|
2023-07-17 07:55:31 -04:00
|
|
|
type fakeKMS struct {
|
|
|
|
presetKey []byte
|
|
|
|
getDEKErr error
|
|
|
|
}
|
2022-09-05 02:42:55 -04:00
|
|
|
|
2023-03-20 06:03:36 -04:00
|
|
|
func (k *fakeKMS) GetDEK(_ context.Context, _ string, dekSize int) ([]byte, error) {
|
2023-07-17 07:55:31 -04:00
|
|
|
if k.getDEKErr != nil {
|
|
|
|
return nil, k.getDEKErr
|
|
|
|
}
|
|
|
|
if k.presetKey != nil {
|
|
|
|
return k.presetKey, nil
|
2022-09-05 02:42:55 -04:00
|
|
|
}
|
2023-07-17 07:55:31 -04:00
|
|
|
return bytes.Repeat([]byte{0xAA}, dekSize), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type stubCryptDevice struct {
|
|
|
|
deviceName string
|
|
|
|
uuid string
|
|
|
|
uuidErr error
|
|
|
|
initErr error
|
|
|
|
initByNameErr error
|
|
|
|
activateErr error
|
|
|
|
activatePassErr error
|
|
|
|
deactivateErr error
|
|
|
|
formatErr error
|
|
|
|
loadErr error
|
|
|
|
keySlotAddCalled bool
|
|
|
|
keySlotAddErr error
|
|
|
|
wipeErr error
|
|
|
|
resizeErr error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Init(_ string) (func(), error) {
|
|
|
|
return func() {}, c.initErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) InitByName(_ string) (func(), error) {
|
|
|
|
return func() {}, c.initByNameErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) ActivateByVolumeKey(_, _ string, _, _ int) error {
|
|
|
|
return c.activateErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) ActivateByPassphrase(_ string, _ int, _ string, _ int) error {
|
|
|
|
return c.activatePassErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Deactivate(_ string) error {
|
|
|
|
return c.deactivateErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Format(_ bool) error {
|
|
|
|
return c.formatErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Free() {}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) GetDeviceName() string {
|
|
|
|
return c.deviceName
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) GetUUID() (string, error) {
|
|
|
|
return c.uuid, c.uuidErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) LoadLUKS2() error {
|
|
|
|
return c.loadErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) KeyslotAddByVolumeKey(_ int, _ string, _ string) error {
|
|
|
|
c.keySlotAddCalled = true
|
|
|
|
return c.keySlotAddErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Wipe(_ string, _ int, _ int, _ func(size, offset uint64), _ time.Duration) error {
|
|
|
|
return c.wipeErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *stubCryptDevice) Resize(_ string, _ uint64) error {
|
|
|
|
return c.resizeErr
|
2022-09-05 02:42:55 -04:00
|
|
|
}
|