Skip to content

Commit 9abfbdb

Browse files
pgodwinclaude
andcommitted
feat(protocol): client-direction DTOs for NCP + SMB file/browse services
Add the CLIENT-direction request builders and reply parsers (self-serialising DTOs, CLAUDE.md rule #10) the file clients drive, each producing the exact function body the ClassicStack server parses so a request round-trips against both our server and the real legacy servers it emulates. NCP (core/protocol/ncp): - clientfileops.go: negotiate buffer size, cleartext login, volume-number lookup, dir-handle allocate, open/create/close/read/write/getsize, erase/rename, and the FCB-era "Search for a File" (0x40) directory walk. - nwcrypt.go: the ncpfs shuffle / nw_encrypt bindery login-key algorithm, a faithful Go port of Volker Lendecke's ncpfs lib/nwcrypt.c (GPL) — attributed inline per rule #7. Client-side only. - functions/client: supporting function-code + request plumbing. SMB (core/protocol/smb): - netserverenum.go: RAP NetServerEnum2 (0x0068) builder/parser over IPC$ \PIPE\LANMAN, byte-compatible with the server's response (26-byte SERVER_INFO_1). ServerTypeAll clears SV_TYPE_DOMAIN_ENUM — mixing the domain-enum bit with server bits makes a master return ERROR_INVALID_FUNCTION. Tolerates ERROR_MORE_DATA (234). - clientfileops.go: additional client-direction file-op builders/parsers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ac618ea commit 9abfbdb

9 files changed

Lines changed: 833 additions & 12 deletions

File tree

core/protocol/ncp/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ func (r *Requester) NextSeq() uint8 {
4444
return r.seq
4545
}
4646

47+
// ResetSeq resets the request sequence to 0 so the NEXT request carries sequence 1. A
48+
// real NetWare server assigns the service connection on CreateConnection and then expects
49+
// the connection's request sequence to restart at 1 (ncpfs sets conn->sequence = 0 right
50+
// after the allocate-slot reply). CreateConnection itself is sequence-exempt on the
51+
// server, so the client must reset here once the connection is assigned — otherwise the
52+
// first post-create request arrives at sequence 2 (Create consumed 1) and the server,
53+
// waiting for sequence 1, silently drops it and every request after.
54+
func (r *Requester) ResetSeq() { r.seq = 0 }
55+
4756
// marshalRequest prepends the 6-byte NCP request header (type, sequence, conn-low,
4857
// task, conn-high, function) to body and returns the whole packet. typ is TypeRequest
4958
// for an ordinary function call. The sequence is bumped here so every packet a

core/protocol/ncp/clientfileops.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,67 @@ func (r *Requester) BuildLogin(user, password string) []byte {
6060
return r.marshalRequest(fnConnBindery, wrapSubfunction(sf17LoginUnencrypted, args))
6161
}
6262

63+
// --- Encrypted bindery login (NetWare 3.x): GetLoginKey / GetBinderyObjectID / Login ---
64+
//
65+
// A default-configured real NetWare server refuses the cleartext login and requires the
66+
// challenge-response bindery login: draw an 8-byte login key, resolve the user name to a
67+
// 4-byte object ID, then send a password digest keyed by both. This is CLIENT-side only;
68+
// the ClassicStack server stays on the cleartext NW-3.1 path. See nwcrypt.go for the
69+
// shuffle/nw_encrypt algorithm (ncpfs / DDJ 11/93 attribution).
70+
71+
// BuildGetLoginKey builds Get Login Key (0x17/0x17): no args; the reply body is the
72+
// 8-byte challenge key.
73+
func (r *Requester) BuildGetLoginKey() []byte {
74+
return r.marshalRequest(fnConnBindery, wrapSubfunction(sf17GetLoginKey, nil))
75+
}
76+
77+
// ParseLoginKey reads the 8-byte login key from a Get Login Key reply body.
78+
func ParseLoginKey(body []byte) ([8]byte, error) {
79+
var key [8]byte
80+
if len(body) < 8 {
81+
return key, ErrShortBody
82+
}
83+
copy(key[:], body[:8])
84+
return key, nil
85+
}
86+
87+
// BuildGetBinderyObjectID builds Get Bindery Object ID (0x17/0x35): object-type(2 BE) +
88+
// length-prefixed name. The reply body is object-id(4 BE), object-type(2 BE), then a
89+
// 48-byte NUL-padded name.
90+
func (r *Requester) BuildGetBinderyObjectID(objType uint16, name string) []byte {
91+
args := beU16b(objType)
92+
args = appendByteString(args, name)
93+
return r.marshalRequest(fnConnBindery, wrapSubfunction(sf17GetBinderyObjectID, args))
94+
}
95+
96+
// ParseBinderyObjectID reads the 4-byte object ID (big-endian) from a Get Bindery Object
97+
// ID reply body.
98+
func ParseBinderyObjectID(body []byte) (uint32, error) {
99+
if len(body) < 4 {
100+
return 0, ErrShortBody
101+
}
102+
return uint32(body[0])<<24 | uint32(body[1])<<16 | uint32(body[2])<<8 | uint32(body[3]), nil
103+
}
104+
105+
// BuildLoginEncrypted builds Login Object Encrypted (0x17/0x18): the 8-byte
106+
// challenge-response (nwEncrypt of the shuffled password), object-type(2 BE), and the
107+
// length-prefixed user name. objectID is the user's bindery object ID from
108+
// BuildGetBinderyObjectID; key is the server's login key from BuildGetLoginKey.
109+
func (r *Requester) BuildLoginEncrypted(objType uint16, name, password string, objectID uint32, key [8]byte) []byte {
110+
// shuffle the password keyed by the object ID in NETWORK byte order (big-endian),
111+
// matching ncpfs (htonl(object_id)); then fold in the challenge key.
112+
lon := [4]byte{byte(objectID >> 24), byte(objectID >> 16), byte(objectID >> 8), byte(objectID)}
113+
var digest [16]byte
114+
shuffle(lon, []byte(password), &digest)
115+
var resp [8]byte
116+
nwEncrypt(key, digest, &resp)
117+
118+
args := append([]byte(nil), resp[:]...)
119+
args = appendBE16(args, objType)
120+
args = appendByteString(args, name)
121+
return r.marshalRequest(fnConnBindery, wrapSubfunction(sf17LoginEncrypted, args))
122+
}
123+
63124
// --- Get Volume Number (0x16/0x05) ---
64125

65126
// BuildGetVolumeNumber builds Get Volume Number (0x16/0x05): a length-prefixed volume
@@ -69,6 +130,30 @@ func (r *Requester) BuildGetVolumeNumber(volume string) []byte {
69130
return r.marshalRequest(fnDirServices, wrapSubfunction(sf16GetVolumeNumber, args))
70131
}
71132

133+
// MaxVolumeSlots is the number of volume-number slots a browse iterates (0..63).
134+
const MaxVolumeSlots = maxVolumeSlots
135+
136+
// BuildGetVolumeName builds Get Volume Name (0x16/0x06): a single volume-number byte. The
137+
// reply body is a 1-byte length followed by the volume name. Iterating the volume number
138+
// 0..MaxVolumeSlots-1 enumerates a server's mounted volumes (a not-OK completion or empty
139+
// name means no volume in that slot) — the NetWare 3.x way to browse a server's volumes.
140+
func (r *Requester) BuildGetVolumeName(volumeNumber uint8) []byte {
141+
return r.marshalRequest(fnDirServices, wrapSubfunction(sf16GetVolumeName, []byte{volumeNumber}))
142+
}
143+
144+
// ParseVolumeName reads the volume name (1-byte length + bytes) from a Get Volume Name
145+
// reply body. An empty name is returned as "".
146+
func ParseVolumeName(body []byte) (string, error) {
147+
if len(body) < 1 {
148+
return "", ErrShortBody
149+
}
150+
n := int(body[0])
151+
if len(body) < 1+n {
152+
return "", ErrShortBody
153+
}
154+
return string(body[1 : 1+n]), nil
155+
}
156+
72157
// ParseVolumeNumber reads the 1-byte volume number from a Get Volume Number reply.
73158
func ParseVolumeNumber(body []byte) (uint8, error) {
74159
if len(body) < 1 {
@@ -387,6 +472,92 @@ func ParseSearchReply(body []byte) (SearchEntry, error) {
387472
// nwAttrDirectory is the NetWare DOS directory attribute bit (fileio.go).
388473
const nwAttrDirectory uint8 = 0x10
389474

475+
// --- File Search Initialize / Continue (0x3E / 0x3F): the NetWare 3.x directory scan ---
476+
//
477+
// A real NetWare 3.x/4.x server enumerates a directory with the two-call File Search
478+
// Initialize (62/0x3E) + File Search Continue (63/0x3F) pair, NOT the FCB-era Search for a
479+
// File (0x40) above (which a real server answers 0xFF/no-files). Initialize takes a dir
480+
// handle + subpath and returns a search context (volume, directory id, sequence); Continue
481+
// pages that context with a wildcard pattern, one entry per call, until completion 0xFF
482+
// (end of scan). Layout ported from ncpfs lib/filemgmt.c ncp_file_search_init /
483+
// ncp_file_search_continue (CLAUDE.md #7).
484+
485+
// searchAllPattern is the NetWare "match every 8.3 name" wildcard for File Search
486+
// Continue: "*.*" with each character's high bit set (0x2A→0xAA '*', 0x2E→0xAE '.'), the
487+
// server's marker for a wildcard match-any (observed on the wire from a real NW 4.1
488+
// client: bytes AA AE AA). Sent as a length-prefixed string.
489+
var searchAllPattern = string([]byte{0xAA, 0xAE, 0xAA})
490+
491+
// FileSearchContext is the search state File Search Initialize returns and File Search
492+
// Continue pages: the volume number, directory id, and the running sequence.
493+
type FileSearchContext struct {
494+
VolumeNumber uint8
495+
DirectoryID uint16
496+
Sequence uint16
497+
}
498+
499+
// BuildFileSearchInit builds File Search Initialize (0x3E): dir-handle(1) + pstring path
500+
// (the directory to scan, relative to the handle; "" scans the handle's own directory).
501+
func (r *Requester) BuildFileSearchInit(handle uint8, path string) []byte {
502+
body := []byte{handle}
503+
body = appendByteString(body, path)
504+
return r.marshalRequest(fnFileSearchInit, body)
505+
}
506+
507+
// ParseFileSearchInit reads the search context from a File Search Initialize reply:
508+
// volume-number(1), directory-id(2 HL/BE), sequence(2 HL/BE), access-rights(1).
509+
func ParseFileSearchInit(body []byte) (FileSearchContext, error) {
510+
if len(body) < 6 {
511+
return FileSearchContext{}, ErrShortBody
512+
}
513+
return FileSearchContext{
514+
VolumeNumber: body[0],
515+
DirectoryID: uint16(body[1])<<8 | uint16(body[2]),
516+
Sequence: uint16(body[3])<<8 | uint16(body[4]),
517+
}, nil
518+
}
519+
520+
// BuildFileSearchContinue builds File Search Continue (0x3F): volume-number(1),
521+
// directory-id(2 HL), sequence(2 HL), search-attributes(1), pstring pattern. Pass
522+
// searchAllPattern to match every entry. attr selects files vs directories via the
523+
// directory bit (0x10), like the 0x40 scan.
524+
func (r *Requester) BuildFileSearchContinue(ctx FileSearchContext, attr uint8, pattern string) []byte {
525+
body := []byte{ctx.VolumeNumber}
526+
body = appendBE16(body, ctx.DirectoryID)
527+
body = appendBE16(body, ctx.Sequence)
528+
body = append(body, attr)
529+
body = appendByteString(body, pattern)
530+
return r.marshalRequest(fnFileSearchCont, body)
531+
}
532+
533+
// SearchAllPattern is the exported match-every-entry wildcard for File Search Continue.
534+
func SearchAllPattern() string { return searchAllPattern }
535+
536+
// ParseFileSearchContinue reads one entry from a File Search Continue reply: sequence(2
537+
// HL) + reserved(2), then the entry record — a 14-byte name at offset 4, the attribute
538+
// byte at offset 18, and (for a file) a 4-byte length at offset 20 (HL/BE). It updates
539+
// ctx.Sequence to page the next call.
540+
func ParseFileSearchContinue(body []byte, ctx *FileSearchContext) (SearchEntry, error) {
541+
const fixed = 2 + 2 + 14 + 1 // sequence, reserved, name, attribute
542+
if len(body) < fixed {
543+
return SearchEntry{}, ErrShortBody
544+
}
545+
ctx.Sequence = uint16(body[0])<<8 | uint16(body[1])
546+
var e SearchEntry
547+
e.NextSeq = ctx.Sequence
548+
e.Name = trimNUL(body[4:18])
549+
attr := body[18]
550+
e.IsDir = attr&nwAttrDirectory != 0
551+
if !e.IsDir {
552+
// File length is a 4-byte HL/BE word at offset 20 (ncpfs file_length,
553+
// ncp_reply_dword_hl(conn, 20)).
554+
if len(body) >= 24 {
555+
e.Size = be32(body[20:])
556+
}
557+
}
558+
return e, nil
559+
}
560+
390561
// --- little wire helpers (big-endian body fields) ---
391562

392563
func be16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) }

core/protocol/ncp/functions.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,37 @@ const (
2323
fnEraseFile uint8 = 0x44 // erase/delete file
2424
fnRenameFile uint8 = 0x45 // rename file
2525
fnSearchForFile uint8 = 0x40 // Search for a File (FCB-era one-call-per-entry)
26+
fnFileSearchInit uint8 = 0x3E // File Search Initialize (62) — NW 3.x dir scan setup
27+
fnFileSearchCont uint8 = 0x3F // File Search Continue (63) — NW 3.x dir scan paging
2628
fnDirServices uint8 = 0x16 // multiplexed dir-handle / volume services
2729
fnConnBindery uint8 = 0x17 // multiplexed connection/bindery services
2830
fnGetServerDateTime uint8 = 0x14 // get file-server date/time
2931
fnNegotiateBuffer uint8 = 0x21 // Negotiate Buffer Size (max read/write packet)
3032
)
3133

32-
// Subfunctions of fnConnBindery (0x17) the client uses.
34+
// Subfunctions of fnConnBindery (0x17) the client uses. The encrypted-login trio
35+
// (GetLoginKey / GetBinderyObjectID / LoginEncrypted) is the classic NetWare 3.x bindery
36+
// login a default-configured real server requires; the cleartext login (0x14) is the
37+
// fallback our own server / mars_nwe also accept. NDS (NetWare 4+) login is NOT handled.
3338
const (
34-
sf17GetServerInfo uint8 = 0x11 // Get File Server Information
35-
sf17LoginUnencrypted uint8 = 0x14 // Login To File Server (cleartext)
39+
sf17GetServerInfo uint8 = 0x11 // Get File Server Information
40+
sf17LoginUnencrypted uint8 = 0x14 // Login To File Server (cleartext)
41+
sf17GetLoginKey uint8 = 0x17 // Get Login Key (8-byte challenge) — 23 decimal
42+
sf17LoginEncrypted uint8 = 0x18 // Login Object (Encrypted) — 24 decimal
43+
sf17GetBinderyObjectID uint8 = 0x35 // Get Bindery Object ID (name → object id) — 53
3644
)
3745

3846
// Subfunctions of fnDirServices (0x16) the client uses.
3947
const (
48+
sf16GetVolumeName uint8 = 0x06 // Get Volume Name (by number) — for volume enumeration
4049
sf16GetVolumeNumber uint8 = 0x05 // Get Volume Number (by name)
4150
sf16CreateDir uint8 = 0x0A // Create Directory
4251
sf16DeleteDir uint8 = 0x0B // Delete Directory
4352
sf16AllocPermDir uint8 = 0x12 // Allocate Permanent Directory Handle
4453
sf16DeallocDirHdl uint8 = 0x14 // Deallocate Directory Handle
4554
sf16GetVolumeInfo uint8 = 0x15 // Get Volume Info with Handle
4655
)
56+
57+
// maxVolumeSlots is the number of volume-number slots a NetWare 3.x server exposes
58+
// (0..63); a browse enumerates them via Get Volume Name.
59+
const maxVolumeSlots = 64

core/protocol/ncp/nwcrypt.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package ncp
2+
3+
// nwcrypt.go is the NetWare bindery password-encryption used by the encrypted login
4+
// handshake a NetWare 3.x/4.x server requires (a cleartext Login To File Server is
5+
// refused by a default-configured real server). The flow is: Get Login Key draws an
6+
// 8-byte challenge from the server; Get Bindery Object ID resolves the user name to its
7+
// 4-byte object ID; then the password is shuffle()d with the object ID into a 16-byte
8+
// digest, and nw_encrypt() folds the challenge key into it to produce the 8-byte response
9+
// carried by Login Object (Encrypted).
10+
//
11+
// ATTRIBUTION (CLAUDE.md #7): the shuffle / nw_encrypt algorithm is the one published in
12+
// Dr. Dobb's Journal 11/93 "Undocumented Corner" by Pawel Szczerbina (itself converted
13+
// from Barry Nance's Pascal in Byte 3/93), and adapted for the free NCP filesystem by
14+
// Volker Lendecke in ncpfs (lib/nwcrypt.c, GPL). This is a faithful Go port of that
15+
// code — the tables and step structure are preserved exactly so it interoperates with a
16+
// real NetWare server and with mars_nwe. Only the surface (Go slices, names) differs.
17+
18+
// encryptTable is the 256-entry nibble substitution table (ncpfs encrypttable).
19+
var encryptTable = [256]byte{
20+
0x7, 0x8, 0x0, 0x8, 0x6, 0x4, 0xE, 0x4, 0x5, 0xC, 0x1, 0x7, 0xB, 0xF, 0xA, 0x8,
21+
0xF, 0x8, 0xC, 0xC, 0x9, 0x4, 0x1, 0xE, 0x4, 0x6, 0x2, 0x4, 0x0, 0xA, 0xB, 0x9,
22+
0x2, 0xF, 0xB, 0x1, 0xD, 0x2, 0x1, 0x9, 0x5, 0xE, 0x7, 0x0, 0x0, 0x2, 0x6, 0x6,
23+
0x0, 0x7, 0x3, 0x8, 0x2, 0x9, 0x3, 0xF, 0x7, 0xF, 0xC, 0xF, 0x6, 0x4, 0xA, 0x0,
24+
0x2, 0x3, 0xA, 0xB, 0xD, 0x8, 0x3, 0xA, 0x1, 0x7, 0xC, 0xF, 0x1, 0x8, 0x9, 0xD,
25+
0x9, 0x1, 0x9, 0x4, 0xE, 0x4, 0xC, 0x5, 0x5, 0xC, 0x8, 0xB, 0x2, 0x3, 0x9, 0xE,
26+
0x7, 0x7, 0x6, 0x9, 0xE, 0xF, 0xC, 0x8, 0xD, 0x1, 0xA, 0x6, 0xE, 0xD, 0x0, 0x7,
27+
0x7, 0xA, 0x0, 0x1, 0xF, 0x5, 0x4, 0xB, 0x7, 0xB, 0xE, 0xC, 0x9, 0x5, 0xD, 0x1,
28+
0xB, 0xD, 0x1, 0x3, 0x5, 0xD, 0xE, 0x6, 0x3, 0x0, 0xB, 0xB, 0xF, 0x3, 0x6, 0x4,
29+
0x9, 0xD, 0xA, 0x3, 0x1, 0x4, 0x9, 0x4, 0x8, 0x3, 0xB, 0xE, 0x5, 0x0, 0x5, 0x2,
30+
0xC, 0xB, 0xD, 0x5, 0xD, 0x5, 0xD, 0x2, 0xD, 0x9, 0xA, 0xC, 0xA, 0x0, 0xB, 0x3,
31+
0x5, 0x3, 0x6, 0x9, 0x5, 0x1, 0xE, 0xE, 0x0, 0xE, 0x8, 0x2, 0xD, 0x2, 0x2, 0x0,
32+
0x4, 0xF, 0x8, 0x5, 0x9, 0x6, 0x8, 0x6, 0xB, 0xA, 0xB, 0xF, 0x0, 0x7, 0x2, 0x8,
33+
0xC, 0x7, 0x3, 0xA, 0x1, 0x4, 0x2, 0x5, 0xF, 0x7, 0xA, 0xC, 0xE, 0x5, 0x9, 0x3,
34+
0xE, 0x7, 0x1, 0x2, 0xE, 0x1, 0xF, 0x4, 0xA, 0x6, 0xC, 0x6, 0xF, 0x4, 0x3, 0x0,
35+
0xC, 0x0, 0x3, 0x6, 0xF, 0x8, 0x7, 0xB, 0x2, 0xD, 0xC, 0x6, 0xA, 0xA, 0x8, 0xD,
36+
}
37+
38+
// encryptKeys is the 32-byte key vector mixed into the shuffle (ncpfs encryptkeys).
39+
var encryptKeys = [32]byte{
40+
0x48, 0x93, 0x46, 0x67, 0x98, 0x3D, 0xE6, 0x8D,
41+
0xB7, 0x10, 0x7A, 0x26, 0x5A, 0xB9, 0xB1, 0x35,
42+
0x6B, 0x0F, 0xD5, 0x70, 0xAE, 0xFB, 0xAD, 0x11,
43+
0xF4, 0x47, 0xDC, 0xA7, 0xEC, 0xCF, 0x50, 0xC0,
44+
}
45+
46+
// shuffle1 mixes the 32-byte temp buffer and folds it to a 16-byte target (ncpfs
47+
// shuffle1): two mixing passes over temp, then a nibble-substitution to target.
48+
func shuffle1(temp *[32]byte, target *[16]byte) {
49+
var b4 int16
50+
for b2 := 0; b2 <= 1; b2++ {
51+
for s := 0; s <= 31; s++ {
52+
b3 := byte((int(temp[s]) + int(b4)) ^ (int(temp[(s+int(b4))&31]) - int(encryptKeys[s])))
53+
b4 += int16(b3)
54+
temp[s] = b3
55+
}
56+
}
57+
for i := 0; i <= 15; i++ {
58+
target[i] = encryptTable[temp[2*i]] | (encryptTable[temp[2*i+1]] << 4)
59+
}
60+
}
61+
62+
// shuffle hashes password bytes buf keyed by the 4-byte lon (the login object ID in
63+
// network byte order) into a 16-byte target (ncpfs shuffle). Trailing NUL bytes of buf
64+
// are dropped first, matching ncpfs.
65+
func shuffle(lon [4]byte, buf []byte, target *[16]byte) {
66+
buflen := len(buf)
67+
for buflen > 0 && buf[buflen-1] == 0 {
68+
buflen--
69+
}
70+
71+
var temp [32]byte
72+
d := 0
73+
for buflen >= 32 {
74+
for s := 0; s <= 31; s++ {
75+
temp[s] ^= buf[d]
76+
d++
77+
}
78+
buflen -= 32
79+
}
80+
b2 := d
81+
if buflen > 0 {
82+
for s := 0; s <= 31; s++ {
83+
if d+buflen == b2 {
84+
b2 = d
85+
temp[s] ^= encryptKeys[s]
86+
} else {
87+
temp[s] ^= buf[b2]
88+
b2++
89+
}
90+
}
91+
}
92+
for s := 0; s <= 31; s++ {
93+
temp[s] ^= lon[s&3]
94+
}
95+
shuffle1(&temp, target)
96+
}
97+
98+
// nwEncrypt folds the 8-byte server login key fra into the 16-byte shuffled password buf
99+
// to produce the 8-byte response til carried by the encrypted login (ncpfs nw_encrypt).
100+
// The two 4-byte halves of the login key each shuffle buf into one 16-byte half of k,
101+
// which is then folded twice down to the 8-byte result.
102+
func nwEncrypt(fra [8]byte, buf [16]byte, til *[8]byte) {
103+
var a, b [16]byte
104+
var fra0, fra4 [4]byte
105+
copy(fra0[:], fra[0:4])
106+
copy(fra4[:], fra[4:8])
107+
shuffle(fra0, buf[:], &a)
108+
shuffle(fra4, buf[:], &b)
109+
110+
var k [32]byte
111+
copy(k[0:16], a[:])
112+
copy(k[16:32], b[:])
113+
for s := 0; s <= 15; s++ {
114+
k[s] ^= k[31-s]
115+
}
116+
for s := 0; s <= 7; s++ {
117+
til[s] = k[s] ^ k[15-s]
118+
}
119+
}

0 commit comments

Comments
 (0)