Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Bouke committed Aug 28, 2016
0 parents commit 8602a3f
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (C) 2016 Bouke Haarsma

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.
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PackageDescription

let package = Package(
name: "SRP",
dependencies: [
.Package(url: "https://github.com/Bouke/CommonCrypto.git", majorVersion: 1),
.Package(url: "https://github.com/Bouke/Bignum.git", majorVersion: 1),
]
)
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Secure Remote Password (SRP) for Swift
======================================

SRP6 for Swift. API designed similar to the Python package [https://pypi.python.org/pypi/srp](srp). For usage, either see the Python package, or the tests.
71 changes: 71 additions & 0 deletions Sources/Client.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import Bignum
import CommonCrypto

public class Client {
internal let a: Bignum
public let A: Data

internal let group: Group
internal let alg: Digest

internal let username: String
internal let password: String

internal var HAMK: Data? = nil

public private(set) var isAuthenticated = false
public private(set) var sessionKey: Data? = nil

public init (group: Group = .N2048, alg: Digest = .SHA1, username: String, password: String, secret: Data? = nil) {
self.group = group
self.alg = alg
self.username = username
self.password = password

if let secret = secret {
a = Bignum(data: secret)
} else {
a = Bignum(data: generateRandomBytes(count: 32))
}
// A = g^a % N
A = mod_exp(group.g, a, group.N).data
}

public func startAuthentication() -> (username: String, A: Data) {
return (username, A)
}

public func processChallenge(salt: Data, B: Data) -> Data {
let H = alg.hash
let N = group.N

let u = calculate_u(group: group, alg: alg, A: A, B: B)
let k = calculate_k(group: group, alg: alg)
let x = calculate_x(alg: alg, salt: salt, username: username, password: password)
let v = calculate_v(group: group, x: x)

let B_ = Bignum(data: B)

// shared secret
// S = (B - kg^x) ^ (a + ux)
let S = mod_exp(B_ - k * v, a + u * x, N)

// session key
sessionKey = H(S.data)

// client verification
let M = calculate_M(group: group, alg: alg, username: username, salt: salt, A: A, B: B, K: sessionKey!)
print(Bignum(data: M))

// server verification
HAMK = calculate_HAMK(alg: alg, A: A, M: M, K: sessionKey!)
return M
}

public func verifySession(HAMK serverHAMK: Data) throws {
guard let HAMK = HAMK else { throw Error.authenticationFailed }
guard HAMK == serverHAMK else { throw Error.authenticationFailed }
isAuthenticated = true
}
}
17 changes: 17 additions & 0 deletions Sources/Data+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

func ^ (lhs: Data, rhs: Data) -> Data? {
guard lhs.count == rhs.count else { return nil }
var result = Data(count: lhs.count)
for index in lhs.indices {
result[index] = lhs[index] ^ rhs[index]
}
return result
}

// Removed in Xcode 8 beta 3
func + (lhs: Data, rhs: Data) -> Data {
var result = lhs
result.append(rhs)
return result
}
5 changes: 5 additions & 0 deletions Sources/Error.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public enum Error: Swift.Error {
case authenticationFailed
}
43 changes: 43 additions & 0 deletions Sources/Group.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Bignum

public enum Group {
case N2048
case N3072

var N: Bignum {
switch self {
case .N2048:
return Bignum(hex: "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" +
"A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" +
"95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" +
"747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" +
"8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" +
"60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" +
"FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73")
case .N3072:
return Bignum(hex: "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" +
"8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" +
"302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" +
"A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" +
"49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8" +
"FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
"670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C" +
"180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718" +
"3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D" +
"04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D" +
"B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226" +
"1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C" +
"BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC" +
"E0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF")
}
}

var g: Bignum {
switch self {
case .N2048:
return Bignum(hex: "2")
case .N3072:
return Bignum(hex: "5")
}
}
}
51 changes: 51 additions & 0 deletions Sources/SRP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import Bignum
import CommonCrypto

public func createSaltedVerificationKey(username: String, password: String, salt: Data? = nil, group: Group = .N2048, alg: Digest = .SHA1) -> (salt: Data, verificationKey: Data) {
let salt = salt ?? generateRandomBytes(count: 16)
let x = calculate_x(alg: alg, salt: salt, username: username, password: password)
let v = calculate_v(group: group, x: x)
return (salt, v.data)
}

func pad(_ data: Data, to size: Int) -> Data {
return Data(count: size - data.count) + data
}

//u = H(PAD(A) | PAD(B))
func calculate_u(group: Group, alg: Digest, A: Data, B: Data) -> Bignum {
let H = alg.hash
return Bignum(data: H(pad(A, to: group.N.data.count) + pad(B, to: group.N.data.count)))
}

//M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
func calculate_M(group: Group, alg: Digest, username: String, salt: Data, A: Data, B: Data, K: Data) -> Data {
let H = alg.hash
let HN_xor_Hg = (H(group.N.data) ^ H(group.g.data))!
let HI = H(username.data(using: .utf8)!)
return H(HN_xor_Hg + HI + salt + A + B + K)
}

//HAMK = H(A | M | K)
func calculate_HAMK(alg: Digest, A: Data, M: Data, K: Data) -> Data {
let H = alg.hash
return H(A + M + K)
}

//k = H(N | PAD(g))
func calculate_k(group: Group, alg: Digest) -> Bignum {
let H = alg.hash
return Bignum(data: H(group.N.data + pad(group.g.data, to: group.N.data.count)))
}

//x = H(s | H(I | ":" | P))
func calculate_x(alg: Digest, salt: Data, username: String, password: String) -> Bignum {
let H = alg.hash
return Bignum(data: H(salt + H("\(username):\(password)".data(using: .utf8)!)))
}

// v = g^x % N
func calculate_v(group: Group, x: Bignum) -> Bignum {
return mod_exp(group.g, x, group.N)
}
63 changes: 63 additions & 0 deletions Sources/Server.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation
import Bignum
import CommonCrypto

public class Server {
internal let b: Bignum
public let B: Data

public let salt: Data
public let username: String

internal let v: Bignum

internal let group: Group
internal let alg: Digest

public private(set) var isAuthenticated = false
public private(set) var sessionKey: Data? = nil

public init (group: Group = .N2048, alg: Digest = .SHA1, salt: Data, username: String, verificationKey: Data, secret: Data? = nil) {
self.group = group
self.alg = alg
self.salt = salt
self.username = username

if let secret = secret {
b = Bignum(data: secret)
} else {
b = Bignum(data: generateRandomBytes(count: 32))
}
let k = calculate_k(group: group, alg: alg)
v = Bignum(data: verificationKey)
let N = group.N
let g = group.g
// B = k*v + g^b % N
B = mod_add(k * v, mod_exp(g, b, N), N).data
}

public func getChallenge() -> (salt: Data, B: Data) {
return (salt, B)
}

public func verifySession(A: Data, M clientM: Data) throws -> Data {
let u = calculate_u(group: group, alg: alg, A: A, B: B)
let A_ = Bignum(data: A)
let N = group.N

// shared secret
// S = (Av^u) mod N
let S = mod_exp(A_ * mod_exp(v, u, N), b, N)

let H = alg.hash
// K = H(S)
sessionKey = H(S.data)

let M = calculate_M(group: group, alg: alg, username: username, salt: salt, A: A, B: B, K: sessionKey!)
print(Bignum(data: M))
guard clientM == M else { throw Error.authenticationFailed }
isAuthenticated = true

return calculate_HAMK(alg: alg, A: A, M: M, K: sessionKey!)
}
}
6 changes: 6 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import XCTest
@testable import SRPTests

XCTMain([
testCase(SRPTests.allTests),
])
72 changes: 72 additions & 0 deletions Tests/SRPTests/SRPTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import XCTest
import CommonCrypto
@testable import SRP

class SRPTests: XCTestCase {
func test() throws {
let username = "Pair-Setup"
let password = "001-02-003"

/* Create a salt+verification key for the user's password. The salt and
* key need to be computed at the time the user's password is set and
* must be stored by the server-side application for use during the
* authentication process.
*/
let (salt, verificationKey) = createSaltedVerificationKey(username: username, password: password)

// Begin authentication process
let client = Client(username: username, password: password)
let (_, A) = client.startAuthentication()

// Client->Server: I (username)
// Server retrieves salt and verificationKey from permanent storage
let server = Server(salt: salt, username: username, verificationKey: verificationKey, secret: generateRandomBytes(count: 32))

// The server generates the challenge: pre-defined salt, public key B
// Server->Client: salt, B
let (_, B) = server.getChallenge()

// Using (salt, B), the client generates the proof M
// Client->Server: M
let M = client.processChallenge(salt: salt, B: B)

XCTAssertFalse(server.isAuthenticated)
XCTAssertFalse(client.isAuthenticated)

let HAMK: Data
do {
// Using M, the server verifies the proof and calculates a proof for the client
// Server->Client: H(AMK)
HAMK = try server.verifySession(A: client.A, M: M)
} catch SRP.Error.authenticationFailed {
return XCTFail("Client generated invalid M")
}

// At this point, the server is authenticated.
XCTAssert(server.isAuthenticated)
XCTAssertFalse(client.isAuthenticated)

do {
// Using H(AMK), the client verifies the server's proof
try client.verifySession(HAMK: HAMK)
} catch SRP.Error.authenticationFailed {
return XCTFail("Server generated invalid H(AMK)")
}

// At this point, the client is authenticated as well
XCTAssert(server.isAuthenticated)
XCTAssert(client.isAuthenticated)

// They now share a secret session key
guard let K0 = server.sessionKey, let K1 = client.sessionKey else {
return XCTFail("Session keys not set")
}
XCTAssertEqual(K0, K1, "Session keys not equal")
}

static var allTests : [(String, (SRPTests) -> () throws -> Void)] {
return [
("test", test),
]
}
}

0 comments on commit 8602a3f

Please sign in to comment.